Skip to content

feat(gateway): add inbound reaction event routing for Discord - #8379

Open
yshen92 wants to merge 8 commits into
NousResearch:mainfrom
yshen92:feat/discord-reaction-events
Open

feat(gateway): add inbound reaction event routing for Discord#8379
yshen92 wants to merge 8 commits into
NousResearch:mainfrom
yshen92:feat/discord-reaction-events

Conversation

@yshen92

@yshen92 yshen92 commented Apr 12, 2026

Copy link
Copy Markdown

What does this PR do?

Adds inbound reaction event handling to the Discord adapter, porting the proven "synthetic reaction event" pattern from the Feishu adapter. Users can now interact with Hermes via emoji reactions on Discord — reacting on a bot message routes a reaction:{action}:{emoji} synthetic text event through the normal message pipeline.

Currently the Discord adapter can send reactions (👀, ✅, ❌) but has no listener to read incoming user reactions. This closes that gap and brings Discord to feature parity with Feishu.

Related Issue

N/A — identified during user feature audit.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • gateway/platforms/discord.py:

    • Added _handle_inbound_reaction() method — filters bot-self reactions, disallowed users, non-bot messages; builds synthetic MessageEvent with reaction:{action}:{emoji} text; routes via handle_message()
    • Added on_raw_reaction_add and on_raw_reaction_remove event handlers in connect()
    • Added dedup guard for Discord RESUME replay protection
    • Added _client/_client.user null guard for post-disconnect race
    • Added TYPE_CHECKING import for RawReactionActionEvent type hint
    • Added Literal["added", "removed"] typing on action parameter
    • Added comment documenting reaction intents coverage in Intents.default()
  • tests/gateway/test_discord_reactions.py (14 new tests):

    • Happy path routing as synthetic text event
    • Non-bot messages ignored
    • Bot self-reactions ignored (loop prevention)
    • Disallowed users ignored
    • DISCORD_REACTIONS=false blocks inbound routing
    • Reaction removal uses "removed" action
    • Uncached channel fetched via fetch_channel
    • Guild context resolves chat_name with server name
    • Member display_name used as user_name
    • Channel fetch failure handled gracefully
    • Feedback loop prevention (raw_message=payload not Message)
    • DM fallback user_name when member is None
    • _client=None graceful handling
    • Duplicate reaction suppression

How to Test

  1. Run tests: pytest tests/gateway/test_discord_reactions.py -v
  2. Manual test on a Discord server:
    • Have the bot respond to a message
    • React with 👍 on the bots message
    • Verify the agent receives reaction:added:👍 and processes it
    • Remove the reaction
    • Verify the agent receives reaction:removed:👍

Checklist

Code

  • I have read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this is not a duplicate
  • My PR contains only changes related to this feature (no unrelated commits)
  • I have run tests and all pass (277 gateway tests including 21 reaction tests)
  • I have added tests for my changes (14 new tests)
  • I have tested on my platform: macOS, Linux

Documentation & Housekeeping

  • N/A — no docs/config/architecture/tool schema changes needed

Screenshots / Logs

Full test_discord_reactions.py suite (21 tests, 0 failures):

$ pytest tests/gateway/test_discord_reactions.py -v --tb=no
...
tests/gateway/test_discord_reactions.py::test_reactions_enabled_by_default PASSED
tests/gateway/test_discord_reactions.py::test_reactions_disabled_via_env PASSED
tests/gateway/test_discord_reactions.py::test_reactions_disabled_via_env_zero PASSED
tests/gateway/test_discord_reactions.py::test_reaction_helper_failures_do_not_break_message_flow PASSED
tests/gateway/test_discord_reactions.py::test_interaction_backed_events_do_not_attempt_reactions PASSED
tests/gateway/test_discord_reactions.py::test_process_message_background_adds_and_swaps_reactions PASSED
tests/gateway/test_discord_reactions.py::test_on_processing_complete_cancelled_removes_eyes_without_terminal_reaction PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_routes_as_synthetic_text_event PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_on_non_bot_message_ignored PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_from_bot_ignored PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_disallowed_user_ignored PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_disabled_via_env PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_remove_routes_correctly PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_fetches_uncached_channel PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_with_guild_resolves_chat_name PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_member_display_name_used PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_channel_fetch_failure_handled PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_no_feedback_loop_on_raw_message PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_dm_fallback_user_name PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_client_none_handled PASSED
tests/gateway/test_discord_reactions.py::test_inbound_reaction_duplicate_suppressed PASSED

============================== 21 passed in 0.63s ==============================

yshen92 added 5 commits June 20, 2026 18:35
Port the Feishu adapter's synthetic reaction event pattern to Discord.
Users reacting on bot messages are now routed as 'reaction:{action}:{emoji}'
synthetic text events through the normal message pipeline.

- Add _handle_inbound_reaction() method to DiscordAdapter
- Add build_session_key import from gateway.session
- Filter: bot's own reactions, disallowed users, non-bot messages
- Use handle_message() for proper session lifecycle management
- Match Feishu's reaction:{action}:{emoji} synthetic text format
Add Discord event listeners for reaction add/remove alongside existing
on_ready, on_message, and on_voice_state_update handlers in connect().
Cover the full _handle_inbound_reaction code path:
- Happy path routing as synthetic text event
- Non-bot messages ignored
- Bot's own reactions ignored (loop prevention)
- Disallowed users ignored
- DISCORD_REACTIONS=false blocks inbound routing
- Reaction removal uses 'removed' action
- Uncached channel fetched via fetch_channel
- Guild context resolves chat_name with server name
- Member display_name used as user_name
- Channel fetch failure handled gracefully
…ging

Critical:
- Fix feedback loop: pass payload as raw_message instead of fetched Message
  to prevent on_processing_start/complete from adding 👀/✅/❌ on bot messages

Important:
- Remove dead build_session_key import (already imported locally where needed)
- Add dedup guard using composite key for Discord RESUME replay protection
- Use logger.warning instead of logger.debug for operational errors
- Use composite message_id for unique event identification

Minor:
- Add _client/_client.user null guard for post-disconnect race condition
- Add comment documenting reaction intents in Intents.default()
- Fix magic number in test_inbound_reaction_from_bot_ignored

Tests:
- Add test for feedback loop prevention (raw_message is payload, not Message)
- Add test for DM fallback user_name when member is None
- Add test for _client=None graceful handling
…reading

- message_id stays as str(payload.message_id) for valid Discord snowflake
  used by send() for reply_to resolution via int()
- dedup_key remains separate for _dedup.is_duplicate() reconnect protection
- Add test_inbound_reaction_duplicate_suppressed for dedup coverage
@yshen92
yshen92 force-pushed the feat/discord-reaction-events branch from 97e788b to b87a2d3 Compare June 20, 2026 11:04
@jasonwc

jasonwc commented Jul 7, 2026

Copy link
Copy Markdown

Would love to see this feature implemented! Currently setting up a Discord bot and want to establish more complex workflows where a cron runs and I can react to it to continue the workflow.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for implementing a real missing Discord capability: current main has outbound lifecycle reactions but no inbound raw-reaction listener.

Problems

  • plugins/platforms/discord/adapter.py:1699 calls _is_allowed_user() with only an ID. Current main’s normal message path supplies member, guild/DM, and channel context (plugins/platforms/discord/adapter.py:1157-1163); without it, role-only and channel-only authorization cannot admit reactions. The synthetic source also lacks the role-authorized signal consumed by gateway/authz_mixin.py:431-437.
  • plugins/platforms/discord/adapter.py:1749 dispatches directly to handle_message(), bypassing the normal DISCORD_ALLOWED_CHANNELS and DISCORD_IGNORED_CHANNELS checks at plugins/platforms/discord/adapter.py:6172-6185.

Suggested changes

  • Resolve the channel/member/guild first, apply the same channel and authorization gates as normal Discord input, and preserve role authorization on the constructed source.
  • Add tests for role-only access plus allowed/ignored channel policy parity.

Automated hermes-sweeper review.

Comment thread plugins/platforms/discord/adapter.py Outdated
Comment thread plugins/platforms/discord/adapter.py
@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 12, 2026
@yshen92
yshen92 requested a review from teknium1 July 14, 2026 09:19
@yshen92

yshen92 commented Jul 14, 2026

Copy link
Copy Markdown
Author

Hi @teknium1, thanks for the review. I have addressed them accordingly. Feel free to let me know if there're any changes needed.

…-events

# Conflicts:
#	plugins/platforms/discord/adapter.py
#	tests/gateway/test_discord_reactions.py
# Conflicts:
#	plugins/platforms/discord/adapter.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have platform/discord Discord bot adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants