social bookmarking for atproto
at main 123 lines 3.2 kB view raw
1/* 2 * clippr: a social bookmarking service for the AT Protocol 3 * Copyright (c) 2025 clippr contributors. 4 * SPDX-License-Identifier: AGPL-3.0-only 5 */ 6 7import { 8 SocialClipprActorDefs, 9 SocialClipprFeedClip, 10 SocialClipprFeedDefs, 11} from "@clipprjs/lexicons"; 12import type { ClipViewQuery, ErrorResponse, TagRef } from "./types.js"; 13import { getHandleFromDid } from "../network/converters.js"; 14import { Database } from "../db/database.js"; 15import { clipsTable } from "../db/schema.js"; 16import { and, eq } from "drizzle-orm"; 17import { createProfileView } from "./profile.js"; 18import { is } from "@atcute/lexicons"; 19import { validateHash } from "../hasher.js"; 20 21const db = Database.getInstance().getDb(); 22 23export async function createClipView( 24 query: ClipViewQuery, 25): Promise<SocialClipprFeedDefs.ClipView | ErrorResponse> { 26 if (!query.did.startsWith("did:")) { 27 let did; 28 try { 29 did = await getHandleFromDid(query.did); 30 } catch (e: unknown) { 31 if (e instanceof Error) { 32 return { 33 error: "InvalidRequest", 34 message: `Error: A queried URI does not have a valid DID or handle: ${e.message}`, 35 }; 36 } else { 37 return { 38 error: "InvalidRequest", 39 message: 40 "Error: A queried URI does not have a valid DID or handle: unknown error", 41 }; 42 } 43 } 44 query.did = did; 45 } 46 47 if (query.collection !== "social.clippr.feed.clip") { 48 return { 49 error: "InvalidRequest", 50 message: "Error: A queried URI is not a proper clip", 51 }; 52 } 53 54 const dbQuery = await db 55 .selectDistinct() 56 .from(clipsTable) 57 .where( 58 and( 59 eq(clipsTable.did, query.did), 60 eq(clipsTable.recordKey, query.recordKey), 61 ), 62 ); 63 64 if (dbQuery.length === 0) { 65 return { 66 error: "InvalidRequest", 67 message: "Could not find a given URI", 68 }; 69 } 70 71 // Yes, the array thing is not ideal. 72 if (!dbQuery[0]?.cid) { 73 return { 74 error: "InvalidRequest", 75 message: "Could not find a given URI", 76 }; 77 } 78 79 if (!(await validateHash(dbQuery[0]?.url, query.recordKey))) { 80 return { 81 error: "InvalidRequest", 82 message: "Could not find a given URI", 83 }; 84 } 85 86 const authorView: ErrorResponse | SocialClipprActorDefs.ProfileView = 87 await createProfileView(query.did); 88 89 if (!is(SocialClipprActorDefs.profileViewSchema, authorView)) { 90 console.log(authorView); 91 return { 92 error: "InvalidRequest", 93 message: "Could not validate profile view", // I can't get the error message, it seems to always assume the type is the ProfileView 94 } as ErrorResponse; 95 } 96 97 let clipTags: TagRef[] | undefined; 98 99 if (dbQuery[0]?.tags === null) { 100 clipTags = undefined; 101 } 102 103 const clipRecord: SocialClipprFeedClip.Main = { 104 $type: "social.clippr.feed.clip", 105 url: dbQuery[0]?.url as `${string}:${string}`, 106 title: dbQuery[0]?.title, 107 description: dbQuery[0]?.description, 108 tags: clipTags || undefined, 109 unlisted: dbQuery[0]?.unlisted, 110 unread: dbQuery[0]?.unread || undefined, 111 notes: dbQuery[0]?.notes || undefined, 112 languages: dbQuery[0]?.languages || undefined, 113 createdAt: dbQuery[0]?.createdAt.toISOString(), 114 }; 115 116 return { 117 cid: dbQuery[0]?.cid, 118 uri: `at://${query.did}/${query.collection}/${query.recordKey}`, 119 author: authorView, 120 record: clipRecord, 121 indexedAt: dbQuery[0]?.indexedAt.toISOString(), 122 }; 123}