import { InvalidRequestError, Server } from "@atp/xrpc-server"; import { AppContext } from "../../../../context.ts"; import { HydrateCtx, Hydrator, mergeStates, } from "../../../../hydration/index.ts"; import { $Params } from "../../../../lex/so/sprk/graph/getFollowers.ts"; import * as so from "../../../../lex/so.ts"; import { createPipeline, filterSkeletonList, HydrationFnInput, mapSkeletonList, PresentationFnInput, RulesFnInput, SkeletonFnInput, } from "../../../../pipeline.ts"; import { uriToDid as didFromUri } from "../../../../utils/uris.ts"; import { Views } from "../../../../views/index.ts"; import { clearlyBadCursor, createHydrateCtxFromAuth, resHeaders, } from "../../../util.ts"; export default function (server: Server, ctx: AppContext) { const getFollowers = createPipeline({ skeleton, hydration, rules: noBlocks, presentation, }); server.add(so.sprk.graph.getFollowers, { auth: ctx.authVerifier.optionalStandardOrRole, handler: async ({ params, auth, req }) => { const hydrateCtx = await createHydrateCtxFromAuth(ctx, req, auth); const result = await getFollowers({ ...params, hydrateCtx }, ctx); return { encoding: "application/json", body: result, headers: resHeaders({}), }; }, }); } const skeleton = async (input: SkeletonFnInput) => { 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 { followers, cursor } = await ctx.hydrator.graph.getActorFollowers({ did: subjectDid, cursor: params.cursor, limit: params.limit, }); return { subjectDid, followUris: followers.map((f) => f.uri), cursor: cursor || undefined, }; }; const hydration = async ( input: HydrationFnInput, ) => { const { ctx, params, skeleton } = input; const { followUris, subjectDid } = skeleton; // DIDs can be derived from URIs directly, enabling parallel fetches const dids = [subjectDid, ...followUris.map(didFromUri)]; const [followState, profileState] = await Promise.all([ ctx.hydrator.hydrateFollows(followUris, params.hydrateCtx), 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; return filterSkeletonList(skeleton, "followUris", (followUri) => { const followerDid = didFromUri(followUri); return ( !hydration.followBlocks?.get(followUri) && (!viewer || !ctx.views.viewerBlockExists(followerDid, hydration)) ); }); }; const presentation = ( input: PresentationFnInput, ) => { const { ctx, hydration, skeleton, params } = input; const { subjectDid, cursor } = skeleton; const isNoHosted = (did: string) => 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 followers = mapSkeletonList(skeleton, "followUris", (followUri) => { const followerDid = didFromUri(followUri); if (!params.hydrateCtx.includeTakedowns && isNoHosted(followerDid)) { return; } return ctx.views.profile(didFromUri(followUri), hydration); }); return { followers, subject, cursor }; }; type Context = { hydrator: Hydrator; views: Views; }; type Params = $Params & { hydrateCtx: HydrateCtx; }; type SkeletonState = { subjectDid: string; followUris: string[]; cursor?: string; };