From 9402f2fd444ca0b2854745778bc7fbe9109492af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Wed, 27 May 2026 17:20:08 -0300 Subject: [PATCH 1/4] fix(channels/whatsapp-web): pass LID JIDs unchanged to whatsapp-rust 0.6+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- Cargo.lock | 20 +++--- crates/zeroclaw-channels/Cargo.toml | 45 +++++++------ crates/zeroclaw-channels/src/whatsapp_web.rs | 67 +++++++------------- 3 files changed, 59 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84d4be9f17d..b40e1295731 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10885,7 +10885,7 @@ dependencies = [ [[package]] name = "wacore" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "aes 0.9.1", "anyhow", @@ -10927,7 +10927,7 @@ dependencies = [ [[package]] name = "wacore-appstate" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "anyhow", "bytemuck", @@ -10948,7 +10948,7 @@ dependencies = [ [[package]] name = "wacore-binary" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "bytes", "compact_str", @@ -10964,7 +10964,7 @@ dependencies = [ [[package]] name = "wacore-derive" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "proc-macro2", "quote", @@ -10974,7 +10974,7 @@ dependencies = [ [[package]] name = "wacore-libsignal" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "aes 0.9.1", "arrayref", @@ -11006,7 +11006,7 @@ dependencies = [ [[package]] name = "wacore-noise" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "anyhow", "bytes", @@ -11043,7 +11043,7 @@ dependencies = [ [[package]] name = "waproto" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "prost", "serde", @@ -11858,7 +11858,7 @@ dependencies = [ [[package]] name = "whatsapp-rust" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "anyhow", "async-channel 2.5.0", @@ -11889,7 +11889,7 @@ dependencies = [ [[package]] name = "whatsapp-rust-tokio-transport" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "anyhow", "async-channel 2.5.0", @@ -11909,7 +11909,7 @@ dependencies = [ [[package]] name = "whatsapp-rust-ureq-http-client" version = "0.6.0" -source = "git+https://github.com/oxidezap/whatsapp-rust?rev=9734fb2ec544e22b7055147aa3e73b6889e3ff0d#9734fb2ec544e22b7055147aa3e73b6889e3ff0d" +source = "git+https://github.com/oxidezap/whatsapp-rust?rev=cbcdd2a6b931d804f3c2693554dce75e654834e3#cbcdd2a6b931d804f3c2693554dce75e654834e3" dependencies = [ "anyhow", "async-trait", diff --git a/crates/zeroclaw-channels/Cargo.toml b/crates/zeroclaw-channels/Cargo.toml index 37c0eddd843..678220881ad 100644 --- a/crates/zeroclaw-channels/Cargo.toml +++ b/crates/zeroclaw-channels/Cargo.toml @@ -120,25 +120,29 @@ webpki-roots = "1.0.6" tokio-socks = "0.5" # WhatsApp Web (optional) -# Upstream crates from oxidezap/whatsapp-rust, temporarily pinned to commit -# 9734fb2 (the MSRV-fix branch carrying oxidezap/whatsapp-rust#632). The -# published 0.6.0 crate uses an `if let` guard at wacore/src/history_sync.rs:93 -# that is stable only on Rust 1.94+; ZeroClaw CI pins 1.93.0. Pinning via a -# plain `git` source (not `[patch.crates-io]`) keeps the workspace buildable -# until oxidezap publishes the fix as 0.6.1, at which point this block -# reverts to versioned crates.io deps. -# Fixes the post-2026-04-24 WhatsApp Web protocol break (#6246). -whatsapp-rust = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false, features = [ +# Upstream crates from oxidezap/whatsapp-rust, pinned to commit cbcdd2a. +# Pinning via a plain `git` source (not `[patch.crates-io]`) keeps the +# workspace buildable until oxidezap publishes the fix as 0.6.1, at which +# point this block reverts to versioned crates.io deps. +# +# Commit history: +# - 9734fb2: MSRV fix for `if let` guard (wacore/src/history_sync.rs:93) +# stable only on Rust 1.94+; ZeroClaw CI pins 1.93.0. +# Fixes the post-2026-04-24 WhatsApp Web protocol break (#6246). +# - cbcdd2a: PR #636 fix for LID namespace conversion. Fixes ACK 400 +# "server requires all participants to share the same JID +# namespace" error when sending DMs to LID-addressed recipients. +whatsapp-rust = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "cbcdd2a6b931d804f3c2693554dce75e654834e3", optional = true, default-features = false, features = [ "tokio-runtime", ] } -wacore = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false } -wacore-binary = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false } -waproto = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false } +wacore = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "cbcdd2a6b931d804f3c2693554dce75e654834e3", optional = true, default-features = false } +wacore-binary = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "cbcdd2a6b931d804f3c2693554dce75e654834e3", optional = true, default-features = false } +waproto = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "cbcdd2a6b931d804f3c2693554dce75e654834e3", optional = true, default-features = false } serde-big-array = { version = "0.5", optional = true } cpal = { version = "0.15", optional = true } -whatsapp-rust-ureq-http-client = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true } -whatsapp-rust-tokio-transport = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "9734fb2ec544e22b7055147aa3e73b6889e3ff0d", optional = true, default-features = false } +whatsapp-rust-ureq-http-client = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "cbcdd2a6b931d804f3c2693554dce75e654834e3", optional = true } +whatsapp-rust-tokio-transport = { git = "https://github.com/oxidezap/whatsapp-rust", rev = "cbcdd2a6b931d804f3c2693554dce75e654834e3", optional = true, default-features = false } qrcode = { version = "0.14", optional = true } # `bytes` is required for the `Bytes` return types in wacore 0.6 storage # traits (SignalStore::get_session / load_prekey). Feature-gated to @@ -155,9 +159,7 @@ ecb = { version = "0.1", optional = true } md5 = { version = "0.8", optional = true } [features] -default = [ - "default-channels", -] +default = ["default-channels"] default-channels = [ "channel-acp-server", "channel-email", @@ -165,7 +167,6 @@ default-channels = [ "channel-webhook", ] channels-full = [ - "default-channels", "channel-bluesky", "channel-clawdtalk", "channel-dingtalk", @@ -190,9 +191,15 @@ channels-full = [ "channel-wecom", "channel-wecom-ws", "channel-whatsapp-cloud", + "default-channels", ] # Channels with optional deps -channel-email = ["dep:async-imap", "dep:lettre", "dep:mail-parser", "dep:pulldown-cmark"] +channel-email = [ + "dep:async-imap", + "dep:lettre", + "dep:mail-parser", + "dep:pulldown-cmark", +] image-normalization = ["dep:image"] channel-telegram = ["image-normalization"] channel-lark = ["dep:prost"] diff --git a/crates/zeroclaw-channels/src/whatsapp_web.rs b/crates/zeroclaw-channels/src/whatsapp_web.rs index 3128abffe04..03684f4ddda 100644 --- a/crates/zeroclaw-channels/src/whatsapp_web.rs +++ b/crates/zeroclaw-channels/src/whatsapp_web.rs @@ -377,29 +377,25 @@ impl WhatsAppWebChannel { candidates } - /// Compute the reply target, converting LID→phone for DMs when necessary. + /// Compute the reply target for a chat. /// - /// LID JIDs (e.g. `76188559093817@lid`) are internal WhatsApp routing - /// identifiers that cannot receive messages. For non-group chats with an - /// LID-based JID, this converts to a phone JID (`digits@s.whatsapp.net`) - /// using `mapped_phone` from the LID→phone lookup. Groups are returned - /// unchanged. + /// As of whatsapp-rust 0.6+ with PR #636, the library handles LID→PN + /// resolution internally and requires consistent LID namespace throughout + /// the message stanza. We now pass the chat JID unchanged and let the + /// library handle addressing. + /// + /// Previously (pre-0.6), this function converted LID JIDs to phone JIDs + /// because LIDs couldn't receive messages directly. Now the library + /// expects LID format when the recipient is LID-addressed. #[cfg(feature = "whatsapp-web")] fn compute_reply_target( chat_jid: &str, - is_lid: bool, - is_group: bool, - mapped_phone: Option<&str>, + _is_lid: bool, + _is_group: bool, + _mapped_phone: Option<&str>, ) -> String { - if !is_group && is_lid { - mapped_phone - .map(|p| p.chars().filter(|c| c.is_ascii_digit()).collect::()) - .filter(|d| !d.is_empty()) - .map(|digits| format!("{digits}@s.whatsapp.net")) - .unwrap_or_else(|| chat_jid.to_string()) - } else { - chat_jid.to_string() - } + // Pass through unchanged - library handles LID resolution internally + chat_jid.to_string() } /// True when the address is a WhatsApp LID JID (not deliverable for outbound). @@ -2087,8 +2083,8 @@ mod tests { #[test] #[cfg(feature = "whatsapp-web")] - fn compute_reply_target_converts_lid_dm_to_phone() { - // Non-group LID DM with mapped_phone → phone JID + fn compute_reply_target_preserves_lid_dm() { + // LID DM → preserved as-is (library handles LID resolution internally) let chat_jid = "76188559093817@lid"; let is_lid = true; let is_group = false; @@ -2099,8 +2095,8 @@ mod tests { Some("15551234567"), ); assert_eq!( - result, "15551234567@s.whatsapp.net", - "LID DM must convert to phone JID for reply delivery" + result, chat_jid, + "LID DM must be preserved - library handles LID addressing natively" ); } @@ -2114,20 +2110,6 @@ mod tests { assert!(!WhatsAppWebChannel::is_lid_jid_string("+15551234567")); } - #[test] - #[cfg(feature = "whatsapp-web")] - fn compute_reply_target_lid_dm_without_phone_fallback() { - // Non-group LID DM without mapped_phone → falls back to chat JID - let chat_jid = "76188559093817@lid"; - let is_lid = true; - let is_group = false; - let result = WhatsAppWebChannel::compute_reply_target(chat_jid, is_lid, is_group, None); - assert_eq!( - result, chat_jid, - "LID DM without mapped_phone must fall back to original chat JID" - ); - } - #[test] #[cfg(feature = "whatsapp-web")] fn resolve_deliverable_reply_target_converts_lid_with_phone_candidate() { @@ -2141,8 +2123,8 @@ mod tests { #[test] #[cfg(feature = "whatsapp-web")] - fn compute_reply_target_non_lid_dm_unchanged() { - // Non-LID DM → original chat JID (no conversion needed) + fn compute_reply_target_preserves_pn_dm() { + // PN DM → preserved as-is let chat_jid = "15551234567@s.whatsapp.net"; let is_lid = false; let is_group = false; @@ -2152,10 +2134,7 @@ mod tests { is_group, Some("15551234567"), ); - assert_eq!( - result, chat_jid, - "Non-LID DM must preserve original chat JID" - ); + assert_eq!(result, chat_jid, "PN DM must preserve original chat JID"); } #[test] @@ -2170,8 +2149,8 @@ mod tests { #[test] #[cfg(feature = "whatsapp-web")] - fn compute_reply_target_group_unchanged() { - // Group chat → original chat JID (groups don't need conversion) + fn compute_reply_target_preserves_group() { + // Group chat → preserved as-is let chat_jid = "120363012345678901@g.us"; let is_lid = false; let is_group = true; From b53d353bf11de6d16479284bed692139c6da30b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Fri, 29 May 2026 16:46:36 -0300 Subject: [PATCH 2/4] chore: remove dead code and fix comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- crates/zeroclaw-channels/src/whatsapp_web.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/zeroclaw-channels/src/whatsapp_web.rs b/crates/zeroclaw-channels/src/whatsapp_web.rs index 03684f4ddda..e0a38d87292 100644 --- a/crates/zeroclaw-channels/src/whatsapp_web.rs +++ b/crates/zeroclaw-channels/src/whatsapp_web.rs @@ -1302,9 +1302,6 @@ impl Channel for WhatsAppWebChannel { is_group, mapped_phone.as_deref(), ); - if reply_target != chat { - ::zeroclaw_log::record!(DEBUG, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_attrs(::serde_json::json!({"from": chat, "to": reply_target})), "LID→phone reply target"); - } // ── Personal-mode chat-type policy filtering ── if wa_mode == zeroclaw_config::schema::WhatsAppWebMode::Personal { @@ -1525,9 +1522,9 @@ impl Channel for WhatsAppWebChannel { channel: "whatsapp".to_string(), channel_alias: Some((*alias).clone()), sender: normalized.clone(), - // Reply to the originating chat JID (DM or group). - // For DMs with LID JIDs, this is the resolved - // phone JID (see LID→phone resolution above). + // Reply to the originating chat JID (DM or group), + // passed through unchanged (library handles + // LID addressing internally). reply_target, content, timestamp: chrono::Utc::now().timestamp() as u64, From 916b679b0702ec5552e01a307ca1f2dfed2de99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Mon, 8 Jun 2026 17:27:36 -0300 Subject: [PATCH 3/4] refactor(channels/whatsapp-web): remove unused fn parameters --- crates/zeroclaw-channels/src/whatsapp_web.rs | 41 +++----------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/crates/zeroclaw-channels/src/whatsapp_web.rs b/crates/zeroclaw-channels/src/whatsapp_web.rs index e0a38d87292..eef0024cd13 100644 --- a/crates/zeroclaw-channels/src/whatsapp_web.rs +++ b/crates/zeroclaw-channels/src/whatsapp_web.rs @@ -388,12 +388,7 @@ impl WhatsAppWebChannel { /// because LIDs couldn't receive messages directly. Now the library /// expects LID format when the recipient is LID-addressed. #[cfg(feature = "whatsapp-web")] - fn compute_reply_target( - chat_jid: &str, - _is_lid: bool, - _is_group: bool, - _mapped_phone: Option<&str>, - ) -> String { + fn compute_reply_target(chat_jid: &str) -> String { // Pass through unchanged - library handles LID resolution internally chat_jid.to_string() } @@ -1296,12 +1291,7 @@ impl Channel for WhatsAppWebChannel { .cloned(); let is_group = info.source.is_group; - let mut reply_target = Self::compute_reply_target( - &chat, - info.source.chat.is_lid(), - is_group, - mapped_phone.as_deref(), - ); + let mut reply_target = Self::compute_reply_target(&chat); // ── Personal-mode chat-type policy filtering ── if wa_mode == zeroclaw_config::schema::WhatsAppWebMode::Personal { @@ -2083,14 +2073,7 @@ mod tests { fn compute_reply_target_preserves_lid_dm() { // LID DM → preserved as-is (library handles LID resolution internally) let chat_jid = "76188559093817@lid"; - let is_lid = true; - let is_group = false; - let result = WhatsAppWebChannel::compute_reply_target( - chat_jid, - is_lid, - is_group, - Some("15551234567"), - ); + let result = WhatsAppWebChannel::compute_reply_target(chat_jid); assert_eq!( result, chat_jid, "LID DM must be preserved - library handles LID addressing natively" @@ -2123,14 +2106,7 @@ mod tests { fn compute_reply_target_preserves_pn_dm() { // PN DM → preserved as-is let chat_jid = "15551234567@s.whatsapp.net"; - let is_lid = false; - let is_group = false; - let result = WhatsAppWebChannel::compute_reply_target( - chat_jid, - is_lid, - is_group, - Some("15551234567"), - ); + let result = WhatsAppWebChannel::compute_reply_target(chat_jid); assert_eq!(result, chat_jid, "PN DM must preserve original chat JID"); } @@ -2149,14 +2125,7 @@ mod tests { fn compute_reply_target_preserves_group() { // Group chat → preserved as-is let chat_jid = "120363012345678901@g.us"; - let is_lid = false; - let is_group = true; - let result = WhatsAppWebChannel::compute_reply_target( - chat_jid, - is_lid, - is_group, - Some("15551234567"), - ); + let result = WhatsAppWebChannel::compute_reply_target(chat_jid); assert_eq!( result, chat_jid, "Group chat must preserve original chat JID" From 85df15d561cc7916eee543af3e2f1dd81a605a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Tue, 9 Jun 2026 00:03:04 -0300 Subject: [PATCH 4/4] =?UTF-8?q?fix(channels/whatsapp-web):=20remove=20LID?= =?UTF-8?q?=E2=86=92phone=20conversion,=20library=20handles=20it=20interna?= =?UTF-8?q?lly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- crates/zeroclaw-channels/src/whatsapp_web.rs | 197 +------------------ 1 file changed, 9 insertions(+), 188 deletions(-) diff --git a/crates/zeroclaw-channels/src/whatsapp_web.rs b/crates/zeroclaw-channels/src/whatsapp_web.rs index eef0024cd13..ccc75103d93 100644 --- a/crates/zeroclaw-channels/src/whatsapp_web.rs +++ b/crates/zeroclaw-channels/src/whatsapp_web.rs @@ -393,75 +393,12 @@ impl WhatsAppWebChannel { chat_jid.to_string() } - /// True when the address is a WhatsApp LID JID (not deliverable for outbound). + /// Resolve an outbound recipient. With whatsapp-rust 0.6+ and PR #636, + /// LID JIDs are handled internally by the library, so we pass through unchanged. #[cfg(feature = "whatsapp-web")] - fn is_lid_jid_string(jid: &str) -> bool { - jid.trim() - .rsplit_once('@') - .is_some_and(|(_, domain)| domain.eq_ignore_ascii_case("lid")) - } - - /// Map an undeliverable LID chat JID to `digits@s.whatsapp.net` when phone - /// candidates are known. Returns `(target, converted)`. - #[cfg(feature = "whatsapp-web")] - fn resolve_deliverable_reply_target(chat: &str, phone_candidates: &[String]) -> (String, bool) { - if !Self::is_lid_jid_string(chat) { - return (chat.to_string(), false); - } - for candidate in phone_candidates { - let digits: String = candidate.chars().filter(|c| c.is_ascii_digit()).collect(); - if !digits.is_empty() { - return (format!("{digits}@s.whatsapp.net"), true); - } - } - (chat.to_string(), false) - } - - /// Best-effort LID→phone lookup via whatsapp-rust 0.6 `get_lid_pn_entry`. - #[cfg(feature = "whatsapp-web")] - async fn lookup_phone_from_lid_jid( - client: &whatsapp_rust::Client, - lid_jid: &str, - ) -> Option { - let lid_user = lid_jid.split('@').next().filter(|u| !u.is_empty())?; - let jid = wacore_binary::jid::Jid::lid(lid_user); - match client.get_lid_pn_entry(&jid).await { - Ok(Some(entry)) => Some(entry.phone_number), - _ => None, - } - } - - /// Resolve an outbound recipient, converting LID JIDs via the live client cache. - #[cfg(feature = "whatsapp-web")] - async fn resolve_outbound_recipient( - client: &whatsapp_rust::Client, - recipient: &str, - ) -> Result { - let trimmed = recipient.trim(); - if !Self::is_lid_jid_string(trimmed) { - return Ok(trimmed.to_string()); - } - if let Some(phone) = Self::lookup_phone_from_lid_jid(client, trimmed).await - && let Some(token) = Self::normalize_phone_token(&phone) - { - let digits: String = token.chars().filter(|c| c.is_ascii_digit()).collect(); - if !digits.is_empty() { - let resolved = format!("{digits}@s.whatsapp.net"); - ::zeroclaw_log::record!( - INFO, - ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note) - .with_attrs(::serde_json::json!({ - "from": trimmed, - "to": resolved, - })), - "outbound LID→phone recipient" - ); - return Ok(resolved); - } - } - anyhow::bail!( - "Cannot deliver to LID JID `{trimmed}`: phone resolution failed (LID JIDs cannot receive messages)" - ) + fn resolve_outbound_recipient(recipient: &str) -> String { + // Pass through unchanged - library handles LID resolution internally + recipient.trim().to_string() } /// Normalize phone number to E.164 format @@ -986,8 +923,7 @@ impl Channel for WhatsAppWebChannel { } } - let deliverable_recipient = - Self::resolve_outbound_recipient(&client, &message.recipient).await?; + let deliverable_recipient = Self::resolve_outbound_recipient(&message.recipient); let to = self.recipient_to_jid(&deliverable_recipient)?; // Voice chat mode: send text normally AND queue a voice note of the @@ -1258,7 +1194,6 @@ impl Channel for WhatsAppWebChannel { let sender_jid = info.source.sender.clone(); let sender_alt = info.source.sender_alt.clone(); let sender = sender_jid.user().to_string(); - let _is_group = info.source.chat.is_group(); let chat = info.source.chat.to_string(); // whatsapp-rust 0.6: `Client::get_phone_number_from_lid` @@ -1291,7 +1226,7 @@ impl Channel for WhatsAppWebChannel { .cloned(); let is_group = info.source.is_group; - let mut reply_target = Self::compute_reply_target(&chat); + let reply_target = Self::compute_reply_target(&chat); // ── Personal-mode chat-type policy filtering ── if wa_mode == zeroclaw_config::schema::WhatsAppWebMode::Personal { @@ -1375,51 +1310,6 @@ impl Channel for WhatsAppWebChannel { let normalized = normalized.unwrap_or_else(|| sender.clone()); - // LID chat JIDs cannot receive outbound messages (typing may - // still work). Resolve to phone JID for all non-group DMs. - if !is_group && Self::is_lid_jid_string(&reply_target) { - let mut lid_candidates = sender_candidates.clone(); - if let Some(phone) = - Self::lookup_phone_from_lid_jid(&client, &reply_target).await - && let Some(token) = Self::normalize_phone_token(&phone) - && !lid_candidates.iter().any(|c| c == &token) - { - lid_candidates.push(token); - } - let (resolved, converted) = - Self::resolve_deliverable_reply_target( - &reply_target, - &lid_candidates, - ); - if converted { - reply_target = resolved; - ::zeroclaw_log::record!( - INFO, - ::zeroclaw_log::Event::new( - module_path!(), - ::zeroclaw_log::Action::Note - ) - .with_attrs(::serde_json::json!({ - "reply_target": reply_target, - })), - "DM LID→phone reply target" - ); - } else { - ::zeroclaw_log::record!( - WARN, - ::zeroclaw_log::Event::new( - module_path!(), - ::zeroclaw_log::Action::Note - ) - .with_outcome(::zeroclaw_log::EventOutcome::Unknown) - .with_attrs(::serde_json::json!({ - "reply_target": reply_target, - })), - "undeliverable LID reply_target; outbound may fail" - ); - } - } - // Attempt voice note transcription (ptt = push-to-talk = voice note). // When `transcribe_non_ptt_audio` is enabled in the transcription // config, also transcribe forwarded / regular audio messages. @@ -1446,8 +1336,6 @@ impl Channel for WhatsAppWebChannel { // Use transcribed voice text, or fall back to text content. // Track whether this chat used a voice note so we reply in kind. - // Key by final reply_target (post-LID resolution): send() checks - // message.recipient, which is the resolved phone JID for LID DMs. let content = if let Some(ref vt) = voice_text { if let Ok(mut vs) = voice_chats.lock() { vs.insert(reply_target.clone()); @@ -1739,7 +1627,7 @@ impl Channel for WhatsAppWebChannel { } } - let deliverable_recipient = Self::resolve_outbound_recipient(&client, recipient).await?; + let deliverable_recipient = Self::resolve_outbound_recipient(recipient); let to = self.recipient_to_jid(&deliverable_recipient)?; client.chatstate().send_composing(&to).await.map_err(|e| { ::zeroclaw_log::record!( @@ -1779,7 +1667,7 @@ impl Channel for WhatsAppWebChannel { } } - let deliverable_recipient = Self::resolve_outbound_recipient(&client, recipient).await?; + let deliverable_recipient = Self::resolve_outbound_recipient(recipient); let to = self.recipient_to_jid(&deliverable_recipient)?; client.chatstate().send_paused(&to).await.map_err(|e| { ::zeroclaw_log::record!( @@ -2080,27 +1968,6 @@ mod tests { ); } - #[test] - #[cfg(feature = "whatsapp-web")] - fn is_lid_jid_string_detects_lid_domain() { - assert!(WhatsAppWebChannel::is_lid_jid_string("76188559093817@lid")); - assert!(!WhatsAppWebChannel::is_lid_jid_string( - "15551234567@s.whatsapp.net" - )); - assert!(!WhatsAppWebChannel::is_lid_jid_string("+15551234567")); - } - - #[test] - #[cfg(feature = "whatsapp-web")] - fn resolve_deliverable_reply_target_converts_lid_with_phone_candidate() { - let (target, converted) = WhatsAppWebChannel::resolve_deliverable_reply_target( - "76188559093817@lid", - &["+15551234567".to_string()], - ); - assert!(converted); - assert_eq!(target, "15551234567@s.whatsapp.net"); - } - #[test] #[cfg(feature = "whatsapp-web")] fn compute_reply_target_preserves_pn_dm() { @@ -2110,16 +1977,6 @@ mod tests { assert_eq!(result, chat_jid, "PN DM must preserve original chat JID"); } - #[test] - #[cfg(feature = "whatsapp-web")] - fn resolve_deliverable_reply_target_leaves_phone_jid_unchanged() { - let chat = "15551234567@s.whatsapp.net"; - let (target, converted) = - WhatsAppWebChannel::resolve_deliverable_reply_target(chat, &["+15551234567".into()]); - assert!(!converted); - assert_eq!(target, chat); - } - #[test] #[cfg(feature = "whatsapp-web")] fn compute_reply_target_preserves_group() { @@ -2132,42 +1989,6 @@ mod tests { ); } - #[test] - #[cfg(feature = "whatsapp-web")] - fn resolve_deliverable_reply_target_warns_via_unchanged_lid_when_no_candidates() { - let chat = "76188559093817@lid"; - let (target, converted) = WhatsAppWebChannel::resolve_deliverable_reply_target(chat, &[]); - assert!(!converted); - assert_eq!(target, chat); - } - - /// Regression: inbound voice tracking must use the resolved `reply_target` - /// (phone JID), not the original LID `chat`, because `send()` looks up - /// `voice_chats` with `message.recipient` (= `reply_target`). - #[test] - #[cfg(feature = "whatsapp-web")] - fn lid_dm_voice_tracking_key_matches_send_recipient() { - let chat_lid = "76188559093817@lid"; - let (reply_target, converted) = WhatsAppWebChannel::resolve_deliverable_reply_target( - chat_lid, - &["+15551234567".to_string()], - ); - assert!(converted); - assert_ne!(chat_lid, reply_target); - - let mut voice_chats = std::collections::HashSet::new(); - voice_chats.insert(reply_target.clone()); - let message_recipient = reply_target.clone(); - assert!( - voice_chats.contains(&message_recipient), - "voice_chats must be keyed by resolved reply_target for send() lookup" - ); - assert!( - !voice_chats.contains(chat_lid), - "original LID chat JID must not be the voice_chats key after LID→phone resolution" - ); - } - // ── lid_rejection_diagnostic: scoped LID warning ──── // // The diagnostic fires only inside the `Allowlist::normalized.is_none()`