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.

web: validate records

+36 -7
+5 -2
web/src/hooks/useThreadReplies.ts
··· 6 6 import { getRecordsBatch, resolveIdentitiesBatch } from "../lib/atproto"; 7 7 import { parseAtUri } from "../lib/util"; 8 8 import type { BBS } from "../lib/bbs"; 9 + import { is } from "@atcute/lexicons/validations"; 10 + import { mainSchema as replySchema } from "../lexicons/types/xyz/atboards/reply"; 9 11 import type { XyzAtboardsReply } from "../lexicons"; 10 12 import type { Reply } from "../components/ReplyCard"; 11 13 ··· 172 174 // Fetch records from Slingshot. 173 175 const records = await getRecordsBatch(slice); 174 176 175 - // Drop moderated content. 177 + // Drop moderated and invalid content. 176 178 const visible = records.filter((r) => { 177 179 const { did } = parseAtUri(r.uri); 178 180 return ( 179 - !bbs.site.bannedDids.has(did) && !bbs.site.hiddenPosts.has(r.uri) 181 + !bbs.site.bannedDids.has(did) && !bbs.site.hiddenPosts.has(r.uri) && is(replySchema, r.value) 180 182 ); 181 183 }); 182 184 ··· 235 237 for (const r of quoteRecords) { 236 238 const { did, rkey } = parseAtUri(r.uri); 237 239 if (!(did in quoteAuthors)) continue; 240 + if (!is(replySchema, r.value)) continue; 238 241 const v = r.value as unknown as XyzAtboardsReply.Main; 239 242 newCache[r.uri] = { 240 243 uri: r.uri,
+17 -3
web/src/lib/bbs.ts
··· 11 11 } from "./atproto"; 12 12 import { SITE, BOARD, NEWS, BAN, HIDE } from "./lexicon"; 13 13 import { makeAtUri, parseAtUri } from "./util"; 14 + import { is } from "@atcute/lexicons/validations"; 15 + import { mainSchema as siteSchema } from "../lexicons/types/xyz/atboards/site"; 16 + import { mainSchema as boardSchema } from "../lexicons/types/xyz/atboards/board"; 17 + import { mainSchema as newsSchema } from "../lexicons/types/xyz/atboards/news"; 18 + import { mainSchema as banSchema } from "../lexicons/types/xyz/atboards/ban"; 19 + import { mainSchema as hideSchema } from "../lexicons/types/xyz/atboards/hide"; 14 20 import type { 15 21 XyzAtboardsSite, 16 22 XyzAtboardsBoard, ··· 74 80 throw new NoBBSError(`${handle} isn't running a BBS.`); 75 81 } 76 82 83 + if (!is(siteSchema, siteRecord.value)) { 84 + throw new NoBBSError(`${handle} has an invalid site record.`); 85 + } 77 86 const sv = siteRecord.value as unknown as XyzAtboardsSite.Main; 78 87 const siteUri = makeAtUri(identity.did, SITE, "self"); 79 88 const boardSlugs: string[] = sv.boards ?? []; ··· 91 100 const boards: Board[] = []; 92 101 boardResults.forEach((r, i) => { 93 102 if (r.status !== "fulfilled") return; 103 + if (!is(boardSchema, r.value.value)) return; 94 104 const v = r.value.value as unknown as XyzAtboardsBoard.Main; 95 105 boards.push({ 96 106 slug: boardSlugs[i], ··· 108 118 (r) => r.did === identity.did, 109 119 ); 110 120 const newsRecords = await getRecordsBatch(sysopRefs); 111 - news = newsRecords.map((r) => { 121 + news = newsRecords.filter((r) => is(newsSchema, r.value)).map((r) => { 112 122 const v = r.value as unknown as XyzAtboardsNews.Main; 113 123 return { 114 124 tid: parseAtUri(r.uri).rkey, ··· 122 132 } 123 133 124 134 const bannedDids = new Set( 125 - banRecords.map((r) => (r.value as unknown as XyzAtboardsBan.Main).did), 135 + banRecords 136 + .filter((r) => is(banSchema, r.value)) 137 + .map((r) => (r.value as unknown as XyzAtboardsBan.Main).did), 126 138 ); 127 139 const hiddenPosts = new Set( 128 - hideRecords.map((r) => (r.value as unknown as XyzAtboardsHide.Main).uri), 140 + hideRecords 141 + .filter((r) => is(hideSchema, r.value)) 142 + .map((r) => (r.value as unknown as XyzAtboardsHide.Main).uri), 129 143 ); 130 144 131 145 return {
+14 -2
web/src/router/loaders.ts
··· 17 17 } from "../lib/atproto"; 18 18 import { SITE, THREAD, REPLY, BAN, HIDE, BOARD } from "../lib/lexicon"; 19 19 import { makeAtUri, parseAtUri } from "../lib/util"; 20 + import { is } from "@atcute/lexicons/validations"; 21 + import { mainSchema as threadSchema } from "../lexicons/types/xyz/atboards/thread"; 22 + import { mainSchema as replySchema } from "../lexicons/types/xyz/atboards/reply"; 23 + import { mainSchema as banSchema } from "../lexicons/types/xyz/atboards/ban"; 24 + import { mainSchema as hideSchema } from "../lexicons/types/xyz/atboards/hide"; 20 25 import type { 21 26 XyzAtboardsThread, 22 27 XyzAtboardsReply, ··· 66 71 const records = await getRecordsBatch(backlinks.records); 67 72 const filtered = records.filter((r) => { 68 73 const { did } = parseAtUri(r.uri); 69 - return !bbs.site.bannedDids.has(did) && !bbs.site.hiddenPosts.has(r.uri); 74 + return !bbs.site.bannedDids.has(did) && !bbs.site.hiddenPosts.has(r.uri) && is(threadSchema, r.value); 70 75 }); 71 76 const authors = await resolveIdentitiesBatch( 72 77 filtered.map((r) => parseAtUri(r.uri).did), ··· 126 131 getRecord(did, THREAD, tid), 127 132 resolveIdentity(did), 128 133 ]); 134 + if (!is(threadSchema, tr.value)) { 135 + throw new Response("Invalid thread record", { status: 404 }); 136 + } 129 137 const tv = tr.value as unknown as XyzAtboardsThread.Main; 130 138 const boardSlug = parseAtUri(tv.board).rkey; 131 139 const thread: ThreadObj = { ··· 195 203 196 204 async function fetchInbox(did: string, pdsUrl: string): Promise<InboxItem[]> { 197 205 const SCAN_LIMIT = 50; 198 - const [threads, replies] = await Promise.all([ 206 + const [allThreads, allReplies] = await Promise.all([ 199 207 listRecords(pdsUrl, did, THREAD, SCAN_LIMIT), 200 208 listRecords(pdsUrl, did, REPLY, SCAN_LIMIT), 201 209 ]); 210 + const threads = allThreads.filter((r) => is(threadSchema, r.value)); 211 + const replies = allReplies.filter((r) => is(replySchema, r.value)); 202 212 203 213 const results = await Promise.all([ 204 214 ...threads.map(async (tr) => { ··· 289 299 const banRecs = await listRecords(user.pdsUrl, user.did, BAN); 290 300 const banRkeys: Record<string, string> = {}; 291 301 for (const r of banRecs) { 302 + if (!is(banSchema, r.value)) continue; 292 303 const v = r.value as unknown as XyzAtboardsBan.Main; 293 304 banRkeys[v.did] = parseAtUri(r.uri).rkey; 294 305 } ··· 307 318 const hideRecs = await listRecords(user.pdsUrl, user.did, HIDE); 308 319 const hideRkeys: Record<string, string> = {}; 309 320 for (const r of hideRecs) { 321 + if (!is(hideSchema, r.value)) continue; 310 322 const v = r.value as unknown as XyzAtboardsHide.Main; 311 323 hideRkeys[v.uri] = parseAtUri(r.uri).rkey; 312 324 }