fix(whatsapp): allow group ids in allowFrom (#4834)

This commit is contained in:
chengyongru
2026-07-19 19:16:37 +08:00
committed by GitHub
parent 2099cb009e
commit a6b68178aa
5 changed files with 104 additions and 3 deletions
+4
View File
@@ -393,6 +393,10 @@ nanobot channels login whatsapp
}
```
For groups, `allowFrom` can contain either a participant sender ID/LID or a
group JID/bare group ID. A participant entry allows that sender wherever the bot
can see them; a group entry allows replies in that group.
Optional session database path:
```json
+10 -2
View File
@@ -224,9 +224,17 @@ class BaseChannel(ABC):
metadata: dict[str, Any] | None = None,
session_key: str | None = None,
is_dm: bool = False,
authorization_id: str | None = None,
) -> None:
"""Handle an incoming message: check permissions, issue pairing codes in DMs, or forward to bus."""
if not self.is_allowed(sender_id):
"""Handle a message after checking its authorization subject.
``sender_id`` is the identity recorded on the inbound message. Channels
where access is scoped to another entity (for example, a group or room)
can pass that entity as ``authorization_id`` without changing the
sender's identity. When omitted, authorization remains sender-based.
"""
permission_id = authorization_id if authorization_id is not None else sender_id
if not self.is_allowed(permission_id):
if is_dm:
code = generate_code(self.name, str(sender_id))
await self.send(
+13 -1
View File
@@ -583,7 +583,10 @@ class WhatsAppChannel(BaseChannel):
"phone": phone_id or None,
"is_reply_to_bot": self._is_reply_to_bot(message),
}
if not self.is_allowed(sender_id):
sender_allowed = self.is_allowed(sender_id)
group_allow_id = self._group_allow_id(chat_jid) if is_group else None
authorization_id = sender_id if sender_allowed else group_allow_id
if authorization_id is None:
self.logger.info(
"Passing unauthorized WhatsApp sender {} to pairing flow "
"(phone={}, lid={}, chat={})",
@@ -628,8 +631,17 @@ class WhatsAppChannel(BaseChannel):
media=media_paths,
metadata=metadata,
is_dm=not is_group,
authorization_id=authorization_id,
)
def _group_allow_id(self, chat_jid: str) -> str | None:
if self.is_allowed(chat_jid):
return chat_jid
bare_chat_id = _bare_jid(chat_jid)
if bare_chat_id and bare_chat_id != chat_jid and self.is_allowed(bare_chat_id):
return bare_chat_id
return None
def _is_addressed_to_bot(self, message: Any) -> bool:
return self._was_mentioned(message) or self._is_reply_to_bot(message)
+32
View File
@@ -93,3 +93,35 @@ async def test_handle_message_group_ignores_unknown() -> None:
assert channel._sent == []
@pytest.mark.asyncio
async def test_handle_message_uses_authorization_id_without_changing_sender() -> None:
bus = MessageBus()
channel = _DummyChannel({"allowFrom": ["group@g.us"]}, bus)
await channel._handle_message(
sender_id="member-lid",
authorization_id="group@g.us",
chat_id="group@g.us",
content="hello",
)
msg = await bus.consume_inbound()
assert msg.sender_id == "member-lid"
assert msg.chat_id == "group@g.us"
@pytest.mark.asyncio
async def test_handle_message_rejects_when_authorization_id_is_not_allowed() -> None:
bus = MessageBus()
channel = _DummyChannel({"allowFrom": ["member-lid"]}, bus)
await channel._handle_message(
sender_id="member-lid",
authorization_id="other-group@g.us",
chat_id="other-group@g.us",
content="hello",
)
assert bus.inbound_size == 0
+45
View File
@@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from nanobot.bus.events import OutboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.channels import whatsapp as whatsapp_module
from nanobot.channels.whatsapp import WhatsAppChannel, _legacy_bridge_config_fields, _NeonizeAPI
@@ -320,6 +321,50 @@ async def test_group_sender_id_uses_participant_not_group_jid() -> None:
assert kwargs["metadata"]["participant"] == "SENDERLID@lid"
@pytest.mark.parametrize("allowed_group", ["120363000@g.us", "120363000"])
@pytest.mark.asyncio
async def test_group_allow_from_accepts_group_jid_or_bare_id(allowed_group: str) -> None:
bus = MessageBus()
ch = WhatsAppChannel({"enabled": True, "allowFrom": [allowed_group]}, bus)
ch._started_at = 0
await ch._handle_neonize_message(
SimpleNamespace(download_any=AsyncMock()),
_event(
message=_Proto(conversation="hi"),
chat=_jid("120363000", "g.us"),
sender=_jid("SENDERLID", "lid"),
is_group=True,
),
)
assert bus.inbound_size == 1
msg = await bus.consume_inbound()
assert msg.sender_id == "SENDERLID"
assert msg.chat_id == "120363000@g.us"
assert msg.content == "hi"
assert msg.metadata["participant"] == "SENDERLID@lid"
@pytest.mark.asyncio
async def test_group_allow_from_does_not_allow_same_participant_in_other_group() -> None:
bus = MessageBus()
ch = WhatsAppChannel({"enabled": True, "allowFrom": ["120363000"]}, bus)
ch._started_at = 0
await ch._handle_neonize_message(
SimpleNamespace(download_any=AsyncMock()),
_event(
message=_Proto(conversation="hi"),
chat=_jid("120363999", "g.us"),
sender=_jid("SENDERLID", "lid"),
is_group=True,
),
)
assert bus.inbound_size == 0
@pytest.mark.asyncio
async def test_read_receipt_is_requested_once_after_dedup() -> None:
ch = _make_channel()