From 7c3808327fda7af0ebccc6d38fa4f74b9056225c Mon Sep 17 00:00:00 2001 From: yorkhellen Date: Wed, 3 Jun 2026 23:35:03 +0800 Subject: [PATCH] fix(qq): send pairing codes for unauthorized C2C users --- nanobot/channels/qq.py | 17 +++++++++-- tests/channels/test_qq_channel.py | 47 ++++++++++++++++++++++++++++++- tests/channels/test_qq_media.py | 38 ++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/nanobot/channels/qq.py b/nanobot/channels/qq.py index 701be8b68..0d5ed9cd6 100644 --- a/nanobot/channels/qq.py +++ b/nanobot/channels/qq.py @@ -490,14 +490,24 @@ class QQChannel(BaseChannel): content = (data.content or "").strip() - if not self.is_allowed(user_id): - return - if data.id in self._processed_ids: return self._processed_ids.append(data.id) self._chat_type_cache[chat_id] = chat_type + # Early permission check — avoid attachment downloads and ack side effects + # for unauthorized users. C2C messages can receive pairing codes; + # group messages remain silently ignored. + if not self.is_allowed(user_id): + if not is_group: + await self._handle_message( + sender_id=user_id, + chat_id=chat_id, + content="", + is_dm=True, + ) + return + # the data used by tests don't contain attachments property # so we use getattr with a default of [] to avoid AttributeError in tests attachments = getattr(data, "attachments", None) or [] @@ -538,6 +548,7 @@ class QQChannel(BaseChannel): "message_id": data.id, "attachments": att_meta, }, + is_dm=not is_group, ) except Exception: self.logger.exception("Error handling inbound message id={}", getattr(data, "id", "?")) diff --git a/tests/channels/test_qq_channel.py b/tests/channels/test_qq_channel.py index 417648adf..10281e066 100644 --- a/tests/channels/test_qq_channel.py +++ b/tests/channels/test_qq_channel.py @@ -1,7 +1,7 @@ import tempfile from pathlib import Path from types import SimpleNamespace -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock import pytest @@ -58,6 +58,51 @@ async def test_on_group_message_routes_to_group_chat_id() -> None: assert msg.chat_id == "group123" +@pytest.mark.asyncio +async def test_on_c2c_message_passes_is_dm_true_to_base_handler() -> None: + channel = QQChannel(QQConfig(app_id="app", secret="secret", allow_from=["user1"]), MessageBus()) + channel._handle_message = AsyncMock() + + data = SimpleNamespace( + id="msg-c2c", + content="hello", + author=SimpleNamespace(user_openid="user1"), + attachments=[], + ) + + await channel._on_message(data, is_group=False) + + channel._handle_message.assert_awaited_once() + kwargs = channel._handle_message.await_args.kwargs + assert kwargs["sender_id"] == "user1" + assert kwargs["chat_id"] == "user1" + assert kwargs["content"] == "hello" + assert kwargs["is_dm"] is True + + +@pytest.mark.asyncio +async def test_on_group_message_passes_is_dm_false_to_base_handler() -> None: + channel = QQChannel(QQConfig(app_id="app", secret="secret", allow_from=["user1"]), MessageBus()) + channel._handle_message = AsyncMock() + + data = SimpleNamespace( + id="msg-group", + content="hello", + group_openid="group123", + author=SimpleNamespace(member_openid="user1"), + attachments=[], + ) + + await channel._on_message(data, is_group=True) + + channel._handle_message.assert_awaited_once() + kwargs = channel._handle_message.await_args.kwargs + assert kwargs["sender_id"] == "user1" + assert kwargs["chat_id"] == "group123" + assert kwargs["content"] == "hello" + assert kwargs["is_dm"] is False + + @pytest.mark.asyncio async def test_send_group_message_uses_plain_text_group_api_with_msg_seq() -> None: channel = QQChannel(QQConfig(app_id="app", secret="secret", allow_from=["*"]), MessageBus()) diff --git a/tests/channels/test_qq_media.py b/tests/channels/test_qq_media.py index e2de72f28..59b85069c 100644 --- a/tests/channels/test_qq_media.py +++ b/tests/channels/test_qq_media.py @@ -183,7 +183,7 @@ async def test_send_media_failure_falls_back_to_text() -> None: @pytest.mark.asyncio -async def test_on_message_ignores_unauthorized_sender_before_attachments_and_ack() -> None: +async def test_on_message_unauthorized_c2c_pairs_before_attachments_and_ack() -> None: channel = QQChannel( QQConfig( app_id="app", @@ -206,9 +206,45 @@ async def test_on_message_ignores_unauthorized_sender_before_attachments_and_ack await channel._on_message(data, is_group=False) + channel._handle_attachments.assert_not_awaited() + channel._handle_message.assert_awaited_once_with( + sender_id="blocked-user", + chat_id="blocked-user", + content="", + is_dm=True, + ) + assert channel._client.api.c2c_calls == [] + + +@pytest.mark.asyncio +async def test_on_message_ignores_unauthorized_group_before_attachments_and_ack() -> None: + channel = QQChannel( + QQConfig( + app_id="app", + secret="secret", + allow_from=["allowed-user"], + ack_message="Processing...", + ), + MessageBus(), + ) + channel._client = _FakeClient() + channel._handle_attachments = AsyncMock(return_value=(["/tmp/a.png"], ["file"], [])) + channel._handle_message = AsyncMock() + + data = SimpleNamespace( + id="msg-blocked-group", + content="hello", + group_openid="group123", + author=SimpleNamespace(member_openid="blocked-user"), + attachments=[SimpleNamespace(filename="a.png")], + ) + + await channel._on_message(data, is_group=True) + channel._handle_attachments.assert_not_awaited() channel._handle_message.assert_not_awaited() assert channel._client.api.c2c_calls == [] + assert channel._client.api.group_calls == [] # ── _on_message() exception handling ────────────────────────────────