From 4f31242956570706dfda305eaa7bd8b9d2a7b802 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Sun, 5 Apr 2026 18:10:31 -0400 Subject: [PATCH] move cli to toplevel --- cli/__init__.py | 0 cli/__main__.py | 82 +++++++++++++++++++++++++++++++++++++++ core/records.py | 4 +- core/resolver.py | 7 +--- pyproject.toml | 4 +- tui/__main__.py | 62 +---------------------------- tui/screens/activity.py | 4 +- tui/screens/login.py | 1 + tui/widgets/breadcrumb.py | 4 +- web/app.py | 4 +- web/routes_sysop.py | 4 +- 11 files changed, 97 insertions(+), 79 deletions(-) create mode 100644 cli/__init__.py create mode 100644 cli/__main__.py diff --git a/cli/__init__.py b/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/__main__.py b/cli/__main__.py new file mode 100644 index 0000000..bae17fd --- /dev/null +++ b/cli/__main__.py @@ -0,0 +1,82 @@ +from importlib.metadata import version as pkg_version + +import rich_click as click +from platformdirs import user_data_dir + +DEFAULT_DATA_DIR = user_data_dir("atbbs") + + +@click.group(invoke_without_command=True) +@click.version_option(version=pkg_version("atbbs"), prog_name="atbbs") +@click.pass_context +def cli(ctx: click.Context): + """Decentralized bulletin boards on atproto.""" + if ctx.invoked_subcommand is None: + ctx.invoke(dial) + + +@cli.command() +@click.argument("handle", required=False) +def dial(handle: str | None): + """Dial a BBS from the terminal.""" + from tui.app import AtbbsApp + + app = AtbbsApp(dial=handle) + app.run() + + +@cli.command() +@click.option("--host", default="127.0.0.1", show_default=True, help="Host to bind to.") +@click.option( + "--port", "-p", default=8000, show_default=True, type=int, help="Port to bind to." +) +@click.option( + "--workers", + "-w", + default=1, + show_default=True, + type=int, + help="Number of worker processes.", +) +@click.option( + "--public-url", + default=None, + help="Public URL for OAuth callbacks. [default: http://{host}:{port}]", +) +@click.option( + "--data-dir", + default=DEFAULT_DATA_DIR, + show_default=True, + help="Directory for secrets and database.", +) +def serve(host: str, port: int, workers: int, public_url: str | None, data_dir: str): + """Start the web server.""" + import asyncio + import os + + from hypercorn.asyncio import serve as hypercorn_serve + from hypercorn.config import Config + + if not public_url: + public_url = f"http://{host}:{port}" + + os.environ.setdefault("ATBBS_DATA_DIR", data_dir) + os.environ.setdefault("PUBLIC_URL", public_url) + + from web.app import create_app + + app = create_app(data_dir=data_dir, public_url=public_url) + + config = Config() + config.bind = [f"{host}:{port}"] + config.workers = workers + + asyncio.run(hypercorn_serve(app, config)) + + +def main(): + cli() + + +if __name__ == "__main__": + main() diff --git a/core/records.py b/core/records.py index 5dd5dd8..df91152 100644 --- a/core/records.py +++ b/core/records.py @@ -344,7 +344,9 @@ async def fetch_inbox( if board_uri: bbs_dids.add(AtUri.parse(board_uri).did) try: - bbs_authors = await resolve_identities_batch(client, list(bbs_dids)) if bbs_dids else {} + bbs_authors = ( + await resolve_identities_batch(client, list(bbs_dids)) if bbs_dids else {} + ) except Exception: bbs_authors = {} diff --git a/core/resolver.py b/core/resolver.py index 81b250a..0763059 100644 --- a/core/resolver.py +++ b/core/resolver.py @@ -27,9 +27,7 @@ async def resolve_bbs(client: httpx.AsyncClient, handle: str) -> BBS: raise NetworkError("Could not reach the network.") try: - site_record = await get_record( - client, identity.did, lexicon.SITE, "self" - ) + site_record = await get_record(client, identity.did, lexicon.SITE, "self") except httpx.HTTPStatusError: raise NoBBSError(f"{handle} isn't running a BBS.") except httpx.TransportError: @@ -41,8 +39,7 @@ async def resolve_bbs(client: httpx.AsyncClient, handle: str) -> BBS: # Fetch boards and news backlinks concurrently board_slugs = sv["boards"] board_tasks = [ - get_record(client, identity.did, lexicon.BOARD, slug) - for slug in board_slugs + get_record(client, identity.did, lexicon.BOARD, slug) for slug in board_slugs ] news_task = get_news(client, site_uri) diff --git a/pyproject.toml b/pyproject.toml index 33bbb5d..cd83a52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,8 +19,8 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project.scripts] -atbbs = "tui.__main__:main" +atbbs = "cli.__main__:main" [tool.hatch.build.targets.wheel] -packages = ["core", "web", "tui"] +packages = ["cli", "core", "web", "tui"] exclude = ["web/ts"] diff --git a/tui/__main__.py b/tui/__main__.py index 6d0ea98..de39ebd 100644 --- a/tui/__main__.py +++ b/tui/__main__.py @@ -1,64 +1,4 @@ -import rich_click as click - -from importlib.metadata import version as pkg_version -from platformdirs import user_data_dir - -DEFAULT_DATA_DIR = user_data_dir("atbbs") - - -@click.group(invoke_without_command=True) -@click.version_option(version=pkg_version("atbbs"), prog_name="atbbs") -@click.pass_context -def cli(ctx: click.Context): - """Decentralized bulletin boards on atproto.""" - if ctx.invoked_subcommand is None: - ctx.invoke(dial) - - -@cli.command() -@click.argument("handle", required=False) -def dial(handle: str | None): - """Launch the TUI. Optionally dial a BBS directly.""" - from tui.app import AtbbsApp - - app = AtbbsApp(dial=handle) - app.run() - - -@cli.command() -@click.option("--host", default="127.0.0.1", show_default=True, help="Host to bind to.") -@click.option("--port", "-p", default=8000, show_default=True, type=int, help="Port to bind to.") -@click.option("--workers", "-w", default=1, show_default=True, type=int, help="Number of worker processes.") -@click.option("--public-url", default=None, help="Public URL for OAuth callbacks. [default: http://{host}:{port}]") -@click.option("--data-dir", default=DEFAULT_DATA_DIR, show_default=True, help="Directory for secrets and database.") -def serve(host: str, port: int, workers: int, public_url: str | None, data_dir: str): - """Start the web server.""" - import asyncio - import os - - from hypercorn.asyncio import serve as hypercorn_serve - from hypercorn.config import Config - - if not public_url: - public_url = f"http://{host}:{port}" - - os.environ.setdefault("ATBBS_DATA_DIR", data_dir) - os.environ.setdefault("PUBLIC_URL", public_url) - - from web.app import create_app - - app = create_app(data_dir=data_dir, public_url=public_url) - - config = Config() - config.bind = [f"{host}:{port}"] - config.workers = workers - - asyncio.run(hypercorn_serve(app, config)) - - -def main(): - cli() - +from cli.__main__ import main if __name__ == "__main__": main() diff --git a/tui/screens/activity.py b/tui/screens/activity.py index 9a3af79..3f17bad 100644 --- a/tui/screens/activity.py +++ b/tui/screens/activity.py @@ -76,9 +76,7 @@ class ActivityScreen(Screen): client = self.app.http_client try: bbs = await resolve_bbs(client, handle) - rec = await get_record( - client, thread_did, lexicon.THREAD, thread_tid - ) + rec = await get_record(client, thread_did, lexicon.THREAD, thread_tid) author = await resolve_identity(client, thread_did) thread = Thread( uri=rec.uri, diff --git a/tui/screens/login.py b/tui/screens/login.py index 13e83fa..9c0d448 100644 --- a/tui/screens/login.py +++ b/tui/screens/login.py @@ -22,6 +22,7 @@ from tui.local_server import wait_for_callback from core.lexicon import OAUTH_SCOPE + CALLBACK_PORT = 23847 diff --git a/tui/widgets/breadcrumb.py b/tui/widgets/breadcrumb.py index 0e02090..b846eca 100644 --- a/tui/widgets/breadcrumb.py +++ b/tui/widgets/breadcrumb.py @@ -93,9 +93,7 @@ class Breadcrumb(Widget): # Show logged-in user on the right session = getattr(self.app, "user_session", None) if session: - yield BreadcrumbUser( - f" {session['handle']} ", markup=False - ) + yield BreadcrumbUser(f" {session['handle']} ", markup=False) for i, (label, pop_count) in enumerate(self._segments): if i > 0: diff --git a/web/app.py b/web/app.py index 8e9d441..a20ae65 100644 --- a/web/app.py +++ b/web/app.py @@ -22,7 +22,9 @@ def create_app( secrets = load_secrets(data_dir) app.secret_key = secrets["secret_key"] app.config["CLIENT_SECRET_JWK"] = secrets["client_secret_jwk"] - app.config["PUBLIC_URL"] = public_url or os.environ.get("PUBLIC_URL", "http://localhost:8000") + app.config["PUBLIC_URL"] = public_url or os.environ.get( + "PUBLIC_URL", "http://localhost:8000" + ) # Session store db_path = os.path.join(data_dir, "atbbs.db") diff --git a/web/routes_sysop.py b/web/routes_sysop.py index c264e86..f631633 100644 --- a/web/routes_sysop.py +++ b/web/routes_sysop.py @@ -184,9 +184,7 @@ async def moderate_bbs(): hidden_posts = [] if bbs.site.hidden_posts: - hidden_dids = list( - {AtUri.parse(uri).did for uri in bbs.site.hidden_posts} - ) + hidden_dids = list({AtUri.parse(uri).did for uri in bbs.site.hidden_posts}) hidden_authors = await resolve_identities_batch(client, hidden_dids) for uri in bbs.site.hidden_posts: -- 2.51.2