[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: getKnownFollowers

+137
+2
api/index.ts
··· 15 15 import getAudioPosts from "./so/sprk/sound/getAudioPosts.ts"; 16 16 import getFollows from "./so/sprk/graph/getFollows.ts"; 17 17 import getFollowers from "./so/sprk/graph/getFollowers.ts"; 18 + import getKnownFollowers from "./so/sprk/graph/getKnownFollowers.ts"; 18 19 import getBlocks from "./so/sprk/graph/getBlocks.ts"; 19 20 import putPreferences from "./so/sprk/actor/putPreferences.ts"; 20 21 import getPreferences from "./so/sprk/actor/getPreferences.ts"; ··· 56 57 getAudioPosts(server, ctx); 57 58 getFollows(server, ctx); 58 59 getFollowers(server, ctx); 60 + getKnownFollowers(server, ctx); 59 61 getBlocks(server, ctx); 60 62 putPreferences(server, ctx); 61 63 getPreferences(server, ctx);
+135
api/so/sprk/graph/getKnownFollowers.ts
··· 1 + import { InvalidRequestError, Server } from "@atp/xrpc-server"; 2 + 3 + import { AppContext } from "../../../../context.ts"; 4 + import { HydrateCtx, Hydrator } from "../../../../hydration/index.ts"; 5 + import { $Params } from "../../../../lex/so/sprk/graph/getKnownFollowers.ts"; 6 + import * as so from "../../../../lex/so.ts"; 7 + import { 8 + createPipeline, 9 + filterSkeletonList, 10 + HydrationFnInput, 11 + mapSkeletonList, 12 + PresentationFnInput, 13 + RulesFnInput, 14 + SkeletonFnInput, 15 + } from "../../../../pipeline.ts"; 16 + import { Views } from "../../../../views/index.ts"; 17 + import { 18 + clearlyBadCursor, 19 + createHydrateCtxFromAuth, 20 + resHeaders, 21 + } from "../../../util.ts"; 22 + 23 + export default function (server: Server, ctx: AppContext) { 24 + const getKnownFollowers = createPipeline({ 25 + skeleton, 26 + hydration, 27 + rules: noBlocks, 28 + presentation, 29 + }); 30 + server.add(so.sprk.graph.getKnownFollowers, { 31 + auth: ctx.authVerifier.standard, 32 + handler: async ({ params, auth, req }) => { 33 + const viewer = auth.credentials.iss; 34 + const hydrateCtx = await createHydrateCtxFromAuth( 35 + ctx, 36 + req, 37 + auth, 38 + { viewer }, 39 + ); 40 + 41 + const result = await getKnownFollowers( 42 + { ...params, hydrateCtx: hydrateCtx.copy({ viewer }) }, 43 + ctx, 44 + ); 45 + 46 + return { 47 + encoding: "application/json", 48 + body: result, 49 + headers: resHeaders({ labelers: hydrateCtx.labelers }), 50 + }; 51 + }, 52 + }); 53 + } 54 + 55 + const skeleton = async ( 56 + input: SkeletonFnInput<Context, Params>, 57 + ): Promise<SkeletonState> => { 58 + const { params, ctx } = input; 59 + const [subjectDid] = await ctx.hydrator.actor.getDidsDefined([params.actor]); 60 + if (!subjectDid) { 61 + throw new InvalidRequestError(`Actor not found: ${params.actor}`); 62 + } 63 + if (clearlyBadCursor(params.cursor)) { 64 + return { subjectDid, knownFollowers: [], cursor: undefined }; 65 + } 66 + 67 + const res = await ctx.hydrator.dataplane.follows.getFollowsFollowing( 68 + params.hydrateCtx.viewer, 69 + [subjectDid], 70 + ); 71 + const result = res.results.at(0); 72 + const knownFollowers = result ? result.dids.slice(0, params.limit ?? 50) : []; 73 + 74 + return { 75 + subjectDid, 76 + knownFollowers, 77 + cursor: undefined, 78 + }; 79 + }; 80 + 81 + const hydration = ( 82 + input: HydrationFnInput<Context, Params, SkeletonState>, 83 + ) => { 84 + const { ctx, params, skeleton } = input; 85 + return ctx.hydrator.hydrateProfiles( 86 + [skeleton.subjectDid, ...skeleton.knownFollowers], 87 + params.hydrateCtx, 88 + ); 89 + }; 90 + 91 + const noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => { 92 + const { ctx, hydration, skeleton } = input; 93 + return filterSkeletonList(skeleton, "knownFollowers", (did) => { 94 + return !ctx.views.viewerBlockExists(did, hydration); 95 + }); 96 + }; 97 + 98 + const presentation = ( 99 + input: PresentationFnInput<Context, Params, SkeletonState>, 100 + ) => { 101 + const { ctx, hydration, params, skeleton } = input; 102 + const isNoHosted = (did: string) => ctx.views.actorIsNoHosted(did, hydration); 103 + 104 + const subject = ctx.views.profile(skeleton.subjectDid, hydration); 105 + if ( 106 + !subject || 107 + (!params.hydrateCtx.includeTakedowns && isNoHosted(skeleton.subjectDid)) 108 + ) { 109 + throw new InvalidRequestError(`Actor not found: ${params.actor}`); 110 + } 111 + 112 + const followers = mapSkeletonList(skeleton, "knownFollowers", (did) => { 113 + if (!params.hydrateCtx.includeTakedowns && isNoHosted(did)) { 114 + return; 115 + } 116 + return ctx.views.profile(did, hydration); 117 + }); 118 + 119 + return { subject, followers, cursor: undefined }; 120 + }; 121 + 122 + type Context = { 123 + hydrator: Hydrator; 124 + views: Views; 125 + }; 126 + 127 + type Params = $Params & { 128 + hydrateCtx: HydrateCtx & { viewer: string }; 129 + }; 130 + 131 + type SkeletonState = { 132 + subjectDid: string; 133 + knownFollowers: string[]; 134 + cursor?: string; 135 + };