From c4853a991285159ffd17c69b523e745f58d657be Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Wed, 6 Nov 2024 20:42:10 +0000 Subject: [PATCH] Migrate frontpage post to be lexicon first --- .../app/(app)/_components/post-card.tsx | 13 ++-- .../moderation/_components/report-card.tsx | 4 +- .../frontpage/app/(app)/moderation/page.tsx | 8 +-- .../post/[postAuthor]/[postRkey]/page.tsx | 4 +- .../frontpage/app/(app)/post/new/_action.ts | 20 ++++-- .../frontpage/app/api/receive_hook/route.ts | 17 +++-- packages/frontpage/lib/auth.ts | 2 +- .../frontpage/lib/data/atproto/comment.ts | 6 +- packages/frontpage/lib/data/atproto/event.ts | 4 +- packages/frontpage/lib/data/atproto/post.ts | 68 ------------------- packages/frontpage/lib/data/atproto/repo.ts | 25 +++++++ packages/frontpage/lib/data/atproto/vote.ts | 4 +- packages/frontpage/lib/data/db/post.ts | 4 +- packages/frontpage/lib/data/db/shared.ts | 4 +- packages/frontpage/package.json | 1 + pnpm-lock.yaml | 3 + 16 files changed, 81 insertions(+), 106 deletions(-) delete mode 100644 packages/frontpage/lib/data/atproto/post.ts create mode 100644 packages/frontpage/lib/data/atproto/repo.ts diff --git a/packages/frontpage/app/(app)/_components/post-card.tsx b/packages/frontpage/app/(app)/_components/post-card.tsx index c0720e5b..1f01b3b6 100644 --- a/packages/frontpage/app/(app)/_components/post-card.tsx +++ b/packages/frontpage/app/(app)/_components/post-card.tsx @@ -4,7 +4,6 @@ import { getVoteForPost } from "@/lib/data/db/vote"; import { ensureUser, getUser } from "@/lib/data/user"; import { TimeAgo } from "@/lib/components/time-ago"; import { VoteButton } from "./vote-button"; -import { PostCollection, deletePost } from "@/lib/data/atproto/post"; import { getVerifiedHandle } from "@/lib/data/atproto/identity"; import { UserHoverCard } from "@/lib/components/user-hover-card"; import type { DID } from "@/lib/data/atproto/did"; @@ -15,6 +14,7 @@ import { revalidatePath } from "next/cache"; import { ReportDialogDropdownButton } from "./report-dialog"; import { DeleteButton } from "./delete-button"; import { ShareDropdownButton } from "./share-button"; +import { atprotoClient, nsids } from "@/lib/data/atproto/repo"; type PostProps = { id: number; @@ -59,7 +59,7 @@ export async function PostCard({ subjectAuthorDid: author, subjectCid: cid, subjectRkey: rkey, - subjectCollection: PostCollection, + subjectCollection: nsids.FyiUnravelFrontpagePost, }); }} unvoteAction={async () => { @@ -147,7 +147,10 @@ export async function PostCard({ export async function deletePostAction(rkey: string) { "use server"; await ensureUser(); - await deletePost(rkey); + const atproto = atprotoClient(); + await atproto.fyi.unravel.frontpage.post.delete({ + rkey, + }); revalidatePath("/"); } @@ -170,9 +173,9 @@ export async function reportPostAction( await createReport({ ...formResult.data, - subjectUri: `at://${input.author}/${PostCollection}/${input.rkey}`, + subjectUri: `at://${input.author}/${nsids.FyiUnravelFrontpagePost}/${input.rkey}`, subjectDid: input.author, - subjectCollection: PostCollection, + subjectCollection: nsids.FyiUnravelFrontpagePost, subjectRkey: input.rkey, subjectCid: input.cid, }); diff --git a/packages/frontpage/app/(app)/moderation/_components/report-card.tsx b/packages/frontpage/app/(app)/moderation/_components/report-card.tsx index 103d232a..558b3485 100644 --- a/packages/frontpage/app/(app)/moderation/_components/report-card.tsx +++ b/packages/frontpage/app/(app)/moderation/_components/report-card.tsx @@ -12,9 +12,9 @@ import { UserHandle } from "./user-handle"; import Link from "next/link"; import { cn } from "@/lib/utils"; import { CommentCollection } from "@/lib/data/atproto/comment"; -import { PostCollection } from "@/lib/data/atproto/post"; import { getPostFromComment } from "@/lib/data/db/post"; import { getCommentLink, getPostLink } from "@/lib/navigation"; +import { nsids } from "@/lib/data/atproto/repo"; const createLink = async ( collection?: string | null, @@ -22,7 +22,7 @@ const createLink = async ( rkey?: string | null, ) => { switch (collection) { - case PostCollection: + case nsids.FyiUnravelFrontpagePost: return getPostLink({ handleOrDid: author!, rkey: rkey! }); case CommentCollection: diff --git a/packages/frontpage/app/(app)/moderation/page.tsx b/packages/frontpage/app/(app)/moderation/page.tsx index bd2a9e87..fd464766 100644 --- a/packages/frontpage/app/(app)/moderation/page.tsx +++ b/packages/frontpage/app/(app)/moderation/page.tsx @@ -17,7 +17,6 @@ import { ModerationEventDTO, createModerationEvent, } from "@/lib/data/db/moderation"; -import { PostCollection } from "@/lib/data/atproto/post"; import { CommentCollection } from "@/lib/data/atproto/comment"; import { revalidatePath } from "next/cache"; import Link from "next/link"; @@ -26,6 +25,7 @@ import { moderatePost } from "@/lib/data/db/post"; import { DID } from "@/lib/data/atproto/did"; import { moderateComment } from "@/lib/data/db/comment"; import { moderateUser } from "@/lib/data/db/user"; +import { nsids } from "@/lib/data/atproto/repo"; export async function performModerationAction( input: { reportId: number; status: "accepted" | "rejected" }, @@ -49,8 +49,8 @@ export async function performModerationAction( }; if (report.subjectCollection) { - if (report.subjectCollection === PostCollection) { - newModEvent.subjectCollection = PostCollection; + if (report.subjectCollection === nsids.FyiUnravelFrontpagePost) { + newModEvent.subjectCollection = nsids.FyiUnravelFrontpagePost; } else if (report.subjectCollection === CommentCollection) { newModEvent.subjectCollection = CommentCollection; } @@ -61,7 +61,7 @@ export async function performModerationAction( const modAction = async () => { switch (report.subjectCollection) { - case PostCollection: + case nsids.FyiUnravelFrontpagePost: return await moderatePost({ rkey: report.subjectRkey!, authorDid: report.subjectDid! as DID, diff --git a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/page.tsx b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/page.tsx index a83f8104..ae8596e9 100644 --- a/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/page.tsx +++ b/packages/frontpage/app/(app)/post/[postAuthor]/[postRkey]/page.tsx @@ -5,7 +5,7 @@ import { Metadata } from "next"; import { getVerifiedHandle } from "@/lib/data/atproto/identity"; import { PostPageParams, getPostPageData } from "./_lib/page-data"; import { LinkAlternateAtUri } from "@/lib/components/link-alternate-at"; -import { PostCollection } from "@/lib/data/atproto/post"; +import { nsids } from "@/lib/data/atproto/repo"; export async function generateMetadata(props: { params: Promise; @@ -47,7 +47,7 @@ export default async function Post(props: { params: Promise }) { <> {post.status === "live" ? ( diff --git a/packages/frontpage/app/(app)/post/new/_action.ts b/packages/frontpage/app/(app)/post/new/_action.ts index 0e1995f4..7313232c 100644 --- a/packages/frontpage/app/(app)/post/new/_action.ts +++ b/packages/frontpage/app/(app)/post/new/_action.ts @@ -2,10 +2,11 @@ import { DID } from "@/lib/data/atproto/did"; import { getVerifiedHandle } from "@/lib/data/atproto/identity"; -import { createPost } from "@/lib/data/atproto/post"; +import { atprotoClient } from "@/lib/data/atproto/repo"; import { uncached_doesPostExist } from "@/lib/data/db/post"; import { DataLayerError } from "@/lib/data/error"; import { ensureUser } from "@/lib/data/user"; +import { AtUri } from "@atproto/syntax"; import { redirect } from "next/navigation"; export async function newPostAction(_prevState: unknown, formData: FormData) { @@ -26,13 +27,24 @@ export async function newPostAction(_prevState: unknown, formData: FormData) { return { error: "Invalid URL" }; } + const atproto = atprotoClient(); + try { - const { rkey } = await createPost({ title, url }); + const record = await atproto.fyi.unravel.frontpage.post.create( + {}, + { + title, + url, + createdAt: new Date().toISOString(), + }, + ); + const uri = new AtUri(record.uri); + const [handle] = await Promise.all([ getVerifiedHandle(user.did), - waitForPost(user.did, rkey), + waitForPost(user.did, uri.rkey), ]); - redirect(`/post/${handle}/${rkey}`); + redirect(`/post/${handle}/${uri.rkey}`); } catch (error) { if (!(error instanceof DataLayerError)) throw error; return { error: "Failed to create post" }; diff --git a/packages/frontpage/app/api/receive_hook/route.ts b/packages/frontpage/app/api/receive_hook/route.ts index 04bda27c..8d190e10 100644 --- a/packages/frontpage/app/api/receive_hook/route.ts +++ b/packages/frontpage/app/api/receive_hook/route.ts @@ -2,7 +2,6 @@ import { db } from "@/lib/db"; import * as schema from "@/lib/schema"; import { atprotoGetRecord } from "@/lib/data/atproto/record"; import { Commit } from "@/lib/data/atproto/event"; -import * as atprotoPost from "@/lib/data/atproto/post"; import * as dbPost from "@/lib/data/db/post"; import * as atprotoComment from "@/lib/data/atproto/comment"; import { VoteRecord } from "@/lib/data/atproto/vote"; @@ -17,6 +16,7 @@ import { unauthed_createCommentVote, } from "@/lib/data/db/vote"; import { unauthed_createNotification } from "@/lib/data/db/notification"; +import { atprotoClient, nsids } from "@/lib/data/atproto/repo"; export async function POST(request: Request) { const auth = request.headers.get("Authorization"); @@ -36,24 +36,23 @@ export async function POST(request: Request) { throw new Error("No AtprotoPersonalDataServer service found"); } + const atproto = atprotoClient(service); + const promises = ops.map(async (op) => { const { collection, rkey } = op.path; console.log("Processing", collection, rkey, op.action); - if (collection === atprotoPost.PostCollection) { + if (collection === nsids.FyiUnravelFrontpagePost) { if (op.action === "create") { - const record = await atprotoGetRecord({ - serviceEndpoint: service, + const postRecord = await atproto.fyi.unravel.frontpage.post.get({ repo, - collection, rkey, }); - const postRecord = atprotoPost.PostRecord.parse(record.value); await dbPost.unauthed_createPost({ - post: postRecord, + post: postRecord.value, rkey, authorDid: repo, - cid: record.cid, + cid: postRecord.cid, offset: seq, }); } else if (op.action === "delete") { @@ -108,7 +107,7 @@ export async function POST(request: Request) { if ( hydratedVoteRecordValue.subject.uri.collection === - atprotoPost.PostCollection + nsids.FyiUnravelFrontpagePost ) { await unauthed_createPostVote({ repo, diff --git a/packages/frontpage/lib/auth.ts b/packages/frontpage/lib/auth.ts index ba161a05..c2f82cc7 100644 --- a/packages/frontpage/lib/auth.ts +++ b/packages/frontpage/lib/auth.ts @@ -448,7 +448,7 @@ export async function importDpopJwks({ } export async function fetchAuthenticatedAtproto( - input: RequestInfo, + input: string | Request | URL, init?: RequestInit, ) { const session = await getSession(); diff --git a/packages/frontpage/lib/data/atproto/comment.ts b/packages/frontpage/lib/data/atproto/comment.ts index 42dfb3ba..eecaaa01 100644 --- a/packages/frontpage/lib/data/atproto/comment.ts +++ b/packages/frontpage/lib/data/atproto/comment.ts @@ -7,9 +7,9 @@ import { import { createAtUriParser } from "./uri"; import { DataLayerError } from "../error"; import { z } from "zod"; -import { PostCollection } from "./post"; import { DID, getPdsUrl } from "./did"; import { MAX_COMMENT_LENGTH } from "../db/constants"; +import { nsids } from "./repo"; export const CommentCollection = "fyi.unravel.frontpage.comment"; @@ -23,7 +23,7 @@ export const CommentRecord = z.object({ .optional(), post: z.object({ cid: z.string(), - uri: createAtUriParser(z.literal(PostCollection)), + uri: createAtUriParser(z.literal(nsids.FyiUnravelFrontpagePost)), }), createdAt: z.string(), }); @@ -49,7 +49,7 @@ export async function createComment({ parent, post, content }: CommentInput) { : undefined, post: { cid: post.cid, - uri: `at://${post.authorDid}/${PostCollection}/${post.rkey}`, + uri: `at://${post.authorDid}/${nsids.FyiUnravelFrontpagePost}/${post.rkey}`, }, createdAt: new Date().toISOString(), }; diff --git a/packages/frontpage/lib/data/atproto/event.ts b/packages/frontpage/lib/data/atproto/event.ts index 04fb4b01..4eb7f5c7 100644 --- a/packages/frontpage/lib/data/atproto/event.ts +++ b/packages/frontpage/lib/data/atproto/event.ts @@ -1,13 +1,13 @@ import "server-only"; import { z } from "zod"; import { CommentCollection } from "./comment"; -import { PostCollection } from "./post"; import { isDid } from "./did"; +import { nsids } from "./repo"; // This module refers to the event emitted by the Firehose export const Collection = z.union([ - z.literal(PostCollection), + z.literal(nsids.FyiUnravelFrontpagePost), z.literal(CommentCollection), z.literal("fyi.unravel.frontpage.vote"), ]); diff --git a/packages/frontpage/lib/data/atproto/post.ts b/packages/frontpage/lib/data/atproto/post.ts deleted file mode 100644 index 8d8f9309..00000000 --- a/packages/frontpage/lib/data/atproto/post.ts +++ /dev/null @@ -1,68 +0,0 @@ -import "server-only"; -import { - atprotoCreateRecord, - atprotoDeleteRecord, - atprotoGetRecord, -} from "./record"; -import { z } from "zod"; -import { DataLayerError } from "../error"; -import { DID, getPdsUrl } from "./did"; -import { MAX_POST_TITLE_LENGTH, MAX_POST_URL_LENGTH } from "../db/constants"; - -export const PostCollection = "fyi.unravel.frontpage.post"; - -export const PostRecord = z.object({ - title: z.string().max(MAX_POST_TITLE_LENGTH), - url: z.string().url().max(MAX_POST_URL_LENGTH), - createdAt: z.string(), -}); - -export type Post = z.infer; - -type PostInput = { - title: string; - url: string; -}; - -export async function createPost({ title, url }: PostInput) { - const record = { title, url, createdAt: new Date().toISOString() }; - const parseResult = PostRecord.safeParse(record); - if (!parseResult.success) { - throw new DataLayerError("Invalid post record", { - cause: parseResult.error, - }); - } - - const result = await atprotoCreateRecord({ - record, - collection: PostCollection, - }); - - return { - rkey: result.uri.rkey, - }; -} - -export async function deletePost(rkey: string) { - await atprotoDeleteRecord({ - rkey, - collection: PostCollection, - }); -} - -export async function getPost({ rkey, repo }: { rkey: string; repo: DID }) { - const service = await getPdsUrl(repo); - - if (!service) { - throw new DataLayerError("Failed to get service url"); - } - - const { value } = await atprotoGetRecord({ - serviceEndpoint: service, - repo, - collection: PostCollection, - rkey, - }); - - return PostRecord.parse(value); -} diff --git a/packages/frontpage/lib/data/atproto/repo.ts b/packages/frontpage/lib/data/atproto/repo.ts new file mode 100644 index 00000000..f8843b8a --- /dev/null +++ b/packages/frontpage/lib/data/atproto/repo.ts @@ -0,0 +1,25 @@ +import { AtpBaseClient } from "@repo/frontpage-atproto-client"; +import { getUser } from "../user"; +import { fetchAuthenticatedAtproto } from "@/lib/auth"; +import { cache } from "react"; + +export { ids as nsids } from "@repo/frontpage-atproto-client/lexicons"; + +export const atprotoClient = cache( + (service?: string) => + new AtpBaseClient(async (url: string, init: RequestInit) => { + const user = await getUser(); + const s = service ?? user?.pdsUrl; + if (!s) { + throw new Error("No service url"); + } + + const u = new URL(url, s); + + if (user) { + return fetchAuthenticatedAtproto(u, init); + } + + return fetch(u, init); + }), +); diff --git a/packages/frontpage/lib/data/atproto/vote.ts b/packages/frontpage/lib/data/atproto/vote.ts index 93bdb788..b1d76095 100644 --- a/packages/frontpage/lib/data/atproto/vote.ts +++ b/packages/frontpage/lib/data/atproto/vote.ts @@ -2,13 +2,13 @@ import "server-only"; import { ensureUser } from "../user"; import { atprotoCreateRecord, atprotoDeleteRecord } from "./record"; import { z } from "zod"; -import { PostCollection } from "./post"; import { CommentCollection } from "./comment"; import { DID } from "./did"; import { createAtUriParser } from "./uri"; +import { nsids } from "./repo"; const VoteSubjectCollection = z.union([ - z.literal(PostCollection), + z.literal(nsids.FyiUnravelFrontpagePost), z.literal(CommentCollection), ]); diff --git a/packages/frontpage/lib/data/db/post.ts b/packages/frontpage/lib/data/db/post.ts index 082d8afd..eab1b572 100644 --- a/packages/frontpage/lib/data/db/post.ts +++ b/packages/frontpage/lib/data/db/post.ts @@ -5,10 +5,10 @@ import { db } from "@/lib/db"; import { eq, sql, desc, and, isNull, or } from "drizzle-orm"; import * as schema from "@/lib/schema"; import { getBlueskyProfile, getUser, isAdmin } from "../user"; -import * as atprotoPost from "../atproto/post"; import { DID } from "../atproto/did"; import { sendDiscordMessage } from "@/lib/discord"; import { newPostAggregateTrigger } from "./triggers"; +import { FyiUnravelFrontpagePost } from "@repo/frontpage-atproto-client"; const buildUserHasVotedQuery = cache(async () => { const user = await getUser(); @@ -165,7 +165,7 @@ export async function uncached_doesPostExist(authorDid: DID, rkey: string) { } type CreatePostInput = { - post: atprotoPost.Post; + post: FyiUnravelFrontpagePost.Record; authorDid: DID; rkey: string; cid: string; diff --git a/packages/frontpage/lib/data/db/shared.ts b/packages/frontpage/lib/data/db/shared.ts index 0b750f5c..66900046 100644 --- a/packages/frontpage/lib/data/db/shared.ts +++ b/packages/frontpage/lib/data/db/shared.ts @@ -1,8 +1,8 @@ import { headers } from "next/headers"; import { CommentCollection } from "../atproto/comment"; import { DID } from "../atproto/did"; -import { PostCollection } from "../atproto/post"; import { getPostFromComment } from "./post"; +import { nsids } from "../atproto/repo"; export const getRootUrl = async () => { const host = @@ -21,7 +21,7 @@ export const createFrontPageLink = async ( rkey?: string, ) => { switch (collection) { - case PostCollection: + case nsids.FyiUnravelFrontpagePost: return `/post/${author}/${rkey}/`; case CommentCollection: diff --git a/packages/frontpage/package.json b/packages/frontpage/package.json index 58749f6f..3b512a40 100644 --- a/packages/frontpage/package.json +++ b/packages/frontpage/package.json @@ -35,6 +35,7 @@ "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-tooltip": "^1.1.1", + "@repo/frontpage-atproto-client": "workspace:*", "@sentry/nextjs": "^8.35.0", "@vercel/analytics": "^1.3.1", "@vercel/speed-insights": "^1.0.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ad1429f..d92abe3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,6 +174,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.10)(react-dom@19.0.0-rc-f994737d14-20240522(react@19.0.0-rc-f994737d14-20240522))(react@19.0.0-rc-f994737d14-20240522) + '@repo/frontpage-atproto-client': + specifier: workspace:* + version: link:../frontpage-atproto-client '@sentry/nextjs': specifier: ^8.35.0 version: 8.35.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.26.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.26.0(@opentelemetry/api@1.9.0))(next@15.0.0(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-8a03594-20241020)(react-dom@19.0.0-rc-f994737d14-20240522(react@19.0.0-rc-f994737d14-20240522))(react@19.0.0-rc-f994737d14-20240522))(react@19.0.0-rc-f994737d14-20240522)(webpack@5.95.0(esbuild@0.19.12))