your personal website on atproto - mirror
blento.app
1import type { Did, Handle } from '@atcute/lexicons';
2import { user } from './auth.svelte';
3import {
4 CompositeDidDocumentResolver,
5 CompositeHandleResolver,
6 DohJsonHandleResolver,
7 PlcDidDocumentResolver,
8 WebDidDocumentResolver,
9 WellKnownHandleResolver
10} from '@atcute/identity-resolver';
11import { Client, simpleFetchHandler } from '@atcute/client';
12import type { AppBskyActorDefs } from '@atcute/bluesky';
13import { redirect } from '@sveltejs/kit';
14
15export type Collection = `${string}.${string}.${string}`;
16
17export function parseUri(uri: string) {
18 const [did, collection, rkey] = uri.replace('at://', '').split('/');
19 return { did, collection, rkey } as {
20 collection: `${string}.${string}.${string}`;
21 rkey: string;
22 did: Did;
23 };
24}
25
26export async function resolveHandle({ handle }: { handle: Handle }) {
27 const handleResolver = new CompositeHandleResolver({
28 methods: {
29 dns: new DohJsonHandleResolver({ dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query' }),
30 http: new WellKnownHandleResolver()
31 }
32 });
33
34 try {
35 const data = await handleResolver.resolve(handle);
36 return data;
37 } catch (error) {
38 redirect(307, '/?error=handle_not_found&handle=' + handle);
39 }
40}
41
42const didResolver = new CompositeDidDocumentResolver({
43 methods: {
44 plc: new PlcDidDocumentResolver(),
45 web: new WebDidDocumentResolver()
46 }
47});
48
49export async function getPDS(did: Did) {
50 const doc = await didResolver.resolve(did as `did:plc:${string}` | `did:web:${string}`);
51 if (!doc.service) throw new Error('No PDS found');
52 for (const service of doc.service) {
53 if (service.id === '#atproto_pds') {
54 return service.serviceEndpoint.toString();
55 }
56 }
57}
58
59export async function getDetailedProfile(data?: { did?: Did; client?: Client }) {
60 data ??= {};
61 data.did ??= user.did;
62
63 if (!data.did) throw new Error('Error getting detailed profile: no did');
64
65 data.client ??= new Client({
66 handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
67 });
68
69 const response = await data.client.get('app.bsky.actor.getProfile', {
70 params: { actor: data.did }
71 });
72
73 if (!response.ok) return;
74
75 return response.data;
76}
77
78export async function getAuthorFeed(data?: {
79 did?: Did;
80 client?: Client;
81 filter?: string;
82 limit?: number;
83}) {
84 data ??= {};
85 data.did ??= user.did;
86
87 if (!data.did) throw new Error('Error getting detailed profile: no did');
88
89 data.client ??= new Client({
90 handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' })
91 });
92
93 const response = await data.client.get('app.bsky.feed.getAuthorFeed', {
94 params: { actor: data.did, filter: data.filter ?? 'posts_with_media', limit: data.limit || 100 }
95 });
96
97 if (!response.ok) return;
98
99 return response.data;
100}
101
102export async function getClient({ did }: { did: Did }) {
103 const pds = await getPDS(did);
104 if (!pds) throw new Error('PDS not found');
105
106 const client = new Client({
107 handler: simpleFetchHandler({ service: pds })
108 });
109
110 return client;
111}
112
113export async function listRecords({
114 did,
115 collection,
116 cursor,
117 limit = 0,
118 client
119}: {
120 did?: Did;
121 collection: `${string}.${string}.${string}`;
122 cursor?: string;
123 limit?: number;
124 client?: Client;
125}) {
126 did ??= user.did;
127 if (!collection) {
128 throw new Error('Missing parameters for listRecords');
129 }
130 if (!did) {
131 throw new Error('Missing did for getRecord');
132 }
133
134 client ??= await getClient({ did });
135
136 const allRecords = [];
137
138 let currentCursor = cursor;
139 do {
140 const response = await client.get('com.atproto.repo.listRecords', {
141 params: {
142 repo: did,
143 collection,
144 limit: limit || 100,
145 cursor: currentCursor
146 }
147 });
148
149 if (!response.ok) {
150 return allRecords;
151 }
152
153 allRecords.push(...response.data.records);
154 currentCursor = response.data.cursor;
155 } while (currentCursor && (!limit || allRecords.length < limit));
156
157 return allRecords;
158}
159
160export async function getRecord({
161 did,
162 collection,
163 rkey,
164 client
165}: {
166 did?: Did;
167 collection: Collection;
168 rkey?: string;
169 client?: Client;
170}) {
171 did ??= user.did;
172 rkey ??= 'self';
173
174 if (!collection) {
175 throw new Error('Missing parameters for getRecord');
176 }
177 if (!did) {
178 throw new Error('Missing did for getRecord');
179 }
180
181 client ??= await getClient({ did });
182
183 const record = await client.get('com.atproto.repo.getRecord', {
184 params: {
185 repo: did,
186 collection,
187 rkey
188 }
189 });
190
191 return JSON.parse(JSON.stringify(record.data));
192}
193
194export async function putRecord({
195 collection,
196 rkey,
197 record
198}: {
199 collection: Collection;
200 rkey: string;
201 record: Record<string, unknown>;
202}) {
203 if (!user.client || !user.did) throw new Error('No rpc or did');
204
205 const response = await user.client.post('com.atproto.repo.putRecord', {
206 input: {
207 collection,
208 repo: user.did,
209 rkey,
210 record: {
211 ...record
212 }
213 }
214 });
215
216 return response;
217}
218
219export async function deleteRecord({ collection, rkey }: { collection: Collection; rkey: string }) {
220 if (!user.client || !user.did) throw new Error('No profile or rpc or did');
221
222 const response = await user.client.post('com.atproto.repo.deleteRecord', {
223 input: {
224 collection,
225 repo: user.did,
226 rkey
227 }
228 });
229
230 return response.ok;
231}
232
233export async function uploadBlob({ blob }: { blob: Blob }) {
234 if (!user.did || !user.client) throw new Error("Can't upload blob: Not logged in");
235
236 const blobResponse = await user.client.post('com.atproto.repo.uploadBlob', {
237 input: blob,
238 data: {
239 repo: user.did
240 }
241 });
242
243 if (!blobResponse?.ok) {
244 return;
245 }
246
247 const blobInfo = blobResponse?.data.blob as {
248 $type: 'blob';
249 ref: {
250 $link: string;
251 };
252 mimeType: string;
253 size: number;
254 };
255
256 return blobInfo;
257}
258
259export async function describeRepo({ client, did }: { client?: Client; did?: Did }) {
260 did ??= user.did;
261 if (!did) {
262 throw new Error('Error describeRepo: No did');
263 }
264 client ??= await getClient({ did });
265
266 const repo = await client.get('com.atproto.repo.describeRepo', {
267 params: {
268 repo: did
269 }
270 });
271 if (!repo.ok) return;
272
273 return repo.data;
274}
275
276export async function getBlobURL({
277 did,
278 blob
279}: {
280 did: Did;
281 blob: {
282 $type: 'blob';
283 ref: {
284 $link: string;
285 };
286 };
287}) {
288 const pds = await getPDS(did);
289 return `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${blob.ref.$link}`;
290}
291
292export function getImageBlobUrl({
293 did,
294 blob
295}: {
296 did: string;
297 blob: {
298 $type: 'blob';
299 ref: {
300 $link: string;
301 };
302 };
303}) {
304 if (!did || !blob?.ref?.$link) return '';
305 return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@webp`;
306}
307
308export async function searchActorsTypeahead(
309 q: string,
310 limit: number = 10,
311 host?: string
312): Promise<{ actors: AppBskyActorDefs.ProfileViewBasic[]; q: string }> {
313 host ??= 'https://public.api.bsky.app';
314
315 const client = new Client({
316 handler: simpleFetchHandler({ service: host })
317 });
318
319 const response = await client.get('app.bsky.actor.searchActorsTypeahead', {
320 params: {
321 q,
322 limit
323 }
324 });
325
326 if (!response.ok) return { actors: [], q };
327
328 return { actors: response.data.actors, q };
329}