your personal website on atproto - mirror blento.app
at profile-stuff-2 329 lines 6.9 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 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}