Skip to content

feat(gateway): route inbound Telegram reactions as synthetic events - #1

Open
RichardAtCT wants to merge 1 commit into
mainfrom
feat/telegram-inbound-reactions
Open

feat(gateway): route inbound Telegram reactions as synthetic events#1
RichardAtCT wants to merge 1 commit into
mainfrom
feat/telegram-inbound-reactions

Conversation

@RichardAtCT

@RichardAtCT RichardAtCT commented Apr 22, 2026

Copy link
Copy Markdown
Owner

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:EMOJI MessageEvents and routed through the normal message pipeline — mirroring the existing Feishu precedent in gateway/platforms/feishu.py.

  • New MessageReactionHandler registered only when TELEGRAM_INBOUND_REACTIONS=true. Polling/webhook already subscribe with Update.ALL_TYPES, so no transport changes.
  • Every bot-authored send path (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.
  • kind tags outbound as regular / approval / model_picker so future logic can gate wake-up to just approval prompts without another refactor.
  • Self-filter: reactions whose actor matches self._bot.id are dropped to avoid a feedback loop with the outbound lifecycle reactions.
  • v1 allowlist: 👍 ✅ 👎 ❌. Other emoji on bot messages, ReactionTypeCustomEmoji, and MessageReactionCountUpdated are silently ignored.
  • New env var TELEGRAM_INBOUND_REACTIONS (default false), with YAML bridge telegram.inbound_reactions in gateway/config.py alongside the existing telegram.reactions bridge.
  • Docs updated: Telegram user guide gains an "Inbound reactions (experimental)" subsection; environment-variables reference gains one row.

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 /approve mechanism. The LLM will just see reaction: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 with kind="approval", and calling resolve_gateway_approval()). That's a deliberate follow-up, not this PR — the cache already tags approval prompts with kind="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_reaction dispatcher, 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_* and test_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

  • Cache scope — every bot message is cached and tagged (kind), but dispatch currently routes any kind. Approval-only gating is a one-line follow-up once we want it.
  • Emoji allowlist — 👍 ✅ 👎 ❌ only. Easier to widen later than to narrow after downstream starts depending on reaction:added:🎉.
  • In-memory cache — no persistence. A reaction on a message sent before a restart is ignored. Acceptable for confirmation UX; upgrade to SQLite only if we need it.
  • No MessageReactionCountUpdated — v1 targets explicit user-attributed reactions; aggregated anonymous counts add complexity without confirmation value.

Test plan

  • 14 new inbound unit tests pass (driven manually via python3 — no venv available in this checkout for scripts/run_tests.sh).
  • Full suite via scripts/run_tests.sh tests/gateway/test_telegram_reactions.py -v once a venv is wired up locally.
  • Broader regression: scripts/run_tests.sh tests/gateway/ -v -k telegram.
  • Manual smoke on a test bot: enable TELEGRAM_INBOUND_REACTIONS=true, have the bot send a message, react with 👍 — confirm the gateway logs a synthetic reaction:added:👍 MessageEvent. React with 🎉 — confirm no event. React on a message the bot didn't send — confirm no event.
  • In a group, verify the bot's own outbound lifecycle reactions (👀👍/👎) 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)

  • Wire approval auto-confirmation: in the approval flow, subscribe to reaction:added:{👍,✅} on MessageEvents whose cached kind=="approval" and call resolve_gateway_approval() to unblock the agent without a text reply.
  • Mirror the canonical-signal helper (👍/✅ → confirm, 👎/❌ → deny) for prompt-layer code.
  • Persistent cache if we need cross-restart reaction routing.
  • Widen emoji allowlist if downstream signals demand it.

Precedent

Feishu implemented the same synthetic-event pattern at gateway/platforms/feishu.py:2054 (_on_reaction_event) + :2163 (_handle_reaction_event). Telegram uses the same reaction:added:EMOJI / reaction:removed:EMOJI text format and the same routing through self.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.

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.
@FridayOpenClawBot

Copy link
Copy Markdown

PR Review
Reviewed head: 4d47ddded240b0f34b0540f811d7b537bca3d3a2

Summary

  • This adds a gated v1 inbound Telegram reaction path that mirrors the existing Feishu synthetic-event pattern, with a bounded in-memory cache so only reactions on recent bot-authored messages are routed.
  • The implementation is deliberately narrow in the right places: allowlisted emoji only, explicit self-filtering, and no cost when the feature flag is off.

What looks good

  • gateway/platforms/telegram.py registers MessageReactionHandler only when TELEGRAM_INBOUND_REACTIONS is enabled, so disabled installs avoid both extra handlers and cache churn.
  • The outbound-message cache is wired through the actual bot send paths (send_message, approvals, model picker, voice/media/document/video/image/animation), which is the right place to enforce “only react to messages we sent”.
  • _handle_message_reaction() does the important safety checks in the right order: unknown message drop, self-reaction drop, allowlist filtering, then synthetic reaction:added:* / reaction:removed:* dispatch.
  • The regression coverage is solid. I reviewed the added tests and also ran the targeted suite locally: python -m pytest tests/gateway/test_telegram_reactions.py -q -n 4 → 34 passed.

Verdict
✅ Ready to merge

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants