From 99100b847bad944498e7acb038c5bcae78fc8f56 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Sat, 13 Jun 2026 14:00:09 +0300 Subject: [PATCH] comenzi telegram --- README.md | 10 ++++ scripts/register_telegram_commands.py | 70 +++++++++++++++++++++++++++ src/atm/commands.py | 44 +++++++++++++++++ src/atm/notifier/AGENTS.md | 10 +++- 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 scripts/register_telegram_commands.py diff --git a/README.md b/README.md index 8600224..9cb2556 100644 --- a/README.md +++ b/README.md @@ -427,6 +427,16 @@ Trimiți în chat-ul bot-ului: Doar `allowed_chat_ids` sunt acceptate. După 3 `401` consecutive, poller-ul intră în mod degradat. +**Meniul slash (autocomplete pe `/` în client).** Se înregistrează **automat** la fiecare `atm run` (deci și prin `atm.bat`) — nu trebuie să faci nimic. Pentru (re)set/list/clear manual fără să pornești aplicația: + +```powershell +.\.venv\Scripts\python.exe scripts\register_telegram_commands.py # înregistrează +.\.venv\Scripts\python.exe scripts\register_telegram_commands.py --list # ce e setat acum +.\.venv\Scripts\python.exe scripts\register_telegram_commands.py --clear # șterge +``` + +Discord este **webhook outbound** în acest proiect (doar trimite alerte) — nu primește comenzi și nu poate avea slash commands fără un bot application separat. + --- ## După sesiune diff --git a/scripts/register_telegram_commands.py b/scripts/register_telegram_commands.py new file mode 100644 index 0000000..5c41442 --- /dev/null +++ b/scripts/register_telegram_commands.py @@ -0,0 +1,70 @@ +r"""Înregistrează comenzile slash ale bot-ului Telegram (meniul "/" din client). + +`atm run` înregistrează deja comenzile automat la pornirea poller-ului +(`TelegramPoller._register_commands`). Acest script e pentru cazurile când vrei +să le (re)setezi fără să pornești aplicația, să le listezi sau să le ștergi. + +Apelează Bot API `setMyCommands` cu aceeași listă (`atm.commands.TELEGRAM_COMMANDS`). +După rulare, în clientul Telegram apare meniul de comenzi cu autocomplete + descriere. + +Notă: Telegram NU suportă parametri tipați pe slash command. Comenzile cu +argument (`/interval`, `/window`, `/rebase`) inserează doar prefixul; formatul +argumentului e documentat în descriere. + +Discord nu e acoperit aici: în acest proiect Discord e webhook *outbound* — +nu există bot application / interactions endpoint, deci nu poate avea slash commands. + +Usage: + .\.venv\Scripts\python.exe scripts\register_telegram_commands.py + .\.venv\Scripts\python.exe scripts\register_telegram_commands.py --list # doar afișează ce e setat acum + .\.venv\Scripts\python.exe scripts\register_telegram_commands.py --clear # șterge toate comenzile +""" +from __future__ import annotations + +import sys + +import requests + +from atm.commands import TELEGRAM_COMMANDS +from atm.config import _ensure_env_loaded, _require_env + +_BASE = "https://api.telegram.org/bot{token}/{method}" + + +def _call(token: str, method: str, payload: dict | None = None) -> dict: + resp = requests.post(_BASE.format(token=token, method=method), json=payload or {}, timeout=10) + body = resp.json() + if not body.get("ok"): + raise SystemExit(f"Telegram {method} eșuat: {body}") + return body + + +def main(argv: list[str]) -> int: + _ensure_env_loaded() # citește .env din rădăcină dacă există + token = _require_env("ATM_TG_TOKEN") + + if "--list" in argv: + body = _call(token, "getMyCommands") + cmds = body.get("result", []) + if not cmds: + print("(niciun command setat)") + for c in cmds: + print(f"/{c['command']:<10} {c['description']}") + return 0 + + if "--clear" in argv: + _call(token, "deleteMyCommands") + print("✓ Comenzi șterse.") + return 0 + + commands = [{"command": c, "description": d} for c, d in TELEGRAM_COMMANDS] + _call(token, "setMyCommands", {"commands": commands}) + print(f"✓ {len(commands)} comenzi înregistrate la bot-ul Telegram:") + for c, d in TELEGRAM_COMMANDS: + print(f" /{c:<10} {d}") + print("\nDeschide chat-ul bot-ului și apasă butonul de meniu (/) ca să le vezi.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/src/atm/commands.py b/src/atm/commands.py index cea039e..56d4848 100644 --- a/src/atm/commands.py +++ b/src/atm/commands.py @@ -23,6 +23,24 @@ CommandAction = Literal[ _BASE = "https://api.telegram.org/bot{token}/{method}" +# Slash-command menu shown in the Telegram client (autocomplete on "/"). +# Single source of truth: the poller registers these at startup via +# setMyCommands, and scripts/register_telegram_commands.py reuses the list. +# Keep in sync with the `/help` handler in main.py (same commands, same order). +# command must match [a-z0-9_]{1,32}; description is 1-256 chars. Telegram has +# no typed args, so arg formats live in the description text. +TELEGRAM_COMMANDS: list[tuple[str, str]] = [ + ("status", "Stare FSM, uptime, ultima detecție, fereastră open/closed"), + ("ss", "Screenshot acum (top-3 buline din ROI)"), + ("pause", "Suspendă detecția (heartbeat-urile continuă)"), + ("resume", "Reia detecția (șterge user-pause + drift-pause)"), + ("rebase", "Propune phash nou pentru canary — aplici cu: /rebase confirm"), + ("interval", "Auto-screenshot la N minute, ex: /interval 3"), + ("stop", "Oprește scheduler-ul de auto-screenshot"), + ("window", "Fereastră monitorizare locală, ex: /window 19:40-21:45 (off = dezactivează)"), + ("help", "Listă scurtă a tuturor comenzilor"), +] + _HHMM = __import__("re").compile(r"^([01]?\d|2[0-3]):[0-5]\d$") @@ -73,6 +91,7 @@ class TelegramPoller: async def run(self) -> None: async with httpx.AsyncClient() as client: await self._drain(client) + await self._register_commands(client) while True: if self._degraded: await asyncio.sleep(5) @@ -88,6 +107,31 @@ class TelegramPoller: self._audit.log({"event": "poller_error", "error": str(exc)}) await asyncio.sleep(5) + async def _register_commands(self, client: httpx.AsyncClient) -> None: + """Populate the Telegram '/' menu via setMyCommands (TELEGRAM_COMMANDS). + + Best-effort: any failure (bad token, network) is logged and swallowed so + it never blocks polling. Idempotent — safe to call on every startup. + """ + try: + resp = await client.post( + _BASE.format(token=self._cfg.bot_token, method="setMyCommands"), + json={"commands": [ + {"command": c, "description": d} for c, d in TELEGRAM_COMMANDS + ]}, + timeout=10, + ) + body = resp.json() + if body.get("ok"): + self._audit.log({ + "event": "telegram_commands_registered", + "count": len(TELEGRAM_COMMANDS), + }) + else: + logger.warning("setMyCommands failed: %s", body) + except Exception as exc: + logger.warning("setMyCommands error: %s", exc) + async def _drain(self, client: httpx.AsyncClient) -> None: """Discard all pending updates at startup so stale commands don't replay.""" try: diff --git a/src/atm/notifier/AGENTS.md b/src/atm/notifier/AGENTS.md index 0655111..9b2477a 100644 --- a/src/atm/notifier/AGENTS.md +++ b/src/atm/notifier/AGENTS.md @@ -18,7 +18,15 @@ comenzi live Telegram. Fan-out trimite același eveniment pe mai multe canale. `TelegramNotifier` (sync) rămâne pe **requests**. Nu amesteca cele două. - **Comenzi live:** `/ss` `/status` `/pause` `/resume` `/rebase` `/3` (interval min) `/stop` `/window`. **Doar Telegram** primește comenzi — Discord e webhook - outbound, fără poller. + outbound, fără poller, deci **nu poate avea slash commands** (ar necesita un + bot application + interactions endpoint, care nu există). +- **Meniul slash din clientul Telegram** (autocomplete pe `/`) vine din + `setMyCommands`. Sursa unică e `commands.TELEGRAM_COMMANDS`; poller-ul îl + înregistrează **automat la startup** (`TelegramPoller._register_commands`, + best-effort — un eșec nu blochează polling-ul). `scripts/register_telegram_commands.py` + refolosește aceeași listă pentru (re)set/list/clear manual fără să pornești app-ul. + `TELEGRAM_COMMANDS` trebuie să oglindească handler-ul `/help` din `main.py` + (aceleași comenzi, aceeași ordine) — modifici una, actualizezi cealaltă în același commit. ### Contracte pe comenzi (nu le slăbi fără update aici)