diff --git a/nanobot/channels/discord.py b/nanobot/channels/discord.py index 9e0f9829..94781913 100644 --- a/nanobot/channels/discord.py +++ b/nanobot/channels/discord.py @@ -664,7 +664,9 @@ class DiscordChannel(BaseChannel): content: str, ) -> bool: """Check if inbound Discord message should be processed.""" - if not self.is_allowed(sender_id): + # Reject unauthorized guild messages before any side effects, but let DMs + # reach BaseChannel._handle_message so it can issue a pairing code. + if message.guild is not None and not self.is_allowed(sender_id): return False # Channel-based filtering: only respond in allowed channels allow_channels = self.config.allow_channels diff --git a/tests/channels/test_discord_channel.py b/tests/channels/test_discord_channel.py index 9ce91a9f..717e3108 100644 --- a/tests/channels/test_discord_channel.py +++ b/tests/channels/test_discord_channel.py @@ -362,6 +362,26 @@ async def test_on_message_accepts_allowlisted_dm() -> None: assert handled[0]["metadata"] == {"message_id": "789", "guild_id": None, "reply_to": None} +@pytest.mark.asyncio +async def test_on_message_unauthorized_dm_sends_pairing_code(monkeypatch) -> None: + channel = DiscordChannel(DiscordConfig(enabled=True, allow_from=[]), MessageBus()) + client = _FakeDiscordClient(channel, intents=None) + message = _make_message(author_id=123, channel_id=456) + client.channels[456] = message.channel + channel._client = client + channel._running = True + monkeypatch.setattr("nanobot.channels.base.is_approved", lambda _ch, _sid: False) + monkeypatch.setattr( + "nanobot.channels.base.generate_code", lambda _ch, _sid: "ABCD-EFGH" + ) + + await channel._on_message(message) + + assert len(message.channel.sent_payloads) == 1 + assert "ABCD-EFGH" in message.channel.sent_payloads[0]["content"] + assert channel._typing_tasks == {} + + @pytest.mark.asyncio async def test_on_message_accepts_when_channel_in_allow_channels() -> None: # When allow_channels is set, messages from listed channels should be forwarded.