open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 108 lines 3.8 kB view raw
1// SPDX-License-Identifier: MIT 2// Copyright (c) 2026 sol pbc 3 4import { errorMessage } from './error-format.js'; 5 6const PLC_DIRECTORY = 'https://plc.directory'; 7const pdsCache = new Map(); 8 9function requestErrorMessage(method, url, err) { 10 const code = err?.cause?.code || err?.code; 11 if (code === 'ECONNREFUSED') return `could not connect to ${url} (refused)`; 12 if (code === 'ENOTFOUND') return `could not resolve ${url}`; 13 if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT') { 14 return `timed out connecting to ${url}`; 15 } 16 return `request to ${url} failed: ${errorMessage(err)}`; 17} 18 19async function fetchJson(method, url) { 20 const requestUrl = url.toString(); 21 try { 22 const res = await fetch(url); 23 if (!res.ok) throw new Error(`${method} ${requestUrl} returned ${res.status}`); 24 return await res.json(); 25 } catch (err) { 26 if (err instanceof Error && err.message.startsWith(`${method} ${requestUrl} returned `)) { 27 throw err; 28 } 29 throw new Error(requestErrorMessage(method, requestUrl, err), { cause: err }); 30 } 31} 32 33async function fetchDidDocument(did) { 34 let url; 35 if (did.startsWith('did:web:')) { 36 const rest = did.slice('did:web:'.length); 37 const parts = rest.split(':'); 38 const domain = decodeURIComponent(parts[0]); 39 if (parts.length === 1) { 40 url = `https://${domain}/.well-known/did.json`; 41 } else { 42 url = `https://${domain}/${parts.slice(1).map(decodeURIComponent).join('/')}/did.json`; 43 } 44 } else { 45 url = `${PLC_DIRECTORY}/${did}`; 46 } 47 48 return fetchJson('GET', url); 49} 50 51export async function resolvePds(did) { 52 if (pdsCache.has(did)) return pdsCache.get(did); 53 const doc = await fetchDidDocument(did); 54 const pds = doc.service?.find(s => s.type === 'AtprotoPersonalDataServer'); 55 if (!pds?.serviceEndpoint) throw new Error(`no PDS found in DID document for ${did}`); 56 pdsCache.set(did, pds.serviceEndpoint); 57 return pds.serviceEndpoint; 58} 59 60export async function listRecordsFromPds(pdsUrl, repo, collection, limit) { 61 const records = []; 62 let cursor; 63 do { 64 const url = new URL('/xrpc/com.atproto.repo.listRecords', pdsUrl); 65 url.searchParams.set('repo', repo); 66 url.searchParams.set('collection', collection); 67 if (limit) url.searchParams.set('limit', String(limit)); 68 if (cursor) url.searchParams.set('cursor', cursor); 69 const data = await fetchJson('GET', url); 70 records.push(...data.records); 71 cursor = data.cursor; 72 } while (cursor); 73 return { records }; 74} 75 76export async function resolveHandleFromDid(did) { 77 try { 78 const doc = await fetchDidDocument(did); 79 const aka = doc.alsoKnownAs?.find(a => a.startsWith('at://')); 80 return aka ? aka.replace('at://', '') : did; 81 } catch (err) { 82 console.warn(`warning: failed to resolve handle for ${did}: ${errorMessage(err)}`); 83 return did; 84 } 85} 86 87export async function resolveHandle(handle) { 88 const url = `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`; 89 const data = await fetchJson('GET', url); 90 return data.did; 91} 92 93export async function batchQuery(items, fn, { batchSize = 10, verbose = false } = {}) { 94 if (verbose) console.log(`[verbose] querying ${items.length} accounts in batches of ${batchSize}`); 95 const results = []; 96 for (let i = 0; i < items.length; i += batchSize) { 97 const chunk = items.slice(i, i + batchSize); 98 const settled = await Promise.allSettled(chunk.map(fn)); 99 for (let j = 0; j < settled.length; j++) { 100 if (settled[j].status === 'fulfilled' && settled[j].value !== undefined) { 101 results.push(settled[j].value); 102 } else if (settled[j].status === 'rejected' && verbose) { 103 console.log(`[verbose] ${chunk[j]}: error: ${settled[j].reason?.message || settled[j].reason}`); 104 } 105 } 106 } 107 return results; 108}