diff --git a/packages/frontpage/app/(app)/moderation/_components/report-card.tsx b/packages/frontpage/app/(app)/moderation/_components/report-card.tsx index 756e196f..de8906ee 100644 --- a/packages/frontpage/app/(app)/moderation/_components/report-card.tsx +++ b/packages/frontpage/app/(app)/moderation/_components/report-card.tsx @@ -11,7 +11,6 @@ import { performModerationAction } from "../page"; import { UserHandle } from "./user-handle"; import Link from "next/link"; import { cn } from "@/lib/utils"; -import { CommentCollection } from "@/lib/data/atproto/comment"; import { getPostFromComment } from "@/lib/data/db/post"; import { getCommentLink, getPostLink } from "@/lib/navigation"; import { nsids } from "@/lib/data/atproto/repo"; @@ -25,7 +24,7 @@ const createLink = async ( case nsids.FyiUnravelFrontpagePost: return getPostLink({ handleOrDid: author!, rkey: rkey! }); - case CommentCollection: { + case nsids.FyiUnravelFrontpageComment: { const { postAuthor, postRkey } = (await getPostFromComment({ rkey: rkey!, did: author!, diff --git a/packages/frontpage/app/(app)/moderation/page.tsx b/packages/frontpage/app/(app)/moderation/page.tsx index 167acd79..487469d6 100644 --- a/packages/frontpage/app/(app)/moderation/page.tsx +++ b/packages/frontpage/app/(app)/moderation/page.tsx @@ -17,7 +17,6 @@ import { type ModerationEventDTO, createModerationEvent, } from "@/lib/data/db/moderation"; -import { CommentCollection } from "@/lib/data/atproto/comment"; import { revalidatePath } from "next/cache"; import Link from "next/link"; import { ReportCard } from "./_components/report-card"; @@ -51,8 +50,8 @@ export async function performModerationAction( if (report.subjectCollection) { if (report.subjectCollection === nsids.FyiUnravelFrontpagePost) { newModEvent.subjectCollection = nsids.FyiUnravelFrontpagePost; - } else if (report.subjectCollection === CommentCollection) { - newModEvent.subjectCollection = CommentCollection; + } else if (report.subjectCollection === nsids.FyiUnravelFrontpageComment) { + newModEvent.subjectCollection = nsids.FyiUnravelFrontpageComment; } newModEvent.subjectRkey = report.subjectRkey; @@ -69,7 +68,7 @@ export async function performModerationAction( hide: input.status === "accepted", }); - case CommentCollection: + case nsids.FyiUnravelFrontpageComment: return await moderateComment({ rkey: report.subjectRkey!, authorDid: report.subjectDid as DID, diff --git a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/[commentAuthor]/[commentRkey]/page.tsx b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/[commentAuthor]/[commentRkey]/page.tsx index e820af78..52fdf671 100644 --- a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/[commentAuthor]/[commentRkey]/page.tsx +++ b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/[commentAuthor]/[commentRkey]/page.tsx @@ -4,7 +4,7 @@ import { type Metadata } from "next"; import { getVerifiedHandle } from "@/lib/data/atproto/identity"; import { type CommentPageParams, getCommentPageData } from "./_lib/page-data"; import { LinkAlternateAtUri } from "@/lib/components/link-alternate-at"; -import { CommentCollection } from "@/lib/data/atproto/comment"; +import { nsids } from "@/lib/data/atproto/repo"; function truncateText(text: string, maxLength: number) { if (text.length > maxLength) { @@ -61,7 +61,7 @@ export default async function CommentPage(props: { <>
diff --git a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx index 3d43a59c..83bbee6b 100644 --- a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx +++ b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/_lib/actions.tsx @@ -1,6 +1,5 @@ "use server"; -import { CommentCollection } from "@/lib/data/atproto/comment"; import { type DID } from "@/lib/data/atproto/did"; import { getComment } from "@/lib/data/db/comment"; import { getPost } from "@/lib/data/db/post"; @@ -12,6 +11,7 @@ import { revalidatePath } from "next/cache"; import { createComment, deleteComment } from "@/lib/api/comment"; import { createVote, deleteVote } from "@/lib/api/vote"; import { invariant } from "@/lib/utils"; +import { nsids } from "@/lib/data/atproto/repo"; export async function createCommentAction( input: { parentRkey?: string; postRkey: string; postAuthorDid: DID }, @@ -76,9 +76,9 @@ export async function reportCommentAction( await createReport({ ...formResult.data, - subjectUri: `at://${input.authorDid}/${CommentCollection}/${input.rkey}`, + subjectUri: `at://${input.authorDid}/${nsids.FyiUnravelFrontpageComment}/${input.rkey}`, subjectDid: input.authorDid, - subjectCollection: CommentCollection, + subjectCollection: nsids.FyiUnravelFrontpageComment, subjectRkey: input.rkey, subjectCid: input.cid, }); @@ -96,7 +96,7 @@ export async function commentVoteAction(input: { rkey: input.rkey, cid: input.cid, authorDid: input.authorDid, - collection: CommentCollection, + collection: nsids.FyiUnravelFrontpageComment, }, }); } diff --git a/packages/frontpage/app/api/receive_hook/handlers.ts b/packages/frontpage/app/api/receive_hook/handlers.ts index 0cd7e0be..b4533840 100644 --- a/packages/frontpage/app/api/receive_hook/handlers.ts +++ b/packages/frontpage/app/api/receive_hook/handlers.ts @@ -1,7 +1,7 @@ -import * as atprotoComment from "@/lib/data/atproto/comment"; import { getPdsUrl, type DID } from "@/lib/data/atproto/did"; import { type Operation } from "@/lib/data/atproto/event"; import { getAtprotoClient, nsids } from "@/lib/data/atproto/repo"; +import { AtUri } from "@/lib/data/atproto/uri"; import * as atprotoVote from "@/lib/data/atproto/vote"; import * as dbComment from "@/lib/data/db/comment"; import * as dbNotification from "@/lib/data/db/notification"; @@ -22,12 +22,17 @@ type HandlerInput = { // Since we use read after write, we need to check if the record exists before creating it // If it's a delete then setting the status to delete again doesn't matter -export async function handlePost({ op, repo, rkey }: HandlerInput) { +async function getAtprotoClientFromRepo(repo: DID) { const pds = await getPdsUrl(repo); if (!pds) { throw new Error("Failed to get PDS"); } - const atproto = getAtprotoClient(pds); + return getAtprotoClient(pds); +} + +export async function handlePost({ op, repo, rkey }: HandlerInput) { + const atproto = await getAtprotoClientFromRepo(repo); + if (op.action === "create") { const postRecord = await atproto.fyi.unravel.frontpage.post.get({ repo, @@ -91,8 +96,10 @@ export async function handlePost({ op, repo, rkey }: HandlerInput) { } export async function handleComment({ op, repo, rkey }: HandlerInput) { + const atproto = await getAtprotoClientFromRepo(repo); + if (op.action === "create") { - const commentRecord = await atprotoComment.getComment({ + const commentRecord = await atproto.fyi.unravel.frontpage.comment.get({ rkey, repo, }); @@ -109,22 +116,24 @@ export async function handleComment({ op, repo, rkey }: HandlerInput) { }); } else { const { content, createdAt, parent, post } = commentRecord.value; + const postUri = AtUri.parse(post.uri); + const parentUri = parent ? AtUri.parse(parent.uri) : null; const createdComment = await dbComment.createComment({ cid: commentRecord.cid, authorDid: repo, rkey, content, createdAt: new Date(createdAt), - parent: parent + parent: parentUri ? { //TODO: is authority a DID? - authorDid: parent.uri.authority as DID, - rkey: parent.uri.rkey, + authorDid: parentUri.authority as DID, + rkey: parentUri.rkey, } : undefined, post: { - authorDid: post.uri.authority as DID, - rkey: post.uri.rkey, + authorDid: postUri.authority as DID, + rkey: postUri.rkey, }, status: "live", }); @@ -133,7 +142,7 @@ export async function handleComment({ op, repo, rkey }: HandlerInput) { throw new Error("Failed to insert comment from relay in database"); } - const didToNotify = parent ? parent.uri.authority : post.uri.authority; + const didToNotify = parentUri ? parentUri.authority : postUri.authority; if (didToNotify !== repo) { await dbNotification.createNotification({ @@ -190,7 +199,7 @@ export async function handleVote({ op, repo, rkey }: HandlerInput) { } break; } - case atprotoComment.CommentCollection: { + case nsids.FyiUnravelFrontpageComment: { const commentVote = await dbVote.uncached_doesCommentVoteExist( repo, rkey, diff --git a/packages/frontpage/app/api/receive_hook/route.ts b/packages/frontpage/app/api/receive_hook/route.ts index 3e8751ce..923f3c3e 100644 --- a/packages/frontpage/app/api/receive_hook/route.ts +++ b/packages/frontpage/app/api/receive_hook/route.ts @@ -1,7 +1,6 @@ import { db } from "@/lib/db"; import * as schema from "@/lib/schema"; import { Commit } from "@/lib/data/atproto/event"; -import * as atprotoComment from "@/lib/data/atproto/comment"; import * as atprotoVote from "@/lib/data/atproto/vote"; import { getPdsUrl } from "@/lib/data/atproto/did"; import { handleComment, handlePost, handleVote } from "./handlers"; @@ -46,7 +45,7 @@ export async function POST(request: Request) { case nsids.FyiUnravelFrontpagePost: await handlePost({ op, repo, rkey }); break; - case atprotoComment.CommentCollection: + case nsids.FyiUnravelFrontpageComment: await handleComment({ op, repo, rkey }); break; case atprotoVote.VoteCollection: diff --git a/packages/frontpage/lib/api/comment.ts b/packages/frontpage/lib/api/comment.ts index c286d5f4..7d0cb6cc 100644 --- a/packages/frontpage/lib/api/comment.ts +++ b/packages/frontpage/lib/api/comment.ts @@ -1,5 +1,4 @@ import "server-only"; -import * as atproto from "../data/atproto/comment"; import { DataLayerError } from "../data/error"; import { ensureUser } from "../data/user"; import * as db from "../data/db/comment"; @@ -8,8 +7,13 @@ import { createNotification } from "../data/db/notification"; import { invariant } from "../utils"; import { TID } from "@atproto/common-web"; import { after } from "next/server"; +import { getAtprotoClient, nsids } from "../data/atproto/repo"; -export type ApiCreateCommentInput = Omit & { +export type ApiCreateCommentInput = { + // TODO: Use strongRef type for parent and post + parent?: { cid: string; rkey: string; authorDid: DID }; + post: { cid: string; rkey: string; authorDid: DID }; + content: string; authorDid: DID; }; @@ -38,12 +42,26 @@ export async function createComment({ invariant(dbCreatedComment, "Failed to insert comment in database"); after(() => - atproto.createComment({ - parent, - post, - content: sanitizedContent, - rkey, - }), + getAtprotoClient().fyi.unravel.frontpage.comment.create( + { + repo: user.did, + rkey, + }, + { + parent: parent + ? { + cid: parent.cid, + uri: `at://${parent.authorDid}/${nsids.FyiUnravelFrontpageComment}/${parent.rkey}`, + } + : undefined, + post: { + cid: post.cid, + uri: `at://${post.authorDid}/${nsids.FyiUnravelFrontpagePost}/${post.rkey}`, + }, + content: sanitizedContent, + createdAt: new Date().toISOString(), + }, + ), ); const didToNotify = parent ? parent.authorDid : post.authorDid; @@ -73,7 +91,12 @@ export async function deleteComment({ } try { - after(() => atproto.deleteComment(authorDid, rkey)); + after(() => + getAtprotoClient().fyi.unravel.frontpage.comment.delete({ + repo: authorDid, + rkey, + }), + ); await db.deleteComment({ authorDid: user.did, rkey }); } catch (e) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions diff --git a/packages/frontpage/lib/api/vote.ts b/packages/frontpage/lib/api/vote.ts index ee3ef25d..25fba022 100644 --- a/packages/frontpage/lib/api/vote.ts +++ b/packages/frontpage/lib/api/vote.ts @@ -4,7 +4,6 @@ import * as atproto from "../data/atproto/vote"; import { DataLayerError } from "../data/error"; import { ensureUser } from "../data/user"; import { type DID } from "../data/atproto/did"; -import { CommentCollection } from "../data/atproto/comment"; import { invariant } from "../utils"; import { TID } from "@atproto/common-web"; import { after } from "next/server"; @@ -16,7 +15,9 @@ export type ApiCreateVoteInput = { rkey: string; cid: string; authorDid: DID; - collection: typeof nsids.FyiUnravelFrontpagePost | typeof CommentCollection; + collection: + | typeof nsids.FyiUnravelFrontpagePost + | typeof nsids.FyiUnravelFrontpageComment; }; }; @@ -42,7 +43,7 @@ export async function createVote({ authorDid, subject }: ApiCreateVoteInput) { }); invariant(dbCreatedVote, "Failed to insert post vote in database"); - } else if (subject.collection == CommentCollection) { + } else if (subject.collection == nsids.FyiUnravelFrontpageComment) { const dbCreatedVote = await db.createCommentVote({ repo: authorDid, rkey, diff --git a/packages/frontpage/lib/data/atproto/comment.ts b/packages/frontpage/lib/data/atproto/comment.ts deleted file mode 100644 index 92f4976a..00000000 --- a/packages/frontpage/lib/data/atproto/comment.ts +++ /dev/null @@ -1,104 +0,0 @@ -import "server-only"; -import { - atprotoCreateRecord, - atprotoDeleteRecord, - atprotoGetRecord, -} from "./record"; -import { createAtUriParser } from "./uri"; -import { DataLayerError } from "../error"; -import { z } from "zod"; -import { type DID, getPdsUrl } from "./did"; -import { MAX_COMMENT_LENGTH } from "../db/constants"; -import { nsids } from "./repo"; - -export const CommentCollection = "fyi.unravel.frontpage.comment"; - -export const CommentRecord = z.object({ - content: z.string().max(MAX_COMMENT_LENGTH), - parent: z - .object({ - cid: z.string(), - uri: createAtUriParser(z.literal(CommentCollection)), - }) - .optional(), - post: z.object({ - cid: z.string(), - uri: createAtUriParser(z.literal(nsids.FyiUnravelFrontpagePost)), - }), - createdAt: z.string(), -}); - -export type Comment = z.infer; - -export type CommentInput = { - parent?: { cid: string; rkey: string; authorDid: DID }; - post: { cid: string; rkey: string; authorDid: DID }; - content: string; - rkey: string; -}; - -export async function createComment({ - parent, - post, - content, - rkey, -}: CommentInput) { - // Collapse newlines into a single \n\n and trim whitespace - const record = { - content, - parent: parent - ? { - cid: parent.cid, - uri: `at://${parent.authorDid}/${CommentCollection}/${parent.rkey}`, - } - : undefined, - post: { - cid: post.cid, - uri: `at://${post.authorDid}/${nsids.FyiUnravelFrontpagePost}/${post.rkey}`, - }, - createdAt: new Date().toISOString(), - }; - - const parseResult = CommentRecord.safeParse(record); - if (!parseResult.success) { - throw new DataLayerError("Invalid comment record", { - cause: parseResult.error, - }); - } - - const result = await atprotoCreateRecord({ - record, - collection: CommentCollection, - rkey, - }); - - return { - rkey: result.uri.rkey, - cid: result.cid, - }; -} - -export async function deleteComment(authorDid: DID, rkey: string) { - await atprotoDeleteRecord({ - authorDid, - rkey, - collection: CommentCollection, - }); -} - -export async function getComment({ rkey, repo }: { rkey: string; repo: DID }) { - const service = await getPdsUrl(repo); - - if (!service) { - throw new DataLayerError("Failed to get service url"); - } - - const { value, cid } = await atprotoGetRecord({ - serviceEndpoint: service, - repo, - collection: CommentCollection, - rkey, - }); - - return { value: CommentRecord.parse(value), cid }; -} diff --git a/packages/frontpage/lib/data/atproto/event.ts b/packages/frontpage/lib/data/atproto/event.ts index 79e7c9cb..746a958b 100644 --- a/packages/frontpage/lib/data/atproto/event.ts +++ b/packages/frontpage/lib/data/atproto/event.ts @@ -1,6 +1,5 @@ import "server-only"; import { z } from "zod"; -import { CommentCollection } from "./comment"; import { isDid } from "./did"; import { nsids } from "./repo"; @@ -8,7 +7,7 @@ import { nsids } from "./repo"; export const Collection = z.union([ z.literal(nsids.FyiUnravelFrontpagePost), - z.literal(CommentCollection), + z.literal(nsids.FyiUnravelFrontpageComment), z.literal("fyi.unravel.frontpage.vote"), ]); diff --git a/packages/frontpage/lib/data/atproto/vote.ts b/packages/frontpage/lib/data/atproto/vote.ts index 622064c5..22ad5baa 100644 --- a/packages/frontpage/lib/data/atproto/vote.ts +++ b/packages/frontpage/lib/data/atproto/vote.ts @@ -5,7 +5,6 @@ import { atprotoGetRecord, } from "./record"; import { z } from "zod"; -import { CommentCollection } from "./comment"; import { type DID, getPdsUrl } from "./did"; import { createAtUriParser } from "./uri"; import { DataLayerError } from "../error"; @@ -15,7 +14,7 @@ export const VoteCollection = "fyi.unravel.frontpage.vote"; const VoteSubjectCollection = z.union([ z.literal(nsids.FyiUnravelFrontpagePost), - z.literal(CommentCollection), + z.literal(nsids.FyiUnravelFrontpageComment), ]); export const VoteRecord = z.object({ @@ -34,7 +33,9 @@ export type VoteInput = { rkey: string; cid: string; authorDid: DID; - collection: typeof nsids.FyiUnravelFrontpagePost | typeof CommentCollection; + collection: + | typeof nsids.FyiUnravelFrontpagePost + | typeof nsids.FyiUnravelFrontpageComment; }; }; diff --git a/packages/frontpage/lib/data/db/shared.ts b/packages/frontpage/lib/data/db/shared.ts index f414cc2a..4783bbe2 100644 --- a/packages/frontpage/lib/data/db/shared.ts +++ b/packages/frontpage/lib/data/db/shared.ts @@ -1,5 +1,4 @@ import { headers } from "next/headers"; -import { CommentCollection } from "../atproto/comment"; import { type DID } from "../atproto/did"; import { getPostFromComment } from "./post"; import { nsids } from "../atproto/repo"; @@ -24,7 +23,7 @@ export const createFrontPageLink = async ( case nsids.FyiUnravelFrontpagePost: return `/post/${author}/${rkey}/`; - case CommentCollection: { + case nsids.FyiUnravelFrontpageComment: { const { postAuthor, postRkey } = (await getPostFromComment({ rkey: rkey!, did: author,