From ab959ca3e47e8ac1a4afc012e68358f0715a03d9 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Thu, 8 Jan 2026 11:05:12 -0500 Subject: [PATCH] getActorLikes --- api/index.ts | 2 + api/so/sprk/feed/getActorLikes.ts | 123 ++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 api/so/sprk/feed/getActorLikes.ts diff --git a/api/index.ts b/api/index.ts index 6e8a8fe..2b234ad 100644 --- a/api/index.ts +++ b/api/index.ts @@ -7,6 +7,7 @@ import getPosts from "./so/sprk/feed/getPosts.ts"; import getProfile from "./so/sprk/actor/getProfile.ts"; import getAuthorFeed from "./so/sprk/feed/getAuthorFeed.ts"; import getPostThread from "./so/sprk/feed/getPostThread.ts"; +import getActorLikes from "./so/sprk/feed/getActorLikes.ts"; import getAudios from "./so/sprk/sound/getAudios.ts"; import getAudioPosts from "./so/sprk/sound/getAudioPosts.ts"; import getFollows from "./so/sprk/graph/getFollows.ts"; @@ -39,6 +40,7 @@ export default function (server: Server, ctx: AppContext) { getProfiles(server, ctx); getAuthorFeed(server, ctx); getPostThread(server, ctx); + getActorLikes(server, ctx); getAudios(server, ctx); getAudioPosts(server, ctx); getFollows(server, ctx); diff --git a/api/so/sprk/feed/getActorLikes.ts b/api/so/sprk/feed/getActorLikes.ts new file mode 100644 index 0000000..1239b0f --- /dev/null +++ b/api/so/sprk/feed/getActorLikes.ts @@ -0,0 +1,123 @@ +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 getActorLikes = createPipeline( + skeleton, + hydration, + noPostBlocks, + presentation, + ); + server.so.sprk.feed.getActorLikes({ + 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 getActorLikes({ ...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 likesRes = await ctx.dataplane.likes.getActor(actorDid, limit, cursor); + + const items = likesRes.likes.map((l) => ({ post: { uri: l.uri } })); + + return { + items, + cursor: parseString(likesRes.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; +}; -- 2.54.0