Retro Bulletin Board Systems on atproto. Web app and TUI.
lazy mirror of alyraffauf/atbbs
atbbs.xyz
forums
python
tui
atproto
bbs
1/** Load a sysop's bans + hides, hydrated with identities and post previews. */
2
3import { getRecordByUri, listRecords, resolveIdentitiesBatch } from "./atproto";
4import { BAN, HIDE } from "./lexicon";
5import { parseAtUri } from "./util";
6import { is } from "@atcute/lexicons/validations";
7import { mainSchema as banSchema } from "../lexicons/types/xyz/atbbs/ban";
8import { mainSchema as hideSchema } from "../lexicons/types/xyz/atbbs/hide";
9import type { XyzAtbbsBan, XyzAtbbsHide } from "../lexicons";
10
11export interface HiddenInfo {
12 uri: string;
13 handle: string;
14 title: string;
15 body: string;
16}
17
18export interface SysopModeration {
19 banRkeys: Record<string, string>;
20 bannedHandles: Record<string, string>;
21 hideRkeys: Record<string, string>;
22 hidden: HiddenInfo[];
23}
24
25function buildRkeyMap<T>(
26 records: { uri: string; value: Record<string, unknown> }[],
27 schema: Parameters<typeof is>[0],
28 getKey: (value: T) => string,
29): Record<string, string> {
30 const map: Record<string, string> = {};
31 for (const record of records) {
32 if (!is(schema, record.value)) continue;
33 map[getKey(record.value as unknown as T)] = parseAtUri(record.uri).rkey;
34 }
35 return map;
36}
37
38async function hydrateHiddenPosts(uris: string[]): Promise<HiddenInfo[]> {
39 if (uris.length === 0) return [];
40
41 const dids = [...new Set(uris.map((uri) => parseAtUri(uri).did))];
42
43 const [identities, records] = await Promise.all([
44 resolveIdentitiesBatch(dids),
45 Promise.allSettled(uris.map(getRecordByUri)),
46 ]);
47
48 return uris.map((uri, index) => {
49 const did = parseAtUri(uri).did;
50 const handle = identities[did]?.handle ?? did;
51 const result = records[index];
52 if (result.status === "fulfilled") {
53 const value = result.value.value as unknown as {
54 title?: string;
55 body?: string;
56 };
57 return {
58 uri,
59 handle,
60 title: value.title ?? "",
61 body: (value.body ?? "").substring(0, 100),
62 };
63 }
64 return { uri, handle, title: "", body: uri };
65 });
66}
67
68export async function fetchSysopModeration(
69 pdsUrl: string,
70 did: string,
71): Promise<SysopModeration> {
72 const [banRecs, hideRecs] = await Promise.all([
73 listRecords(pdsUrl, did, BAN),
74 listRecords(pdsUrl, did, HIDE),
75 ]);
76
77 const banRkeys = buildRkeyMap<XyzAtbbsBan.Main>(
78 banRecs,
79 banSchema,
80 (ban) => ban.did,
81 );
82 const hideRkeys = buildRkeyMap<XyzAtbbsHide.Main>(
83 hideRecs,
84 hideSchema,
85 (hide) => hide.uri,
86 );
87
88 const bannedDids = Object.keys(banRkeys);
89 let bannedHandles: Record<string, string> = {};
90 if (bannedDids.length) {
91 try {
92 const authors = await resolveIdentitiesBatch(bannedDids);
93 for (const did of bannedDids)
94 bannedHandles[did] = authors[did]?.handle ?? did;
95 } catch {
96 for (const did of bannedDids) bannedHandles[did] = did;
97 }
98 }
99
100 const hidden = await hydrateHiddenPosts(Object.keys(hideRkeys));
101
102 return { banRkeys, bannedHandles, hideRkeys, hidden };
103}