feat(whatsapp): seed LID->phone mappings on startup

WhatsApp can deliver a sender LID instead of a phone number. The channel
already learns the LID->phone mapping at runtime, but only after a message
that carries both values, so the first message from a contact can't be
resolved to a phone number.

Seed the mapping on startup from two sources:

- reverse mapping files the bridge persists in the auth directory
  (lid-mapping-<lid>_reverse.json), resolved via get_runtime_subdir so it
  respects a custom runtime dir
- a new optional channels.whatsapp.lidMappings config dict for static
  mappings (takes precedence over the on-disk files)

Malformed/empty mapping files are ignored rather than failing startup.
Adds tests for both sources, precedence, malformed files and the
no-auth-dir case, plus docs for the new config field.
This commit is contained in:
franciscomaestre
2026-06-21 13:05:06 +08:00
committed by Xubin Ren
parent e81968a32b
commit 99e158c062
3 changed files with 111 additions and 1 deletions
+56
View File
@@ -449,3 +449,59 @@ async def test_start_sends_auth_message_with_generated_token(monkeypatch, tmp_pa
assert sent_messages == [
json.dumps({"type": "auth", "token": token_path.read_text(encoding="utf-8")})
]
# ---------------------------------------------------------------------------
# LID -> phone mapping seeding (startup): static config + bridge reverse files.
# ---------------------------------------------------------------------------
def test_lid_mappings_from_config():
ch = WhatsAppChannel(
{"enabled": True, "lidMappings": {"123456789012345": "15551234567"}},
MagicMock(),
)
assert ch._lid_to_phone["123456789012345"] == "15551234567"
def test_lid_mappings_from_bridge_reverse_files(tmp_path, monkeypatch):
auth_dir = tmp_path / "whatsapp-auth"
auth_dir.mkdir()
(auth_dir / "lid-mapping-999888777666555_reverse.json").write_text(
json.dumps("15559998888"), encoding="utf-8"
)
# malformed / empty files must be ignored, not crash startup
(auth_dir / "lid-mapping-broken_reverse.json").write_text("{not json", encoding="utf-8")
(auth_dir / "lid-mapping-empty_reverse.json").write_text(json.dumps(""), encoding="utf-8")
monkeypatch.setattr(
"nanobot.config.paths.get_runtime_subdir", lambda name: auth_dir
)
ch = WhatsAppChannel({"enabled": True}, MagicMock())
assert ch._lid_to_phone == {"999888777666555": "15559998888"}
def test_lid_mappings_config_takes_precedence_over_files(tmp_path, monkeypatch):
auth_dir = tmp_path / "whatsapp-auth"
auth_dir.mkdir()
(auth_dir / "lid-mapping-555_reverse.json").write_text(
json.dumps("from-file"), encoding="utf-8"
)
monkeypatch.setattr(
"nanobot.config.paths.get_runtime_subdir", lambda name: auth_dir
)
ch = WhatsAppChannel(
{"enabled": True, "lidMappings": {"555": "from-config"}}, MagicMock()
)
assert ch._lid_to_phone["555"] == "from-config"
def test_lid_mappings_empty_when_no_auth_dir(tmp_path, monkeypatch):
missing = tmp_path / "does-not-exist"
monkeypatch.setattr(
"nanobot.config.paths.get_runtime_subdir", lambda name: missing
)
ch = WhatsAppChannel({"enabled": True}, MagicMock())
assert ch._lid_to_phone == {}