diff --git a/tui/screens/news.py b/tui/screens/news.py index 329adf6..09f2199 100644 --- a/tui/screens/news.py +++ b/tui/screens/news.py @@ -7,7 +7,7 @@ from textual.widgets import Footer from core import lexicon from core.models import AtUri, AuthError, BBS, Post as PostModel from core.records import delete_record -from tui.util import make_session_updater +from tui.util import make_session_updater, require_sysop from tui.widgets.breadcrumb import Breadcrumb from tui.widgets.post import Post @@ -43,9 +43,7 @@ class NewsScreen(Screen): yield Footer() def action_delete(self) -> None: - session = self.app.user_session - if not session or session["did"] != self.bbs.identity.did: - self.notify("Only the sysop can delete news.", severity="error") + if not require_sysop(self, self.bbs): return self._do_delete() diff --git a/tui/screens/site.py b/tui/screens/site.py index dd0d842..7e14bac 100644 --- a/tui/screens/site.py +++ b/tui/screens/site.py @@ -11,7 +11,7 @@ from tui.screens.board import BoardScreen from tui.screens.compose import ComposeNewsScreen from tui.screens.news import NewsScreen from tui.screens.sysop import SysopScreen -from tui.util import require_session +from tui.util import require_sysop from tui.widgets.breadcrumb import Breadcrumb @@ -79,20 +79,12 @@ class SiteScreen(Screen): self.notify("Could not refresh.", severity="error") def action_sysop(self) -> None: - session = require_session(self) - if not session: - return - if session["did"] != self.bbs.identity.did: - self.notify("Only the sysop can manage this BBS.", severity="error") + if not require_sysop(self, self.bbs): return self.app.push_screen(SysopScreen(self.bbs, self.handle)) def action_new_news(self) -> None: - session = require_session(self) - if not session: - return - if session["did"] != self.bbs.identity.did: - self.notify("Only the sysop can post news.", severity="error") + if not require_sysop(self, self.bbs): return self.app.push_screen(ComposeNewsScreen(self.bbs, self.handle)) diff --git a/tui/screens/sysop/moderate.py b/tui/screens/sysop/moderate.py index a983188..1cd402e 100644 --- a/tui/screens/sysop/moderate.py +++ b/tui/screens/sysop/moderate.py @@ -8,15 +8,10 @@ from textual.widgets import Footer, Input, ListItem, ListView, Static from core import lexicon from core.models import AtUri, AuthError, BBS -from core.records import ( - create_ban_record, - create_hidden_record, - delete_record, - list_pds_records, -) +from core.records import delete_record, list_pds_records from core.resolver import invalidate_bbs_cache from core.slingshot import resolve_identities_batch, resolve_identity -from tui.util import make_session_updater +from tui.util import ban_user, hide_post, make_session_updater from tui.widgets.breadcrumb import Breadcrumb @@ -154,14 +149,10 @@ class SysopModerateScreen(Screen): @work async def _do_add_ban(self, identifier: str) -> None: - session = self.app.user_session - client = self.app.http_client - updater = make_session_updater(self.app.session_store) - did = identifier if not identifier.startswith("did:"): try: - identity = await resolve_identity(client, identifier) + identity = await resolve_identity(self.app.http_client, identifier) did = identity.did except Exception: self.notify(f"Could not resolve {identifier}.", severity="error") @@ -171,16 +162,9 @@ class SysopModerateScreen(Screen): self.notify("Already banned.", severity="warning") return - try: - await create_ban_record(client, session, did, updater) - invalidate_bbs_cache() - self.notify(f"Banned {did}.") + if await ban_user(self, did): self.query_one("#ban-input", Input).value = "" self._load_data() - except AuthError: - self.notify("Session expired. Please log in again.", severity="error") - except Exception: - self.notify("Could not ban user.", severity="error") def action_add_hide(self) -> None: uri = self.query_one("#hide-input", Input).value.strip() @@ -191,23 +175,13 @@ class SysopModerateScreen(Screen): @work async def _do_add_hide(self, uri: str) -> None: - session = self.app.user_session - updater = make_session_updater(self.app.session_store) - if uri in self._hide_rkeys: self.notify("Already hidden.", severity="warning") return - try: - await create_hidden_record(self.app.http_client, session, uri, updater) - invalidate_bbs_cache() - self.notify("Post hidden.") + if await hide_post(self, uri): self.query_one("#hide-input", Input).value = "" self._load_data() - except AuthError: - self.notify("Session expired. Please log in again.", severity="error") - except Exception: - self.notify("Could not hide post.", severity="error") def refresh_data(self) -> None: self._load_data() diff --git a/tui/screens/thread.py b/tui/screens/thread.py index 4047ef3..4743c28 100644 --- a/tui/screens/thread.py +++ b/tui/screens/thread.py @@ -11,17 +11,10 @@ from textual.widgets import Footer, Static from core import lexicon from core.models import BBS, AtUri, AuthError, Post as PostModel -from core.records import ( - create_ban_record, - create_hidden_record, - delete_record, - post_from_record, -) -from core.resolver import invalidate_bbs_cache -from core.records import hydrate_replies as fetch_replies +from core.records import delete_record, hydrate_replies as fetch_replies, post_from_record from core.slingshot import get_record, resolve_identity from tui.screens.compose import ComposeReplyScreen -from tui.util import make_session_updater, require_session +from tui.util import ban_user, hide_post, require_session, require_sysop from tui.widgets.breadcrumb import Breadcrumb from tui.widgets.post import Post @@ -180,33 +173,22 @@ class ThreadScreen(Screen): replies[0].focus() def action_ban(self) -> None: - session = self.app.user_session - if not session or session["did"] != self.bbs.identity.did: - self.notify("User not authorized.", severity="error") + if not require_sysop(self, self.bbs): return focused = self.focused if not isinstance(focused, Post) or not focused.author_did: return - if focused.author_did == session["did"]: + if focused.author_did == self.app.user_session["did"]: self.notify("Cannot ban yourself.", severity="warning") return self._do_ban(focused.author_did) @work async def _do_ban(self, did: str) -> None: - session = self.app.user_session - updater = make_session_updater(self.app.session_store) - try: - await create_ban_record(self.app.http_client, session, did, updater) - invalidate_bbs_cache() - self.notify(f"Banned {did}.") - except Exception: - self.notify("Could not ban user.", severity="error") + await ban_user(self, did) def action_hide(self) -> None: - session = self.app.user_session - if not session or session["did"] != self.bbs.identity.did: - self.notify("User not authorized.", severity="error") + if not require_sysop(self, self.bbs): return focused = self.focused if not isinstance(focused, Post) or not focused.record_uri: @@ -215,17 +197,8 @@ class ThreadScreen(Screen): @work async def _do_hide(self, post: Post) -> None: - session = self.app.user_session - updater = make_session_updater(self.app.session_store) - try: - await create_hidden_record( - self.app.http_client, session, post.record_uri, updater - ) - invalidate_bbs_cache() + if await hide_post(self, post.record_uri): await post.remove() - self.notify("Post hidden.") - except Exception: - self.notify("Could not hide post.", severity="error") def action_next_page(self) -> None: if self._page < self._total_pages: diff --git a/tui/util.py b/tui/util.py index 8f22352..55dfe30 100644 --- a/tui/util.py +++ b/tui/util.py @@ -1,6 +1,9 @@ """TUI utilities.""" from core.auth.session import SessionStore +from core.models import AuthError, BBS +from core.records import create_ban_record, create_hidden_record +from core.resolver import invalidate_bbs_cache def require_session(screen) -> dict | None: @@ -12,6 +15,21 @@ def require_session(screen) -> dict | None: return session +def require_sysop(screen, bbs: BBS) -> dict | None: + """Return the user session if logged in AND is the BBS sysop. + + Shows an error notification and returns None otherwise. + """ + session = screen.app.user_session + if not session: + screen.notify("You must be logged in to do that.", severity="error") + return None + if session["did"] != bbs.identity.did: + screen.notify("Only the sysop can do that.", severity="error") + return None + return session + + def make_session_updater(store: SessionStore): """Create a session_updater callback for PDS write operations.""" @@ -19,3 +37,45 @@ def make_session_updater(store: SessionStore): store.update_session_field(did, field, value) return updater + + +async def ban_user(screen, did: str) -> bool: + """Ban a user by DID. Returns True on success, False on failure. + + Handles the full workflow: create ban record, invalidate cache, + and show a success or error notification. + """ + session = screen.app.user_session + updater = make_session_updater(screen.app.session_store) + try: + await create_ban_record(screen.app.http_client, session, did, updater) + invalidate_bbs_cache() + screen.notify(f"Banned {did}.") + return True + except AuthError: + screen.notify("Session expired. Please log in again.", severity="error") + return False + except Exception: + screen.notify("Could not ban user.", severity="error") + return False + + +async def hide_post(screen, uri: str) -> bool: + """Hide a post by AT-URI. Returns True on success, False on failure. + + Handles the full workflow: create hidden record, invalidate cache, + and show a success or error notification. + """ + session = screen.app.user_session + updater = make_session_updater(screen.app.session_store) + try: + await create_hidden_record(screen.app.http_client, session, uri, updater) + invalidate_bbs_cache() + screen.notify("Post hidden.") + return True + except AuthError: + screen.notify("Session expired. Please log in again.", severity="error") + return False + except Exception: + screen.notify("Could not hide post.", severity="error") + return False