import type { Post } from './types.js'; const API = 'https://public.api.bsky.app/xrpc'; const CACHE_TTL = 15 * 60 * 1000; // 15 minutes interface CacheEntry { posts: Post[]; timestamp: number; } function getCached(did: string): Post[] | null { try { const raw = localStorage.getItem(`bhr:${did}`); if (!raw) return null; const entry: CacheEntry = JSON.parse(raw); if (Date.now() - entry.timestamp > CACHE_TTL) { localStorage.removeItem(`bhr:${did}`); return null; } return entry.posts; } catch { return null; } } function setCache(did: string, posts: Post[]) { try { localStorage.setItem(`bhr:${did}`, JSON.stringify({ posts, timestamp: Date.now() })); } catch { // storage full, no big deal } } export async function resolveHandle(handle: string): Promise { const clean = handle.replace(/^@/, ''); const res = await fetch( `${API}/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(clean)}` ); if (!res.ok) { throw new Error(`could not resolve handle "${clean}"`); } const data = await res.json(); return data.did; } export async function checkEmbedOptOut(did: string): Promise { const res = await fetch(`${API}/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`); if (!res.ok) return false; const data = await res.json(); const labels: Array<{ val: string }> = data.labels ?? []; return labels.some((l) => l.val === '!no-unauthenticated'); } export async function fetchAllPosts( did: string, onPage?: (posts: Post[], done: boolean) => void ): Promise { const cached = getCached(did); if (cached) { onPage?.(cached, true); return cached; } const posts: Post[] = []; let cursor: string | undefined; while (true) { const params = new URLSearchParams({ actor: did, limit: '100', filter: 'posts_no_replies' }); if (cursor) params.set('cursor', cursor); const res = await fetch(`${API}/app.bsky.feed.getAuthorFeed?${params}`); if (!res.ok) { throw new Error(`failed to fetch posts: ${res.status}`); } const data = await res.json(); for (const item of data.feed) { // skip reposts of other people's content if (item.reason) continue; // skip posts by other authors if (item.post.author.did !== did) continue; const post = item.post; const record = post.record; const likes = post.likeCount ?? 0; const reposts = post.repostCount ?? 0; const quotes = post.quoteCount ?? 0; const replies = post.replyCount ?? 0; const rkey = post.uri.split('/').pop()!; posts.push({ text: record.text ?? '', createdAt: record.createdAt ?? '', likes, reposts, quotes, replies, uri: post.uri, rkey, handle: post.author.handle, did: post.author.did, score: likes + reposts * 2 + quotes * 3 }); } const done = !data.cursor; onPage?.([...posts], done); if (done) break; cursor = data.cursor; } setCache(did, posts); return posts; }