top poasts
highlights.waow.tech
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}