import { mapDefined } from '@atproto/common' import { DidString } from '@atproto/lex' import { InvalidRequestError, Server } from '@atproto/xrpc-server' import { AppContext } from '../../../../context.js' import { HydrateCtxWithViewer, Hydrator, } 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 getKnownFollowers = createPipeline( skeleton, hydration, noBlocks, presentation, ) server.add(app.bsky.graph.getKnownFollowers, { auth: ctx.authVerifier.standard, handler: async ({ params, auth, req }) => { const viewer = auth.credentials.iss const labelers = ctx.reqLabelers(req) const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer, }) const result = await getKnownFollowers({ ...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, knownFollowers: [], cursor: undefined } } const res = await ctx.hydrator.dataplane.getFollowsFollowing({ actorDid: params.hydrateCtx.viewer, targetDids: [subjectDid], }) const result = res.results.at(0) const knownFollowers = result ? (result.dids.slice(0, params.limit) as DidString[]) : [] return { subjectDid, knownFollowers, cursor: undefined, } } const hydration = async ( input: HydrationFnInput, ) => { const { ctx, params, skeleton } = input const { knownFollowers } = skeleton const profilesState = await ctx.hydrator.hydrateProfiles( knownFollowers.concat(skeleton.subjectDid), params.hydrateCtx, ) return profilesState } const noBlocks = (input: RulesFnInput) => { const { skeleton, hydration, ctx } = input skeleton.knownFollowers = skeleton.knownFollowers.filter((did) => { return !ctx.views.viewerBlockExists(did, hydration) }) return skeleton } const presentation = ( input: PresentationFnInput, ) => { const { ctx, hydration, skeleton } = input const { knownFollowers } = skeleton const followers = mapDefined(knownFollowers, (did) => { return ctx.views.profile(did, hydration) }) const subject = ctx.views.profile(skeleton.subjectDid, hydration)! return { subject, followers, cursor: undefined } } type Context = { hydrator: Hydrator views: Views } type Params = app.bsky.graph.getKnownFollowers.$Params & { hydrateCtx: HydrateCtxWithViewer } type SkeletonState = { subjectDid: DidString knownFollowers: DidString[] cursor?: string }