From 1f216ecbb4797035362891e584039fc386ec247f Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:14:33 +0530 Subject: [PATCH] feat(gateway/slack): add SLACK_REACTIONS env toggle for reaction lifecycle Adds _reactions_enabled() gating to match Discord (DISCORD_REACTIONS) and Telegram (TELEGRAM_REACTIONS) pattern. Defaults to true to preserve existing behavior. Gates at three levels: - _handle_slack_message: skips _reacting_message_ids registration - on_processing_start: early return - on_processing_complete: early return Also adds config.yaml bridge (slack.reactions) and two new tests. --- gateway/config.py | 2 ++ gateway/platforms/slack.py | 10 +++++++- tests/gateway/test_slack.py | 50 +++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/gateway/config.py b/gateway/config.py index d1d84da10..67ebf7346 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -616,6 +616,8 @@ def load_gateway_config() -> GatewayConfig: if isinstance(frc, list): frc = ",".join(str(v) for v in frc) os.environ["SLACK_FREE_RESPONSE_CHANNELS"] = str(frc) + if "reactions" in slack_cfg and not os.getenv("SLACK_REACTIONS"): + os.environ["SLACK_REACTIONS"] = str(slack_cfg["reactions"]).lower() # Discord settings → env vars (env vars take precedence) discord_cfg = yaml_cfg.get("discord", {}) diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index a4ea5febd..191689a5a 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -607,8 +607,14 @@ class SlackAdapter(BasePlatformAdapter): logger.debug("[Slack] reactions.remove failed (%s): %s", emoji, e) return False + def _reactions_enabled(self) -> bool: + """Check if message reactions are enabled via config/env.""" + return os.getenv("SLACK_REACTIONS", "true").lower() not in ("false", "0", "no") + async def on_processing_start(self, event: MessageEvent) -> None: """Add an in-progress reaction when message processing begins.""" + if not self._reactions_enabled(): + return ts = getattr(event, "message_id", None) if not ts or ts not in self._reacting_message_ids: return @@ -618,6 +624,8 @@ class SlackAdapter(BasePlatformAdapter): async def on_processing_complete(self, event: MessageEvent, outcome: ProcessingOutcome) -> None: """Swap the in-progress reaction for a final success/failure reaction.""" + if not self._reactions_enabled(): + return ts = getattr(event, "message_id", None) if not ts or ts not in self._reacting_message_ids: return @@ -1260,7 +1268,7 @@ class SlackAdapter(BasePlatformAdapter): # Only react when bot is directly addressed (DM or @mention). # In listen-all channels (require_mention=false), reacting to every # casual message would be noisy. - _should_react = is_dm or is_mentioned + _should_react = (is_dm or is_mentioned) and self._reactions_enabled() if _should_react: self._reacting_message_ids.add(ts) diff --git a/tests/gateway/test_slack.py b/tests/gateway/test_slack.py index 1681a87c6..cdd27364b 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -1138,6 +1138,56 @@ class TestReactions: adapter._app.client.reactions_add.assert_not_called() adapter._app.client.reactions_remove.assert_not_called() + @pytest.mark.asyncio + async def test_reactions_disabled_via_env(self, adapter, monkeypatch): + """SLACK_REACTIONS=false should suppress all reaction lifecycle.""" + monkeypatch.setenv("SLACK_REACTIONS", "false") + adapter._app.client.reactions_add = AsyncMock() + adapter._app.client.reactions_remove = AsyncMock() + adapter._app.client.users_info = AsyncMock(return_value={ + "user": {"profile": {"display_name": "Tyler"}} + }) + + event = { + "text": "hello", + "user": "U_USER", + "channel": "C123", + "channel_type": "im", + "ts": "1234567890.000004", + } + await adapter._handle_slack_message(event) + + # Should NOT register for reactions when toggle is off + assert "1234567890.000004" not in adapter._reacting_message_ids + + # Hooks should also be no-ops when disabled + from gateway.platforms.base import MessageEvent, MessageType, SessionSource, ProcessingOutcome + from gateway.config import Platform + source = SessionSource( + platform=Platform.SLACK, + chat_id="C123", + chat_type="dm", + user_id="U_USER", + ) + msg_event = MessageEvent( + text="hello", + message_type=MessageType.TEXT, + source=source, + message_id="1234567890.000004", + ) + # Force-add to verify hooks respect the toggle independently + adapter._reacting_message_ids.add("1234567890.000004") + await adapter.on_processing_start(msg_event) + await adapter.on_processing_complete(msg_event, ProcessingOutcome.SUCCESS) + + adapter._app.client.reactions_add.assert_not_called() + adapter._app.client.reactions_remove.assert_not_called() + + @pytest.mark.asyncio + async def test_reactions_enabled_by_default(self, adapter): + """SLACK_REACTIONS defaults to true (matches existing behavior).""" + assert adapter._reactions_enabled() is True + # --------------------------------------------------------------------------- # TestThreadReplyHandling