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 if (!record.ok) return;
192
193 return JSON.parse(JSON.stringify(record.data));
194}
195
196export async function putRecord({
197 collection,
198 rkey,
199 record
200}: {
201 collection: Collection;
202 rkey: string;
203 record: Record<string, unknown>;
204}) {
205 if (!user.client || !user.did) throw new Error('No rpc or did');
206
207 const response = await user.client.post('com.atproto.repo.putRecord', {
208 input: {
209 collection,
210 repo: user.did,
211 rkey,
212 record: {
213 ...record
214 }
215 }
216 });
217
218 return response;
219}
220
221export async function deleteRecord({ collection, rkey }: { collection: Collection; rkey: string }) {
222 if (!user.client || !user.did) throw new Error('No profile or rpc or did');
223
224 const response = await user.client.post('com.atproto.repo.deleteRecord', {
225 input: {
226 collection,
227 repo: user.did,
228 rkey
229 }
230 });
231
232 return response.ok;
233}
234
235export async function uploadBlob({ blob }: { blob: Blob }) {
236 if (!user.did || !user.client) throw new Error("Can't upload blob: Not logged in");
237
238 const blobResponse = await user.client.post('com.atproto.repo.uploadBlob', {
239 input: blob,
240 data: {
241 repo: user.did
242 }
243 });
244
245 if (!blobResponse?.ok) {
246 return;
247 }
248
249 const blobInfo = blobResponse?.data.blob as {
250 $type: 'blob';
251 ref: {
252 $link: string;
253 };
254 mimeType: string;
255 size: number;
256 };
257
258 return blobInfo;
259}
260
261export async function describeRepo({ client, did }: { client?: Client; did?: Did }) {
262 did ??= user.did;
263 if (!did) {
264 throw new Error('Error describeRepo: No did');
265 }
266 client ??= await getClient({ did });
267
268 const repo = await client.get('com.atproto.repo.describeRepo', {
269 params: {
270 repo: did
271 }
272 });
273 if (!repo.ok) return;
274
275 return repo.data;
276}
277
278export async function getBlobURL({
279 did,
280 blob
281}: {
282 did: Did;
283 blob: {
284 $type: 'blob';
285 ref: {
286 $link: string;
287 };
288 };
289}) {
290 const pds = await getPDS(did);
291 return `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${blob.ref.$link}`;
292}
293
294export function getImageBlobUrl({
295 did,
296 blob
297}: {
298 did: string;
299 blob: {
300 $type: 'blob';
301 ref: {
302 $link: string;
303 };
304 };
305}) {
306 if (!did || !blob?.ref?.$link) return '';
307 return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@webp`;
308}
309
310export async function searchActorsTypeahead(
311 q: string,
312 limit: number = 10,
313 host?: string
314): Promise<{ actors: AppBskyActorDefs.ProfileViewBasic[]; q: string }> {
315 host ??= 'https://public.api.bsky.app';
316
317 const client = new Client({
318 handler: simpleFetchHandler({ service: host })
319 });
320
321 const response = await client.get('app.bsky.actor.searchActorsTypeahead', {
322 params: {
323 q,
324 limit
325 }
326 });
327
328 if (!response.ok) return { actors: [], q };
329
330 return { actors: response.data.actors, q };
331}