at main 120 lines 3.0 kB view raw
1import type { Post } from './types.js'; 2 3const API = 'https://public.api.bsky.app/xrpc'; 4const CACHE_TTL = 15 * 60 * 1000; // 15 minutes 5 6interface CacheEntry { 7 posts: Post[]; 8 timestamp: number; 9} 10 11function getCached(did: string): Post[] | null { 12 try { 13 const raw = localStorage.getItem(`bhr:${did}`); 14 if (!raw) return null; 15 const entry: CacheEntry = JSON.parse(raw); 16 if (Date.now() - entry.timestamp > CACHE_TTL) { 17 localStorage.removeItem(`bhr:${did}`); 18 return null; 19 } 20 return entry.posts; 21 } catch { 22 return null; 23 } 24} 25 26function setCache(did: string, posts: Post[]) { 27 try { 28 localStorage.setItem(`bhr:${did}`, JSON.stringify({ posts, timestamp: Date.now() })); 29 } catch { 30 // storage full, no big deal 31 } 32} 33 34export async function resolveHandle(handle: string): Promise<string> { 35 const clean = handle.replace(/^@/, ''); 36 const res = await fetch( 37 `${API}/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(clean)}` 38 ); 39 if (!res.ok) { 40 throw new Error(`could not resolve handle "${clean}"`); 41 } 42 const data = await res.json(); 43 return data.did; 44} 45 46export async function checkEmbedOptOut(did: string): Promise<boolean> { 47 const res = await fetch(`${API}/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`); 48 if (!res.ok) return false; 49 const data = await res.json(); 50 const labels: Array<{ val: string }> = data.labels ?? []; 51 return labels.some((l) => l.val === '!no-unauthenticated'); 52} 53 54export async function fetchAllPosts( 55 did: string, 56 onPage?: (posts: Post[], done: boolean) => void 57): Promise<Post[]> { 58 const cached = getCached(did); 59 if (cached) { 60 onPage?.(cached, true); 61 return cached; 62 } 63 64 const posts: Post[] = []; 65 let cursor: string | undefined; 66 67 while (true) { 68 const params = new URLSearchParams({ 69 actor: did, 70 limit: '100', 71 filter: 'posts_no_replies' 72 }); 73 if (cursor) params.set('cursor', cursor); 74 75 const res = await fetch(`${API}/app.bsky.feed.getAuthorFeed?${params}`); 76 if (!res.ok) { 77 throw new Error(`failed to fetch posts: ${res.status}`); 78 } 79 80 const data = await res.json(); 81 82 for (const item of data.feed) { 83 // skip reposts of other people's content 84 if (item.reason) continue; 85 // skip posts by other authors 86 if (item.post.author.did !== did) continue; 87 88 const post = item.post; 89 const record = post.record; 90 const likes = post.likeCount ?? 0; 91 const reposts = post.repostCount ?? 0; 92 const quotes = post.quoteCount ?? 0; 93 const replies = post.replyCount ?? 0; 94 const rkey = post.uri.split('/').pop()!; 95 96 posts.push({ 97 text: record.text ?? '', 98 createdAt: record.createdAt ?? '', 99 likes, 100 reposts, 101 quotes, 102 replies, 103 uri: post.uri, 104 rkey, 105 handle: post.author.handle, 106 did: post.author.did, 107 score: likes + reposts * 2 + quotes * 3 108 }); 109 } 110 111 const done = !data.cursor; 112 onPage?.([...posts], done); 113 114 if (done) break; 115 cursor = data.cursor; 116 } 117 118 setCache(did, posts); 119 return posts; 120}