fix(slack): honor disable_dms setting

This commit is contained in:
vexclawx31 2026-06-26 12:42:49 +02:00 committed by Teknium
parent f8f5ce7da5
commit ee62aab1a7
3 changed files with 92 additions and 4 deletions

View file

@ -5423,6 +5423,13 @@ class SlackAdapter(BasePlatformAdapter):
if not channel_type and channel_id.startswith("D"):
channel_type = "im"
is_dm = channel_type in {"im", "mpim"} # Both 1:1 and group DMs
if is_dm and self._slack_disable_dms():
logger.info(
"[Slack] Ignoring DM because Slack DMs are disabled: channel=%s user=%s",
channel_id,
user_id,
)
return
# A 1:1 IM is a private conversation with a single human — mention-exempt
# and safe to react to unconditionally, like any DM. An MPIM (group DM)
# is a SHARED surface: multiple humans can see and trigger the bot, so it
@ -7627,6 +7634,13 @@ class SlackAdapter(BasePlatformAdapter):
if thread_id:
break
is_dm = str(channel_id).startswith("D")
if is_dm and self._slack_disable_dms():
logger.info(
"[Slack] Ignoring slash command from DM because Slack DMs are disabled: channel=%s user=%s",
channel_id,
user_id,
)
return
source = self.build_source(
chat_id=channel_id,
chat_type="dm" if is_dm else "group",
@ -8265,12 +8279,27 @@ class SlackAdapter(BasePlatformAdapter):
return {part.strip() for part in s.split(",") if part.strip()}
return set()
def _slack_disable_dms(self) -> bool:
"""Return whether incoming Slack DMs should be ignored.
Supports both profile config (``slack.disable_dms`` bridged into
``PlatformConfig.extra``) and the environment override
``SLACK_DISABLE_DMS``. Defaults to False for backward compatibility.
"""
raw = self.config.extra.get("disable_dms")
if raw is None:
raw = os.getenv("SLACK_DISABLE_DMS", "false")
if isinstance(raw, str):
return raw.strip().lower() in {"true", "1", "yes", "on"}
return bool(raw)
def _slack_allowed_channels(self) -> set:
"""Return the whitelist of channel IDs the bot will respond in.
When non-empty, messages from channels NOT in this set are silently
ignored even if the bot is @mentioned. DMs are never filtered.
Empty set means no restriction (fully backward compatible).
ignored even if the bot is @mentioned. DMs are controlled separately
by ``_slack_disable_dms()``. Empty set means no channel restriction
(fully backward compatible).
"""
raw = self.config.extra.get("allowed_channels")
if raw is None:
@ -8859,6 +8888,9 @@ def _apply_yaml_config(yaml_cfg: dict, slack_cfg: dict) -> dict | None:
rtt = slack_cfg.get("reaction_trigger_target")
if rtt is not None and not os.getenv("SLACK_REACTION_TRIGGER_TARGET"):
os.environ["SLACK_REACTION_TRIGGER_TARGET"] = str(rtt)
if "disable_dms" in slack_cfg and not os.getenv("SLACK_DISABLE_DMS"):
os.environ["SLACK_DISABLE_DMS"] = str(slack_cfg["disable_dms"]).lower()
ac = slack_cfg.get("allowed_channels")
if ac is not None and not os.getenv("SLACK_ALLOWED_CHANNELS"):
if isinstance(ac, list):
@ -8901,8 +8933,8 @@ def register(ctx) -> None:
# YAML→env config bridge — owns the translation of config.yaml slack:
# keys (require_mention, strict_mention, ignore_other_user_mentions,
# thread_require_mention, allow_bots, free_response_channels,
# reactions, allowed_channels) into SLACK_* env vars that the adapter
# reads via os.getenv(). Replaces the
# reactions, disable_dms, allowed_channels) into SLACK_* env vars that
# the adapter reads via os.getenv(). Replaces the
# hardcoded block in gateway/config.py. Hook contract: #24849.
apply_yaml_config_fn=_apply_yaml_config,
# Auth env vars for _is_user_authorized() integration

View file

@ -575,6 +575,23 @@ class TestLoadGatewayConfig:
assert config.quick_commands == {"limits": {"type": "exec", "command": "echo ok"}}
def test_slack_disable_dms_config_sets_env_bridge(self, tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
config_path = hermes_home / "config.yaml"
config_path.write_text(
"slack:\n"
" disable_dms: true\n",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("SLACK_DISABLE_DMS", raising=False)
load_gateway_config()
assert os.getenv("SLACK_DISABLE_DMS") == "true"
def test_typing_status_text_from_toplevel_platform_block(self, tmp_path, monkeypatch):
"""A top-level ``slack:`` block reaches PlatformConfig via the
shared-key bridge (bridged into extra, then the from_dict extra

View file

@ -366,6 +366,20 @@ class TestSlashCommandSessionIsolation:
assert event.source.user_id == "U123"
assert event.source.thread_id == "1700000000.123456"
@pytest.mark.asyncio
async def test_disable_dms_drops_dm_slash_command(self, adapter):
adapter.config.extra["disable_dms"] = True
command = {
"text": "hello",
"user_id": "U123",
"channel_id": "D123",
"team_id": "T123",
}
await adapter._handle_slash_command(command)
adapter.handle_message.assert_not_awaited()
class TestSlackWorkspaceCollisionIsolation:
@pytest.mark.asyncio
@ -2385,6 +2399,31 @@ class TestBangPrefixCommands:
assert "quoted context" not in msg_event.text
assert msg_event.message_type == MessageType.COMMAND
@pytest.mark.asyncio
async def test_disable_dms_drops_text_dm(self, adapter):
adapter.config.extra["disable_dms"] = True
await adapter._handle_slack_message(self._make_event("hello from DM"))
adapter.handle_message.assert_not_awaited()
@pytest.mark.asyncio
async def test_disable_dms_does_not_drop_channel_mentions(self, adapter):
adapter.config.extra["disable_dms"] = True
await adapter._handle_slack_message(
self._make_event(
"<@U_BOT> hello from channel",
channel_type="channel",
channel="C123",
)
)
adapter.handle_message.assert_awaited_once()
msg_event = adapter.handle_message.await_args.args[0]
assert msg_event.source.chat_type == "group"
assert msg_event.source.chat_id == "C123"
# ---------------------------------------------------------------------------
# TestIncomingDocumentHandling