Retro Bulletin Board Systems on atproto. Web app and TUI.
lazy mirror of alyraffauf/atbbs
atbbs.xyz
forums
python
tui
atproto
bbs
1/** Resolve a handle to a fully hydrated BBS via Slingshot/Constellation. */
2
3import {
4 getRecord,
5 resolveIdentity,
6 type MiniDoc,
7 type ATRecord,
8} from "./atproto";
9import { queryClient } from "./queryClient";
10import { SITE } from "./lexicon";
11import { parseAtUri } from "./util";
12import { isBoardRecord, isSiteRecord } from "./recordGuards";
13
14export class BBSNotFoundError extends Error {}
15export class NoBBSError extends Error {}
16
17export interface Board {
18 slug: string;
19 name: string;
20 description: string;
21 createdAt: string;
22 updatedAt?: string;
23}
24
25export interface PostAttachment {
26 file: { ref: { $link: string } };
27 name: string;
28}
29
30export interface NewsPost {
31 uri: string;
32 rkey: string;
33 title: string;
34 body: string;
35 createdAt: string;
36 attachments?: PostAttachment[];
37}
38
39export interface Site {
40 name: string;
41 description: string;
42 intro: string;
43 boards: Board[];
44 createdAt: string;
45 updatedAt?: string;
46}
47
48export interface BBS {
49 identity: MiniDoc;
50 site: Site;
51}
52
53export function invalidateAllBBSCaches() {
54 queryClient.invalidateQueries({ queryKey: ["bbs"] });
55 queryClient.invalidateQueries({ queryKey: ["bbs-moderation"] });
56 queryClient.invalidateQueries({ queryKey: ["sysop-moderation"] });
57}
58
59export async function resolveBBS(handle: string): Promise<BBS> {
60 let identity: MiniDoc;
61 try {
62 identity = await resolveIdentity(handle);
63 } catch {
64 throw new BBSNotFoundError(`Could not resolve handle: ${handle}`);
65 }
66 if (!identity.pds) {
67 throw new BBSNotFoundError(`No PDS for ${handle}`);
68 }
69
70 let siteRecord: ATRecord;
71 try {
72 siteRecord = await getRecord(identity.did, SITE, "self");
73 } catch {
74 throw new NoBBSError(`${handle} isn't running a BBS.`);
75 }
76
77 if (!isSiteRecord(siteRecord)) {
78 throw new NoBBSError(`${handle} has an invalid site record.`);
79 }
80 const siteValue = siteRecord.value;
81 const boardUris: string[] = siteValue.boards ?? [];
82
83 const boardResults = await Promise.allSettled(
84 boardUris.map((uri) => {
85 const parsed = parseAtUri(uri);
86 return getRecord(parsed.did, parsed.collection, parsed.rkey);
87 }),
88 );
89
90 const boards: Board[] = [];
91 boardResults.forEach((result, index) => {
92 if (result.status !== "fulfilled") return;
93 if (!isBoardRecord(result.value)) return;
94 const board = result.value.value;
95 const parsed = parseAtUri(boardUris[index]);
96 boards.push({
97 slug: parsed.rkey,
98 name: board.name,
99 description: board.description,
100 createdAt: board.createdAt,
101 updatedAt: board.updatedAt,
102 });
103 });
104
105 return {
106 identity,
107 site: {
108 name: siteValue.name,
109 description: siteValue.description,
110 intro: siteValue.intro,
111 boards,
112 createdAt: siteValue.createdAt ?? "",
113 updatedAt: siteValue.updatedAt,
114 },
115 };
116}