fix(whatsapp): allow group ids in allowFrom (#4834)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user