import { dedupeStrs, mapDefined, noUndefinedVals } from '@atproto/common' import { Client, DidString } from '@atproto/lex' import { MethodNotImplementedError, Server } from '@atproto/xrpc-server' import { AppContext } from '../../../../context.js' import { HydrateCtx, Hydrator, mergeManyStates, } 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' export default function (server: Server, ctx: AppContext) { const getSuggestedUsersForSeeMore = createPipeline( skeleton, hydration, noBlocksOrFollows, presentation, ) server.add(app.bsky.unspecced.getSuggestedUsersForSeeMore, { auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params, req }) => { const viewer = auth.credentials.iss const labelers = ctx.reqLabelers(req) const hydrateCtx = await ctx.hydrator.createContext({ labelers, viewer, features: ctx.featureGatesClient.scope( ctx.featureGatesClient.parseUserContextFromHandler({ viewer, req, }), ), }) const headers = noUndefinedVals({ 'accept-language': req.headers['accept-language'], }) const result = await getSuggestedUsersForSeeMore( { ...params, hydrateCtx, headers, }, ctx, ) return { encoding: 'application/json', body: result, } }, }) } const skeletonFromGetSuggestedUsersSkeleton = async ( input: SkeletonFnInput, ): Promise => { const { params, ctx } = input if (!ctx.suggestionsClient) { throw new MethodNotImplementedError('Suggestions agent not available') } return ctx.suggestionsClient.call( app.bsky.unspecced.getSuggestedUsersSkeleton, { limit: params.limit, category: params.category, viewer: params.hydrateCtx.viewer ?? undefined, }, { headers: params.headers, }, ) } // TODO: rename to `skeleton` once we can fully migrate to the new endpoint const skeletonFromGetSuggestedUsersForSeeMoreSkeleton = async ( input: SkeletonFnInput, ): Promise => { const { params, ctx } = input if (!ctx.suggestionsClient) { throw new MethodNotImplementedError('Suggestions agent not available') } return ctx.suggestionsClient.call( app.bsky.unspecced.getSuggestedUsersForSeeMoreSkeleton, { limit: params.limit, category: params.category, viewer: params.hydrateCtx.viewer ?? undefined, }, { headers: params.headers, }, ) } const skeleton = async (input: SkeletonFnInput) => { const useSeeMore = input.params.hydrateCtx.features.checkGate( input.params.hydrateCtx.features.Gate.SuggestedUsersForSeeMoreEnable, ) const skeletonFn = useSeeMore ? skeletonFromGetSuggestedUsersForSeeMoreSkeleton : skeletonFromGetSuggestedUsersSkeleton return skeletonFn(input) } const hydration = async ( input: HydrationFnInput, ) => { const { ctx, params, skeleton } = input const dids = dedupeStrs(skeleton.dids) const pairs: Map = new Map() const viewer = params.hydrateCtx.viewer if (viewer) { pairs.set(viewer, dids) } const [profilesState, bidirectionalBlocks] = await Promise.all([ ctx.hydrator.hydrateProfiles(dids, params.hydrateCtx), ctx.hydrator.hydrateBidirectionalBlocks(pairs, params.hydrateCtx), ]) return mergeManyStates(profilesState, { bidirectionalBlocks }) } const noBlocksOrFollows = ( input: RulesFnInput, ) => { const { ctx, skeleton, params, hydration } = input const viewer = params.hydrateCtx.viewer if (!viewer) { return skeleton } const blocks = hydration.bidirectionalBlocks?.get(viewer) return { ...skeleton, dids: skeleton.dids.filter((did) => { const viewer = ctx.views.profileViewer(did, hydration) return !blocks?.get(did) && !viewer?.following }), } } const presentation = ( input: PresentationFnInput, ) => { const { ctx, skeleton, hydration } = input return { recIdStr: skeleton.recIdStr, actors: mapDefined(skeleton.dids, (did) => ctx.views.profile(did, hydration), ), } } type Context = { hydrator: Hydrator views: Views suggestionsClient: Client | undefined } type Params = app.bsky.unspecced.getSuggestedUsersForSeeMore.$Params & { hydrateCtx: HydrateCtx & { viewer: string | null } headers: Record category?: string } type SkeletonState = { dids: DidString[] recIdStr?: string }