diff --git a/api/so/sprk/feed/getActorReposts.ts b/api/so/sprk/feed/getActorReposts.ts new file mode 100644 index 0000000..8986fcb --- /dev/null +++ b/api/so/sprk/feed/getActorReposts.ts @@ -0,0 +1,127 @@ +import { mapDefined } from "@atp/common"; +import { InvalidRequestError } from "@atp/xrpc-server"; +import { AppContext } from "../../../../context.ts"; +import { DataPlane } from "../../../../data-plane/index.ts"; +import { FeedItem } from "../../../../hydration/feed.ts"; +import { + HydrateCtx, + HydrationState, + Hydrator, +} from "../../../../hydration/index.ts"; +import { parseString } from "../../../../hydration/util.ts"; +import { Server } from "../../../../lex/index.ts"; +import { QueryParams } from "../../../../lex/types/so/sprk/feed/getActorLikes.ts"; +import { createPipeline } from "../../../../pipeline.ts"; +import { uriToDid as creatorFromUri } from "../../../../utils/uris.ts"; +import { Views } from "../../../../views/index.ts"; +import { clearlyBadCursor, resHeaders } from "../../../util.ts"; + +export default function (server: Server, ctx: AppContext) { + const getActorReposts = createPipeline( + skeleton, + hydration, + noPostBlocks, + presentation, + ); + server.so.sprk.feed.getActorReposts({ + auth: ctx.authVerifier.standardOptional, + 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 getActorReposts({ ...params, hydrateCtx }, ctx); + + const repoRev = await ctx.hydrator.actor.getRepoRevSafe(viewer); + + return { + encoding: "application/json", + body: result, + headers: resHeaders({ + repoRev, + labelers: hydrateCtx.labelers, + }), + }; + }, + }); +} + +const skeleton = async (inputs: { + ctx: Context; + params: Params; +}): Promise => { + const { ctx, params } = inputs; + const { actor, limit, cursor } = params; + const viewer = params.hydrateCtx.viewer; + if (clearlyBadCursor(cursor)) { + return { items: [] }; + } + const [actorDid] = await ctx.hydrator.actor.getDids([actor]); + if (!actorDid || !viewer || viewer !== actorDid) { + throw new InvalidRequestError("Profile not found"); + } + + const repostsRes = await ctx.dataplane.reposts.getActor( + actorDid, + limit, + cursor, + ); + + const items = repostsRes.reposts.map((r) => ({ post: { uri: r.uri } })); + + return { + items, + cursor: parseString(repostsRes.cursor), + }; +}; + +const hydration = async (inputs: { + ctx: Context; + params: Params; + skeleton: Skeleton; +}) => { + const { ctx, params, skeleton } = inputs; + return await ctx.hydrator.hydrateFeedItems(skeleton.items, params.hydrateCtx); +}; + +const noPostBlocks = (inputs: { + ctx: Context; + skeleton: Skeleton; + hydration: HydrationState; +}) => { + const { ctx, skeleton, hydration } = inputs; + skeleton.items = skeleton.items.filter((item) => { + const creator = creatorFromUri(item.post.uri); + return !ctx.views.viewerBlockExists(creator, hydration); + }); + return skeleton; +}; + +const presentation = (inputs: { + ctx: Context; + skeleton: Skeleton; + hydration: HydrationState; +}) => { + const { ctx, skeleton, hydration } = inputs; + const feed = mapDefined( + skeleton.items, + (item) => ctx.views.feedViewPost(item, hydration), + ); + return { + feed, + cursor: skeleton.cursor, + }; +}; + +type Context = { + hydrator: Hydrator; + views: Views; + dataplane: DataPlane; +}; + +type Params = QueryParams & { hydrateCtx: HydrateCtx }; + +type Skeleton = { + items: FeedItem[]; + cursor?: string; +}; diff --git a/data-plane/routes/reposts.ts b/data-plane/routes/reposts.ts index 6dceb14..c2877b8 100644 --- a/data-plane/routes/reposts.ts +++ b/data-plane/routes/reposts.ts @@ -95,7 +95,10 @@ export class Reposts { } return { - uris: reposts.map((r) => r.uri), + reposts: reposts.map((r) => ({ + uri: r.uri, + subject: r.subject, + })), cursor: nextCursor, }; }