your personal website on atproto - mirror blento.app
at small-fixes 331 lines 7.0 kB view raw
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}