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.

fmt

+78 -52
+3 -1
core/constellation.py
··· 30 30 return BacklinksResponse( 31 31 total=data["total"], 32 32 records=[ 33 - BacklinkRef(did=entry["did"], collection=entry["collection"], rkey=entry["rkey"]) 33 + BacklinkRef( 34 + did=entry["did"], collection=entry["collection"], rkey=entry["rkey"] 35 + ) 34 36 for entry in data["records"] 35 37 ], 36 38 cursor=data.get("cursor"),
+20 -6
core/records.py
··· 11 11 from core.constellation import get_board_activity, get_replies, get_root_posts 12 12 from core.filters import filter_moderated 13 13 from core.models import AtUri, AuthError, BBS, Board, MiniDoc, Post, Record 14 - from core.slingshot import get_records_batch, get_records_by_uri, resolve_identities_batch 14 + from core.slingshot import ( 15 + get_records_batch, 16 + get_records_by_uri, 17 + resolve_identities_batch, 18 + ) 15 19 from core.util import now_iso 16 20 17 21 ··· 65 69 66 70 records = await get_records_batch(client, backlinks.records) 67 71 if banned_dids or hidden_posts: 68 - records = filter_moderated(records, banned_dids or set(), hidden_posts or set()) 72 + records = filter_moderated( 73 + records, banned_dids or set(), hidden_posts or set() 74 + ) 69 75 70 76 for record in records: 71 77 thread_uri = record.value.get("root") or record.uri ··· 81 87 root_records = await get_records_by_uri(client, thread_uris) 82 88 root_records = [record for record in root_records if not record.value.get("root")] 83 89 if banned_dids or hidden_posts: 84 - root_records = filter_moderated(root_records, banned_dids or set(), hidden_posts or set()) 90 + root_records = filter_moderated( 91 + root_records, banned_dids or set(), hidden_posts or set() 92 + ) 85 93 86 94 # Phase 3: Resolve authors and build Post objects 87 95 uri_to_did = {record.uri: AtUri.parse(record.uri).did for record in root_records} ··· 96 104 # Set last_activity_at and sort by it (bump order) 97 105 for thread in threads: 98 106 thread.last_activity_at = last_activity.get(thread.uri, thread.created_at) 99 - threads.sort(key=lambda thread: thread.last_activity_at or thread.created_at, reverse=True) 107 + threads.sort( 108 + key=lambda thread: thread.last_activity_at or thread.created_at, reverse=True 109 + ) 100 110 101 111 return threads, scan_cursor 102 112 ··· 553 563 backlinks = await get_replies(client, post_uri, limit=BACKLINK_LIMIT) 554 564 records = await get_records_batch(client, backlinks.records) 555 565 parsed = {record.uri: AtUri.parse(record.uri) for record in records} 556 - records = [record for record in records if parsed[record.uri].did != did] 566 + records = [ 567 + record for record in records if parsed[record.uri].did != did 568 + ] 557 569 if not records: 558 570 return [] 559 571 ··· 598 610 599 611 records = await get_records_batch(client, backlinks.records) 600 612 parsed = {record.uri: AtUri.parse(record.uri) for record in records} 601 - records = [record for record in records if parsed[record.uri].did != did] 613 + records = [ 614 + record for record in records if parsed[record.uri].did != did 615 + ] 602 616 if not records: 603 617 return [] 604 618
+5 -8
core/resolver.py
··· 58 58 board_tasks = [] 59 59 for uri in board_uris: 60 60 parsed = AtUri.parse(uri) 61 - board_tasks.append(get_record(client, parsed.did, parsed.collection, parsed.rkey)) 61 + board_tasks.append( 62 + get_record(client, parsed.did, parsed.collection, parsed.rkey) 63 + ) 62 64 news_task = get_root_posts(client, site_uri) 63 65 64 - results = await asyncio.gather( 65 - *board_tasks, news_task, return_exceptions=True 66 - ) 66 + results = await asyncio.gather(*board_tasks, news_task, return_exceptions=True) 67 67 board_records = results[: len(board_uris)] 68 68 news_result = results[len(board_uris)] 69 69 ··· 88 88 else: 89 89 sysop_news = [ref for ref in news_result.records if ref.did == identity.did] 90 90 news_records = await get_records_batch(client, sysop_news) 91 - news = [ 92 - post_from_record(record, identity) 93 - for record in news_records 94 - ] 91 + news = [post_from_record(record, identity) for record in news_records] 95 92 news.sort(key=lambda post: post.created_at, reverse=True) 96 93 97 94 site = Site(
+4 -1
core/slingshot.py
··· 71 71 ) -> list[Record]: 72 72 """Fetch multiple records by AT-URI, skipping failures.""" 73 73 parsed = [AtUri.parse(uri) for uri in uris] 74 - tasks = [get_record(client, at_uri.did, at_uri.collection, at_uri.rkey) for at_uri in parsed] 74 + tasks = [ 75 + get_record(client, at_uri.did, at_uri.collection, at_uri.rkey) 76 + for at_uri in parsed 77 + ] 75 78 results = await asyncio.gather(*tasks, return_exceptions=True) 76 79 return [result for result in results if isinstance(result, Record)] 77 80
+4 -2
telnet/server.py
··· 126 126 for index, thread in enumerate(threads, 1): 127 127 date = format_datetime_utc(thread.last_activity_at or thread.created_at) 128 128 await write( 129 - writer, f" {index}. {thread.title} · {thread.author.handle} · {date}\r\n" 129 + writer, 130 + f" {index}. {thread.title} · {thread.author.handle} · {date}\r\n", 130 131 ) 131 132 132 133 cmds = ["[#] open thread"] ··· 151 152 async def show_replies(writer, replies): 152 153 for reply in replies: 153 154 await write( 154 - writer, f" {reply.author.handle} · {format_datetime_utc(reply.created_at)}\r\n" 155 + writer, 156 + f" {reply.author.handle} · {format_datetime_utc(reply.created_at)}\r\n", 155 157 ) 156 158 for line in reply.body.splitlines(): 157 159 await write(writer, f" {line}\r\n")
+5 -1
tui/screens/compose/upload.py
··· 36 36 37 37 try: 38 38 blob_ref = await upload_blob( 39 - screen.app.http_client, session, cleaned_bytes, mime_type, session_updater=nonce_updater 39 + screen.app.http_client, 40 + session, 41 + cleaned_bytes, 42 + mime_type, 43 + session_updater=nonce_updater, 40 44 ) 41 45 return [{"file": blob_ref, "name": path.name}] 42 46 except Exception as error:
+1 -3
tui/screens/home.py
··· 80 80 """Check the user doesn't already have a BBS, then open the create screen.""" 81 81 session = self.app.user_session 82 82 try: 83 - await get_record( 84 - self.app.http_client, session["did"], lexicon.SITE, "self" 85 - ) 83 + await get_record(self.app.http_client, session["did"], lexicon.SITE, "self") 86 84 # If we got here the record exists — they already have a BBS. 87 85 self.notify( 88 86 "You already have a BBS. Dial your handle to manage it.",
+1 -3
tui/screens/sysop/bbs_form.py
··· 53 53 ) 54 54 for board in self._boards: 55 55 slug = board["slug"] 56 - yield Static( 57 - f" {slug}", classes="subtitle", id=f"board-label-{slug}" 58 - ) 56 + yield Static(f" {slug}", classes="subtitle", id=f"board-label-{slug}") 59 57 yield Input( 60 58 value=board["name"], 61 59 id=f"board-name-{slug}",
+6 -4
tui/screens/thread.py
··· 11 11 12 12 from core import lexicon 13 13 from core.models import BBS, AtUri, AuthError, Post as PostModel 14 - from core.records import delete_record, hydrate_replies as fetch_replies, post_from_record 14 + from core.records import ( 15 + delete_record, 16 + hydrate_replies as fetch_replies, 17 + post_from_record, 18 + ) 15 19 from core.slingshot import get_record, resolve_identity 16 20 from tui.screens.compose import ComposeReplyScreen 17 21 from tui.util import ban_user, hide_post, require_session, require_sysop ··· 171 175 ) 172 176 173 177 # Focus first reply 174 - replies = [ 175 - post for post in self.query(Post) if self._is_reply_widget(post) 176 - ] 178 + replies = [post for post in self.query(Post) if self._is_reply_widget(post)] 177 179 if replies: 178 180 replies[0].focus() 179 181
+6 -2
web/src/components/ErrorPage.tsx
··· 9 9 10 10 let title = "Something went wrong."; 11 11 let detail: string | null = null; 12 - let action: { to: string; label: string } = { to: "/", label: "← back to home" }; 12 + let action: { to: string; label: string } = { 13 + to: "/", 14 + label: "← back to home", 15 + }; 13 16 14 17 if (error instanceof BBSNotFoundError) { 15 18 title = "BBS not found."; ··· 19 22 if (user) { 20 23 detail = "This account isn't running a BBS yet."; 21 24 } else { 22 - detail = "This account isn't running a BBS yet. Is this you? Log in to start one."; 25 + detail = 26 + "This account isn't running a BBS yet. Is this you? Log in to start one."; 23 27 action = { to: "/login", label: "log in" }; 24 28 } 25 29 } else if (error instanceof NetworkError) {
+3 -1
web/src/hooks/useThreadReplies.ts
··· 178 178 parseAtUri(uri), 179 179 ); 180 180 const parentRecords = await getRecordsBatch(parentRefs); 181 - const parentDids = parentRecords.map((record) => parseAtUri(record.uri).did); 181 + const parentDids = parentRecords.map( 182 + (record) => parseAtUri(record.uri).did, 183 + ); 182 184 const parentAuthors = await resolveIdentitiesBatch(parentDids); 183 185 for (const record of parentRecords) { 184 186 const reply = recordToReply(record, parentAuthors);
+1 -3
web/src/lib/atproto.ts
··· 82 82 return getRecord(did, collection, rkey); 83 83 } 84 84 85 - export async function getRecordsByUri( 86 - uris: string[], 87 - ): Promise<ATRecord[]> { 85 + export async function getRecordsByUri(uris: string[]): Promise<ATRecord[]> { 88 86 const results = await Promise.allSettled( 89 87 uris.map((uri) => getRecordByUri(uri)), 90 88 );
+1 -5
web/src/lib/bbs.ts
··· 15 15 import { mainSchema as siteSchema } from "../lexicons/types/xyz/atbbs/site"; 16 16 import { mainSchema as boardSchema } from "../lexicons/types/xyz/atbbs/board"; 17 17 import { mainSchema as postSchema } from "../lexicons/types/xyz/atbbs/post"; 18 - import type { 19 - XyzAtbbsSite, 20 - XyzAtbbsBoard, 21 - XyzAtbbsPost, 22 - } from "../lexicons"; 18 + import type { XyzAtbbsSite, XyzAtbbsBoard, XyzAtbbsPost } from "../lexicons"; 23 19 24 20 export class BBSNotFoundError extends Error {} 25 21 export class NoBBSError extends Error {}
+1 -2
web/src/lib/profile.ts
··· 42 42 profileResult.status === "fulfilled" && 43 43 is(profileSchema, profileResult.value.value) 44 44 ) { 45 - const value = profileResult.value 46 - .value as unknown as XyzAtbbsProfile.Main; 45 + const value = profileResult.value.value as unknown as XyzAtbbsProfile.Main; 47 46 profile.name = value.name; 48 47 profile.pronouns = value.pronouns; 49 48 profile.bio = value.bio;
+3 -1
web/src/pages/BBS.tsx
··· 67 67 setNewsBody(""); 68 68 setNewsFiles([]); 69 69 } catch (error: unknown) { 70 - alert(`Could not post: ${error instanceof Error ? error.message : error}`); 70 + alert( 71 + `Could not post: ${error instanceof Error ? error.message : error}`, 72 + ); 71 73 } finally { 72 74 setPostingNews(false); 73 75 }
+3 -1
web/src/pages/SysopCreate.tsx
··· 60 60 name: name.trim(), 61 61 description: description.trim(), 62 62 intro, 63 - boards: cleanBoards.map((board) => makeAtUri(user.did, BOARD, board.slug)), 63 + boards: cleanBoards.map((board) => 64 + makeAtUri(user.did, BOARD, board.slug), 65 + ), 64 66 createdAt: now, 65 67 }); 66 68 navigate(`/bbs/${user.handle}`);
+3 -1
web/src/pages/SysopEdit.tsx
··· 62 62 name: name.trim(), 63 63 description: description.trim(), 64 64 intro, 65 - boards: cleanBoards.map((board) => makeAtUri(user.did, BOARD, board.slug)), 65 + boards: cleanBoards.map((board) => 66 + makeAtUri(user.did, BOARD, board.slug), 67 + ), 66 68 createdAt: bbs.site.createdAt || now, 67 69 updatedAt: now, 68 70 });
+2 -6
web/src/pages/Thread.tsx
··· 172 172 reply={reply} 173 173 userDid={user?.did ?? ""} 174 174 sysopDid={bbs.identity.did} 175 - parentPost={ 176 - reply.parent ? replyCache[reply.parent] : undefined 177 - } 175 + parentPost={reply.parent ? replyCache[reply.parent] : undefined} 178 176 onReplyTo={() => 179 177 setReplyingTo({ uri: reply.uri, handle: reply.handle }) 180 178 } 181 179 onParentClick={ 182 - reply.parent 183 - ? () => scrollToReply(reply.parent!) 184 - : undefined 180 + reply.parent ? () => scrollToReply(reply.parent!) : undefined 185 181 } 186 182 onDelete={() => onDeleteReply(reply)} 187 183 onBan={() => onBan(reply.did)}
+6 -1
web/src/router/loaders/board.ts
··· 49 49 for (let scanCount = 0; scanCount < MAX_SCANS; scanCount++) { 50 50 if (lastActivity.size >= PAGE_SIZE) break; 51 51 52 - const backlinks = await getBacklinks(boardUri, `${POST}:scope`, 100, scanCursor); 52 + const backlinks = await getBacklinks( 53 + boardUri, 54 + `${POST}:scope`, 55 + 100, 56 + scanCursor, 57 + ); 53 58 if (!backlinks.records.length) break; 54 59 55 60 const records = await getRecordsBatch(backlinks.records);