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.

tui: cleanup

+56 -68
+8 -10
tui/screens/activity.py
··· 1 + from textual import work 1 2 from textual.app import ComposeResult 2 3 from textual.containers import VerticalScroll 3 4 from textual.screen import Screen 4 5 from textual.widgets import Footer, Static 5 - from textual import work 6 6 7 + from core.models import AtUri, Thread 8 + from core.records import fetch_inbox 9 + from core.resolver import resolve_bbs 10 + from core.slingshot import get_record, resolve_identity 7 11 from tui.screens.thread import ThreadScreen 8 12 from tui.widgets.breadcrumb import Breadcrumb 9 13 from tui.widgets.post import Post ··· 61 65 62 66 @work(exclusive=True) 63 67 async def _navigate(self, item: dict) -> None: 64 - from core.resolver import resolve_bbs 65 - from core.slingshot import get_record, resolve_identity 66 - from core.models import Thread 67 - 68 - parts = item["thread_uri"].split("/") 69 - thread_did = parts[2] 70 - thread_tid = parts[-1] 68 + parsed = AtUri.parse(item["thread_uri"]) 69 + thread_did = parsed.did 70 + thread_tid = parsed.rkey 71 71 handle = item.get("bbs_handle") or self.app.user_session.get("handle", "") 72 72 73 73 client = self.app.http_client ··· 98 98 for w in self.query("#activity-loading"): 99 99 w.update("Log in to see your inbox.") 100 100 return 101 - 102 - from core.records import fetch_inbox 103 101 104 102 client = self.app.http_client 105 103
+1 -1
tui/screens/board.py
··· 6 6 7 7 from core.models import BBS, Board 8 8 from core.records import hydrate_threads as fetch_threads 9 + from core.util import format_datetime_local as format_datetime 9 10 from tui.screens.compose import ComposeThreadScreen 10 11 from tui.screens.thread import ThreadScreen 11 - from core.util import format_datetime_local as format_datetime 12 12 from tui.util import require_session 13 13 from tui.widgets.breadcrumb import Breadcrumb 14 14
+9 -10
tui/screens/compose.py
··· 1 + import mimetypes 2 + from pathlib import Path 3 + 4 + from textual import work 1 5 from textual.app import ComposeResult 2 6 from textual.containers import Vertical 3 7 from textual.screen import Screen 4 8 from textual.widgets import Footer, Input, Static, TextArea 5 - from textual import work 6 - 7 - import mimetypes 8 - from pathlib import Path 9 9 10 10 from core import lexicon 11 - from core.models import AtUri 12 - from core.models import AuthError 13 - from core.records import create_news_record, create_thread_record, create_reply_record, upload_blob 11 + from core.models import AtUri, AuthError, BBS, Board, Reply, Thread 12 + from core.records import create_news_record, create_reply_record, create_thread_record, upload_blob 14 13 from tui.util import require_session 15 14 from tui.widgets.breadcrumb import Breadcrumb 16 15 ··· 47 46 ("ctrl+s", "post", "post"), 48 47 ] 49 48 50 - def __init__(self, bbs, handle: str, board) -> None: 49 + def __init__(self, bbs: BBS, handle: str, board: Board) -> None: 51 50 super().__init__() 52 51 self.bbs = bbs 53 52 self.handle = handle ··· 122 121 ("ctrl+g", "toggle_quote", "toggle quote"), 123 122 ] 124 123 125 - def __init__(self, bbs, handle: str, thread, quote=None) -> None: 124 + def __init__(self, bbs: BBS, handle: str, thread: Thread, quote: Reply | None = None) -> None: 126 125 super().__init__() 127 126 self.bbs = bbs 128 127 self.handle = handle ··· 225 224 ("ctrl+s", "post", "post"), 226 225 ] 227 226 228 - def __init__(self, bbs, handle: str) -> None: 227 + def __init__(self, bbs: BBS, handle: str) -> None: 229 228 super().__init__() 230 229 self.bbs = bbs 231 230 self.handle = handle
+3 -6
tui/screens/login.py
··· 2 2 import webbrowser 3 3 from urllib.parse import quote, urlencode 4 4 5 + from authlib.jose import JsonWebKey 6 + from textual import work 5 7 from textual.app import ComposeResult 6 8 from textual.containers import Vertical 7 9 from textual.screen import Screen 8 10 from textual.widgets import Footer, Input, Static 9 - from textual import work 10 - 11 - from authlib.jose import JsonWebKey 12 11 13 12 from core.auth.config import load_secrets 14 13 from core.auth.oauth import ( ··· 17 16 resolve_pds_authserver, 18 17 send_par_request, 19 18 ) 19 + from core.lexicon import OAUTH_SCOPE 20 20 from core.slingshot import resolve_identity 21 21 from tui.local_server import wait_for_callback 22 - 23 - 24 - from core.lexicon import OAUTH_SCOPE 25 22 from tui.paths import DATA_DIR 26 23 from tui.widgets.breadcrumb import Breadcrumb 27 24
+2 -5
tui/screens/news.py
··· 7 7 from core import lexicon 8 8 from core.models import AuthError, BBS, News 9 9 from core.records import delete_record 10 + from tui.util import make_session_updater 10 11 from tui.widgets.breadcrumb import Breadcrumb 11 12 from tui.widgets.post import Post 12 13 ··· 48 49 @work 49 50 async def _do_delete(self) -> None: 50 51 session = self.app.user_session 51 - store = self.app.session_store 52 - 53 - async def updater(d, field, value): 54 - store.update_session_field(d, field, value) 55 - 52 + updater = make_session_updater(self.app.session_store) 56 53 try: 57 54 await delete_record( 58 55 self.app.http_client,
+2 -2
tui/screens/site.py
··· 1 + from textual import work 1 2 from textual.app import ComposeResult 2 3 from textual.containers import VerticalScroll 3 4 from textual.screen import Screen 4 5 from textual.widgets import Footer, ListItem, ListView, Static 5 - from textual import work 6 6 7 7 from core.models import BBS 8 8 from core.resolver import resolve_bbs 9 + from core.util import format_datetime_local as format_datetime 9 10 from tui.screens.board import BoardScreen 10 11 from tui.screens.compose import ComposeNewsScreen 11 12 from tui.screens.news import NewsScreen 12 13 from tui.screens.sysop import SysopScreen 13 14 from tui.util import require_session 14 - from core.util import format_datetime_local as format_datetime 15 15 from tui.widgets.breadcrumb import Breadcrumb 16 16 17 17
+12 -17
tui/screens/sysop.py
··· 14 14 put_board_record, 15 15 put_site_record, 16 16 ) 17 + from core.constellation import get_news 17 18 from core.slingshot import resolve_identities_batch, resolve_identity 18 19 from core.util import now_iso 19 - from tui.util import require_session 20 + from tui.util import make_session_updater, require_session 20 21 from tui.widgets.breadcrumb import Breadcrumb 21 22 22 23 ··· 131 132 if not session: 132 133 return 133 134 134 - store = self.app.session_store 135 - 136 - async def updater(d, field, value): 137 - store.update_session_field(d, field, value) 135 + updater = make_session_updater(self.app.session_store) 138 136 139 137 name = self.query_one("#edit-name", Input).value.strip() 140 138 description = self.query_one("#edit-desc", Input).value.strip() ··· 241 239 client, session["pds_url"], session["did"], lexicon.BAN 242 240 ) 243 241 self._ban_rkeys = { 244 - r["value"]["did"]: r["uri"].split("/")[-1] for r in ban_records 242 + r["value"]["did"]: AtUri.parse(r["uri"]).rkey 243 + for r in ban_records 244 + if r.get("value", {}).get("did") 245 245 } 246 246 except Exception: 247 247 self._ban_rkeys = {} ··· 271 271 client, session["pds_url"], session["did"], lexicon.HIDE 272 272 ) 273 273 self._hide_rkeys = { 274 - r["value"]["uri"]: r["uri"].split("/")[-1] for r in hide_records 274 + r["value"]["uri"]: AtUri.parse(r["uri"]).rkey 275 + for r in hide_records 276 + if r.get("value", {}).get("uri") 275 277 } 276 278 except Exception: 277 279 self._hide_rkeys = {} ··· 301 303 @work 302 304 async def _do_remove(self, key: str, item) -> None: 303 305 session = self.app.user_session 304 - store = self.app.session_store 305 - 306 - async def updater(d, field, value): 307 - store.update_session_field(d, field, value) 306 + updater = make_session_updater(self.app.session_store) 308 307 309 308 kind, _, value = key.partition(":") 310 309 try: ··· 378 377 @work 379 378 async def _do_add_hide(self, uri: str) -> None: 380 379 session = self.app.user_session 381 - store = self.app.session_store 382 - 383 - async def updater(d, field, value): 384 - store.update_session_field(d, field, value) 380 + updater = make_session_updater(self.app.session_store) 385 381 386 382 if uri in self._hide_rkeys: 387 383 self.notify("Already hidden.", severity="warning") ··· 451 447 failed.append(f"board/{board.slug}") 452 448 453 449 # Delete news 454 - from core.constellation import get_news 455 450 456 451 site_uri = str(AtUri(session["did"], lexicon.SITE, "self")) 457 452 try: ··· 474 469 client, session["pds_url"], session["did"], collection 475 470 ) 476 471 for r in records: 477 - rkey = r["uri"].split("/")[-1] 472 + rkey = AtUri.parse(r["uri"]).rkey 478 473 try: 479 474 await delete_record( 480 475 client, session, collection, rkey, updater
+8 -17
tui/screens/thread.py
··· 1 + from pathlib import Path 2 + 3 + from platformdirs import user_downloads_dir 1 4 from textual import work 2 5 from textual.app import ComposeResult 3 6 from textual.binding import Binding ··· 6 9 from textual.widgets import Footer, Static 7 10 8 11 from core import lexicon 9 - from core.models import BBS, AtUri, AuthError, Thread 12 + from core.models import BBS, AtUri, AuthError, Reply, Thread 10 13 from core.records import create_ban_record, create_hidden_record, delete_record 11 14 from core.records import hydrate_replies as fetch_replies 12 15 from tui.screens.compose import ComposeReplyScreen 13 - from tui.util import require_session 16 + from tui.util import make_session_updater, require_session 14 17 from tui.widgets.breadcrumb import Breadcrumb 15 18 from tui.widgets.post import Post 16 19 ··· 34 37 self.thread = thread 35 38 self._page: int = 1 36 39 self._total_pages: int = 1 37 - self._replies_map: dict[str, object] = {} 40 + self._replies_map: dict[str, Reply] = {} 38 41 39 42 def compose(self) -> ComposeResult: 40 43 board_slug = AtUri.parse(self.thread.board_uri).rkey ··· 148 151 @work 149 152 async def _do_ban(self, did: str) -> None: 150 153 session = self.app.user_session 151 - store = self.app.session_store 152 - 153 - async def updater(d, field, value): 154 - store.update_session_field(d, field, value) 155 - 154 + updater = make_session_updater(self.app.session_store) 156 155 try: 157 156 await create_ban_record(self.app.http_client, session, did, updater) 158 157 self.notify(f"Banned {did}.") ··· 172 171 @work 173 172 async def _do_hide(self, post: Post) -> None: 174 173 session = self.app.user_session 175 - store = self.app.session_store 176 - 177 - async def updater(d, field, value): 178 - store.update_session_field(d, field, value) 179 - 174 + updater = make_session_updater(self.app.session_store) 180 175 try: 181 176 await create_hidden_record( 182 177 self.app.http_client, session, post.record_uri, updater ··· 265 260 266 261 @work(exclusive=True) 267 262 async def _do_save(self, post: Post) -> None: 268 - from pathlib import Path 269 - 270 - from platformdirs import user_downloads_dir 271 - 272 263 downloads = Path(user_downloads_dir()) 273 264 downloads.mkdir(parents=True, exist_ok=True) 274 265
+11
tui/util.py
··· 1 1 """TUI utilities.""" 2 2 3 + from core.auth.session import SessionStore 4 + 3 5 4 6 def require_session(screen) -> dict | None: 5 7 """Return the user session if logged in and not banned, else notify and return None.""" ··· 11 13 screen.notify("You have been banned from this BBS.", severity="error") 12 14 return None 13 15 return session 16 + 17 + 18 + def make_session_updater(store: SessionStore): 19 + """Create a session_updater callback for PDS write operations.""" 20 + 21 + async def updater(did: str, field: str, value: str): 22 + store.update_session_field(did, field, value) 23 + 24 + return updater