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