The Appview for the kipclip.com atproto bookmarking service
1/**
2 * Annotation sidecar record helpers.
3 * Handles reading and writing com.kipclip.annotation records on the PDS.
4 */
5
6import { ANNOTATION_COLLECTION, listAllRecords } from "./route-utils.ts";
7import type { AnnotationRecord, EnrichedBookmark } from "../shared/types.ts";
8
9/**
10 * Extract rkey from an AT Protocol URI.
11 * e.g. "at://did:plc:abc/collection/rkey123" → "rkey123"
12 */
13export function extractRkey(uri: string): string | undefined {
14 return uri.split("/").pop();
15}
16
17/**
18 * Merge annotation data onto a bookmark record.
19 * Falls back to $enriched on the bookmark if no annotation exists.
20 */
21export function mapBookmarkRecord(
22 record: any,
23 annotation?: AnnotationRecord,
24): EnrichedBookmark {
25 return {
26 uri: record.uri,
27 cid: record.cid,
28 subject: record.value.subject,
29 createdAt: record.value.createdAt,
30 tags: record.value.tags || [],
31 title: annotation?.title || record.value.$enriched?.title ||
32 record.value.title,
33 description: annotation?.description ||
34 record.value.$enriched?.description,
35 favicon: annotation?.favicon || record.value.$enriched?.favicon,
36 image: annotation?.image || record.value.$enriched?.image,
37 note: annotation?.note,
38 };
39}
40
41/**
42 * Fetch all annotation records and build an rkey → annotation lookup map.
43 */
44export async function fetchAnnotationMap(
45 oauthSession: any,
46): Promise<{ map: Map<string, AnnotationRecord>; ok: boolean }> {
47 try {
48 const records = await listAllRecords(oauthSession, ANNOTATION_COLLECTION);
49 const map = new Map<string, AnnotationRecord>();
50 for (const record of records) {
51 const rkey = extractRkey(record.uri);
52 if (rkey) {
53 map.set(rkey, record.value as AnnotationRecord);
54 }
55 }
56 return { map, ok: true };
57 } catch {
58 return { map: new Map(), ok: false };
59 }
60}
61
62/**
63 * Write an annotation sidecar record via putRecord (upsert).
64 * Returns true on success, false on failure (e.g. missing scope).
65 */
66export async function writeAnnotation(
67 oauthSession: any,
68 rkey: string,
69 annotation: AnnotationRecord,
70): Promise<boolean> {
71 try {
72 const response = await oauthSession.makeRequest(
73 "POST",
74 `${oauthSession.pdsUrl}/xrpc/com.atproto.repo.putRecord`,
75 {
76 headers: { "Content-Type": "application/json" },
77 body: JSON.stringify({
78 repo: oauthSession.did,
79 collection: ANNOTATION_COLLECTION,
80 rkey,
81 record: annotation,
82 }),
83 },
84 );
85 return response.ok;
86 } catch {
87 return false;
88 }
89}