import { AtpAgent } from '@atproto/api' import type { Profile, BlogPost, NetworkConfig } from '../types.js' const agents: Map = new Map() let networkConfig: NetworkConfig | null = null export function setNetworkConfig(config: NetworkConfig): void { networkConfig = config } function getPlc(): string { return networkConfig?.plc || 'https://plc.directory' } function getBsky(): string { return networkConfig?.bsky || 'https://public.api.bsky.app' } export function getAgent(service: string): AtpAgent { if (!agents.has(service)) { agents.set(service, new AtpAgent({ service })) } return agents.get(service)! } export async function resolvePds(did: string): Promise { const res = await fetch(`${getPlc()}/${did}`) const doc = await res.json() const service = doc.service?.find((s: any) => s.type === 'AtprotoPersonalDataServer') return service?.serviceEndpoint || getBsky() } export async function resolveHandle(handle: string): Promise { const agent = getAgent(getBsky()) const res = await agent.resolveHandle({ handle }) return res.data.did } export async function getProfile(actor: string): Promise { const agent = getAgent(getBsky()) const res = await agent.getProfile({ actor }) return { did: res.data.did, handle: res.data.handle, displayName: res.data.displayName, description: res.data.description, avatar: res.data.avatar, banner: res.data.banner, } } export async function listRecords( did: string, collection: string, limit = 50 ): Promise { const pds = await resolvePds(did) const agent = getAgent(pds) const res = await agent.com.atproto.repo.listRecords({ repo: did, collection, limit, }) return res.data.records.map((record: any) => ({ uri: record.uri, cid: record.cid, title: record.value.title || '', content: record.value.content || '', createdAt: record.value.createdAt || '', })) } export async function getRecord( did: string, collection: string, rkey: string ): Promise { const pds = await resolvePds(did) const agent = getAgent(pds) try { const res = await agent.com.atproto.repo.getRecord({ repo: did, collection, rkey, }) return { uri: res.data.uri, cid: res.data.cid || '', title: (res.data.value as any).title || '', content: (res.data.value as any).content || '', createdAt: (res.data.value as any).createdAt || '', } } catch { return null } } export async function describeRepo(did: string): Promise { const pds = await resolvePds(did) const agent = getAgent(pds) const res = await agent.com.atproto.repo.describeRepo({ repo: did }) return res.data.collections || [] } export async function listRecordsRaw( did: string, collection: string, limit = 100 ): Promise { const pds = await resolvePds(did) const agent = getAgent(pds) const res = await agent.com.atproto.repo.listRecords({ repo: did, collection, limit, }) return res.data.records } export async function getRecordRaw( did: string, collection: string, rkey: string ): Promise { const pds = await resolvePds(did) const agent = getAgent(pds) try { const res = await agent.com.atproto.repo.getRecord({ repo: did, collection, rkey, }) return res.data } catch { return null } } // Known lexicon prefixes that have schemas const KNOWN_LEXICON_PREFIXES = [ 'app.bsky.', 'chat.bsky.', 'com.atproto.', 'sh.tangled.', 'pub.leaflet.', 'blue.linkat.', 'fyi.unravel.frontpage.', 'com.whtwnd.', 'com.shinolabs.pinksea.', ] export function hasKnownSchema(nsid: string): boolean { return KNOWN_LEXICON_PREFIXES.some(prefix => nsid.startsWith(prefix)) } export async function fetchLexicon(nsid: string): Promise { // Check if it's a known lexicon first if (hasKnownSchema(nsid)) { return { id: nsid, known: true } } // Extract authority from NSID (e.g., "ai.syui.log.post" -> "syui.ai") const parts = nsid.split('.') if (parts.length < 3) return null const authority = parts.slice(0, 2).reverse().join('.') const url = `https://${authority}/.well-known/lexicon/${nsid}.json` try { const res = await fetch(url) if (!res.ok) return null return await res.json() } catch { return null } } // Known service mappings for collections const SERVICE_MAP: Record = { 'app.bsky': { domain: 'bsky.app', icon: 'https://bsky.app/static/favicon-32x32.png' }, 'chat.bsky': { domain: 'bsky.app', icon: 'https://bsky.app/static/favicon-32x32.png' }, 'ai.syui': { domain: 'syui.ai' }, 'com.whtwnd': { domain: 'whtwnd.com' }, 'fyi.unravel.frontpage': { domain: 'frontpage.fyi' }, 'com.shinolabs.pinksea': { domain: 'pinksea.art' }, 'blue.linkat': { domain: 'linkat.blue' }, 'sh.tangled': { domain: 'tangled.sh' }, 'pub.leaflet': { domain: 'leaflet.pub' }, } // Search Bluesky posts mentioning a URL export async function searchPostsForUrl(url: string): Promise { try { const res = await fetch( `https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=${encodeURIComponent(url)}&limit=20` ) if (!res.ok) return [] const data = await res.json() return data.posts || [] } catch { return [] } } export function getServiceInfo(collection: string): { name: string; domain: string; favicon: string } | null { // Try to find matching service prefix for (const [prefix, info] of Object.entries(SERVICE_MAP)) { if (collection.startsWith(prefix)) { return { name: info.domain, domain: info.domain, favicon: info.icon || `https://www.google.com/s2/favicons?domain=${info.domain}&sz=32` } } } // Fallback: extract domain from first 2 parts of NSID const parts = collection.split('.') if (parts.length >= 2) { const domain = parts.slice(0, 2).reverse().join('.') return { name: domain, domain: domain, favicon: `https://www.google.com/s2/favicons?domain=${domain}&sz=32` } } return null }