From 9e5068698320bbbc4bc45ef5d7a0c369df5131fa Mon Sep 17 00:00:00 2001 From: Danny Date: Sat, 23 May 2026 12:01:39 +0200 Subject: [PATCH] feat: scripts/set-bot-presence.py for chat-side bot presence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Idempotent one-shot script that calls the Telegram Bot API to: - set the persistent menu button → "Open Workout Tracker" launching the Mini App at $WEBAPP_URL - publish a short description + long description so the chat tells users what to do before they /start (which now returns silence — we removed the polling bot) - clear the published commands list (no more stale /start, /history, etc. in the / menu) Loads BOT_TOKEN from env first, then ~/.secrets/bigbiggerbiggestbot to match start.py. Pure-stdlib (urllib) so it has no extra deps. The prod systemd unit gets an ExecStartPost hook for this (dotfiles change in a sister commit). Errors are non-fatal — the dash prefix on ExecStartPost means a failed presence update never blocks the backend from being healthy. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/set-bot-presence.py | 115 ++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100755 scripts/set-bot-presence.py diff --git a/scripts/set-bot-presence.py b/scripts/set-bot-presence.py new file mode 100755 index 0000000..7aa41e2 --- /dev/null +++ b/scripts/set-bot-presence.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +One-shot script that configures @BBBot's chat-side presence on Telegram: + +- Menu button → "Open Workout Tracker" launching the Mini App at WEBAPP_URL. +- Short description (shown next to the bot name in search / chat list). +- Description (shown in the empty-chat splash before the user sends /start). +- Commands list cleared (we used to have /start, /history, etc.; not anymore). + +We removed the polling bot, so this is how users get a useful "what's this bot +do?" experience without the bot ever needing to be online. Idempotent — set it +once or run on every service start; result is the same. + +Reads BOT_TOKEN and WEBAPP_URL from the environment. Exits non-zero only on +network failures; per-call HTTP errors are logged but treated as warnings. +""" +import json +import os +import pathlib +import sys +import urllib.error +import urllib.request + +API_BASE = "https://api.telegram.org/bot{token}/{method}" + +SECRETS_FILE = pathlib.Path.home() / ".secrets" / "bigbiggerbiggestbot" + +SHORT_DESCRIPTION = "Log workouts, track sets, view history & stats. Tap the menu button to open the Mini App." + +DESCRIPTION = ( + "Open the Mini App from the menu button below to log workouts, " + "track sets, view history & stats, and configure settings.\n\n" + "This bot used to accept slash commands and text-message workouts, " + "but everything moved into the Mini App. The chat itself is now a " + "doorway." +) + + +def call(token: str, method: str, payload: dict) -> bool: + """Call a Bot API method; return True on success.""" + url = API_BASE.format(token=token, method=method) + data = json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + url, data=data, headers={"Content-Type": "application/json"} + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + body = json.loads(resp.read().decode("utf-8")) + if not body.get("ok"): + print(f" {method}: API reported error: {body}", file=sys.stderr) + return False + print(f" {method}: ok") + return True + except urllib.error.HTTPError as e: + # Telegram returns 4xx with a JSON error body — read it for the why. + try: + err_body = json.loads(e.read().decode("utf-8")) + except Exception: + err_body = {"raw": str(e)} + print(f" {method}: HTTP {e.code} — {err_body}", file=sys.stderr) + return False + except urllib.error.URLError as e: + print(f" {method}: network error: {e}", file=sys.stderr) + return False + + +def _load_token() -> str: + """BOT_TOKEN env first, then ~/.secrets/bigbiggerbiggestbot (matches start.py).""" + token = os.environ.get("BOT_TOKEN", "").strip() + if token: + return token + if SECRETS_FILE.is_file(): + return SECRETS_FILE.read_text().strip() + return "" + + +def main() -> int: + token = _load_token() + webapp_url = os.environ.get("WEBAPP_URL", "").strip() + + if not token: + print(f"ERROR: no BOT_TOKEN env var and {SECRETS_FILE} missing/empty", file=sys.stderr) + return 2 + if not webapp_url: + print("ERROR: WEBAPP_URL env var is empty", file=sys.stderr) + return 2 + if not webapp_url.startswith("https://"): + print(f"ERROR: WEBAPP_URL must be https:// (got {webapp_url!r})", file=sys.stderr) + return 2 + + print(f"Configuring presence — webapp={webapp_url}") + + # Telegram requires the menu button URL to be HTTPS. + menu_ok = call(token, "setChatMenuButton", { + "menu_button": { + "type": "web_app", + "text": "Open Workout Tracker", + "web_app": {"url": webapp_url}, + }, + }) + desc_ok = call(token, "setMyDescription", {"description": DESCRIPTION}) + short_ok = call(token, "setMyShortDescription", {"short_description": SHORT_DESCRIPTION}) + # Clear any leftover slash-command list (we used to publish /start etc.). + cmds_ok = call(token, "setMyCommands", {"commands": []}) + + if menu_ok and desc_ok and short_ok and cmds_ok: + return 0 + print("WARN: at least one presence call failed (see above)", file=sys.stderr) + # Non-zero so systemd can see something went wrong, but with the `-` prefix + # on ExecStartPost the service still considers itself healthy. + return 1 + + +if __name__ == "__main__": + sys.exit(main())