From 59396bdbef4a2ac1ae16edb83473bb11468c57f4 Mon Sep 17 00:00:00 2001 From: comadreja Date: Thu, 26 Mar 2026 21:48:30 -0500 Subject: [PATCH 1/2] fix(whatsapp): detect phone vs LID by JID suffix, not field name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bridge's pn/sender fields don't consistently map to phone/LID across different versions. Classify by JID suffix instead: @s.whatsapp.net → phone number @lid.whatsapp.net → LID (internal WhatsApp identifier) This ensures allowFrom works reliably with phone numbers regardless of which field the bridge populates. --- nanobot/channels/whatsapp.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/nanobot/channels/whatsapp.py b/nanobot/channels/whatsapp.py index 95bde46e9..c4c011304 100644 --- a/nanobot/channels/whatsapp.py +++ b/nanobot/channels/whatsapp.py @@ -51,6 +51,7 @@ class WhatsAppChannel(BaseChannel): self._ws = None self._connected = False self._processed_message_ids: OrderedDict[str, None] = OrderedDict() + self._lid_to_phone: dict[str, str] = {} async def login(self, force: bool = False) -> bool: """ @@ -197,9 +198,28 @@ class WhatsAppChannel(BaseChannel): if not was_mentioned: return - user_id = pn if pn else sender - sender_id = user_id.split("@")[0] if "@" in user_id else user_id - logger.info("Sender {}", sender) + # Classify by JID suffix: @s.whatsapp.net = phone, @lid.whatsapp.net = LID + # The bridge's pn/sender fields don't consistently map to phone/LID across versions. + raw_a = pn or "" + raw_b = sender or "" + id_a = raw_a.split("@")[0] if "@" in raw_a else raw_a + id_b = raw_b.split("@")[0] if "@" in raw_b else raw_b + + phone_id = "" + lid_id = "" + for raw, extracted in [(raw_a, id_a), (raw_b, id_b)]: + if "@s.whatsapp.net" in raw: + phone_id = extracted + elif "@lid.whatsapp.net" in raw: + lid_id = extracted + elif extracted and not phone_id: + phone_id = extracted # best guess for bare values + + if phone_id and lid_id: + self._lid_to_phone[lid_id] = phone_id + sender_id = phone_id or self._lid_to_phone.get(lid_id, "") or lid_id or id_a or id_b + + logger.info("Sender phone={} lid={} → sender_id={}", phone_id or "(empty)", lid_id or "(empty)", sender_id) # Handle voice transcription if it's a voice message if content == "[Voice Message]": From 897d5a7e584370974407ab0df2f7d97722130686 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Mon, 6 Apr 2026 06:19:06 +0000 Subject: [PATCH 2/2] test: add regression tests for JID suffix classification and LID cache Made-with: Cursor --- tests/channels/test_whatsapp_channel.py | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/channels/test_whatsapp_channel.py b/tests/channels/test_whatsapp_channel.py index f285e4dbe..b61033677 100644 --- a/tests/channels/test_whatsapp_channel.py +++ b/tests/channels/test_whatsapp_channel.py @@ -163,6 +163,60 @@ async def test_group_policy_mention_accepts_mentioned_group_message(): assert kwargs["sender_id"] == "user" +@pytest.mark.asyncio +async def test_sender_id_prefers_phone_jid_over_lid(): + """sender_id should resolve to phone number when @s.whatsapp.net JID is present.""" + ch = WhatsAppChannel({"enabled": True}, MagicMock()) + ch._handle_message = AsyncMock() + + await ch._handle_bridge_message( + json.dumps({ + "type": "message", + "id": "lid1", + "sender": "ABC123@lid.whatsapp.net", + "pn": "5551234@s.whatsapp.net", + "content": "hi", + "timestamp": 1, + }) + ) + + kwargs = ch._handle_message.await_args.kwargs + assert kwargs["sender_id"] == "5551234" + + +@pytest.mark.asyncio +async def test_lid_to_phone_cache_resolves_lid_only_messages(): + """When only LID is present, a cached LID→phone mapping should be used.""" + ch = WhatsAppChannel({"enabled": True}, MagicMock()) + ch._handle_message = AsyncMock() + + # First message: both phone and LID → builds cache + await ch._handle_bridge_message( + json.dumps({ + "type": "message", + "id": "c1", + "sender": "LID99@lid.whatsapp.net", + "pn": "5559999@s.whatsapp.net", + "content": "first", + "timestamp": 1, + }) + ) + # Second message: only LID, no phone + await ch._handle_bridge_message( + json.dumps({ + "type": "message", + "id": "c2", + "sender": "LID99@lid.whatsapp.net", + "pn": "", + "content": "second", + "timestamp": 2, + }) + ) + + second_kwargs = ch._handle_message.await_args_list[1].kwargs + assert second_kwargs["sender_id"] == "5559999" + + @pytest.mark.asyncio async def test_voice_message_transcription_uses_media_path(): """Voice messages are transcribed when media path is available."""