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 { SocialClipprActorDefs } from "@clipprjs/lexicons";
8import type { ErrorResponse } from "./types.js";
9import { getDidFromHandle, getHandleFromDid } from "../network/converters.js";
10import { Database } from "../db/database.js";
11import { usersTable } from "../db/schema.js";
12import { eq } from "drizzle-orm";
13
14const db = Database.getInstance().getDb();
15
16// TODO: Stop leeching off the Bluesky CDN and get the blob directly from the user's PDS
17//
18// Get a CDN URI from a blob's CID
19export async function createAvatarLink(
20 did: string,
21 cid: string,
22): Promise<string> {
23 return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`;
24}
25
26export async function createProfileView(
27 actor: string,
28): Promise<SocialClipprActorDefs.ProfileView | ErrorResponse> {
29 let viewHandle;
30 let viewDid;
31
32 if (actor.startsWith("did:")) {
33 viewDid = actor;
34 try {
35 viewHandle = await getHandleFromDid(viewDid);
36 viewHandle = viewHandle.replace("at://", "");
37 } catch (e: unknown) {
38 if (e instanceof Error) {
39 return {
40 error: "InvalidRequest",
41 message: `Error: A queried URI does not have a valid DID or handle: ${e.message}`,
42 };
43 } else {
44 return {
45 error: "InvalidRequest",
46 message:
47 "Error: A queried URI does not have a valid DID or handle: unknown error",
48 };
49 }
50 }
51 } else {
52 viewHandle = actor;
53 viewHandle = viewHandle.replace("at://", "");
54 try {
55 viewDid = await getDidFromHandle(viewHandle);
56 } catch (e: unknown) {
57 if (e instanceof Error) {
58 return {
59 error: "InvalidRequest",
60 message: `Error: A queried URI does not have a valid DID or handle: ${e.message}`,
61 };
62 } else {
63 return {
64 error: "InvalidRequest",
65 message:
66 "Error: A queried URI does not have a valid DID or handle: unknown error",
67 };
68 }
69 }
70 }
71
72 const dbQuery = await db
73 .selectDistinct()
74 .from(usersTable)
75 .where(eq(usersTable.did, viewDid));
76
77 if (dbQuery.length === 0) {
78 return {
79 error: "InvalidRequest",
80 message: "Could not find a queried URI's profile",
81 } as ErrorResponse;
82 }
83
84 const avatarCid = dbQuery[0]?.avatar;
85
86 let viewAvatar;
87
88 if (avatarCid === undefined || avatarCid === null) {
89 viewAvatar = "https://missing.avatar";
90 } else viewAvatar = await createAvatarLink(viewDid, avatarCid);
91
92 return {
93 $type: "social.clippr.actor.defs#profileView",
94 did: viewDid as `did:${string}:${string}`,
95 handle: viewHandle as `${string}.${string}`,
96 avatar: (viewAvatar as `${string}:${string}`) || undefined,
97 createdAt: dbQuery[0]?.createdAt.toISOString(),
98 description: dbQuery[0]?.description || undefined,
99 displayName: dbQuery[0]?.displayName || viewHandle,
100 };
101}