Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,8 @@ def load_gateway_config() -> GatewayConfig:
os.environ["DISCORD_FREE_RESPONSE_CHANNELS"] = str(frc)
if "auto_thread" in discord_cfg and not os.getenv("DISCORD_AUTO_THREAD"):
os.environ["DISCORD_AUTO_THREAD"] = str(discord_cfg["auto_thread"]).lower()
if "reactions" in discord_cfg and not os.getenv("DISCORD_REACTIONS"):
os.environ["DISCORD_REACTIONS"] = str(discord_cfg["reactions"]).lower()

# Telegram settings β†’ env vars (env vars take precedence)
telegram_cfg = yaml_cfg.get("telegram", {})
Expand Down
8 changes: 8 additions & 0 deletions gateway/platforms/discord.py
Original file line number Diff line number Diff line change
Expand Up @@ -683,14 +683,22 @@ async def _remove_reaction(self, message: Any, emoji: str) -> bool:
logger.debug("[%s] remove_reaction failed (%s): %s", self.name, emoji, e)
return False

def _reactions_enabled(self) -> bool:
"""Check if message reactions are enabled via config/env."""
return os.getenv("DISCORD_REACTIONS", "true").lower() not in ("false", "0", "no")

async def on_processing_start(self, event: MessageEvent) -> None:
"""Add an in-progress reaction for normal Discord message events."""
if not self._reactions_enabled():
return
message = event.raw_message
if hasattr(message, "add_reaction"):
await self._add_reaction(message, "πŸ‘€")

async def on_processing_complete(self, event: MessageEvent, success: bool) -> None:
"""Swap the in-progress reaction for a final success/failure reaction."""
if not self._reactions_enabled():
return
message = event.raw_message
if hasattr(message, "add_reaction"):
await self._remove_reaction(message, "πŸ‘€")
Expand Down
1 change: 1 addition & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ def ensure_hermes_home():
"require_mention": True, # Require @mention to respond in server channels
"free_response_channels": "", # Comma-separated channel IDs where bot responds without mention
"auto_thread": True, # Auto-create threads on @mention in channels (like Slack)
"reactions": True, # Add πŸ‘€/βœ…/❌ reactions to messages during processing
},

# WhatsApp platform settings (gateway mode)
Expand Down
64 changes: 64 additions & 0 deletions tests/gateway/test_discord_reactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,67 @@ async def hold_typing(_chat_id, interval=2.0, metadata=None):
await adapter._process_message_background(event, build_session_key(event.source))

adapter.send.assert_awaited_once()


@pytest.mark.asyncio
async def test_reactions_disabled_via_env(adapter, monkeypatch):
"""When DISCORD_REACTIONS=false, no reactions should be added."""
monkeypatch.setenv("DISCORD_REACTIONS", "false")

raw_message = SimpleNamespace(
add_reaction=AsyncMock(),
remove_reaction=AsyncMock(),
)

async def handler(_event):
await asyncio.sleep(0)
return "ack"

async def hold_typing(_chat_id, interval=2.0, metadata=None):
await asyncio.Event().wait()

adapter.set_message_handler(handler)
adapter.send = AsyncMock(return_value=SendResult(success=True, message_id="999"))
adapter._keep_typing = hold_typing

event = _make_event("4", raw_message)
await adapter._process_message_background(event, build_session_key(event.source))

raw_message.add_reaction.assert_not_awaited()
raw_message.remove_reaction.assert_not_awaited()
# Response should still be sent
adapter.send.assert_awaited_once()


@pytest.mark.asyncio
async def test_reactions_disabled_via_env_zero(adapter, monkeypatch):
"""DISCORD_REACTIONS=0 should also disable reactions."""
monkeypatch.setenv("DISCORD_REACTIONS", "0")

raw_message = SimpleNamespace(
add_reaction=AsyncMock(),
remove_reaction=AsyncMock(),
)

event = _make_event("5", raw_message)
await adapter.on_processing_start(event)
await adapter.on_processing_complete(event, success=True)

raw_message.add_reaction.assert_not_awaited()
raw_message.remove_reaction.assert_not_awaited()


@pytest.mark.asyncio
async def test_reactions_enabled_by_default(adapter, monkeypatch):
"""When DISCORD_REACTIONS is unset, reactions should still work (default: true)."""
monkeypatch.delenv("DISCORD_REACTIONS", raising=False)

raw_message = SimpleNamespace(
add_reaction=AsyncMock(),
remove_reaction=AsyncMock(),
)

event = _make_event("6", raw_message)
await adapter.on_processing_start(event)

raw_message.add_reaction.assert_awaited_once_with("πŸ‘€")
Loading