diff --git a/api/so/sprk/feed/getCrosspostThread.ts b/api/so/sprk/feed/getCrosspostThread.ts index 20c7285..d2105a1 100644 --- a/api/so/sprk/feed/getCrosspostThread.ts +++ b/api/so/sprk/feed/getCrosspostThread.ts @@ -88,12 +88,10 @@ const skeleton = async ( bigThreadDepth: ctx.cfg.bigThreadDepth, }), params.sort, + { includeTakedowns: !!params.hydrateCtx.includeTakedowns }, ); - const visibleItems = params.hydrateCtx.includeTakedowns - ? result.items - : await filterTakenDownItems(ctx.dataplane, result.items); - const anchorFound = visibleItems.some((item) => item.uri === anchor); - const page = paginateThreadItems(visibleItems, limit, params.cursor); + const anchorFound = result.items.some((item) => item.uri === anchor); + const page = paginateThreadItems(result.items, limit, params.cursor); return { anchor, anchorFound, @@ -253,23 +251,6 @@ const paginateThreadItems = ( return { items: items.slice(start, end), cursor: nextCursor }; }; -const filterTakenDownItems = async ( - dataplane: DataPlane, - items: CrosspostThreadItem[], -): Promise => { - if (items.length === 0) { - return items; - } - const uris = Array.from(new Set(items.map((item) => item.uri))); - const records = await dataplane.records.getRecords(uris); - const takenDownUris = new Set( - records.records - .filter((record) => record.takenDown) - .map((record) => record.uri), - ); - return items.filter((item) => !takenDownUris.has(item.uri)); -}; - type RelationshipPair = [didA: string, didB: string]; type PostBlockPairs = { parent?: RelationshipPair; diff --git a/data-plane/routes/crosspost-threads.ts b/data-plane/routes/crosspost-threads.ts index 66e5acf..6f60d89 100644 --- a/data-plane/routes/crosspost-threads.ts +++ b/data-plane/routes/crosspost-threads.ts @@ -5,6 +5,7 @@ import { } from "../db/models.ts"; import { Database } from "../db/index.ts"; import { Code, DataPlaneError } from "../util.ts"; +import { getRecords } from "./records.ts"; type NodeKind = "post" | "reply" | "crosspostReply"; @@ -25,6 +26,9 @@ type CrosspostReplyThreadNode = { type ThreadNode = PostThreadNode | ReplyThreadNode | CrosspostReplyThreadNode; type ThreadSort = "oldest" | "newest" | "top"; +type CrosspostThreadOptions = { + includeTakedowns?: boolean; +}; export type CrosspostThreadItem = { uri: string; @@ -136,6 +140,51 @@ function toThreadItem(node: ThreadNode, depth: number): CrosspostThreadItem { }; } +const applyCanonicalRecords = async ( + db: Database, + items: CrosspostThreadItem[], + includeTakedowns = false, +): Promise => { + if (items.length === 0) { + return items; + } + + const uris = Array.from(new Set(items.map((item) => item.uri))); + const { records } = await getRecords(db, uris); + const canonicalRecords = new Map>(); + const takenDownUris = new Set(); + + for (const record of records) { + const canonicalRecord = parseCanonicalRecord(record.record); + if (canonicalRecord) { + canonicalRecords.set(record.uri, canonicalRecord); + } + if (record.takenDown) { + takenDownUris.add(record.uri); + } + } + + const canonicalItems = items.map((item) => { + const canonicalRecord = canonicalRecords.get(item.uri); + return canonicalRecord ? { ...item, record: canonicalRecord } : item; + }); + + return includeTakedowns + ? canonicalItems + : canonicalItems.filter((item) => !takenDownUris.has(item.uri)); +}; + +const parseCanonicalRecord = ( + record: string, +): Record | undefined => { + const parsed = JSON.parse(record) as unknown; + return isObject(parsed) ? parsed : undefined; +}; + +const isObject = (value: unknown): value is Record => { + return !!value && typeof value === "object"; +}; + const parentUriFromNode = (node: ThreadNode): string | undefined => { if (node.kind === "post") return undefined; return node.doc.reply?.parent?.uri; @@ -179,6 +228,7 @@ export class CrosspostThread { parentHeight = 80, depth = 6, sort: string = "oldest", + options: CrosspostThreadOptions = {}, ): Promise<{ items: CrosspostThreadItem[] }> { validateThreadParams(parentHeight, depth); @@ -264,7 +314,13 @@ export class CrosspostThread { } } - return { items }; + return { + items: await applyCanonicalRecords( + this.db, + items, + !!options.includeTakedowns, + ), + }; } catch (error) { if (error instanceof DataPlaneError) { throw error; diff --git a/tests/crosspost_thread_test.ts b/tests/crosspost_thread_test.ts index cf49788..7574f9c 100644 --- a/tests/crosspost_thread_test.ts +++ b/tests/crosspost_thread_test.ts @@ -54,7 +54,7 @@ Deno.test({ uri: parentUri, cid: parentCid, authorDid: TEST_USERS[0].did, - caption: { text: "root" }, + caption: { text: "root from model" }, media: { $type: "so.sprk.media.images", images: [], @@ -66,6 +66,25 @@ Deno.test({ repostCount: 0, }); + await ctx.db.models.Record.create({ + uri: parentUri, + cid: parentCid, + did: TEST_USERS[0].did, + collectionName: "so.sprk.feed.post", + rkey: "post1", + createdAt: time0, + indexedAt: time0, + json: JSON.stringify({ + $type: "so.sprk.feed.post", + caption: { text: "root from canonical record" }, + media: { + $type: "so.sprk.media.images", + images: [], + }, + createdAt: time0, + }), + }); + await ctx.db.models.CrosspostReply.create([ { uri: reply1Uri, @@ -177,6 +196,12 @@ Deno.test({ assertEquals(body.thread.length, 5); assertEquals(body.thread[0].uri, parentUri); assertEquals(body.thread[0].depth, 0); + assertEquals( + (body.thread[0].value as { + post: { record: { caption?: { text?: string } } }; + }).post.record.caption?.text, + "root from canonical record", + ); assertEquals(body.thread[1].uri, reply1Uri); assertEquals(body.thread[1].depth, 1); assertEquals(body.thread[2].uri, reply3Uri);