Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227import { 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 { attest } from "$lib/api/attestation";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 refusesexport 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<RecordView<CommentRecord> & { 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<Written> => { const target = await writeTargetFor(bobbinUrl, repoDid); if (target.kind === "knot") { const record = commentRecord(input, input.createdAt); const written = withRkey( await createCob(agent, target.knot, repoDid, COMMENT_COLLECTION, record) ); attest(agent, written, target.knot); return written; } 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<Written> => { const target = await writeTargetFor(bobbinUrl, repoDid); const uri = atUriOf(`at://${repoDid}/${COMMENT_COLLECTION}/${input.rkey}`); if (target.kind === "knot") { const saved = await updateCob<CommentRecord>(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<CobRecord<CommentRecord>> => { 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<CobRecord<CommentRecord>> => updateCob<CommentRecord>(agent, knot, uri, (current) => ({ ...current, body: { ...current.body, text } }));
export const eraseComment = ( agent: OAuthUserAgent, knot: KnotOrigin, from: CobRef): Promise<void> => 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<CommentListPage>(ctx, "sh.tangled.feed.listComments", { subject, ...filter }, init);