Files
roboco/.claude/rules/telegram-bridge.md
2026-07-29 16:38:49 +02:00

7.3 KiB

paths
paths
roboco/services/telegram*.py
roboco/api/routes/telegram.py
roboco/utils/telegram_initdata.py
panel/src/app/(tg)/**
panel/src/components/tg/**

Telegram bridge (V1-V6)

Telegram notifications bridge V1+V2+V3 (default-off ROBOCO_TELEGRAM_ENABLED). V1: best-effort, outbound-only Telegram DMs to the CEO on escalation and completion. Mirrors the x_credentials pattern: a singleton Fernet-encrypted telegram_credentials row (migration 074, bot token + chat id; the API returns has_credentials only) behind CEO-only /telegram/credentials routes and a panel credentials card. _notify_telegram (roboco/services/notification_delivery.py) fans out from notify_ceo_of_escalation / notify_ceo_of_completion, sending only the notification's subject plus an optional panel deep-link (panel_base_url) — never the body — via a deferred, best-effort send that never raises into the producer (NullTelegramClient when unconfigured or the flag is off, LiveTelegramClient posting to the Bot API otherwise). V2 (ROBOCO_TELEGRAM_INBOUND_ENABLED, sub-switch on top of V1's flag — both plus stored credentials are required, otherwise the bot only sends and never listens) makes the bridge two-way: TelegramInboundEngine (roboco/services/telegram_inbound.py) long-polls getUpdates from a dedicated orchestrator loop (_telegram_poll_loop), authorizing every update by BOTH chat id and sender id, and routes /status / /queue / /task commands plus Approve/Reject/Open inline-keyboard taps (a compact apv|rej:<kind>:<id8> callback codec; a reject reason or a task-approve note is collected via a force_reply prompt held in a TTL'd in-memory pending-action map) through the SAME CEO-gated service calls the HTTP routes make (task/release/xpost/video/roadmap), stamping a via=telegram audit row on each. Escalation DMs (not completion DMs) carry the actionable keyboard when V2 is armed. All bot/bridge messages are HTML-styled (parse_mode=HTML with mandatory _esc/_esc_attr escaping at every dynamic interpolation and balance-aware 4096 truncation — the injection posture moved from no-parse_mode to escaping discipline), and every held-draft origination (release proposal, X post, video post, roadmap item via propose_roadmap) pushes a styled DM with its Approve/Reject keyboard the moment it materializes (notify_ceo_of_queue_item, best-effort, sharing /queue's renderer). Closing the loop exposed a real hole: a stale Approve/Reject button targets its item by id regardless of current status, so ReleaseProposalService.approve/.reject, XPostService.approve, and VideoPostService.approve now all refuse an already-CANCELLED (rejected) or already-COMPLETED (published/posted) target instead of silently re-executing — a fix that also closes the identical hole via a replayed HTTP call, not just Telegram. V3 adds a Telegram Mini App sign-in: POST /api/telegram/webapp-auth (roboco/api/routes/telegram.py, mounted only when telegram_miniapp_enabled AND cloud_auth_enabled are both armed — telegram_miniapp_enabled is env-only like cloud_auth_enabled, deliberately off the panel feature-flags card, and fails loud at startup if armed without cloud auth on) validates Telegram's signed initData (roboco/utils/telegram_initdata.py — pure HMAC-SHA256 WebAppData-keyed validation, constant-time compare, a telegram_initdata_max_age_seconds freshness window with 60s clock-skew tolerance) against the stored bot token and the CEO's own chat_id, then mints the same cloud-auth session cookie /api/auth/login issues — turning the CEO's phone into a real panel client at the new (tg) route group (/tg: Approvals/Inbox/Board/Chat tabs, outside the normal dashboard shell; proxy.ts's matcher excludes tg(?:/|$) so a phone session is never bounced to the password /login page it can't reach). Requires a public HTTPS origin (the cookie is secure-only) and BotFather's /setmenubutton pointed at https://<host>/tg. V4 (Mini App V4) rebuilds the cockpit and the command tier on both sides. Panel: the (tg) surface opens on a "Today" brief (GET /api/telegram/today, CEO-gated, one DB-only round trip via TgCockpitService — needs-you items, held-draft counts, fleet with per-agent task titles, day-rollup spend, ship state), the Approvals tab is a native card stack over all four held-draft queues (MainButton/BackButton/haptics with visible fallbacks; X 280-counter editing, blob-fetched video player, per-AC release view; a failed queue source is surfaced, never rendered as "queue is clear"), Chat/Today ride the shared /ws/system socket (invalidate-on-frame, poll fallback), theme adopts the user's Telegram themeParams scoped to #tg-shell, a dev-only mock bridge + /tg?demo=1 fixtures make the whole surface workable in a plain browser, and shared primitives (panel/src/components/tg/ui.tsx) carry the visual language. Bot: BOT_COMMANDS is the single registry driving /help AND a once-per-process Bot API setMyCommands sync; /agents /usage /blocked join the read tier, and /secretary + /newtask bridge the chat into the SAME in-process live runtimes the panel drives (roboco/services/telegram_bridge.py): a per-chat consumer task drains the PrompterLiveRegistry stream (sole consumer — no sync reply seam exists) and pushes one Telegram message per turn_end; free text routes into the live session; a draft event becomes a Send-to-Board/Discard keyboard whose confirm runs PrompterService.confirm_live_draft(route="board") and PARKS the session so board feedback streams back into the thread; /end reaps; the bridge sweeps its own idle TTL (the held stream arms the registry keepalive, so the registry's reap never fires), parked sessions exempt. Intake/secretary containers are process-wide singletons, so a bridged session preempts a live panel session of the same kind by construction; MegaTask batches still confirm in the panel only. V6 (Mini App V6) is the premium overhaul: a native-type design system on the #tg-shell tokens (borderless elevated cards, wallet-style tabular-numeral heroes, floating dock; Share Tech Mono demoted to the ROBOCO_ wordmark only) with Telegram window-chrome painting riding the theme bridge (setHeaderColor/setBackgroundColor/setBottomBarColor); Inbox moves behind a header bell as a pushed sub-page with humanized notifications (UUIDs resolve to task names via the Board's shared task index, tg-format.tsx); a new Metrics tab (period-segmented spend hero + by-agent/team/model + delivery/efficiency; tapping an agent pushes a drilldown over the previously-untapped /usage/time-series?agent_slug plus the member scorecard); Chat is rebuilt with honest scopes — Mine rides the participant-scoped /a2a/chat/conversations (resolved peer, real unread counts, mark-read on open, plain CEO send) while Fleet rides the admin list (task-linked threads interject via replyAsCeo with a recipient chip, task-less threads are watch-only), both with markdown transcripts, live pulse flashes, and a pinned Secretary live chat on the same secretary_live SSE session runtime the panel drives; and the Board task sheet carries the CEO's own decide verbs (approve / request-changes / unblock) instead of being read-only. The /api/dashboard/* router is now require_panel_token-gated at router level (mirroring /api/usage), closing the unauthenticated metrics/scorecard exposure.