feat: scripts/set-bot-presence.py for chat-side bot presence
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) <noreply@anthropic.com>
This commit is contained in:
parent
9f146d60fa
commit
9e50686983
1 changed files with 115 additions and 0 deletions
115
scripts/set-bot-presence.py
Executable file
115
scripts/set-bot-presence.py
Executable file
|
|
@ -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())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue