social bookmarking for atproto
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}