The recipes.blue monorepo
recipes.blue
recipes
appview
atproto
1import type {} from '@atcute/atproto';
2import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from '@atcute/identity-resolver';
3import { CompositeHandleResolver, DohJsonHandleResolver, WellKnownHandleResolver } from '@atcute/identity-resolver';
4import { ActorIdentifier, AtprotoDid, Handle, isHandle } from '@atcute/lexicons/syntax';
5import { isAtprotoDid } from '@atcute/identity';
6import { RedisClient } from 'bun';
7import { ProfileViewBasic } from '../../../../libs/lexicons/dist/types/blue/recipes/actor/defs.js';
8import { Blob, LegacyBlob } from '@atcute/lexicons';
9import { buildCdnUrl } from './cdn.js';
10
11const handleResolver = new CompositeHandleResolver({
12 strategy: 'race',
13 methods: {
14 dns: new DohJsonHandleResolver({ dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query' }),
15 http: new WellKnownHandleResolver({ fetch }),
16 },
17});
18
19const didResolver = new CompositeDidDocumentResolver({
20 methods: {
21 plc: new PlcDidDocumentResolver(),
22 web: new WebDidDocumentResolver(),
23 }
24});
25
26const HANDLE_CACHE_TTL = 5 * 60; // 5 minutes
27
28export const parseDid = async (id: ActorIdentifier): Promise<AtprotoDid> => {
29 if (isAtprotoDid(id)) return id;
30 if (isHandle(id)) {
31 return await handleResolver.resolve(id);
32 }
33 throw Error("Invalid DID or Handle!");
34}
35
36export const getHandle = async (did: AtprotoDid, redis: RedisClient): Promise<Handle> => {
37 let handle = await redis.get(`handle:${did}`) as Handle | null;
38 if (!handle) {
39 const didDoc = await didResolver.resolve(did);
40 if (didDoc.alsoKnownAs == null || didDoc.alsoKnownAs.length < 1) {
41 throw new Error(`User ${did} had no resolvable DID document.`);
42 }
43 handle = didDoc.alsoKnownAs[0]!.substring(5) as Handle;
44 redis.setex(`handle:${did}`, HANDLE_CACHE_TTL, handle);
45 }
46
47 return handle;
48}
49
50export const buildProfileViewBasic = async (author: {
51 did: AtprotoDid;
52 displayName: string;
53 pronouns: string | null;
54 avatarRef: Blob | LegacyBlob | null;
55 createdAt: Date;
56}, redis: RedisClient): Promise<ProfileViewBasic> => ({
57 did: author.did,
58 handle: await getHandle(author.did, redis),
59 displayName: author.displayName,
60 pronouns: author.pronouns ?? undefined,
61 avatar: author.avatarRef ? buildCdnUrl('avatar', author.did, author.avatarRef) : undefined,
62 createdAt: author.createdAt.toISOString(),
63});