import { Client, ok } from "@atcute/client"; import { now as tidNow } from "@atcute/tid"; import type { Did, Nsid } from "@atcute/lexicons"; import { COLLECTIONS } from "./lib/collections"; import { createDiff } from "./lib/diff"; /** Build an AT-URI from a did, collection, and rkey. */ export function atUri(did: string, collection: string, rkey: string): string { return `at://${did}/${collection}/${rkey}`; } export interface CreateNoteInput { /** Permanent human-readable slug (from the title). */ slug: string; /** Display title (freely updatable). */ title: string; /** AT-URI of the parent wiki record. */ wikiRef: string; /** Initial markdown content (stored as the first revision's diff from ""). */ content: string; /** Optional commit message for the first revision. */ message?: string; } export interface CreatedNote { noteUri: string; revisionUri: string; } /** * Create a new note: write the `wiki.lichen.note` record (rkey = TID) then create * its first `wiki.lichen.noteRevision` (diff from the empty string). */ export async function createNote( client: Client, did: Did, input: CreateNoteInput, ): Promise { const noteTid = tidNow(); const revisionTid = tidNow(); const now = new Date().toISOString(); const noteUri = atUri(did, COLLECTIONS.note, noteTid); const revisionUri = atUri(did, COLLECTIONS.noteRevision, revisionTid); const noteRecord = { $type: COLLECTIONS.note, slug: input.slug, title: input.title, wikiRef: input.wikiRef, createdAt: now, }; await ok( client.post("com.atproto.repo.putRecord", { input: { repo: did, collection: COLLECTIONS.note as Nsid, rkey: noteTid, record: noteRecord, }, }), ); const revisionRecord = { $type: COLLECTIONS.noteRevision, noteRef: noteUri, diff: createDiff("", input.content), diffFormat: "diff-match-patch", createdAt: now, ...(input.message ? { message: input.message } : {}), }; await ok( client.post("com.atproto.repo.putRecord", { input: { repo: did, collection: COLLECTIONS.noteRevision as Nsid, rkey: revisionTid, record: revisionRecord, }, }), ); return { noteUri, revisionUri }; } export interface PushEditInput { /** AT-URI of the note being edited. */ noteRef: string; /** AT-URI of the current head revision (previous revision); omit for the first. */ parentRevision?: string; /** Current known content to diff from. */ oldContent: string; /** New content to store (as the diff). */ newContent: string; /** Optional commit message. */ message?: string; } /** * Push an edit to a note as a new `wiki.lichen.noteRevision` diff. Returns the new * revision URI (to use as `parentRevision` for the next edit). */ export async function pushNoteEdit( client: Client, did: Did, input: PushEditInput, ): Promise { const revisionTid = tidNow(); const revisionUri = atUri(did, COLLECTIONS.noteRevision, revisionTid); const revisionRecord = { $type: COLLECTIONS.noteRevision, noteRef: input.noteRef, diff: createDiff(input.oldContent, input.newContent), diffFormat: "diff-match-patch", createdAt: new Date().toISOString(), ...(input.parentRevision ? { parentRevision: input.parentRevision } : {}), ...(input.message ? { message: input.message } : {}), }; await ok( client.post("com.atproto.repo.putRecord", { input: { repo: did, collection: COLLECTIONS.noteRevision as Nsid, rkey: revisionTid, record: revisionRecord, }, }), ); return revisionUri; } /** Delete a note record on the PDS (revisions remain as history). */ export async function deleteNote( client: Client, did: Did, noteUri: string, ): Promise { const rkey = noteUri.split("/").pop() ?? ""; if (!rkey) throw new Error(`Invalid note URI: ${noteUri}`); await ok( client.post("com.atproto.repo.deleteRecord", { input: { repo: did, collection: COLLECTIONS.note as Nsid, rkey, }, }), ); }