Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

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

at master 104 lines 3.0 kB view raw
1/** Activity data — replies to your posts from other users. */ 2 3import { fetchAndHydrate, listRecords, resolveIdentitiesBatch } from "./atproto"; 4import { POST } from "./lexicon"; 5import { isPostRecord } from "./recordGuards"; 6import { parseAtUri } from "./util"; 7 8export interface ActivityItem { 9 type: "reply" | "parent_reply"; 10 threadTitle: string; 11 threadUri: string; 12 bbsHandle: string; 13 replyUri: string; 14 handle: string; 15 body: string; 16 createdAt: string; 17} 18 19async function fetchBacklinkItems( 20 sourceUri: string, 21 backlinkSource: string, 22 excludeDid: string, 23 type: ActivityItem["type"], 24 threadTitle: string, 25 threadUri: string, 26 bbsHandle: string, 27): Promise<ActivityItem[]> { 28 try { 29 const { records } = await fetchAndHydrate(sourceUri, backlinkSource, { 30 limit: 50, 31 excludeDid, 32 }); 33 return records.map((record) => ({ 34 type, 35 threadTitle, 36 threadUri, 37 bbsHandle, 38 replyUri: record.uri, 39 handle: record.handle, 40 body: ((record.value.body as string) ?? "").substring(0, 200), 41 createdAt: (record.value.createdAt as string) ?? "", 42 })); 43 } catch { 44 return []; 45 } 46} 47 48export async function fetchActivity( 49 did: string, 50 pdsUrl: string, 51): Promise<ActivityItem[]> { 52 const SCAN_LIMIT = 50; 53 const allPosts = await listRecords(pdsUrl, did, POST, SCAN_LIMIT); 54 const validPosts = allPosts.filter(isPostRecord); 55 56 const rootPosts = validPosts.filter((record) => !record.value.root); 57 const replyPosts = validPosts.filter((record) => !!record.value.root); 58 59 const bbsDids = new Set( 60 validPosts.map((record) => parseAtUri(record.value.scope).did), 61 ); 62 const bbsIdentities = await resolveIdentitiesBatch([...bbsDids]); 63 64 const results = await Promise.all([ 65 ...rootPosts.map((post) => { 66 const bbsDid = parseAtUri(post.value.scope).did; 67 const bbsHandle = bbsIdentities[bbsDid]?.handle; 68 if (!bbsHandle) return Promise.resolve([] as ActivityItem[]); 69 return fetchBacklinkItems( 70 post.uri, 71 `${POST}:root`, 72 did, 73 "reply", 74 post.value.title ?? "", 75 post.uri, 76 bbsHandle, 77 ); 78 }), 79 ...replyPosts.map((reply) => { 80 const bbsDid = parseAtUri(reply.value.scope).did; 81 const bbsHandle = bbsIdentities[bbsDid]?.handle; 82 if (!bbsHandle) return Promise.resolve([] as ActivityItem[]); 83 return fetchBacklinkItems( 84 reply.uri, 85 `${POST}:parent`, 86 did, 87 "parent_reply", 88 "", 89 reply.value.root ?? "", 90 bbsHandle, 91 ); 92 }), 93 ]); 94 95 // Deduplicate — prefer "parent-reply" type when the same reply appears as both. 96 const seen = new Map<string, ActivityItem>(); 97 for (const item of results.flat()) { 98 const key = item.handle + item.body + item.createdAt; 99 if (!seen.has(key) || item.type === "parent_reply") seen.set(key, item); 100 } 101 return [...seen.values()].sort((a, b) => 102 b.createdAt.localeCompare(a.createdAt), 103 ); 104}