diff --git a/packages/bsky/src/api/app/bsky/feed/describeFeedGenerator.ts b/packages/bsky/src/api/app/bsky/feed/describeFeedGenerator.ts deleted file mode 100644 index 6ff900ca4c0..00000000000 --- a/packages/bsky/src/api/app/bsky/feed/describeFeedGenerator.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MethodNotImplementedError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.describeFeedGenerator(async () => { - if (!ctx.cfg.feedGenDid) { - throw new MethodNotImplementedError() - } - - const feeds = Object.keys(ctx.algos).map((uri) => ({ uri })) - - return { - encoding: 'application/json', - body: { - did: ctx.cfg.feedGenDid, - feeds, - }, - } - }) -} diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index b7cd668c996..15305b00a99 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -93,10 +93,7 @@ const hydration = async (inputs: { const [feedPostState, profileViewerState = {}] = await Promise.all([ ctx.hydrator.hydrateFeedItems(skeleton.items, params.viewer), params.viewer - ? ctx.hydrator.actor.getProfileViewerStates( - [skeleton.actor.did], - params.viewer, - ) + ? ctx.hydrator.hydrateProfileViewers([skeleton.actor.did], params.viewer) : undefined, ]) return mergeStates(feedPostState, profileViewerState) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index 1a89d2f678a..00dd417ce3b 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -12,7 +12,6 @@ import { QueryParams as GetFeedParams } from '../../../../lexicon/types/app/bsky import { OutputSchema as SkeletonOutput } from '../../../../lexicon/types/app/bsky/feed/getFeedSkeleton' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { AlgoResponse, toFeedItem } from '../../../feed-gen/types' import { HydrationFnInput, PresentationFnInput, @@ -68,15 +67,12 @@ const skeleton = async ( ): Promise => { const { ctx, params } = inputs const timerSkele = new ServerTimer('skele').start() - const localAlgo = ctx.algos[params.feed] const { feedItems: algoItems, cursor, resHeaders, ...passthrough - } = localAlgo !== undefined - ? await localAlgo(ctx, params, params.viewer) - : await skeletonFromFeedGen(ctx, params) + } = await skeletonFromFeedGen(ctx, params) return { cursor, @@ -229,3 +225,22 @@ const skeletonFromFeedGen = async ( return { ...skele, resHeaders, feedItems } } + +export type AlgoResponse = { + feedItems: AlgoResponseItem[] + resHeaders?: Record + cursor?: string +} + +export type AlgoResponseItem = { + itemUri: string + postUri: string +} + +export const toFeedItem = (feedItem: AlgoResponseItem): FeedItem => ({ + post: { uri: feedItem.postUri }, + repost: + feedItem.itemUri === feedItem.postUri + ? undefined + : { uri: feedItem.itemUri }, +}) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts b/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts deleted file mode 100644 index 97f3deefa5c..00000000000 --- a/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { toSkeletonItem } from '../../../feed-gen/types' - -export default function (server: Server, ctx: AppContext) { - server.app.bsky.feed.getFeedSkeleton({ - auth: ctx.authVerifier.standardOptional, - handler: async ({ params, auth }) => { - const { feed } = params - const viewer = auth.credentials.iss - const localAlgo = ctx.algos[feed] - - if (!localAlgo) { - throw new InvalidRequestError('Unknown feed', 'UnknownFeed') - } - - const result = await localAlgo(ctx, params, viewer) - - return { - encoding: 'application/json', - body: { - // @TODO should we proactively filter blocks/mutes from the skeleton, or treat this similar to other custom feeds? - feed: result.feedItems.map(toSkeletonItem), - cursor: result.cursor, - }, - } - }, - }) -} diff --git a/packages/bsky/src/api/app/bsky/graph/getRelationships.ts b/packages/bsky/src/api/app/bsky/graph/getRelationships.ts index d0fc43d53ab..47aaa6cd083 100644 --- a/packages/bsky/src/api/app/bsky/graph/getRelationships.ts +++ b/packages/bsky/src/api/app/bsky/graph/getRelationships.ts @@ -14,7 +14,10 @@ export default function (server: Server, ctx: AppContext) { }, } } - const res = await ctx.hydrator.actor.getProfileViewerStates(others, actor) + const res = await ctx.hydrator.actor.getProfileViewerStatesNaive( + others, + actor, + ) const relationships = others.map((did) => { const subject = res.get(did) return subject diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts deleted file mode 100644 index 9f4e2335554..00000000000 --- a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { skeleton } from '../feed/getTimeline' -import { toSkeletonItem } from '../../../feed-gen/types' - -// THIS IS A TEMPORARY UNSPECCED ROUTE -export default function (server: Server, ctx: AppContext) { - server.app.bsky.unspecced.getTimelineSkeleton({ - auth: ctx.authVerifier.standard, - handler: async ({ auth, params }) => { - const viewer = auth.credentials.iss - // @NOTE bad cursor during appview swap handled within skeleton() - const result = await skeleton({ ctx, params: { ...params, viewer } }) - const feed = result.items.map((item) => { - return toSkeletonItem({ - postUri: item.post.uri, - itemUri: item.repost ? item.repost.uri : item.post.uri, - }) - }) - return { - encoding: 'application/json', - body: { - feed, - cursor: result.cursor, - }, - } - }, - }) -} diff --git a/packages/bsky/src/api/feed-gen/index.ts b/packages/bsky/src/api/feed-gen/index.ts deleted file mode 100644 index ede5c861459..00000000000 --- a/packages/bsky/src/api/feed-gen/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MountedAlgos } from './types' - -// These are custom algorithms that will be mounted directly onto an AppView -// Feel free to remove, update to your own, or serve the following logic at a record that you control -// @TODO currently empty during appview v2 build out -export const makeAlgos = (_did: string): MountedAlgos => ({}) diff --git a/packages/bsky/src/api/feed-gen/types.ts b/packages/bsky/src/api/feed-gen/types.ts deleted file mode 100644 index 1f337f37497..00000000000 --- a/packages/bsky/src/api/feed-gen/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -import AppContext from '../../context' -import { FeedItem } from '../../hydration/feed' -import { SkeletonFeedPost } from '../../lexicon/types/app/bsky/feed/defs' -import { QueryParams as SkeletonParams } from '../../lexicon/types/app/bsky/feed/getFeedSkeleton' - -export type AlgoResponseItem = { - itemUri: string - postUri: string -} - -export type AlgoResponse = { - feedItems: AlgoResponseItem[] - resHeaders?: Record - cursor?: string -} - -export type AlgoHandler = ( - ctx: AppContext, - params: SkeletonParams, - viewer: string | null, -) => Promise - -export type MountedAlgos = Record - -export const toSkeletonItem = (feedItem: { - itemUri: string - postUri: string -}): SkeletonFeedPost => ({ - post: feedItem.postUri, - reason: - feedItem.itemUri === feedItem.postUri - ? undefined - : { - $type: 'app.bsky.feed.defs#skeletonReasonRepost', - repost: feedItem.itemUri, - }, -}) - -export const toFeedItem = (feedItem: AlgoResponseItem): FeedItem => ({ - post: { uri: feedItem.postUri }, - repost: - feedItem.itemUri === feedItem.postUri - ? undefined - : { uri: feedItem.itemUri }, -}) diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index 550f2d8596c..17e5ff2473f 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -1,6 +1,5 @@ import { Server } from '../lexicon' import AppContext from '../context' -import describeFeedGenerator from './app/bsky/feed/describeFeedGenerator' import getTimeline from './app/bsky/feed/getTimeline' import getActorFeeds from './app/bsky/feed/getActorFeeds' import getSuggestedFeeds from './app/bsky/feed/getSuggestedFeeds' @@ -8,7 +7,6 @@ import getAuthorFeed from './app/bsky/feed/getAuthorFeed' import getFeed from './app/bsky/feed/getFeed' import getFeedGenerator from './app/bsky/feed/getFeedGenerator' import getFeedGenerators from './app/bsky/feed/getFeedGenerators' -import getFeedSkeleton from './app/bsky/feed/getFeedSkeleton' import getLikes from './app/bsky/feed/getLikes' import getListFeed from './app/bsky/feed/getListFeed' import getPostThread from './app/bsky/feed/getPostThread' @@ -40,7 +38,6 @@ import listNotifications from './app/bsky/notification/listNotifications' import updateSeen from './app/bsky/notification/updateSeen' import registerPush from './app/bsky/notification/registerPush' import getPopularFeedGenerators from './app/bsky/unspecced/getPopularFeedGenerators' -import getTimelineSkeleton from './app/bsky/unspecced/getTimelineSkeleton' import getTaggedSuggestions from './app/bsky/unspecced/getTaggedSuggestions' import getSubjectStatus from './com/atproto/admin/getSubjectStatus' import updateSubjectStatus from './com/atproto/admin/updateSubjectStatus' @@ -57,7 +54,6 @@ export * as blobResolver from './blob-resolver' export default function (server: Server, ctx: AppContext) { // app.bsky - describeFeedGenerator(server, ctx) getTimeline(server, ctx) getActorFeeds(server, ctx) getSuggestedFeeds(server, ctx) @@ -65,7 +61,6 @@ export default function (server: Server, ctx: AppContext) { getFeed(server, ctx) getFeedGenerator(server, ctx) getFeedGenerators(server, ctx) - getFeedSkeleton(server, ctx) getLikes(server, ctx) getListFeed(server, ctx) getPostThread(server, ctx) @@ -97,7 +92,6 @@ export default function (server: Server, ctx: AppContext) { updateSeen(server, ctx) registerPush(server, ctx) getPopularFeedGenerators(server, ctx) - getTimelineSkeleton(server, ctx) getTaggedSuggestions(server, ctx) // com.atproto getSubjectStatus(server, ctx) diff --git a/packages/bsky/src/auth-verifier.ts b/packages/bsky/src/auth-verifier.ts index 17226917002..7798efa99b2 100644 --- a/packages/bsky/src/auth-verifier.ts +++ b/packages/bsky/src/auth-verifier.ts @@ -42,8 +42,6 @@ type RoleOutput = { credentials: { type: 'role' admin: boolean - moderator: boolean - triage: boolean } } @@ -58,24 +56,18 @@ type AdminServiceOutput = { export type AuthVerifierOpts = { ownDid: string adminDid: string - adminPass: string - moderatorPass: string - triagePass: string + adminPasses: string[] } export class AuthVerifier { - private _adminPass: string - private _moderatorPass: string - private _triagePass: string public ownDid: string public adminDid: string + private adminPasses: Set constructor(public dataplane: DataPlaneClient, opts: AuthVerifierOpts) { - this._adminPass = opts.adminPass - this._moderatorPass = opts.moderatorPass - this._triagePass = opts.triagePass this.ownDid = opts.ownDid this.adminDid = opts.adminDid + this.adminPasses = new Set(opts.adminPasses) } // verifiers (arrow fns to preserve scope) @@ -192,16 +184,10 @@ export class AuthVerifier { return { status: Missing, admin: false, moderator: false, triage: false } } const { username, password } = parsed - if (username === 'admin' && password === this._adminPass) { - return { status: Valid, admin: true, moderator: true, triage: true } + if (username === 'admin' && this.adminPasses.has(password)) { + return { status: Valid, admin: true } } - if (username === 'admin' && password === this._moderatorPass) { - return { status: Valid, admin: false, moderator: true, triage: true } - } - if (username === 'admin' && password === this._triagePass) { - return { status: Valid, admin: false, moderator: false, triage: true } - } - return { status: Invalid, admin: false, moderator: false, triage: false } + return { status: Invalid, admin: false } } async verifyServiceJwt( @@ -255,10 +241,10 @@ export class AuthVerifier { const viewer = creds.credentials.type === 'standard' ? creds.credentials.iss : null const canViewTakedowns = - (creds.credentials.type === 'role' && creds.credentials.triage) || + (creds.credentials.type === 'role' && creds.credentials.admin) || creds.credentials.type === 'admin_service' const canPerformTakedown = - (creds.credentials.type === 'role' && creds.credentials.moderator) || + (creds.credentials.type === 'role' && creds.credentials.admin) || creds.credentials.type === 'admin_service' return { viewer, diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 2cf9a871cc7..c48da98cd3e 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -1,21 +1,17 @@ -import assert from 'assert' +import assert from 'node:assert' +import { envList } from '@atproto/common' export interface ServerConfigValues { + // service version?: string debugMode?: boolean port?: number publicUrl?: string serverDid: string - feedGenDid?: string + // external services dataplaneUrls: string[] dataplaneHttpVersion?: '1.1' | '2' dataplaneIgnoreBadTls?: boolean - searchEndpoint?: string - didPlcUrl: string - labelsFromIssuerDids?: string[] - handleResolveNameservers?: string[] - imgUriEndpoint?: string - blobCacheLocation?: string bsyncUrl: string bsyncApiKey?: string bsyncHttpVersion?: '1.1' | '2' @@ -24,10 +20,17 @@ export interface ServerConfigValues { courierApiKey?: string courierHttpVersion?: '1.1' | '2' courierIgnoreBadTls?: boolean - adminPassword: string - moderatorPassword: string - triagePassword: string + searchUrl?: string + cdnUrl?: string + // identity + didPlcUrl: string + handleResolveNameservers?: string[] + // moderation and administration modServiceDid: string + adminPasswords: string[] + labelsFromIssuerDids?: string[] + // misc/dev + blobCacheLocation?: string } export class ServerConfig { @@ -39,16 +42,18 @@ export class ServerConfig { const debugMode = process.env.NODE_ENV !== 'production' const publicUrl = process.env.BSKY_PUBLIC_URL || undefined const serverDid = process.env.BSKY_SERVER_DID || 'did:example:test' - const feedGenDid = process.env.BSKY_FEED_GEN_DID const envPort = parseInt(process.env.BSKY_PORT || '', 10) const port = isNaN(envPort) ? 2584 : envPort const didPlcUrl = process.env.BSKY_DID_PLC_URL || 'http://localhost:2582' const handleResolveNameservers = process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS ? process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS.split(',') : [] - const imgUriEndpoint = process.env.BSKY_IMG_URI_ENDPOINT + const cdnUrl = process.env.BSKY_CDN_URL || process.env.BSKY_IMG_URI_ENDPOINT const blobCacheLocation = process.env.BSKY_BLOB_CACHE_LOC - const searchEndpoint = process.env.BSKY_SEARCH_ENDPOINT || undefined + const searchUrl = + process.env.BSKY_SEARCH_URL || + process.env.BSKY_SEARCH_ENDPOINT || + undefined let dataplaneUrls = overrides?.dataplaneUrls dataplaneUrls ??= process.env.BSKY_DATAPLANE_URLS ? process.env.BSKY_DATAPLANE_URLS.split(',') @@ -72,32 +77,27 @@ export class ServerConfig { const courierIgnoreBadTls = process.env.BSKY_COURIER_IGNORE_BAD_TLS === 'true' assert(courierHttpVersion === '1.1' || courierHttpVersion === '2') - const adminPassword = process.env.ADMIN_PASSWORD || undefined - assert(adminPassword) - const moderatorPassword = process.env.BSKY_MODERATOR_PASSWORD - assert(moderatorPassword) - const triagePassword = process.env.BSKY_TRIAGE_PASSWORD - assert(triagePassword) + const adminPasswords = envList( + process.env.BSKY_ADMIN_PASSWORDS || process.env.BSKY_ADMIN_PASSWORD || '', + ) const modServiceDid = process.env.MOD_SERVICE_DID assert(modServiceDid) assert(dataplaneUrls.length) assert(dataplaneHttpVersion === '1.1' || dataplaneHttpVersion === '2') - return new ServerConfig({ version, debugMode, port, publicUrl, serverDid, - feedGenDid, dataplaneUrls, dataplaneHttpVersion, dataplaneIgnoreBadTls, - searchEndpoint, + searchUrl, didPlcUrl, labelsFromIssuerDids, handleResolveNameservers, - imgUriEndpoint, + cdnUrl, blobCacheLocation, bsyncUrl, bsyncApiKey, @@ -107,9 +107,7 @@ export class ServerConfig { courierApiKey, courierHttpVersion, courierIgnoreBadTls, - adminPassword, - moderatorPassword, - triagePassword, + adminPasswords, modServiceDid, ...stripUndefineds(overrides ?? {}), }) @@ -148,10 +146,6 @@ export class ServerConfig { return this.cfg.serverDid } - get feedGenDid() { - return this.cfg.feedGenDid - } - get dataplaneUrls() { return this.cfg.dataplaneUrls } @@ -164,30 +158,6 @@ export class ServerConfig { return this.cfg.dataplaneIgnoreBadTls } - get searchEndpoint() { - return this.cfg.searchEndpoint - } - - get labelsFromIssuerDids() { - return this.cfg.labelsFromIssuerDids ?? [] - } - - get handleResolveNameservers() { - return this.cfg.handleResolveNameservers - } - - get didPlcUrl() { - return this.cfg.didPlcUrl - } - - get imgUriEndpoint() { - return this.cfg.imgUriEndpoint - } - - get blobCacheLocation() { - return this.cfg.blobCacheLocation - } - get bsyncUrl() { return this.cfg.bsyncUrl } @@ -220,21 +190,37 @@ export class ServerConfig { return this.cfg.courierIgnoreBadTls } - get adminPassword() { - return this.cfg.adminPassword + get searchUrl() { + return this.cfg.searchUrl + } + + get cdnUrl() { + return this.cfg.cdnUrl + } + + get didPlcUrl() { + return this.cfg.didPlcUrl } - get moderatorPassword() { - return this.cfg.moderatorPassword + get handleResolveNameservers() { + return this.cfg.handleResolveNameservers } - get triagePassword() { - return this.cfg.triagePassword + get adminPasswords() { + return this.cfg.adminPasswords } get modServiceDid() { return this.cfg.modServiceDid } + + get labelsFromIssuerDids() { + return this.cfg.labelsFromIssuerDids ?? [] + } + + get blobCacheLocation() { + return this.cfg.blobCacheLocation + } } function stripUndefineds( diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index 01670f6e19e..3b297caf095 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -4,7 +4,6 @@ import AtpAgent from '@atproto/api' import { Keypair } from '@atproto/crypto' import { createServiceJwt } from '@atproto/xrpc-server' import { ServerConfig } from './config' -import { MountedAlgos } from './api/feed-gen/types' import { DataPlaneClient } from './data-plane/client' import { Hydrator } from './hydration/hydrator' import { Views } from './views' @@ -24,7 +23,6 @@ export class AppContext { idResolver: IdResolver bsyncClient: BsyncClient courierClient: CourierClient - algos: MountedAlgos authVerifier: AuthVerifier }, ) {} @@ -81,10 +79,6 @@ export class AppContext { keypair: this.signingKey, }) } - - get algos(): MountedAlgos { - return this.opts.algos - } } export default AppContext diff --git a/packages/bsky/src/data-plane/server/routes/relationships.ts b/packages/bsky/src/data-plane/server/routes/relationships.ts index c877c1112f3..d6029e169e8 100644 --- a/packages/bsky/src/data-plane/server/routes/relationships.ts +++ b/packages/bsky/src/data-plane/server/routes/relationships.ts @@ -26,8 +26,6 @@ export default (db: Database): Partial> => ({ db.db .selectFrom('list_item') .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .innerJoin('list', 'list.uri', 'list_item.listUri') - .where('list.purpose', '=', 'app.bsky.graph.defs#modlist') .where('list_mute.mutedByDid', '=', actorDid) .whereRef('list_item.subjectDid', '=', ref('actor.did')) .select('list_item.listUri') @@ -47,8 +45,6 @@ export default (db: Database): Partial> => ({ db.db .selectFrom('list_item') .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') - .innerJoin('list', 'list.uri', 'list_item.listUri') - .where('list.purpose', '=', 'app.bsky.graph.defs#modlist') .where('list_block.creator', '=', actorDid) .whereRef('list_item.subjectDid', '=', ref('actor.did')) .select('list_item.listUri') @@ -56,8 +52,6 @@ export default (db: Database): Partial> => ({ db.db .selectFrom('list_item') .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') - .innerJoin('list', 'list.uri', 'list_item.listUri') - .where('list.purpose', '=', 'app.bsky.graph.defs#modlist') .where('list_item.subjectDid', '=', actorDid) .whereRef('list_block.creator', '=', ref('actor.did')) .select('list_item.listUri') diff --git a/packages/bsky/src/hydration/actor.ts b/packages/bsky/src/hydration/actor.ts index 1fc2eedd7fa..3ed7f1b036e 100644 --- a/packages/bsky/src/hydration/actor.ts +++ b/packages/bsky/src/hydration/actor.ts @@ -104,7 +104,10 @@ export class ActorHydrator { }, new HydrationMap()) } - async getProfileViewerStates( + // "naive" because this method does not verify the existence of the list itself + // a later check in the main hydrator will remove list uris that have been deleted or + // repurposed to "curate lists" + async getProfileViewerStatesNaive( dids: string[], viewer: string, ): Promise { diff --git a/packages/bsky/src/hydration/hydrator.ts b/packages/bsky/src/hydration/hydrator.ts index 485726325ca..e6fcc70ff4d 100644 --- a/packages/bsky/src/hydration/hydrator.ts +++ b/packages/bsky/src/hydration/hydrator.ts @@ -86,6 +86,33 @@ export class Hydrator { this.label = new LabelHydrator(dataplane, opts) } + // app.bsky.actor.defs#profileView + // - profile viewer + // - list basic + // Note: builds on the naive profile viewer hydrator and removes references to lists that have been deleted + async hydrateProfileViewers( + dids: string[], + viewer: string, + ): Promise { + const profileViewers = await this.actor.getProfileViewerStatesNaive( + dids, + viewer, + ) + const listUris: string[] = [] + profileViewers?.forEach((item) => { + listUris.push(...listUrisFromProfileViewer(item)) + }) + const listState = await this.hydrateListsBasic(listUris, viewer) + // if a list no longer exists or is not a mod list, then remove from viewer state + profileViewers?.forEach((item) => { + removeNonModListsFromProfileViewer(item, listState) + }) + return mergeStates(listState, { + profileViewers, + viewer, + }) + } + // app.bsky.actor.defs#profileView // - profile // - list basic @@ -94,20 +121,14 @@ export class Hydrator { viewer: string | null, includeTakedowns = false, ): Promise { - const [actors, labels, profileViewers] = await Promise.all([ + const [actors, labels, profileViewersState] = await Promise.all([ this.actor.getActors(dids, includeTakedowns), this.label.getLabelsForSubjects(labelSubjectsForDid(dids)), - viewer ? this.actor.getProfileViewerStates(dids, viewer) : undefined, + viewer ? this.hydrateProfileViewers(dids, viewer) : undefined, ]) - const listUris: string[] = [] - profileViewers?.forEach((item) => { - listUris.push(...listUrisFromProfileViewer(item)) - }) - const listState = await this.hydrateListsBasic(listUris, viewer) - return mergeStates(listState, { + return mergeStates(profileViewersState ?? {}, { actors, labels, - profileViewers, viewer, }) } @@ -563,9 +584,37 @@ const listUrisFromProfileViewer = (item: ProfileViewerState | null) => { if (item?.blockingByList) { listUris.push(item.blockingByList) } + // blocked-by list does not appear in views, but will be used to evaluate the existence of a block between users. + if (item?.blockedByList) { + listUris.push(item.blockedByList) + } return listUris } +const removeNonModListsFromProfileViewer = ( + item: ProfileViewerState | null, + state: HydrationState, +) => { + if (!isModList(item?.mutedByList, state)) { + delete item?.mutedByList + } + if (!isModList(item?.blockingByList, state)) { + delete item?.blockingByList + } + if (!isModList(item?.blockedByList, state)) { + delete item?.blockedByList + } +} + +const isModList = ( + listUri: string | undefined, + state: HydrationState, +): boolean => { + if (!listUri) return false + const list = state.lists?.get(listUri) + return list?.record.purpose === 'app.bsky.graph.defs#modlist' +} + const labelSubjectsForDid = (dids: string[]) => { return [ ...dids, diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 5f09ae63ce1..a968d85b584 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -15,7 +15,6 @@ import { createServer } from './lexicon' import { ImageUriBuilder } from './image/uri' import { BlobDiskCache, ImageProcessingServer } from './image/server' import AppContext from './context' -import { MountedAlgos } from './api/feed-gen/types' import { Keypair } from '@atproto/crypto' import { createDataPlaneClient } from './data-plane/client' import { Hydrator } from './hydration/hydrator' @@ -26,12 +25,10 @@ import { authWithApiKey as courierAuth, createCourierClient } from './courier' export * from './data-plane' export type { ServerConfigValues } from './config' -export type { MountedAlgos } from './api/feed-gen/types' export { ServerConfig } from './config' export { Database } from './data-plane/server/db' export { Redis } from './redis' export { AppContext } from './context' -export { makeAlgos } from './api/feed-gen' export class BskyAppView { public ctx: AppContext @@ -47,9 +44,8 @@ export class BskyAppView { static create(opts: { config: ServerConfig signingKey: Keypair - algos?: MountedAlgos }): BskyAppView { - const { config, signingKey, algos = {} } = opts + const { config, signingKey } = opts const app = express() app.use(cors()) app.use(loggerMiddleware) @@ -62,11 +58,11 @@ export class BskyAppView { }) const imgUriBuilder = new ImageUriBuilder( - config.imgUriEndpoint || `${config.publicUrl}/img`, + config.cdnUrl || `${config.publicUrl}/img`, ) let imgProcessingServer: ImageProcessingServer | undefined - if (!config.imgUriEndpoint) { + if (!config.cdnUrl) { const imgProcessingCache = new BlobDiskCache(config.blobCacheLocation) imgProcessingServer = new ImageProcessingServer( config, @@ -74,8 +70,8 @@ export class BskyAppView { ) } - const searchAgent = config.searchEndpoint - ? new AtpAgent({ service: config.searchEndpoint }) + const searchAgent = config.searchUrl + ? new AtpAgent({ service: config.searchUrl }) : undefined const dataplane = createDataPlaneClient(config.dataplaneUrls, { httpVersion: config.dataplaneHttpVersion, @@ -105,9 +101,7 @@ export class BskyAppView { const authVerifier = new AuthVerifier(dataplane, { ownDid: config.serverDid, adminDid: config.modServiceDid, - adminPass: config.adminPassword, - moderatorPass: config.moderatorPassword, - triagePass: config.triagePassword, + adminPasses: config.adminPasswords, }) const ctx = new AppContext({ @@ -121,7 +115,6 @@ export class BskyAppView { bsyncClient, courierClient, authVerifier, - algos, }) let server = createServer({ diff --git a/packages/bsky/src/views/index.ts b/packages/bsky/src/views/index.ts index eb2f9887461..35b86c94dd3 100644 --- a/packages/bsky/src/views/index.ts +++ b/packages/bsky/src/views/index.ts @@ -46,7 +46,7 @@ import { isRecordWithMedia, } from './types' import { Label } from '../hydration/label' -import { FeedItem, Repost } from '../hydration/feed' +import { FeedItem, Post, Repost } from '../hydration/feed' import { RecordInfo } from '../hydration/util' import { Notification } from '../proto/bsky_pb' @@ -494,7 +494,8 @@ export class Views { ): ThreadViewPost | NotFoundPost | BlockedPost { const { anchor, uris } = skele const post = this.post(anchor, state) - if (!post) return this.notFoundPost(anchor) + const postInfo = state.posts?.get(anchor) + if (!postInfo || !post) return this.notFoundPost(anchor) if (this.viewerBlockExists(post.author.did, state)) { return this.blockedPost(anchor, post.author.did, state) } @@ -509,22 +510,30 @@ export class Views { childrenByParentUri[parentUri] ??= [] childrenByParentUri[parentUri].push(uri) }) - const violatesThreadGate = state.posts?.get(anchor)?.violatesThreadGate + const rootUri = getRootUri(anchor, postInfo) + const violatesThreadGate = postInfo.violatesThreadGate return { $type: 'app.bsky.feed.defs#threadViewPost', post, parent: !violatesThreadGate - ? this.threadParent(anchor, state, opts.height) + ? this.threadParent(anchor, rootUri, state, opts.height) : undefined, replies: !violatesThreadGate - ? this.threadReplies(anchor, childrenByParentUri, state, opts.depth) + ? this.threadReplies( + anchor, + rootUri, + childrenByParentUri, + state, + opts.depth, + ) : undefined, } } threadParent( childUri: string, + rootUri: string, state: HydrationState, height: number, ): ThreadViewPost | NotFoundPost | BlockedPost | undefined { @@ -535,19 +544,22 @@ export class Views { return this.blockedPost(parentUri, creatorFromUri(parentUri), state) } const post = this.post(parentUri, state) - if (!post) return this.notFoundPost(parentUri) + const postInfo = state.posts?.get(parentUri) + if (!postInfo || !post) return this.notFoundPost(parentUri) + if (rootUri !== getRootUri(parentUri, postInfo)) return // outside thread boundary if (this.viewerBlockExists(post.author.did, state)) { return this.blockedPost(parentUri, post.author.did, state) } return { $type: 'app.bsky.feed.defs#threadViewPost', post, - parent: this.threadParent(parentUri, state, height - 1), + parent: this.threadParent(parentUri, rootUri, state, height - 1), } } threadReplies( parentUri: string, + rootUri: string, childrenByParentUri: Record, state: HydrationState, depth: number, @@ -555,21 +567,29 @@ export class Views { if (depth < 1) return undefined const childrenUris = childrenByParentUri[parentUri] ?? [] return mapDefined(childrenUris, (uri) => { - if (state.posts?.get(uri)?.violatesThreadGate) { + const postInfo = state.posts?.get(uri) + if (postInfo?.violatesThreadGate) { return undefined } if (state.postBlocks?.get(uri)?.reply) { return undefined } const post = this.post(uri, state) - if (!post) return this.notFoundPost(parentUri) + if (!postInfo || !post) return this.notFoundPost(parentUri) + if (rootUri !== getRootUri(uri, postInfo)) return // outside thread boundary if (this.viewerBlockExists(post.author.did, state)) { return this.blockedPost(parentUri, post.author.did, state) } return { $type: 'app.bsky.feed.defs#threadViewPost', post, - replies: this.threadReplies(uri, childrenByParentUri, state, depth - 1), + replies: this.threadReplies( + uri, + rootUri, + childrenByParentUri, + state, + depth - 1, + ), } }) } @@ -835,3 +855,7 @@ const postToGateUri = (uri: string) => { } return aturi.toString() } + +const getRootUri = (uri: string, post: Post): string => { + return post.record.reply?.root.uri ?? uri +} diff --git a/packages/bsky/tests/admin/moderation.test.ts b/packages/bsky/tests/admin/moderation.test.ts index 754d5ba0ef1..2f6caa8d17d 100644 --- a/packages/bsky/tests/admin/moderation.test.ts +++ b/packages/bsky/tests/admin/moderation.test.ts @@ -55,14 +55,14 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.bsky.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { did: repoSubject.did, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.did).toEqual(sc.dids.bob) expect(res.data.takedown?.applied).toBe(true) @@ -77,14 +77,14 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.bsky.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { did: repoSubject.did, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.did).toEqual(sc.dids.bob) expect(res.data.takedown?.applied).toBe(false) @@ -99,14 +99,14 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.bsky.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { uri: recordSubject.uri, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.uri).toEqual(recordSubject.uri) expect(res.data.takedown?.applied).toBe(true) @@ -121,48 +121,20 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.bsky.adminAuthHeaders(), }, ) const res = await agent.api.com.atproto.admin.getSubjectStatus( { uri: recordSubject.uri, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.uri).toEqual(recordSubject.uri) expect(res.data.takedown?.applied).toBe(false) expect(res.data.takedown?.ref).toBeUndefined() }) - it('does not allow non-full moderators to update subject state', async () => { - const subject = { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, - } - const attemptTakedownTriage = - agent.api.com.atproto.admin.updateSubjectStatus( - { - subject, - takedown: { applied: true }, - }, - { - encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('triage'), - }, - ) - await expect(attemptTakedownTriage).rejects.toThrow( - 'Must be a full moderator to update subject state', - ) - const res = await agent.api.com.atproto.admin.getSubjectStatus( - { - did: subject.did, - }, - { headers: network.bsky.adminAuthHeaders('moderator') }, - ) - expect(res.data.takedown?.applied).toBe(false) - }) - describe('blob takedown', () => { let blobUri: string let imageUri: string @@ -194,7 +166,7 @@ describe('moderation', () => { did: blobSubject.did, blob: blobSubject.cid, }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.bsky.adminAuthHeaders() }, ) expect(res.data.subject.did).toEqual(blobSubject.did) expect(res.data.subject.cid).toEqual(blobSubject.cid) diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index 00426075fe3..44f3760808b 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -85,12 +85,6 @@ describe('feed generation', () => { await network.close() }) - // @TODO enable once getFeed is implemented - it('describes the feed generator', async () => { - const res = await agent.api.app.bsky.feed.describeFeedGenerator() - expect(res.data.did).toBe(network.bsky.ctx.cfg.feedGenDid) - }) - it('feed gen records can be created.', async () => { const all = await pdsAgent.api.app.bsky.feed.generator.create( { repo: alice, rkey: 'all' }, diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index 292322e914c..c3496f7cf50 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -136,6 +136,56 @@ describe('pds thread views', () => { expect(forSnapshot(thread3.data.thread)).toMatchSnapshot() }) + it('omits parents and replies w/ different root than anchor post.', async () => { + const badRoot = sc.posts[alice][0] + const goodRoot = await sc.post(alice, 'good root') + const goodReply1 = await sc.reply( + alice, + goodRoot.ref, + goodRoot.ref, + 'good reply 1', + ) + const goodReply2 = await sc.reply( + alice, + goodRoot.ref, + goodReply1.ref, + 'good reply 2', + ) + const badReply = await sc.reply( + alice, + badRoot.ref, + goodReply1.ref, + 'bad reply', + ) + await network.processAll() + // good reply doesn't have replies w/ different root + const { data: goodReply1Thread } = + await agent.api.app.bsky.feed.getPostThread( + { uri: goodReply1.ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(goodReply1Thread.thread)) + assert(isThreadViewPost(goodReply1Thread.thread.parent)) + expect(goodReply1Thread.thread.parent.post.uri).toEqual(goodRoot.ref.uriStr) + expect( + goodReply1Thread.thread.replies?.map((r) => { + assert(isThreadViewPost(r)) + return r.post.uri + }), + ).toEqual([ + goodReply2.ref.uriStr, // does not contain badReply + ]) + expect(goodReply1Thread.thread.parent.replies).toBeUndefined() + // bad reply doesn't have a parent, which would have a different root + const { data: badReplyThread } = + await agent.api.app.bsky.feed.getPostThread( + { uri: badReply.ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(badReplyThread.thread)) + expect(badReplyThread.thread.parent).toBeUndefined() // is not goodReply1 + }) + it('reflects self-labels', async () => { const { data: thread } = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][0].ref.uriStr }, diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index a86b124fe9d..461ef4d07df 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -64,10 +64,7 @@ export class TestBsky { modServiceDid: cfg.modServiceDid ?? 'did:example:invalidMod', labelsFromIssuerDids: ['did:example:labeler'], // this did is also used as the labeler in seeds ...cfg, - adminPassword: ADMIN_PASSWORD, - moderatorPassword: MOD_PASSWORD, - triagePassword: TRIAGE_PASSWORD, - feedGenDid: 'did:example:feedGen', + adminPasswords: [ADMIN_PASSWORD, MOD_PASSWORD, TRIAGE_PASSWORD], }) // Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." @@ -86,7 +83,6 @@ export class TestBsky { const server = bsky.BskyAppView.create({ config, signingKey: serviceKeypair, - algos: cfg.algos, }) const sub = new bsky.RepoSubscription({ @@ -110,22 +106,17 @@ export class TestBsky { return new AtpAgent({ service: this.url }) } - adminAuth(role: 'admin' | 'moderator' | 'triage' = 'admin'): string { - const password = - role === 'triage' - ? this.ctx.cfg.triagePassword - : role === 'moderator' - ? this.ctx.cfg.moderatorPassword - : this.ctx.cfg.adminPassword + adminAuth(): string { + const [password] = this.ctx.cfg.adminPasswords return ( 'Basic ' + ui8.toString(ui8.fromString(`admin:${password}`, 'utf8'), 'base64pad') ) } - adminAuthHeaders(role?: 'admin' | 'moderator' | 'triage') { + adminAuthHeaders() { return { - authorization: this.adminAuth(role), + authorization: this.adminAuth(), } } diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index aa96412124b..dbedfaced4f 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -22,7 +22,6 @@ export type BskyConfig = Partial & { redisHost: string pdsPort: number migration?: string - algos?: bsky.MountedAlgos } export type BsyncConfig = Partial & { diff --git a/packages/ozone/tests/moderation-events.test.ts b/packages/ozone/tests/moderation-events.test.ts index 18593183cfa..670fa4beaa6 100644 --- a/packages/ozone/tests/moderation-events.test.ts +++ b/packages/ozone/tests/moderation-events.test.ts @@ -15,13 +15,13 @@ describe('moderation-events', () => { const emitModerationEvent = async (eventData) => { return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.ozone.adminAuthHeaders('moderator'), }) } const queryModerationEvents = (eventQuery) => agent.api.com.atproto.admin.queryModerationEvents(eventQuery, { - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.ozone.adminAuthHeaders('moderator'), }) const seedEvents = async () => { @@ -210,7 +210,7 @@ describe('moderation-events', () => { id: 1, }, { - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.ozone.adminAuthHeaders('moderator'), }, ) diff --git a/packages/ozone/tests/moderation-statuses.test.ts b/packages/ozone/tests/moderation-statuses.test.ts index 5f63dbfe9a4..b1a1ed95c77 100644 --- a/packages/ozone/tests/moderation-statuses.test.ts +++ b/packages/ozone/tests/moderation-statuses.test.ts @@ -18,13 +18,13 @@ describe('moderation-statuses', () => { const emitModerationEvent = async (eventData) => { return pdsAgent.api.com.atproto.admin.emitModerationEvent(eventData, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.ozone.adminAuthHeaders('moderator'), }) } const queryModerationStatuses = (statusQuery) => agent.api.com.atproto.admin.queryModerationStatuses(statusQuery, { - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.ozone.adminAuthHeaders('moderator'), }) const seedEvents = async () => { diff --git a/packages/ozone/tests/moderation.test.ts b/packages/ozone/tests/moderation.test.ts index 8433c9ec294..e20157428c4 100644 --- a/packages/ozone/tests/moderation.test.ts +++ b/packages/ozone/tests/moderation.test.ts @@ -603,7 +603,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('triage'), + headers: network.ozone.adminAuthHeaders('triage'), }, ) await expect(attemptLabel).rejects.toThrow( @@ -698,7 +698,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('moderator'), + headers: network.ozone.adminAuthHeaders('moderator'), }, ) // cleanup @@ -725,7 +725,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders('triage'), + headers: network.ozone.adminAuthHeaders('triage'), }, ) await expect(attemptTakedownTriage).rejects.toThrow( @@ -749,7 +749,7 @@ describe('moderation', () => { const { data: statusesAfterTakedown } = await agent.api.com.atproto.admin.queryModerationStatuses( { subject: sc.dids.bob }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.ozone.adminAuthHeaders('moderator') }, ) expect(statusesAfterTakedown.subjectStatuses[0]).toMatchObject({ @@ -767,11 +767,11 @@ describe('moderation', () => { const [{ data: eventList }, { data: statuses }] = await Promise.all([ agent.api.com.atproto.admin.queryModerationEvents( { subject: sc.dids.bob }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.ozone.adminAuthHeaders('moderator') }, ), agent.api.com.atproto.admin.queryModerationStatuses( { subject: sc.dids.bob }, - { headers: network.bsky.adminAuthHeaders('moderator') }, + { headers: network.ozone.adminAuthHeaders('moderator') }, ), ]) @@ -812,7 +812,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), + headers: network.ozone.adminAuthHeaders(), }, ) return result.data @@ -834,7 +834,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.bsky.adminAuthHeaders(), + headers: network.ozone.adminAuthHeaders(), }, ) } @@ -842,7 +842,7 @@ describe('moderation', () => { async function getRecordLabels(uri: string) { const result = await agent.api.com.atproto.admin.getRecord( { uri }, - { headers: network.bsky.adminAuthHeaders() }, + { headers: network.ozone.adminAuthHeaders() }, ) const labels = result.data.labels ?? [] return labels.map((l) => l.val) @@ -851,7 +851,7 @@ describe('moderation', () => { async function getRepoLabels(did: string) { const result = await agent.api.com.atproto.admin.getRepo( { did }, - { headers: network.bsky.adminAuthHeaders() }, + { headers: network.ozone.adminAuthHeaders() }, ) const labels = result.data.labels ?? [] return labels.map((l) => l.val) diff --git a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap index 01002de0be5..e4c09b14c51 100644 --- a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap @@ -2,6 +2,185 @@ exports[`feedgen proxy view performs basic proxy of getFeed 1`] = ` Object { - "feed": Array [], + "feed": Array [ + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(4)", + "following": "record(3)", + "muted": false, + }, + }, + "cid": "cids(3)", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia#view", + "media": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", + }, + Object { + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)@jpeg", + }, + ], + }, + "record": Object { + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(7)", + "following": "record(6)", + "muted": false, + }, + }, + "cid": "cids(6)", + "embeds": Array [], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(5)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 2, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "../dev-env/src/seed/img/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(4)", + }, + "size": 4114, + }, + }, + Object { + "alt": "../dev-env/src/seed/img/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(6)", + "uri": "record(5)", + }, + }, + }, + "text": "hi im carol", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(2)", + "viewer": Object { + "like": "record(8)", + }, + }, + }, + ], } `; diff --git a/packages/pds/tests/proxied/feedgen.test.ts b/packages/pds/tests/proxied/feedgen.test.ts index 39a064217b6..0e9e40f2a5c 100644 --- a/packages/pds/tests/proxied/feedgen.test.ts +++ b/packages/pds/tests/proxied/feedgen.test.ts @@ -1,70 +1,55 @@ -import AtpAgent, { AtUri, AppBskyFeedNS } from '@atproto/api' +import AtpAgent, { AtUri } from '@atproto/api' import { TestNetwork, SeedClient } from '@atproto/dev-env' import basicSeed from '../seeds/basic' import { forSnapshot } from '../_util' +import { InvalidRequestError } from '@atproto/xrpc-server' describe('feedgen proxy view', () => { let network: TestNetwork let agent: AtpAgent let sc: SeedClient - - const origGetFeedGenerator = AppBskyFeedNS.prototype.getFeedGenerator - const feedUri = AtUri.make( - 'did:example:feed-publisher', - 'app.bsky.feed.generator', - 'mutuals', - ) + let feedUri: AtUri beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'proxy_feedgen', - bsky: { - // @TODO consider using makeAlgos() here if the appview begins supporting any feeds out of the box - algos: { - [feedUri.toString()]: async () => ({ feedItems: [] }), - }, - }, }) agent = network.pds.getClient() sc = network.getSeedClient() await basicSeed(sc, { addModLabels: network.bsky }) + feedUri = AtUri.make(sc.dids.alice, 'app.bsky.feed.generator', 'mutuals') + + const feedGen = await network.createFeedGen({ + [feedUri.toString()]: ({ params }) => { + if (params.feed !== feedUri.toString()) { + throw new InvalidRequestError('Unknown feed') + } + return { + encoding: 'application/json', + body: { + feed: [ + { post: sc.posts[sc.dids.alice][0].ref.uriStr }, + { post: sc.posts[sc.dids.carol][0].ref.uriStr }, + ], + }, + } + }, + }) + // publish feed - const feed = await agent.api.app.bsky.feed.generator.create( + await agent.api.app.bsky.feed.generator.create( { repo: sc.dids.alice, rkey: feedUri.rkey }, { - did: network.bsky.ctx.cfg.feedGenDid ?? '', - displayName: 'Mutuals', + did: feedGen.did, + displayName: 'Test feed', createdAt: new Date().toISOString(), }, sc.getHeaders(sc.dids.alice), ) await network.processAll() - // mock getFeedGenerator() for use by pds's getFeed since we don't have a proper feedGenDid or feed publisher - AppBskyFeedNS.prototype.getFeedGenerator = async function (params, opts) { - if (params?.feed === feedUri.toString()) { - return { - success: true, - data: { - isOnline: true, - isValid: true, - view: { - cid: feed.cid, - uri: feed.uri, - did: network.bsky.ctx.cfg.feedGenDid ?? '', - creator: { did: sc.dids.alice, handle: 'alice.test' }, - displayName: 'Mutuals', - indexedAt: new Date().toISOString(), - }, - }, - headers: {}, - } - } - return origGetFeedGenerator.call(this, params, opts) - } }) afterAll(async () => { - AppBskyFeedNS.prototype.getFeedGenerator = origGetFeedGenerator await network.close() })