import { atUriOf, datetimeOf, didOf, nsidOf, recordKeyOf, type AtUri, type Cid, type Datetime, type Did, type GitOid, type KnotOrigin, type Rkey, type RawResourceUri } from "$lib/api/syntax"; import { parseCanonicalResourceUri } from "@atcute/lexicons/syntax"; import { now as tidNow } from "@atcute/tid"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import { jsonGet } from "$lib/api/_request"; import { awaitReadable, awaitRemoved } from "$lib/api/awaitRecord"; import { writeTargetFor } from "$lib/api/knot"; import { createCob, eraseCob, updateCob, withRkey, type CobRecord, type CobRef, type KnotBlobReceipt, type Written } from "$lib/api/cob"; import type { RecordView } from "$lib/api/records"; import { deleteRecord, putRecord } from "$lib/api/write"; import type * as OrgTangledFeedComment from "$lib/api/lexicons/types/org/tangled/feed/comment"; import type * as ShTangledFeedComment from "$lib/api/lexicons/types/sh/tangled/feed/comment"; import type * as ShTangledFeedDefs from "$lib/api/lexicons/types/sh/tangled/feed/defs"; export type CommentRecord = OrgTangledFeedComment.Main; export type LegacyCommentRecord = ShTangledFeedComment.Main; export const COMMENT_COLLECTION = nsidOf("org.tangled.feed.comment"); const LEGACY_COMMENT_COLLECTION = nsidOf("sh.tangled.feed.comment"); // subject and replyTo have to name the current version or the knot refuses export interface CommentDraft { subject: CobRef; replyTo?: CobRef; text: string; blobs?: KnotBlobReceipt[]; } const imageEmbeds = (blobs: readonly KnotBlobReceipt[]) => blobs.map((image) => ({ $type: "org.tangled.markup.embed.image" as const, image })); const commentRecord = ( { subject, replyTo, text, blobs }: CommentDraft, createdAt = datetimeOf(new Date().toISOString()) ): CommentRecord => ({ $type: "org.tangled.feed.comment", subject: { $type: "com.atproto.repo.strongRef", ...subject }, replyTo, body: { $type: "org.tangled.markup.markdown", text, ...(blobs?.length ? { blobs: imageEmbeds(blobs) } : {}) }, createdAt }); export const putComment = async ( agent: OAuthUserAgent, rkey: Rkey, draft: CommentDraft, createdAt?: Datetime ): Promise & { uri: RawResourceUri }> => { const record = commentRecord(draft, createdAt); const { uri, cid } = await putRecord(agent, COMMENT_COLLECTION, rkey, record); return { uri, cid, value: record }; }; export const deleteComment = async (agent: OAuthUserAgent, rkey: Rkey): Promise<{ uri: AtUri }> => { const uri = atUriOf(`at://${agent.sub}/${COMMENT_COLLECTION}/${rkey}`); await deleteRecord(agent, COMMENT_COLLECTION, rkey); return { uri }; }; export interface CommentInput { subject: { uri: AtUri; cid: Cid }; replyTo?: { uri: AtUri; cid: Cid }; text: string; blobs?: KnotBlobReceipt[]; embed?: { repo: Did; commit: GitOid }; createdAt: Datetime; } const legacyRecord = (input: CommentInput): LegacyCommentRecord => ({ $type: "sh.tangled.feed.comment", subject: input.subject as LegacyCommentRecord["subject"], ...(input.replyTo ? { replyTo: input.replyTo as LegacyCommentRecord["replyTo"] } : {}), ...(input.embed ? { embed: { $type: "sh.tangled.embed.commit", repo: input.embed.repo, commit: { $type: "sh.tangled.git.oid", oid: input.embed.commit } } as LegacyCommentRecord["embed"] } : {}), body: { $type: "sh.tangled.markup.markdown", text: input.text } as LegacyCommentRecord["body"], createdAt: input.createdAt as LegacyCommentRecord["createdAt"] }); export const openComment = async ( agent: OAuthUserAgent, bobbinUrl: string | undefined, repoDid: Did, input: CommentInput ): Promise => { const target = await writeTargetFor(bobbinUrl, repoDid); if (target.kind === "knot") { const record = commentRecord(input, input.createdAt); return withRkey(await createCob(agent, target.knot, repoDid, COMMENT_COLLECTION, record)); } const saved = await putRecord( agent, LEGACY_COMMENT_COLLECTION, recordKeyOf(tidNow()), legacyRecord(input) ); await awaitReadable(bobbinUrl, saved); return withRkey(saved); }; export const editCommentOn = async ( agent: OAuthUserAgent, bobbinUrl: string | undefined, repoDid: Did, input: CommentInput & { rkey: Rkey; swapRecord: Cid } ): Promise => { const target = await writeTargetFor(bobbinUrl, repoDid); const uri = atUriOf(`at://${repoDid}/${COMMENT_COLLECTION}/${input.rkey}`); if (target.kind === "knot") { const saved = await updateCob(agent, target.knot, uri, (current) => ({ ...current, body: { ...current.body, text: input.text, ...(input.blobs?.length ? { blobs: imageEmbeds(input.blobs) } : {}) } })); return withRkey(saved); } const saved = await putRecord( agent, LEGACY_COMMENT_COLLECTION, input.rkey, legacyRecord(input), input.swapRecord ); await awaitReadable(bobbinUrl, saved); return withRkey(saved); }; export const removeComment = async ( agent: OAuthUserAgent, bobbinUrl: string | undefined, repoDid: Did, input: { rkey: Rkey; swapRecord: Cid } ): Promise<{ uri: AtUri }> => { const target = await writeTargetFor(bobbinUrl, repoDid); const uri = atUriOf(`at://${repoDid}/${COMMENT_COLLECTION}/${input.rkey}`); if (target.kind === "knot") await eraseCob(agent, target.knot, { uri, cid: input.swapRecord }); else { await deleteRecord(agent, LEGACY_COMMENT_COLLECTION, input.rkey, input.swapRecord); await awaitRemoved(bobbinUrl, uri); } return { uri }; }; export const createComment = async ( agent: OAuthUserAgent, knot: KnotOrigin, draft: CommentDraft ): Promise> => { const record = commentRecord(draft); const { repo } = parseCanonicalResourceUri(draft.subject.uri); return { ...(await createCob(agent, knot, didOf(repo), COMMENT_COLLECTION, record)), value: record }; }; export const editComment = ( agent: OAuthUserAgent, knot: KnotOrigin, uri: AtUri, text: string ): Promise> => updateCob(agent, knot, uri, (current) => ({ ...current, body: { ...current.body, text } })); export const eraseComment = ( agent: OAuthUserAgent, knot: KnotOrigin, from: CobRef ): Promise => eraseCob(agent, knot, from); export interface CommentListPage { cursor?: string | null; items: ShTangledFeedDefs.CommentView[]; } export const listComments = ( ctx: BobbinContext, subject: AtUri, filter: { limit?: number; cursor?: string; order?: "asc" | "desc" } = {}, init?: XrpcRequestInit ) => jsonGet(ctx, "sh.tangled.feed.listComments", { subject, ...filter }, init);