The Appview for the kipclip.com atproto bookmarking service
at main 89 lines 2.5 kB view raw
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}