feat(gateway): route inbound Telegram reactions as synthetic events - #1
Open
RichardAtCT wants to merge 1 commit into
Open
feat(gateway): route inbound Telegram reactions as synthetic events#1RichardAtCT wants to merge 1 commit into
RichardAtCT wants to merge 1 commit into
Conversation
When TELEGRAM_INBOUND_REACTIONS=true, reactions a user places on bot-authored messages (👍 ✅ 👎 ❌) are routed through the normal message pipeline as synthetic "reaction:added:EMOJI" / "reaction:removed:EMOJI" events — mirroring the Feishu precedent in gateway/platforms/feishu.py. The bot can observe 👍 on its own question as a lightweight confirmation signal instead of needing a follow-up "yes" message. Implementation: - Register MessageReactionHandler when the feature is on (PTB's polling/webhook already allows Update.ALL_TYPES). - Cache recent bot-authored outbound messages in a bounded, TTL-pruned OrderedDict keyed by (chat_id, message_id) so reactions on third-party messages and stale messages are ignored. Send paths are hooked to record each outbound with a kind tag (regular / approval / model_picker). - Drop reactions from the bot's own account to avoid feedback loops with lifecycle reactions. - Only route allowlisted emoji (👍 ✅ 👎 ❌) in v1; custom premium emoji and anonymous aggregated counts are ignored. Config: new TELEGRAM_INBOUND_REACTIONS env var (default false), with YAML bridge telegram.inbound_reactions → env var alongside the existing telegram.reactions bridge. Docs: adds an "Inbound reactions (experimental)" section in the Telegram user guide and a row in the environment variables reference. Tests: adds 14 unit tests covering env toggle, cache insert/evict/TTL, unknown-message drop, added/removed/swap routing, bot-self filter, unsupported emoji, anonymous admin, custom-emoji skip, and two config-bridge tests.
|
PR Review Summary
What looks good
Verdict — Friday, AI assistant to @RichardAtCT |
RichardAtCT
pushed a commit
that referenced
this pull request
Jun 12, 2026
Add an official, production-grade WhatsApp integration via Meta's Business Cloud API as a complement to the existing Baileys bridge. No bridge subprocess, no QR codes, no account-ban risk — at the cost of a Meta Business account and a public HTTPS webhook URL. Setup is fully wizard-driven: 'hermes whatsapp-cloud' walks through every credential with paste-time validation (catches the #1 trap of pasting a phone number into the Phone Number ID field), generates a verify token, and ends with copy-paste instructions for the cloudflared / Meta-dashboard / Business Manager pieces that can't be automated. The wizard also points users at Meta's Business Manager for setting the bot's display name and profile picture. Feature set: - Inbound: text, images (with native-vision routing), voice notes (STT), documents (small text inlined, larger cached), reply context. - Outbound: text with WhatsApp-flavored markdown conversion, images, videos, documents, opus voice notes via ffmpeg with MP3 fallback. - Native interactive buttons for clarify, dangerous-command approval, and slash-command confirmation flows — matches the Telegram / Discord UX, graceful degrades to plain text. - Read receipts (blue double-checkmarks) and typing indicator, using Meta's combined endpoint so they fire in a single API call. - Webhook security: X-Hub-Signature-256 HMAC verification (raw body, constant-time), wamid deduplication, group-shaped-message refusal (groups deferred to v2 — Baileys still covers them). - Full integration with the gateway's session, cron, display-tier, prompt-hint, and auth-allowlist systems. Cloud and Baileys can run side-by-side against different phone numbers. Also wires STT (speech-to-text) through Nous's managed audio gateway for Nous subscribers — previously the default stt.provider=local required a separate faster-whisper install. New subscribers now get voice-note transcription out of the box. Docs: 418-line user guide at website/docs/user-guide/messaging/ whatsapp-cloud.md, sidebar entry, environment-variables reference, ADDING_A_PLATFORM.md updated with the optional interactive-UX contract for future adapter authors. Tests: 100 dedicated tests for the adapter, 32 for the setup wizard, 20 for the Nous subscription STT wiring, plus regression coverage across display_config, prompt_builder, and the cron scheduler. Known limitations (deferred until clear demand signal): - Group chats — use the Baileys bridge if you need them. - Message templates for 24-hour-window outside-conversation sends — reactive chat is unaffected; cron / delegate_task with gaps > 24h will fail with a clear error. The agent's system prompt warns the model about this so it knows to mention it when scheduling delayed messages.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds v1 inbound Telegram reaction handling so users can confirm a bot question with 👍/✅ instead of typing "yes". Reactions on bot-authored messages are converted into synthetic
reaction:added:EMOJI/reaction:removed:EMOJIMessageEvents and routed through the normal message pipeline — mirroring the existing Feishu precedent ingateway/platforms/feishu.py.MessageReactionHandlerregistered only whenTELEGRAM_INBOUND_REACTIONS=true. Polling/webhook already subscribe withUpdate.ALL_TYPES, so no transport changes.send_message,send_update_prompt,send_exec_approval,send_model_picker,send_voice,send_image_file,send_document,send_video,send_image×2,send_animation) records(chat_id, message_id) → {ts, kind, thread_id, …}in an in-memory OrderedDict with size cap (512) and TTL (24h). Reactions on non-cached messages are ignored.kindtags outbound asregular/approval/model_pickerso future logic can gate wake-up to just approval prompts without another refactor.self._bot.idare dropped to avoid a feedback loop with the outbound lifecycle reactions.ReactionTypeCustomEmoji, andMessageReactionCountUpdatedare silently ignored.TELEGRAM_INBOUND_REACTIONS(defaultfalse), with YAML bridgetelegram.inbound_reactionsingateway/config.pyalongside the existingtelegram.reactionsbridge.Scope: observability only, not auto-confirmation (important)
This PR delivers the transport layer — a user reaction on a bot message reaches the adapter and enters the normal message pipeline as a synthetic
MessageEvent. It does not yet wire those events into the approval/clarify flow, so reacting 👍 on "Should I install this?" will not shortcut the existing/approvemechanism. The LLM will just seereaction:added:👍as user text and respond in natural language.Making 👍 actually auto-confirm a pending approval prompt requires prompt-layer work (e.g. the approval flow listening for
reaction:added:👍on messages it authored withkind="approval", and callingresolve_gateway_approval()). That's a deliberate follow-up, not this PR — the cache already tags approval prompts withkind="approval"so the follow-up is a one-place hook, not a refactor.Test this PR by confirming the event reaches
handle_message, not by judging the agent's natural-language reply.Files changed
gateway/platforms/telegram.py— imports, cache + lock in__init__,_inbound_reactions_enabled,_remember_bot_message,_extract_reaction_emojis,_handle_message_reactiondispatcher, conditional handler registration, 10 send-path cache hooks.gateway/config.py— one two-line YAML bridge.tests/gateway/test_telegram_reactions.py— +14 unit tests (test_inbound_*andtest_remember_bot_message_*) and 2 config-bridge tests, mirroring the existing outbound-reaction test style.website/docs/user-guide/messaging/telegram.md,website/docs/reference/environment-variables.md— user-facing docs.Diff: +547 / -1 across 5 files, no unrelated reflow.
Why v1 is deliberately narrow
kind), but dispatch currently routes any kind. Approval-only gating is a one-line follow-up once we want it.reaction:added:🎉.MessageReactionCountUpdated— v1 targets explicit user-attributed reactions; aggregated anonymous counts add complexity without confirmation value.Test plan
python3— no venv available in this checkout forscripts/run_tests.sh).scripts/run_tests.sh tests/gateway/test_telegram_reactions.py -vonce a venv is wired up locally.scripts/run_tests.sh tests/gateway/ -v -k telegram.TELEGRAM_INBOUND_REACTIONS=true, have the bot send a message, react with 👍 — confirm the gateway logs a syntheticreaction:added:👍MessageEvent. React with 🎉 — confirm no event. React on a message the bot didn't send — confirm no event.👀→👍/👎) do not loop back as inbound events (the self-filter). Requires the bot to be an admin or privacy mode off — DMs always deliver.Follow-ups (not in this PR)
reaction:added:{👍,✅}onMessageEvents whose cachedkind=="approval"and callresolve_gateway_approval()to unblock the agent without a text reply.confirm, 👎/❌ →deny) for prompt-layer code.Precedent
Feishu implemented the same synthetic-event pattern at
gateway/platforms/feishu.py:2054(_on_reaction_event) +:2163(_handle_reaction_event). Telegram uses the samereaction:added:EMOJI/reaction:removed:EMOJItext format and the same routing throughself.handle_message(event).Related tracking: issue NousResearch#10583 (Slack inbound reactions) and PR NousResearch#8379 (Discord inbound reactions) in upstream NousResearch/hermes-agent show the same cross-platform direction.