From 882946fabe47ac01dfec5eb20a6f4b6b7b75a2a2 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Thu, 23 Oct 2025 22:26:10 +0100 Subject: [PATCH 01/10] Receive new lexicon types --- .../app/api/receive_hook/handlers.ts | 238 +++++++++++++----- packages/frontpage/lib/data/atproto/event.ts | 5 + 2 files changed, 174 insertions(+), 69 deletions(-) diff --git a/packages/frontpage/app/api/receive_hook/handlers.ts b/packages/frontpage/app/api/receive_hook/handlers.ts index f9639bf1..4351c299 100644 --- a/packages/frontpage/app/api/receive_hook/handlers.ts +++ b/packages/frontpage/app/api/receive_hook/handlers.ts @@ -10,6 +10,10 @@ import { getBlueskyProfile } from "@/lib/data/user"; import { sendDiscordMessage } from "@/lib/discord"; import { invariant } from "@/lib/utils"; import { AtUri } from "@atproto/syntax"; +import { + FyiFrontpageFeedPost, + FyiFrontpageRichtextBlock, +} from "@repo/frontpage-atproto-client"; import type z from "zod"; type HandlerInput = { @@ -31,34 +35,61 @@ async function getAtprotoClientFromRepo(repo: DID) { return getAtprotoClient(pds); } -export async function handlePost({ op, repo, rkey }: HandlerInput) { +async function hydratePost( + repo: DID, + collection: string, + rkey: string, +): Promise<{ + title: string; + url: string; + createdAt: Date; + cid: string; +}> { const atproto = await getAtprotoClientFromRepo(repo); + if (collection === nsids.FyiUnravelFrontpagePost) { + const record = await atproto.fyi.unravel.frontpage.post.get({ repo, rkey }); + return { + title: record.value.title, + url: record.value.url, + createdAt: new Date(record.value.createdAt), + cid: record.cid, + }; + } else if (collection === nsids.FyiFrontpageFeedPost) { + const record = await atproto.fyi.frontpage.feed.post.get({ repo, rkey }); + const subject = record.value.subject; + invariant( + FyiFrontpageFeedPost.isUrlSubject(subject), + `Received non-url subject in frontpage feed post: at://${repo}/${collection}/${rkey}`, + ); + return { + title: record.value.title, + url: subject.url, + createdAt: new Date(record.value.createdAt), + cid: record.cid, + }; + } else { + throw new Error("Unknown collection for post hydration: ${collection}"); + } +} +export async function handlePost({ op, repo, rkey }: HandlerInput) { if (op.action === "create") { - const postRecord = await atproto.fyi.unravel.frontpage.post.get({ - repo, - rkey, - }); - - invariant(postRecord, "atproto post record not found"); - - const post = await dbPost.uncached_doesPostExist(repo, rkey); - const { title, url, createdAt } = postRecord.value; + const post = await hydratePost(repo, op.path.collection, rkey); - if (post) { + if (await dbPost.uncached_doesPostExist(repo, rkey)) { await dbPost.updatePost(repo, rkey, { status: "live", - cid: postRecord.cid, + cid: post.cid, }); } else { await dbPost.createPost({ post: { - title, - url, - createdAt: new Date(createdAt), + title: post.title, + url: post.url, + createdAt: post.createdAt, }, rkey, - cid: postRecord.cid, + cid: post.cid, authorDid: repo, status: "live", }); @@ -69,7 +100,7 @@ export async function handlePost({ op, repo, rkey }: HandlerInput) { embeds: [ { title: "New post on Frontpage", - description: title, + description: post.title, url: `https://frontpage.fyi/post/${repo}/${rkey}`, color: 10181046, author: bskyProfile @@ -82,7 +113,7 @@ export async function handlePost({ op, repo, rkey }: HandlerInput) { fields: [ { name: "Link", - value: url, + value: post.url, }, ], }, @@ -96,43 +127,90 @@ export async function handlePost({ op, repo, rkey }: HandlerInput) { } } -export async function handleComment({ op, repo, rkey }: HandlerInput) { +async function hydrateComment( + repo: DID, + collection: string, + rkey: string, +): Promise<{ + cid: string; + content: string; + createdAt: Date; + parentUri: AtUri | null; + postUri: AtUri; +}> { const atproto = await getAtprotoClientFromRepo(repo); - - if (op.action === "create") { - const commentRecord = await atproto.fyi.unravel.frontpage.comment.get({ + if (collection === nsids.FyiUnravelFrontpageComment) { + const record = await atproto.fyi.unravel.frontpage.comment.get({ + repo, rkey, + }); + return { + cid: record.cid, + content: record.value.content, + createdAt: new Date(record.value.createdAt), + parentUri: record.value.parent + ? new AtUri(record.value.parent.uri) + : null, + postUri: new AtUri(record.value.post.uri), + }; + } else if (collection === nsids.FyiFrontpageFeedComment) { + const record = await atproto.fyi.frontpage.feed.comment.get({ repo, + rkey, }); - invariant(commentRecord, "atproto comment record not found"); + const blockContents = record.value.blocks.flatMap((block) => { + if (FyiFrontpageRichtextBlock.isPlaintextParagraph(block.content)) { + return [block.content.text]; + } else { + return []; + } + }); - const comment = await dbComment.uncached_doesCommentExist(repo, rkey); + invariant( + blockContents.length !== record.value.blocks.length, + `Received non plaintext blocks in frontpage feed comment: at://${repo}/${collection}/${rkey}`, + ); - if (comment) { - console.log("comment already exists", commentRecord.value); + return { + cid: record.cid, + content: blockContents.join("\n\n"), + createdAt: new Date(record.value.createdAt), + parentUri: record.value.parent + ? new AtUri(record.value.parent.uri) + : null, + postUri: new AtUri(record.value.post.uri), + }; + } else { + throw new Error("Unknown collection for comment hydration: ${collection}"); + } +} + +export async function handleComment({ op, repo, rkey }: HandlerInput) { + if (op.action === "create") { + const comment = await hydrateComment(repo, op.path.collection, rkey); + + if (await dbComment.uncached_doesCommentExist(repo, rkey)) { await dbComment.updateComment(repo, rkey, { status: "live", - cid: commentRecord.cid, + cid: comment.cid, }); } else { - const { content, createdAt, parent, post } = commentRecord.value; - const postUri = new AtUri(post.uri); - const parentData = parent + const parentData = comment.parentUri ? { - uri: new AtUri(parent.uri), - authorDid: await getDidOrThrow(new AtUri(parent.uri).host), + uri: comment.parentUri, + authorDid: await getDidOrThrow(comment.parentUri.host), } : null; - const postAuthorDid = await getDidOrThrow(postUri.host); + const postAuthorDid = await getDidOrThrow(comment.postUri.host); const createdComment = await dbComment.createComment({ - cid: commentRecord.cid, + cid: comment.cid, authorDid: repo, rkey, - content, - createdAt: new Date(createdAt), + content: comment.content, + createdAt: comment.createdAt, parent: parentData ? { authorDid: parentData.authorDid, @@ -141,7 +219,7 @@ export async function handleComment({ op, repo, rkey }: HandlerInput) { : undefined, post: { authorDid: postAuthorDid, - rkey: postUri.rkey, + rkey: comment.postUri.rkey, }, status: "live", }); @@ -165,38 +243,61 @@ export async function handleComment({ op, repo, rkey }: HandlerInput) { } } -export async function handleVote({ op, repo, rkey }: HandlerInput) { +async function hydrateVote( + repo: DID, + collection: string, + rkey: string, +): Promise<{ + cid: string; + createdAt: Date; + subject: { + uri: AtUri; + cid: string; + }; +}> { const atproto = await getAtprotoClientFromRepo(repo); - if (op.action === "create") { - const hydratedRecord = await atproto.fyi.unravel.frontpage.vote.get({ - repo, - rkey, - }); + let record; + if (collection === nsids.FyiUnravelFrontpageVote) { + record = await atproto.fyi.unravel.frontpage.vote.get({ repo, rkey }); + } else if (collection === nsids.FyiFrontpageFeedVote) { + record = await atproto.fyi.frontpage.feed.vote.get({ repo, rkey }); + } else { + throw new Error("Unknown collection for vote hydration: ${collection}"); + } - invariant(hydratedRecord, "atproto vote record not found"); + return { + cid: record.cid, + createdAt: new Date(record.value.createdAt), + subject: { + uri: new AtUri(record.value.subject.uri), + cid: record.value.subject.cid, + }, + }; +} - const { subject } = hydratedRecord.value; - const subjectUri = new AtUri(subject.uri); +export async function handleVote({ op, repo, rkey }: HandlerInput) { + if (op.action === "create") { + const vote = await hydrateVote(repo, op.path.collection, rkey); - switch (subjectUri.collection) { - case nsids.FyiUnravelFrontpagePost: { - const postVote = await dbVote.uncached_doesPostVoteExist(repo, rkey); - if (postVote) { + switch (vote.subject.uri.collection) { + case nsids.FyiUnravelFrontpagePost: + case nsids.FyiFrontpageFeedPost: { + if (await dbVote.uncached_doesPostVoteExist(repo, rkey)) { await dbVote.updatePostVote({ authorDid: repo, rkey, status: "live", - cid: hydratedRecord.cid, + cid: vote.cid, }); } else { const createdDbPostVote = await dbVote.createPostVote({ repo, rkey, - cid: hydratedRecord.cid, + cid: vote.cid, subject: { - rkey: subjectUri.rkey, - authorDid: await getDidOrThrow(subjectUri.host), - cid: subject.cid, + rkey: vote.subject.uri.rkey, + authorDid: await getDidOrThrow(vote.subject.uri.host), + cid: vote.subject.cid, }, status: "live", }); @@ -209,27 +310,24 @@ export async function handleVote({ op, repo, rkey }: HandlerInput) { } break; } - case nsids.FyiUnravelFrontpageComment: { - const commentVote = await dbVote.uncached_doesCommentVoteExist( - repo, - rkey, - ); - if (commentVote) { + case nsids.FyiUnravelFrontpageComment: + case nsids.FyiFrontpageFeedComment: { + if (await dbVote.uncached_doesCommentVoteExist(repo, rkey)) { await dbVote.updateCommentVote({ authorDid: repo, rkey, status: "live", - cid: hydratedRecord.cid, + cid: vote.cid, }); } else { const createdDbCommentVote = await dbVote.createCommentVote({ repo, rkey, - cid: hydratedRecord.cid, + cid: vote.cid, subject: { - rkey: subjectUri.rkey, - authorDid: await getDidOrThrow(subjectUri.host), - cid: subject.cid, + rkey: vote.subject.uri.rkey, + authorDid: await getDidOrThrow(vote.subject.uri.host), + cid: vote.subject.cid, }, status: "live", }); @@ -242,11 +340,13 @@ export async function handleVote({ op, repo, rkey }: HandlerInput) { } break; } - default: - invariant(subjectUri.collection, "Unknown collection"); + default: { + throw new Error( + `Unknown vote subject collection: ${vote.subject.uri.collection} received from at://${repo}/${op.path.collection}/${rkey}`, + ); + } } } else if (op.action === "delete") { - console.log("deleting vote", rkey); await dbVote.deleteVote({ authorDid: repo, rkey }); } } diff --git a/packages/frontpage/lib/data/atproto/event.ts b/packages/frontpage/lib/data/atproto/event.ts index 1c84590e..275a49dd 100644 --- a/packages/frontpage/lib/data/atproto/event.ts +++ b/packages/frontpage/lib/data/atproto/event.ts @@ -7,10 +7,15 @@ import { nsids } from "./repo"; export const Collection = z.union([ z.literal(nsids.FyiUnravelFrontpagePost), + z.literal(nsids.FyiFrontpageFeedPost), z.literal(nsids.FyiUnravelFrontpageComment), + z.literal(nsids.FyiFrontpageFeedComment), z.literal(nsids.FyiUnravelFrontpageVote), + z.literal(nsids.FyiFrontpageFeedVote), ]); +export type Collection = z.infer; + const Path = z.string().transform((p, ctx) => { const collectionResult = Collection.safeParse(p.split("/")[0]); if (!collectionResult.success) { From 0f0aacb96adc374302e53272dc39ca4352b43ef0 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 18 Nov 2025 10:52:12 +0000 Subject: [PATCH 02/10] Route types --- packages/frontpage/app/api/receive_hook/route.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/frontpage/app/api/receive_hook/route.ts b/packages/frontpage/app/api/receive_hook/route.ts index 4c8c58ef..70bfd2b6 100644 --- a/packages/frontpage/app/api/receive_hook/route.ts +++ b/packages/frontpage/app/api/receive_hook/route.ts @@ -47,15 +47,24 @@ export async function POST(request: Request) { console.log("Processing", collection, rkey, op.action); switch (collection) { - case nsids.FyiUnravelFrontpagePost: + case nsids.FyiFrontpageFeedPost: + case nsids.FyiUnravelFrontpagePost: { await handlePost({ op, repo, rkey }); break; - case nsids.FyiUnravelFrontpageComment: + } + + case nsids.FyiFrontpageFeedComment: + case nsids.FyiUnravelFrontpageComment: { await handleComment({ op, repo, rkey }); break; - case nsids.FyiUnravelFrontpageVote: + } + + case nsids.FyiFrontpageFeedVote: + case nsids.FyiUnravelFrontpageVote: { await handleVote({ op, repo, rkey }); break; + } + default: exhaustiveCheck(collection, `Unknown collection ${JSON.stringify(op)}`); } From 5de56f7cab85cc11d98229e771d61a2dc7ac161d Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 18 Nov 2025 11:12:54 +0000 Subject: [PATCH 03/10] Add cid to error logs --- packages/frontpage/app/api/receive_hook/handlers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontpage/app/api/receive_hook/handlers.ts b/packages/frontpage/app/api/receive_hook/handlers.ts index 4351c299..1c67f50d 100644 --- a/packages/frontpage/app/api/receive_hook/handlers.ts +++ b/packages/frontpage/app/api/receive_hook/handlers.ts @@ -59,7 +59,7 @@ async function hydratePost( const subject = record.value.subject; invariant( FyiFrontpageFeedPost.isUrlSubject(subject), - `Received non-url subject in frontpage feed post: at://${repo}/${collection}/${rkey}`, + `Received non-url subject in frontpage feed post: at://${repo}/${collection}/${rkey}#${record.cid}`, ); return { title: record.value.title, @@ -169,7 +169,7 @@ async function hydrateComment( invariant( blockContents.length !== record.value.blocks.length, - `Received non plaintext blocks in frontpage feed comment: at://${repo}/${collection}/${rkey}`, + `Received non plaintext blocks in frontpage feed comment: at://${repo}/${collection}/${rkey}#${record.cid}`, ); return { @@ -342,7 +342,7 @@ export async function handleVote({ op, repo, rkey }: HandlerInput) { } default: { throw new Error( - `Unknown vote subject collection: ${vote.subject.uri.collection} received from at://${repo}/${op.path.collection}/${rkey}`, + `Unknown vote subject collection: ${vote.subject.uri.collection} received from at://${repo}/${op.path.collection}/${rkey}#${vote.cid}`, ); } } From ee75d3d150a67fcf86305098c886d98afa327848 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 18 Nov 2025 15:21:57 +0000 Subject: [PATCH 04/10] Wire collection through api, db, and receive hook methods --- .../app/api/receive_hook/handlers.ts | 20 ++++++++++++++++- packages/frontpage/lib/api/comment.ts | 1 + packages/frontpage/lib/api/post.ts | 3 ++- packages/frontpage/lib/api/vote.ts | 2 ++ packages/frontpage/lib/data/atproto/repo.ts | 22 ++++++++++++++++++- packages/frontpage/lib/data/db/comment.ts | 6 +++-- packages/frontpage/lib/data/db/post.ts | 6 +++-- packages/frontpage/lib/data/db/vote.ts | 9 +++++--- 8 files changed, 59 insertions(+), 10 deletions(-) diff --git a/packages/frontpage/app/api/receive_hook/handlers.ts b/packages/frontpage/app/api/receive_hook/handlers.ts index 1c67f50d..393eb89f 100644 --- a/packages/frontpage/app/api/receive_hook/handlers.ts +++ b/packages/frontpage/app/api/receive_hook/handlers.ts @@ -1,7 +1,13 @@ import { getPdsUrl, type DID } from "@/lib/data/atproto/did"; import { type Operation } from "@/lib/data/atproto/event"; import { getDidFromHandleOrDid } from "@/lib/data/atproto/identity"; -import { getAtprotoClient, nsids } from "@/lib/data/atproto/repo"; +import { + CommentCollectionType, + getAtprotoClient, + nsids, + VoteCollectionType, + type PostCollectionType, +} from "@/lib/data/atproto/repo"; import * as dbComment from "@/lib/data/db/comment"; import * as dbNotification from "@/lib/data/db/notification"; import * as dbPost from "@/lib/data/db/post"; @@ -44,6 +50,7 @@ async function hydratePost( url: string; createdAt: Date; cid: string; + $type: PostCollectionType; }> { const atproto = await getAtprotoClientFromRepo(repo); if (collection === nsids.FyiUnravelFrontpagePost) { @@ -53,6 +60,7 @@ async function hydratePost( url: record.value.url, createdAt: new Date(record.value.createdAt), cid: record.cid, + $type: record.value.$type, }; } else if (collection === nsids.FyiFrontpageFeedPost) { const record = await atproto.fyi.frontpage.feed.post.get({ repo, rkey }); @@ -66,6 +74,7 @@ async function hydratePost( url: subject.url, createdAt: new Date(record.value.createdAt), cid: record.cid, + $type: record.value.$type, }; } else { throw new Error("Unknown collection for post hydration: ${collection}"); @@ -92,6 +101,7 @@ export async function handlePost({ op, repo, rkey }: HandlerInput) { cid: post.cid, authorDid: repo, status: "live", + collection: post.$type, }); } @@ -137,6 +147,7 @@ async function hydrateComment( createdAt: Date; parentUri: AtUri | null; postUri: AtUri; + $type: CommentCollectionType; }> { const atproto = await getAtprotoClientFromRepo(repo); if (collection === nsids.FyiUnravelFrontpageComment) { @@ -152,6 +163,7 @@ async function hydrateComment( ? new AtUri(record.value.parent.uri) : null, postUri: new AtUri(record.value.post.uri), + $type: record.value.$type, }; } else if (collection === nsids.FyiFrontpageFeedComment) { const record = await atproto.fyi.frontpage.feed.comment.get({ @@ -180,6 +192,7 @@ async function hydrateComment( ? new AtUri(record.value.parent.uri) : null, postUri: new AtUri(record.value.post.uri), + $type: record.value.$type, }; } else { throw new Error("Unknown collection for comment hydration: ${collection}"); @@ -222,6 +235,7 @@ export async function handleComment({ op, repo, rkey }: HandlerInput) { rkey: comment.postUri.rkey, }, status: "live", + collection: comment.$type, }); if (!createdComment) { @@ -254,6 +268,7 @@ async function hydrateVote( uri: AtUri; cid: string; }; + $type: VoteCollectionType; }> { const atproto = await getAtprotoClientFromRepo(repo); let record; @@ -272,6 +287,7 @@ async function hydrateVote( uri: new AtUri(record.value.subject.uri), cid: record.value.subject.cid, }, + $type: record.value.$type, }; } @@ -300,6 +316,7 @@ export async function handleVote({ op, repo, rkey }: HandlerInput) { cid: vote.subject.cid, }, status: "live", + collection: vote.$type, }); if (!createdDbPostVote) { @@ -330,6 +347,7 @@ export async function handleVote({ op, repo, rkey }: HandlerInput) { cid: vote.subject.cid, }, status: "live", + collection: vote.$type, }); if (!createdDbCommentVote) { diff --git a/packages/frontpage/lib/api/comment.ts b/packages/frontpage/lib/api/comment.ts index 7d0cb6cc..4114f4b5 100644 --- a/packages/frontpage/lib/api/comment.ts +++ b/packages/frontpage/lib/api/comment.ts @@ -37,6 +37,7 @@ export async function createComment({ parent, post, status: "pending", + collection: nsids.FyiUnravelFrontpageComment, }); invariant(dbCreatedComment, "Failed to insert comment in database"); diff --git a/packages/frontpage/lib/api/post.ts b/packages/frontpage/lib/api/post.ts index 6f2c6a37..4e7204bc 100644 --- a/packages/frontpage/lib/api/post.ts +++ b/packages/frontpage/lib/api/post.ts @@ -6,7 +6,7 @@ import { invariant } from "../utils"; import { TID } from "@atproto/common-web"; import { type DID } from "../data/atproto/did"; import { after } from "next/server"; -import { getAtprotoClient } from "../data/atproto/repo"; +import { getAtprotoClient, nsids } from "../data/atproto/repo"; export type ApiCreatePostInput = { authorDid: DID; @@ -32,6 +32,7 @@ export async function createPost({ rkey, authorDid: user.did, status: "pending", + collection: nsids.FyiUnravelFrontpagePost, }); invariant(dbCreatedPost, "Failed to insert post in database"); diff --git a/packages/frontpage/lib/api/vote.ts b/packages/frontpage/lib/api/vote.ts index b7abe616..67435ebe 100644 --- a/packages/frontpage/lib/api/vote.ts +++ b/packages/frontpage/lib/api/vote.ts @@ -33,6 +33,7 @@ export async function createVote(subject: ApiCreateVoteInput) { cid: subject.cid, }, status: "pending", + collection: nsids.FyiFrontpageFeedVote, }); invariant(dbCreatedVote, "Failed to insert post vote in database"); @@ -46,6 +47,7 @@ export async function createVote(subject: ApiCreateVoteInput) { cid: subject.cid, }, status: "pending", + collection: nsids.FyiFrontpageFeedVote, }); invariant(dbCreatedVote, "Failed to insert comment vote in database"); diff --git a/packages/frontpage/lib/data/atproto/repo.ts b/packages/frontpage/lib/data/atproto/repo.ts index 0a1000a8..3a5b99f4 100644 --- a/packages/frontpage/lib/data/atproto/repo.ts +++ b/packages/frontpage/lib/data/atproto/repo.ts @@ -1,4 +1,12 @@ -import { AtpBaseClient } from "@repo/frontpage-atproto-client"; +import { + AtpBaseClient, + type FyiFrontpageFeedComment, + type FyiFrontpageFeedPost, + type FyiFrontpageFeedVote, + type FyiUnravelFrontpageComment, + type FyiUnravelFrontpagePost, + type FyiUnravelFrontpageVote, +} from "@repo/frontpage-atproto-client"; import { getUser } from "../user"; import { fetchAuthenticatedAtproto } from "@/lib/auth"; import { cache } from "react"; @@ -28,3 +36,15 @@ export const getAtprotoClient = cache( return fetch(u, init); }), ); + +export type PostCollectionType = + | FyiFrontpageFeedPost.Record["$type"] + | FyiUnravelFrontpagePost.Record["$type"]; + +export type CommentCollectionType = + | FyiUnravelFrontpageComment.Record["$type"] + | FyiFrontpageFeedComment.Record["$type"]; + +export type VoteCollectionType = + | FyiUnravelFrontpageVote.Record["$type"] + | FyiFrontpageFeedVote.Record["$type"]; diff --git a/packages/frontpage/lib/data/db/comment.ts b/packages/frontpage/lib/data/db/comment.ts index f6026450..337824b5 100644 --- a/packages/frontpage/lib/data/db/comment.ts +++ b/packages/frontpage/lib/data/db/comment.ts @@ -18,7 +18,7 @@ import { deleteCommentAggregateTrigger, newCommentAggregateTrigger, } from "./triggers"; -import { nsids } from "../atproto/repo"; +import type { CommentCollectionType } from "../atproto/repo"; type CommentRow = Omit< InferSelectModel, @@ -322,6 +322,7 @@ export type CreateCommentInput = { rkey: string; }; status: "live" | "pending"; + collection: CommentCollectionType; }; export async function createComment({ @@ -333,6 +334,7 @@ export async function createComment({ parent, post, status, + collection, }: CreateCommentInput) { return await db.transaction(async (tx) => { const existingPost = ( @@ -381,7 +383,7 @@ export async function createComment({ createdAt: createdAt, parentCommentId: existingParent?.id ?? null, status, - collection: nsids.FyiUnravelFrontpageComment, + collection, }) .returning({ id: schema.Comment.id, diff --git a/packages/frontpage/lib/data/db/post.ts b/packages/frontpage/lib/data/db/post.ts index 0c3273f3..09532577 100644 --- a/packages/frontpage/lib/data/db/post.ts +++ b/packages/frontpage/lib/data/db/post.ts @@ -17,7 +17,7 @@ import { getUser, isAdmin } from "../user"; import { type DID } from "../atproto/did"; import { newPostAggregateTrigger } from "./triggers"; import { invariant } from "@/lib/utils"; -import { nsids } from "../atproto/repo"; +import type { PostCollectionType } from "../atproto/repo"; const buildUserHasVotedQuery = cache(async () => { const user = await getUser(); @@ -180,6 +180,7 @@ export type CreatePostInput = { rkey: string; cid?: string; status: "live" | "pending"; + collection: PostCollectionType; }; export async function createPost({ @@ -188,6 +189,7 @@ export async function createPost({ rkey, cid, status, + collection, }: CreatePostInput) { return await db.transaction(async (tx) => { const [insertedPostRow] = await tx @@ -200,7 +202,7 @@ export async function createPost({ url: post.url, createdAt: post.createdAt, status, - collection: nsids.FyiUnravelFrontpagePost, + collection, }) .returning({ postId: schema.Post.id }); diff --git a/packages/frontpage/lib/data/db/vote.ts b/packages/frontpage/lib/data/db/vote.ts index 890c0317..a04da959 100644 --- a/packages/frontpage/lib/data/db/vote.ts +++ b/packages/frontpage/lib/data/db/vote.ts @@ -12,7 +12,7 @@ import { newPostVoteAggregateTrigger, } from "./triggers"; import { invariant } from "@/lib/utils"; -import { nsids } from "../atproto/repo"; +import type { VoteCollectionType } from "../atproto/repo"; export const getVoteForPost = cache(async (postId: number) => { const user = await getUser(); @@ -95,6 +95,7 @@ export type CreateVoteInput = { cid: string; }; status: "live" | "pending"; + collection: VoteCollectionType; }; export const createPostVote = async ({ @@ -102,6 +103,7 @@ export const createPostVote = async ({ rkey, cid, subject, + collection, }: CreateVoteInput) => { return await db.transaction(async (tx) => { const post = ( @@ -133,7 +135,7 @@ export const createPostVote = async ({ createdAt: new Date(), cid: cid ?? "", rkey, - collection: nsids.FyiUnravelFrontpageVote, + collection, }) .returning({ id: schema.PostVote.id }); @@ -152,6 +154,7 @@ export async function createCommentVote({ rkey, cid, subject, + collection, }: CreateVoteInput) { return await db.transaction(async (tx) => { const comment = ( @@ -180,7 +183,7 @@ export async function createCommentVote({ createdAt: new Date(), cid: cid ?? "", rkey, - collection: nsids.FyiUnravelFrontpageVote, + collection, }) .returning({ id: schema.CommentVote.id }); From 5185847fceb1d823216e2f51a1ef2709d6c56d8c Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 18 Nov 2025 15:53:06 +0000 Subject: [PATCH 05/10] import type --- packages/frontpage/app/api/receive_hook/handlers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontpage/app/api/receive_hook/handlers.ts b/packages/frontpage/app/api/receive_hook/handlers.ts index 393eb89f..f2fa5cc9 100644 --- a/packages/frontpage/app/api/receive_hook/handlers.ts +++ b/packages/frontpage/app/api/receive_hook/handlers.ts @@ -2,10 +2,10 @@ import { getPdsUrl, type DID } from "@/lib/data/atproto/did"; import { type Operation } from "@/lib/data/atproto/event"; import { getDidFromHandleOrDid } from "@/lib/data/atproto/identity"; import { - CommentCollectionType, + type CommentCollectionType, getAtprotoClient, nsids, - VoteCollectionType, + type VoteCollectionType, type PostCollectionType, } from "@/lib/data/atproto/repo"; import * as dbComment from "@/lib/data/db/comment"; From 0882a317f7cdd94bec0151e1be7bfa756e514710 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 18 Nov 2025 16:28:06 +0000 Subject: [PATCH 06/10] Update packages/frontpage/app/api/receive_hook/handlers.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/frontpage/app/api/receive_hook/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontpage/app/api/receive_hook/handlers.ts b/packages/frontpage/app/api/receive_hook/handlers.ts index f2fa5cc9..e3cca25d 100644 --- a/packages/frontpage/app/api/receive_hook/handlers.ts +++ b/packages/frontpage/app/api/receive_hook/handlers.ts @@ -277,7 +277,7 @@ async function hydrateVote( } else if (collection === nsids.FyiFrontpageFeedVote) { record = await atproto.fyi.frontpage.feed.vote.get({ repo, rkey }); } else { - throw new Error("Unknown collection for vote hydration: ${collection}"); + throw new Error(`Unknown collection for vote hydration: ${collection}`); } return { From 9bbe93d2ea476180bf7f4a1da230509e6d31d671 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 18 Nov 2025 16:28:21 +0000 Subject: [PATCH 07/10] Update packages/frontpage/app/api/receive_hook/handlers.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/frontpage/app/api/receive_hook/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontpage/app/api/receive_hook/handlers.ts b/packages/frontpage/app/api/receive_hook/handlers.ts index e3cca25d..fbda3572 100644 --- a/packages/frontpage/app/api/receive_hook/handlers.ts +++ b/packages/frontpage/app/api/receive_hook/handlers.ts @@ -77,7 +77,7 @@ async function hydratePost( $type: record.value.$type, }; } else { - throw new Error("Unknown collection for post hydration: ${collection}"); + throw new Error(`Unknown collection for post hydration: ${collection}`); } } From 4058d9d1fba1c8152254129991d36994043341c3 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 18 Nov 2025 16:28:30 +0000 Subject: [PATCH 08/10] Update packages/frontpage/app/api/receive_hook/handlers.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/frontpage/app/api/receive_hook/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontpage/app/api/receive_hook/handlers.ts b/packages/frontpage/app/api/receive_hook/handlers.ts index fbda3572..54f562d8 100644 --- a/packages/frontpage/app/api/receive_hook/handlers.ts +++ b/packages/frontpage/app/api/receive_hook/handlers.ts @@ -195,7 +195,7 @@ async function hydrateComment( $type: record.value.$type, }; } else { - throw new Error("Unknown collection for comment hydration: ${collection}"); + throw new Error(`Unknown collection for comment hydration: ${collection}`); } } From 3da5f6f87f14e3ab17902056c13db2139937b3c1 Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Tue, 18 Nov 2025 17:06:38 +0000 Subject: [PATCH 09/10] Fix log error (thank u copilot) --- packages/frontpage/app/api/receive_hook/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontpage/app/api/receive_hook/handlers.ts b/packages/frontpage/app/api/receive_hook/handlers.ts index 54f562d8..7d82226b 100644 --- a/packages/frontpage/app/api/receive_hook/handlers.ts +++ b/packages/frontpage/app/api/receive_hook/handlers.ts @@ -180,7 +180,7 @@ async function hydrateComment( }); invariant( - blockContents.length !== record.value.blocks.length, + blockContents.length === record.value.blocks.length, `Received non plaintext blocks in frontpage feed comment: at://${repo}/${collection}/${rkey}#${record.cid}`, ); From bb7b7a55194f06654422f29a4c732e6715370edc Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Fri, 21 Nov 2025 11:51:19 +0000 Subject: [PATCH 10/10] Correct NSIDs --- packages/frontpage/lib/api/vote.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontpage/lib/api/vote.ts b/packages/frontpage/lib/api/vote.ts index 67435ebe..2ad3ac2f 100644 --- a/packages/frontpage/lib/api/vote.ts +++ b/packages/frontpage/lib/api/vote.ts @@ -33,7 +33,7 @@ export async function createVote(subject: ApiCreateVoteInput) { cid: subject.cid, }, status: "pending", - collection: nsids.FyiFrontpageFeedVote, + collection: nsids.FyiUnravelFrontpageVote, }); invariant(dbCreatedVote, "Failed to insert post vote in database"); @@ -47,7 +47,7 @@ export async function createVote(subject: ApiCreateVoteInput) { cid: subject.cid, }, status: "pending", - collection: nsids.FyiFrontpageFeedVote, + collection: nsids.FyiUnravelFrontpageVote, }); invariant(dbCreatedVote, "Failed to insert comment vote in database");