import { mapDefined } from '@atproto/common' import { AtUriString, DidString } from '@atproto/lex' import { InvalidRequestError, Server } from '@atproto/xrpc-server' import { AppContext } from '../../../../context.js' import { HydrateCtx, Hydrator, mergeStates, } from '../../../../hydration/hydrator.js' import { app } from '../../../../lexicons/index.js' import { HydrationFnInput, PresentationFnInput, RulesFnInput, SkeletonFnInput, createPipeline, } from '../../../../pipeline.js' import { Views } from '../../../../views/index.js' import { clearlyBadCursor, resHeaders } from '../../../util.js' export default function (server: Server, ctx: AppContext) { const getFollows = createPipeline(skeleton, hydration, noBlocks, presentation) server.add(app.bsky.graph.getFollows, { auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth, req }) => { const { viewer, includeTakedowns, skipViewerBlocks } = ctx.authVerifier.parseCreds(auth) const labelers = ctx.reqLabelers(req) const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer, includeTakedowns, skipViewerBlocks, }) // @TODO ensure canViewTakedowns gets threaded through and applied properly const result = await getFollows({ ...params, hydrateCtx }, ctx) return { encoding: 'application/json', body: result, headers: resHeaders({ labelers: hydrateCtx.labelers }), } }, }) } const skeleton = async ( input: SkeletonFnInput, ): Promise => { const { params, ctx } = input const [subjectDid] = await ctx.hydrator.actor.getDidsDefined([params.actor]) if (!subjectDid) { throw new InvalidRequestError(`Actor not found: ${params.actor}`) } if (clearlyBadCursor(params.cursor)) { return { subjectDid, followUris: [] } } const { follows, cursor } = await ctx.hydrator.graph.getActorFollows({ did: subjectDid, cursor: params.cursor, limit: params.limit, }) return { subjectDid, followUris: follows.map((f) => f.uri as AtUriString), cursor: cursor || undefined, } } const hydration = async ( input: HydrationFnInput, ) => { const { ctx, params, skeleton } = input const { followUris, subjectDid } = skeleton const followState = await ctx.hydrator.hydrateFollows( followUris, params.hydrateCtx, ) const dids = [subjectDid] if (followState.follows) { for (const follow of followState.follows.values()) { if (follow) { dids.push(follow.record.subject) } } } const profileState = await ctx.hydrator.hydrateProfiles( dids, params.hydrateCtx, ) return mergeStates(followState, profileState) } const noBlocks = (input: RulesFnInput) => { const { skeleton, params, hydration, ctx } = input const viewer = params.hydrateCtx.viewer skeleton.followUris = skeleton.followUris.filter((followUri) => { const follow = hydration.follows?.get(followUri) if (!follow) return false return ( !hydration.followBlocks?.get(followUri) && (!viewer || !ctx.views.viewerBlockExists(follow.record.subject, hydration)) ) }) return skeleton } const presentation = ( input: PresentationFnInput, ) => { const { ctx, hydration, skeleton, params } = input const { subjectDid, followUris, cursor } = skeleton const isNoHosted = (did: DidString) => ctx.views.actorIsNoHosted(did, hydration) const subject = ctx.views.profile(subjectDid, hydration) if ( !subject || (!params.hydrateCtx.includeTakedowns && isNoHosted(subjectDid)) ) { throw new InvalidRequestError(`Actor not found: ${params.actor}`) } const follows = mapDefined(followUris, (followUri) => { const followDid = hydration.follows?.get(followUri)?.record.subject if (!followDid) return if (!params.hydrateCtx.includeTakedowns && isNoHosted(followDid)) { return } return ctx.views.profile(followDid, hydration) }) return { follows, subject, cursor } } type Context = { hydrator: Hydrator views: Views } type Params = app.bsky.graph.getFollowers.$Params & { hydrateCtx: HydrateCtx } type SkeletonState = { subjectDid: DidString followUris: AtUriString[] cursor?: string }