Skip to content

Commit 4db158d

Browse files
author
Ciprian Cosma
committed
fix(whatsapp): resolve LID↔phone aliases in Python DM allowlist
Baileys 7.x delivers 1:1 message sender IDs in LID format (e.g. `<numeric>@lid`) while user allowlists typically use phone numbers with `+` prefix (e.g. `+<numeric>`). The JS bridge already handles this — `scripts/whatsapp-bridge/allowlist.js` strips the `+` prefix and walks `lid-mapping-<id>{,_reverse}.json` files Baileys writes to the session directory, so either form resolves to the other. The Python adapter does not: # Old _is_dm_allowed return sender_id in self._allow_from # always False for LID senders This silently drops every WhatsApp DM after the JS bridge queues it — no error in gateway.log, no notification, just a `[]` response to repeated `/messages` polls. Outbound keeps working (different code path), so the symptom is "WhatsApp stopped replying" rather than "WhatsApp is broken." Fix: mirror the JS allowlist walk in `WhatsAppBehaviorMixin` via two new helpers — `_lid_aliases()` resolves a sender ID to all known aliases (phone ↔ LID) by walking the same mapping files the JS bridge reads, and `_sender_in_allowlist()` checks the DM allowlist against every alias. `_is_dm_allowed` now dispatches to it when policy is `allowlist`. Mixins don't see host-adapter attributes through static analysis, so the `_allow_from` read carries a `# type: ignore[attr-defined]` comment with a pointer to the documented mixin contract at the top of the file. New `Path` import required for the file-system walk. Symptom: WhatsApp DMs stop working after a Hermes update or any time the bridge reconnects; outbound keeps working. Verified with bridge debug logs showing the JS allowlist passing but the Python gate silently rejecting every message until this fix was deployed.
1 parent 6681f28 commit 4db158d

1 file changed

Lines changed: 62 additions & 2 deletions

File tree

gateway/platforms/whatsapp_common.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import logging
3636
import os
3737
import re
38+
from pathlib import Path
3839
from typing import Any, Dict, Optional
3940

4041

@@ -148,14 +149,73 @@ def _is_broadcast_chat(chat_id: str) -> bool:
148149

149150
# ------------------------------------------------------------------ gating
150151
def _is_dm_allowed(self, sender_id: str) -> bool:
151-
"""Check whether a DM from the given sender should be processed."""
152+
"""Check whether a DM from the given sender should be processed.
153+
154+
Baileys 7.x sends ``senderId`` in LID format (e.g. ``<numeric>@lid``)
155+
while user allowlists are typically phone numbers (``+<numeric>``).
156+
Without LID↔phone resolution the allowlist never matches known users.
157+
See ``scripts/whatsapp-bridge/allowlist.js`` for the JS-side mirror.
158+
"""
152159
if self._dm_policy == "disabled":
153160
return False
154161
if self._dm_policy == "allowlist":
155-
return sender_id in self._allow_from
162+
return self._sender_in_allowlist(sender_id)
156163
# "open" — all DMs allowed
157164
return True
158165

166+
def _lid_aliases(self, sender_id: str) -> set:
167+
"""Resolve a WhatsApp identifier to all known aliases (LID ↔ phone).
168+
169+
Reads ``lid-mapping-<id>.json`` and ``lid-mapping-<id>_reverse.json``
170+
from the session directory (same files the JS bridge writes) and
171+
walks both directions so either form resolves to the other.
172+
"""
173+
if not sender_id:
174+
return set()
175+
normalized = self._normalize_whatsapp_id(sender_id).split("@", 1)[0].lstrip("+")
176+
aliases = {normalized}
177+
session_dir = getattr(self, "_session_path", None)
178+
if session_dir is None:
179+
return aliases
180+
try:
181+
queue = [normalized]
182+
seen = set()
183+
while queue:
184+
current = queue.pop(0)
185+
if not current or current in seen:
186+
continue
187+
seen.add(current)
188+
for suffix in ("", "_reverse"):
189+
mapping_file = Path(session_dir) / f"lid-mapping-{current}{suffix}.json"
190+
if not mapping_file.exists():
191+
continue
192+
try:
193+
import json as _json
194+
mapped = _json.loads(mapping_file.read_text(encoding="utf-8")).strip().lstrip("+")
195+
if mapped and mapped not in aliases:
196+
aliases.add(mapped)
197+
if mapped not in seen:
198+
queue.append(mapped)
199+
except Exception:
200+
continue
201+
except Exception:
202+
pass
203+
return aliases
204+
205+
def _sender_in_allowlist(self, sender_id: str) -> bool:
206+
"""Check whether sender_id (any format) is in the DM allowlist."""
207+
aliases = self._lid_aliases(sender_id)
208+
# ``_allow_from`` is provided by the host adapter per the mixin contract
209+
# (see module docstring). Pyright can't see across the mixin boundary.
210+
allow_from = self._allow_from # type: ignore[attr-defined]
211+
for alias in aliases:
212+
if alias in allow_from:
213+
return True
214+
# Also try with + prefix (allowlist entries may include +)
215+
if f"+{alias}" in allow_from:
216+
return True
217+
return False
218+
159219
def _is_group_allowed(self, chat_id: str) -> bool:
160220
"""Check whether a group chat should be processed."""
161221
if self._group_policy == "disabled":

0 commit comments

Comments
 (0)