From 8a1e247c6c984098da4c8455b3bf1f872125ad4c Mon Sep 17 00:00:00 2001 From: Teknium Date: Fri, 24 Apr 2026 02:57:59 -0700 Subject: [PATCH] fix(discord): honor wildcard '*' in ignored_channels and free_response_channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the allowed_channels wildcard fix in the preceding commit. The same '*' literal trap affected two other Discord channel config lists: - DISCORD_IGNORED_CHANNELS: '*' was stored as the literal string in the ignored set, and the intersection check never matched real channel IDs, so '*' was a no-op instead of silencing every channel. - DISCORD_FREE_RESPONSE_CHANNELS: same shape — '*' never matched, so the bot still required a mention everywhere. Add a '*' short-circuit to both checks, matching the allowed_channels semantics. Extend tests/gateway/test_discord_allowed_channels.py with regression coverage for all three lists. Refs: #14920 --- gateway/platforms/discord.py | 15 +++- .../gateway/test_discord_allowed_channels.py | 68 +++++++++++++++++-- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index 45df48924..2b89511ab 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -2719,7 +2719,12 @@ class DiscordAdapter(BasePlatformAdapter): return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off") def _discord_free_response_channels(self) -> set: - """Return Discord channel IDs where no bot mention is required.""" + """Return Discord channel IDs where no bot mention is required. + + A single ``"*"`` entry (either from a list or a comma-separated + string) is preserved in the returned set so callers can short-circuit + on wildcard membership, consistent with ``allowed_channels``. + """ raw = self.config.extra.get("free_response_channels") if raw is None: raw = os.getenv("DISCORD_FREE_RESPONSE_CHANNELS", "") @@ -3219,7 +3224,7 @@ class DiscordAdapter(BasePlatformAdapter): # Check ignored channels - never respond even when mentioned ignored_channels_raw = os.getenv("DISCORD_IGNORED_CHANNELS", "") ignored_channels = {ch.strip() for ch in ignored_channels_raw.split(",") if ch.strip()} - if channel_ids & ignored_channels: + if "*" in ignored_channels or (channel_ids & ignored_channels): logger.debug("[%s] Ignoring message in ignored channel: %s", self.name, channel_ids) return @@ -3233,7 +3238,11 @@ class DiscordAdapter(BasePlatformAdapter): voice_linked_ids = {str(ch_id) for ch_id in self._voice_text_channels.values()} current_channel_id = str(message.channel.id) is_voice_linked_channel = current_channel_id in voice_linked_ids - is_free_channel = bool(channel_ids & free_channels) or is_voice_linked_channel + is_free_channel = ( + "*" in free_channels + or bool(channel_ids & free_channels) + or is_voice_linked_channel + ) # Skip the mention check if the message is in a thread where # the bot has previously participated (auto-created or replied in). diff --git a/tests/gateway/test_discord_allowed_channels.py b/tests/gateway/test_discord_allowed_channels.py index 83f53138a..abc79bc76 100644 --- a/tests/gateway/test_discord_allowed_channels.py +++ b/tests/gateway/test_discord_allowed_channels.py @@ -1,10 +1,13 @@ -"""Regression guard for #14920: DISCORD_ALLOWED_CHANNELS="*" should allow all channels. +"""Regression guard for #14920: wildcard "*" in Discord channel config lists. -Setting allowed_channels: "*" in config (or DISCORD_ALLOWED_CHANNELS="*" as env var) -must behave as a wildcard — i.e. the bot responds in every channel. Previously the -literal string "*" was placed into the set and compared against numeric channel IDs via -set-intersection, which always produced an empty set, causing every message to be -silently dropped. +Setting ``allowed_channels: "*"``, ``free_response_channels: "*"``, or +``ignored_channels: "*"`` in config (or their ``DISCORD_*_CHANNELS`` env var +equivalents) must behave as a wildcard — i.e. the bot responds in every +channel (or is silenced in every channel, for the ignored list). Previously +the literal string "*" was placed into a set and compared against numeric +channel IDs via set-intersection, which always produced an empty set and +caused every message to be silently dropped (for ``allowed_channels``) or +every ``free_response`` / ``ignored`` check to fail open. """ import unittest @@ -20,6 +23,22 @@ def _channel_is_allowed(channel_id: str, allowed_channels_raw: str) -> bool: return bool({channel_id} & allowed_channels) +def _channel_is_ignored(channel_id: str, ignored_channels_raw: str) -> bool: + """Replicate the ignored-channel check from discord.py on_message.""" + ignored_channels = { + ch.strip() for ch in ignored_channels_raw.split(",") if ch.strip() + } + return "*" in ignored_channels or bool({channel_id} & ignored_channels) + + +def _channel_is_free_response(channel_id: str, free_channels_raw: str) -> bool: + """Replicate the free-response-channel check from discord.py on_message.""" + free_channels = { + ch.strip() for ch in free_channels_raw.split(",") if ch.strip() + } + return "*" in free_channels or bool({channel_id} & free_channels) + + class TestDiscordAllowedChannelsWildcard(unittest.TestCase): """Wildcard and channel-list behaviour for DISCORD_ALLOWED_CHANNELS.""" @@ -46,3 +65,40 @@ class TestDiscordAllowedChannelsWildcard(unittest.TestCase): def test_whitespace_only_entry_ignored(self): """Entries that are only whitespace are stripped and ignored.""" self.assertFalse(_channel_is_allowed("1234567890", " , ")) + + +class TestDiscordIgnoredChannelsWildcard(unittest.TestCase): + """Wildcard and channel-list behaviour for DISCORD_IGNORED_CHANNELS.""" + + def test_wildcard_silences_every_channel(self): + """'*' in ignored_channels silences the bot everywhere.""" + self.assertTrue(_channel_is_ignored("1234567890", "*")) + + def test_empty_ignored_list_silences_nothing(self): + self.assertFalse(_channel_is_ignored("1234567890", "")) + + def test_exact_match_is_ignored(self): + self.assertTrue(_channel_is_ignored("111", "111,222")) + + def test_non_match_not_ignored(self): + self.assertFalse(_channel_is_ignored("333", "111,222")) + + +class TestDiscordFreeResponseChannelsWildcard(unittest.TestCase): + """Wildcard and channel-list behaviour for DISCORD_FREE_RESPONSE_CHANNELS.""" + + def test_wildcard_makes_every_channel_free_response(self): + """'*' in free_response_channels exempts every channel from mention-required.""" + self.assertTrue(_channel_is_free_response("1234567890", "*")) + + def test_wildcard_in_list_applies_everywhere(self): + self.assertTrue(_channel_is_free_response("9999999999", "111,*,222")) + + def test_exact_match_is_free_response(self): + self.assertTrue(_channel_is_free_response("111", "111,222")) + + def test_non_match_not_free_response(self): + self.assertFalse(_channel_is_free_response("333", "111,222")) + + def test_empty_list_no_free_response(self): + self.assertFalse(_channel_is_free_response("111", ""))