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: add news posting

+116 -4
+63 -1
tui/screens/compose.py
··· 10 10 from core import lexicon 11 11 from core.models import AtUri 12 12 from core.models import AuthError 13 - from core.records import create_thread_record, create_reply_record, upload_blob 13 + from core.records import create_news_record, create_thread_record, create_reply_record, upload_blob 14 14 from tui.util import require_session 15 15 from tui.widgets.breadcrumb import Breadcrumb 16 16 ··· 217 217 return 218 218 219 219 self.app.pop_screen() 220 + 221 + 222 + class ComposeNewsScreen(Screen): 223 + BINDINGS = [ 224 + ("escape", "app.pop_screen", "back"), 225 + ("ctrl+s", "post", "post"), 226 + ] 227 + 228 + def __init__(self, bbs, handle: str) -> None: 229 + super().__init__() 230 + self.bbs = bbs 231 + self.handle = handle 232 + 233 + def compose(self) -> ComposeResult: 234 + yield Breadcrumb( 235 + ("@bbs", 2), 236 + (self.bbs.site.name, 1), 237 + ("news", 0), 238 + ) 239 + with Vertical(): 240 + yield Static("news", classes="title") 241 + yield Input(placeholder="Title", id="news-title") 242 + yield TextArea(id="news-body", language=None) 243 + yield Footer() 244 + 245 + def on_mount(self) -> None: 246 + self.query_one("#news-title", Input).focus() 247 + 248 + def action_post(self) -> None: 249 + self.post_news() 250 + 251 + @work(exclusive=True) 252 + async def post_news(self) -> None: 253 + session = require_session(self) 254 + if not session: 255 + return 256 + 257 + title = self.query_one("#news-title", Input).value.strip() 258 + body = self.query_one("#news-body", TextArea).text.strip() 259 + if not title or not body: 260 + self.notify("Title and body cannot be empty.", severity="error") 261 + return 262 + 263 + site_uri = str(AtUri(self.bbs.identity.did, lexicon.SITE, "self")) 264 + 265 + try: 266 + resp = await create_news_record( 267 + self.app.http_client, 268 + session, 269 + site_uri, 270 + title, 271 + body, 272 + ) 273 + resp.raise_for_status() 274 + except AuthError: 275 + self.notify("Session expired. Please log in again.", severity="error") 276 + return 277 + except Exception as e: 278 + self.notify(f"Failed to post news: {e}", severity="error") 279 + return 280 + 281 + self.app.pop_screen()
+38 -2
tui/screens/news.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 5 6 6 - from core.models import BBS, News 7 + from core import lexicon 8 + from core.models import AuthError, BBS, News 9 + from core.records import delete_record 7 10 from tui.widgets.breadcrumb import Breadcrumb 8 11 from tui.widgets.post import Post 9 12 10 13 11 14 class NewsScreen(Screen): 12 - BINDINGS = [("escape", "app.pop_screen", "back")] 15 + BINDINGS = [ 16 + ("escape", "app.pop_screen", "back"), 17 + ("ctrl+d", "delete", "delete"), 18 + ] 13 19 14 20 def __init__(self, bbs: BBS, handle: str, news: News) -> None: 15 21 super().__init__() ··· 31 37 body=self.news.body, 32 38 ) 33 39 yield Footer() 40 + 41 + def action_delete(self) -> None: 42 + session = self.app.user_session 43 + if not session or session["did"] != self.bbs.identity.did: 44 + self.notify("Only the sysop can delete news.", severity="error") 45 + return 46 + self._do_delete() 47 + 48 + @work 49 + async def _do_delete(self) -> None: 50 + 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 + 56 + try: 57 + await delete_record( 58 + self.app.http_client, 59 + session, 60 + lexicon.NEWS, 61 + self.news.tid, 62 + updater, 63 + ) 64 + self.app.pop_screen() 65 + self.notify("News post deleted.") 66 + except AuthError: 67 + self.notify("Session expired. Please log in again.", severity="error") 68 + except Exception: 69 + self.notify("Could not delete news post.", severity="error")
+15 -1
tui/screens/site.py
··· 7 7 from core.models import BBS 8 8 from core.resolver import resolve_bbs 9 9 from tui.screens.board import BoardScreen 10 + from tui.screens.compose import ComposeNewsScreen 10 11 from tui.screens.news import NewsScreen 12 + from tui.util import require_session 11 13 from core.util import format_datetime_local as format_datetime 12 14 from tui.widgets.breadcrumb import Breadcrumb 13 15 14 16 15 17 class SiteScreen(Screen): 16 - BINDINGS = [("escape", "app.pop_screen", "back")] 18 + BINDINGS = [ 19 + ("escape", "app.pop_screen", "back"), 20 + ("ctrl+n", "new_news", "news"), 21 + ] 17 22 18 23 def __init__(self, bbs: BBS, handle: str) -> None: 19 24 super().__init__() ··· 70 75 self.app.push_screen(SiteScreen(bbs, self.handle)) 71 76 except Exception: 72 77 self.notify("Could not refresh.", severity="error") 78 + 79 + def action_new_news(self) -> None: 80 + session = require_session(self) 81 + if not session: 82 + return 83 + if session["did"] != self.bbs.identity.did: 84 + self.notify("Only the sysop can post news.", severity="error") 85 + return 86 + self.app.push_screen(ComposeNewsScreen(self.bbs, self.handle)) 73 87 74 88 def on_list_view_selected(self, event: ListView.Selected) -> None: 75 89 if event.list_view.id == "board-list":