I hit this while deploying the whatsapp_native channel on an account that WhatsApp has migrated to Linked-Device ID (LID) format. Every incoming message was silently dropped despite the sender being in allow_from. Took a while to track down because nothing appears in the logs at INFO level.
Reproduction
Config (~/.picoclaw/config.json):
{
"channels": {
"whatsapp": {
"enabled": true,
"use_native": true,
"session_store_path": "/absolute/path/to/session/",
"allow_from": ["+15550001234"]
}
}
}
- Build with
-tags whatsapp_native, pair via QR.
- The paired device is LID-migrated —
evt.Info.Sender.String() returns 100000000000001:1@lid.
- Send a message from that number. No reply. No log output at INFO.
- Enable debug logging:
"sender_id": "100000000000001:1@lid" appears, but the allow check already returned before that line is reached.
Observed symptom
[DEBUG] whatsapp_native: WhatsApp message received sender_id=100000000000001:1@lid content_preview=<redacted test message>
…but only after the IsAllowedSender check passes. When it fails (as it always does with a phone-format allow list vs. a LID sender), the handler returns silently at line 277 of pkg/channels/base.go with zero log output at WARN or above.
Root cause
Two separate problems compound each other.
1. Format mismatch between documentation and wire format.
PR #1620 (docs/channels/whatsapp/README.md) documents allow_from as taking E.164 phone numbers:
allow_from — Phone number whitelist in E.164 format (e.g. ["+15550001234"]). Leave empty [] to accept messages from all numbers.
In whatsapp_native.go (line 349), the sender ID is set from evt.Info.Sender.String(). For a LID-migrated account whatsmeow returns 100000000000001:1@lid — not an E.164 string.
That value lands in SenderInfo.PlatformID and SenderInfo.CanonicalID (lines 383–384):
sender := bus.SenderInfo{
Platform: "whatsapp",
PlatformID: senderID, // "100000000000001:1@lid"
CanonicalID: identity.BuildCanonicalID("whatsapp", senderID), // "whatsapp:100000000000001:1@lid"
}
IsAllowedSender in pkg/channels/base.go (line 249) iterates the allow list and calls identity.MatchAllowed for each entry (line 255).
MatchAllowed in pkg/identity/identity.go (line 41) first tries canonical matching. It calls ParseCanonicalID("+15550001234") — finds no colon, returns ok=false — then falls through to the PlatformID exact-string comparison at line 77:
if sender.PlatformID != "" && sender.PlatformID == allowedID {
return true
}
"100000000000001:1@lid" == "+15550001234" is false. Every non-LID entry in the list fails the same way. The function returns false, the handler returns at line 277, and nothing is logged.
2. Device-index drift.
Even after an operator discovers the mismatch (e.g. via debug logs) and locks the list to "100000000000001:1@lid", the :1 device/agent index is not stable. WhatsApp session housekeeping can reassign it (:2, :3, …) during normal operation. The exact-string check then silently breaks again without any log warning.
Possible directions
I'm not sure which fix is right here — happy to defer to maintainers:
- Strip device suffix for LID matching: accept
<user>@lid in allow_from and match it against any <user>:<N>@lid on the wire. This is the approach least likely to break non-LID paths.
- Resolve LID → phone before comparison: use
client.Store.Contacts (whatsmeow) to look up the E.164 number for a LID sender, then compare against the documented phone format. More work but makes the documented behaviour actually work.
- At minimum, emit a WARN when formats are mismatched: if every entry in the allow list is E.164 but the incoming sender is
@lid (or vice versa), log a breadcrumb like "allow_from entries appear to be phone numbers but sender is LID-format; messages will be silently dropped". This alone would have saved me a couple of hours.
Happy to put together a PR if there's a preferred direction — just let me know if there's related work in flight so I can coordinate.
I hit this while deploying the
whatsapp_nativechannel on an account that WhatsApp has migrated to Linked-Device ID (LID) format. Every incoming message was silently dropped despite the sender being inallow_from. Took a while to track down because nothing appears in the logs at INFO level.Reproduction
Config (
~/.picoclaw/config.json):{ "channels": { "whatsapp": { "enabled": true, "use_native": true, "session_store_path": "/absolute/path/to/session/", "allow_from": ["+15550001234"] } } }-tags whatsapp_native, pair via QR.evt.Info.Sender.String()returns100000000000001:1@lid."sender_id": "100000000000001:1@lid"appears, but the allow check already returned before that line is reached.Observed symptom
…but only after the
IsAllowedSendercheck passes. When it fails (as it always does with a phone-format allow list vs. a LID sender), the handler returns silently at line 277 ofpkg/channels/base.gowith zero log output at WARN or above.Root cause
Two separate problems compound each other.
1. Format mismatch between documentation and wire format.
PR #1620 (
docs/channels/whatsapp/README.md) documentsallow_fromas taking E.164 phone numbers:In
whatsapp_native.go(line 349), the sender ID is set fromevt.Info.Sender.String(). For a LID-migrated account whatsmeow returns100000000000001:1@lid— not an E.164 string.That value lands in
SenderInfo.PlatformIDandSenderInfo.CanonicalID(lines 383–384):IsAllowedSenderinpkg/channels/base.go(line 249) iterates the allow list and callsidentity.MatchAllowedfor each entry (line 255).MatchAllowedinpkg/identity/identity.go(line 41) first tries canonical matching. It callsParseCanonicalID("+15550001234")— finds no colon, returnsok=false— then falls through to thePlatformIDexact-string comparison at line 77:"100000000000001:1@lid" == "+15550001234"is false. Every non-LID entry in the list fails the same way. The function returnsfalse, the handler returns at line 277, and nothing is logged.2. Device-index drift.
Even after an operator discovers the mismatch (e.g. via debug logs) and locks the list to
"100000000000001:1@lid", the:1device/agent index is not stable. WhatsApp session housekeeping can reassign it (:2,:3, …) during normal operation. The exact-string check then silently breaks again without any log warning.Possible directions
I'm not sure which fix is right here — happy to defer to maintainers:
<user>@lidinallow_fromand match it against any<user>:<N>@lidon the wire. This is the approach least likely to break non-LID paths.client.Store.Contacts(whatsmeow) to look up the E.164 number for a LID sender, then compare against the documented phone format. More work but makes the documented behaviour actually work.@lid(or vice versa), log a breadcrumb like"allow_from entries appear to be phone numbers but sender is LID-format; messages will be silently dropped". This alone would have saved me a couple of hours.Happy to put together a PR if there's a preferred direction — just let me know if there's related work in flight so I can coordinate.