From 6ed1c3c9b2ae5534e451f7f57fcffc0f597d912d Mon Sep 17 00:00:00 2001 From: Salientekill Date: Mon, 27 Apr 2026 21:31:22 +0200 Subject: [PATCH 1/3] fix(blocklist): include LID + pn_jid in block IQ (modern WA) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modern WhatsApp servers (2026+) reject `` with `code=400 bad-request`. After the LID rollout the blocklist IQ requires both halves of the LID↔PN pair: Verified against Baileys' `updateBlockStatus` (`Socket/chats.ts`), which now resolves both sides via its LID mapping store before sending. Changes: * `BlocklistItemRequest` gains an optional `pn_jid` attribute and a new `block_with_pn(lid, pn)` constructor. Old `block(jid)` / `unblock(jid)` keep working for callers that already have the right shape. * `Blocking::block` and `Blocking::unblock` now resolve LID↔PN through `client.get_lid_pn_entry` regardless of which side the caller passes, then send the modern stanza: - block: `jid=LID, pn_jid=PN` - unblock: `jid=LID` (PN not required by the server for removals) * Returns a structured `IqError::ServerError` when no LID↔PN mapping is available — block silently no-op'd before, masking the bug. Tests: * `test_block_with_pn_emits_pn_jid_attr` — asserts both attrs on the wire * `test_unblock_omits_pn_jid_attr` — asserts unblock keeps the old shape --- src/features/blocking.rs | 75 ++++++++++++++++++++++++++++++++++++-- wacore/src/iq/blocklist.rs | 57 ++++++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/src/features/blocking.rs b/src/features/blocking.rs index 2e4239a9f..39e6e48e0 100644 --- a/src/features/blocking.rs +++ b/src/features/blocking.rs @@ -21,20 +21,87 @@ impl<'a> Blocking<'a> { } /// Block a contact. + /// + /// Modern WA exige `jid=LID` + `pn_jid=PN` no stanza; sem `pn_jid` o + /// servidor responde `400 bad-request`. Resolve o par via + /// `get_lid_pn_entry` antes de enviar — aceita LID ou PN como input. pub async fn block(&self, jid: &Jid) -> Result<(), IqError> { debug!(target: "Blocking", "Blocking contact: {}", jid); - self.client.execute(UpdateBlocklistSpec::block(jid)).await?; - debug!(target: "Blocking", "Successfully blocked contact: {}", jid); + let bare = jid.to_non_ad(); + let (lid_jid, pn_jid) = if bare.is_lid() { + let entry = self + .client + .get_lid_pn_entry(&bare) + .await + .map_err(|e| IqError::ServerError { + code: 0, + text: format!("get_lid_pn_entry: {e}"), + })? + .ok_or(IqError::ServerError { + code: 0, + text: format!("no LID↔PN mapping for {bare}"), + })?; + (bare, Jid::pn(entry.phone_number)) + } else if bare.is_pn() { + let entry = self + .client + .get_lid_pn_entry(&bare) + .await + .map_err(|e| IqError::ServerError { + code: 0, + text: format!("get_lid_pn_entry: {e}"), + })? + .ok_or(IqError::ServerError { + code: 0, + text: format!("no LID↔PN mapping for {bare}"), + })?; + (Jid::lid(entry.lid), bare) + } else { + return Err(IqError::ServerError { + code: 0, + text: format!("block: jid {bare} is neither PN nor LID"), + }); + }; + self.client + .execute(UpdateBlocklistSpec::block_with_pn(&lid_jid, &pn_jid)) + .await?; + debug!(target: "Blocking", "Successfully blocked contact: lid={lid_jid} pn={pn_jid}"); Ok(()) } /// Unblock a contact. + /// + /// Modern WA exige `jid=LID` no ``. Resolve para + /// LID via mapping se o input for PN. pub async fn unblock(&self, jid: &Jid) -> Result<(), IqError> { debug!(target: "Blocking", "Unblocking contact: {}", jid); + let bare = jid.to_non_ad(); + let lid_jid = if bare.is_lid() { + bare + } else if bare.is_pn() { + let entry = self + .client + .get_lid_pn_entry(&bare) + .await + .map_err(|e| IqError::ServerError { + code: 0, + text: format!("get_lid_pn_entry: {e}"), + })? + .ok_or(IqError::ServerError { + code: 0, + text: format!("no LID↔PN mapping for {bare}"), + })?; + Jid::lid(entry.lid) + } else { + return Err(IqError::ServerError { + code: 0, + text: format!("unblock: jid {bare} is neither PN nor LID"), + }); + }; self.client - .execute(UpdateBlocklistSpec::unblock(jid)) + .execute(UpdateBlocklistSpec::unblock(&lid_jid)) .await?; - debug!(target: "Blocking", "Successfully unblocked contact: {}", jid); + debug!(target: "Blocking", "Successfully unblocked contact: lid={lid_jid}"); Ok(()) } diff --git a/wacore/src/iq/blocklist.rs b/wacore/src/iq/blocklist.rs index 18c197f89..4cc956d89 100644 --- a/wacore/src/iq/blocklist.rs +++ b/wacore/src/iq/blocklist.rs @@ -25,7 +25,11 @@ pub enum BlocklistAction { } /// Request node for updating blocklist. /// -/// Wire format: `` +/// Modern WA exige `jid` em formato **LID** e, ao bloquear, `pn_jid` +/// adicional com o PN do contato. Sem `pn_jid` o servidor responde +/// `code=400 bad-request` (verificado contra Baileys e WA Web 2026-04). +/// +/// Wire format: `` #[derive(Debug, Clone, crate::ProtocolNode)] #[protocol(tag = "item")] pub struct BlocklistItemRequest { @@ -33,6 +37,8 @@ pub struct BlocklistItemRequest { pub jid: Jid, #[attr(name = "action", string_enum)] pub action: BlocklistAction, + #[attr(name = "pn_jid", jid, optional)] + pub pn_jid: Option, } impl BlocklistItemRequest { @@ -40,6 +46,7 @@ impl BlocklistItemRequest { Self { jid: jid.clone(), action, + pn_jid: None, } } @@ -50,6 +57,15 @@ impl BlocklistItemRequest { pub fn unblock(jid: &Jid) -> Self { Self::new(jid, BlocklistAction::Unblock) } + + /// Block usando `lid` e `pn_jid` explícitos (formato moderno do WA). + pub fn block_with_pn(lid: &Jid, pn_jid: &Jid) -> Self { + Self { + jid: lid.clone(), + action: BlocklistAction::Block, + pn_jid: Some(pn_jid.clone()), + } + } } /// A single blocklist entry from the response. /// @@ -156,6 +172,13 @@ impl UpdateBlocklistSpec { request: BlocklistItemRequest::unblock(jid), } } + + /// Block com `lid` + `pn_jid` (formato moderno exigido por WA 2026+). + pub fn block_with_pn(lid: &Jid, pn_jid: &Jid) -> Self { + Self { + request: BlocklistItemRequest::block_with_pn(lid, pn_jid), + } + } } impl IqSpec for UpdateBlocklistSpec { @@ -283,4 +306,36 @@ mod tests { let unblock_spec = UpdateBlocklistSpec::unblock(&jid); assert_eq!(unblock_spec.request.action, BlocklistAction::Unblock); } + + #[test] + fn test_block_with_pn_emits_pn_jid_attr() { + let lid: Jid = "12345678901234@lid".parse().unwrap(); + let pn: Jid = "5511999999999@s.whatsapp.net".parse().unwrap(); + let request = BlocklistItemRequest::block_with_pn(&lid, &pn); + let node = request.into_node(); + + assert_eq!(node.tag, "item"); + assert!(node.attrs.get("action").is_some_and(|v| v == "block")); + assert!( + node.attrs + .get("jid") + .is_some_and(|v| v == "12345678901234@lid") + ); + assert!( + node.attrs + .get("pn_jid") + .is_some_and(|v| v == "5511999999999@s.whatsapp.net") + ); + } + + #[test] + fn test_unblock_omits_pn_jid_attr() { + let lid: Jid = "12345678901234@lid".parse().unwrap(); + let request = BlocklistItemRequest::unblock(&lid); + let node = request.into_node(); + + assert_eq!(node.tag, "item"); + assert!(node.attrs.get("action").is_some_and(|v| v == "unblock")); + assert!(node.attrs.get("pn_jid").is_none()); + } } From abc2e4d6a6570f1be193688fecf7c45e2d13600d Mon Sep 17 00:00:00 2001 From: Salientekill Date: Mon, 27 Apr 2026 21:58:48 +0200 Subject: [PATCH 2/3] refactor(blocklist): address CodeRabbit review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Extract `Blocking::resolve_lid_pn` to share the LID↔PN resolution between `block` and `unblock` (was duplicated, would drift). * Stop interpolating raw PN/LID identifiers into error messages and debug logs — the `Blocking` API now returns generic "blocklist: no LID↔PN mapping for provided jid" / "lookup failed" / "neither PN nor LID" so user IDs don't end up in surfaces that get logged or surfaced upstream. * Trim the protocol comment on `BlocklistItemRequest` and the `block_with_pn` constructors down to a single "why" line; drop dated empirical notes (those belong in tests, which already cover the wire shape). --- src/features/blocking.rs | 80 ++++++++++++++------------------------ wacore/src/iq/blocklist.rs | 13 ++----- 2 files changed, 33 insertions(+), 60 deletions(-) diff --git a/src/features/blocking.rs b/src/features/blocking.rs index 39e6e48e0..1069b52c8 100644 --- a/src/features/blocking.rs +++ b/src/features/blocking.rs @@ -20,88 +20,66 @@ impl<'a> Blocking<'a> { Self { client } } - /// Block a contact. - /// - /// Modern WA exige `jid=LID` + `pn_jid=PN` no stanza; sem `pn_jid` o - /// servidor responde `400 bad-request`. Resolve o par via - /// `get_lid_pn_entry` antes de enviar — aceita LID ou PN como input. - pub async fn block(&self, jid: &Jid) -> Result<(), IqError> { - debug!(target: "Blocking", "Blocking contact: {}", jid); - let bare = jid.to_non_ad(); - let (lid_jid, pn_jid) = if bare.is_lid() { + /// Resolve `bare` (LID or PN) into the `(lid, pn)` pair the server expects + /// on blocklist stanzas. Errors stay generic to avoid leaking user IDs. + async fn resolve_lid_pn(&self, bare: Jid) -> Result<(Jid, Jid), IqError> { + if bare.is_lid() { let entry = self .client .get_lid_pn_entry(&bare) .await - .map_err(|e| IqError::ServerError { + .map_err(|_| IqError::ServerError { code: 0, - text: format!("get_lid_pn_entry: {e}"), + text: "blocklist: LID↔PN lookup failed".to_string(), })? .ok_or(IqError::ServerError { code: 0, - text: format!("no LID↔PN mapping for {bare}"), + text: "blocklist: no LID↔PN mapping for provided jid".to_string(), })?; - (bare, Jid::pn(entry.phone_number)) + Ok((bare, Jid::pn(entry.phone_number))) } else if bare.is_pn() { let entry = self .client .get_lid_pn_entry(&bare) .await - .map_err(|e| IqError::ServerError { + .map_err(|_| IqError::ServerError { code: 0, - text: format!("get_lid_pn_entry: {e}"), + text: "blocklist: LID↔PN lookup failed".to_string(), })? .ok_or(IqError::ServerError { code: 0, - text: format!("no LID↔PN mapping for {bare}"), + text: "blocklist: no LID↔PN mapping for provided jid".to_string(), })?; - (Jid::lid(entry.lid), bare) + Ok((Jid::lid(entry.lid), bare)) } else { - return Err(IqError::ServerError { + Err(IqError::ServerError { code: 0, - text: format!("block: jid {bare} is neither PN nor LID"), - }); - }; + text: "blocklist: jid is neither PN nor LID".to_string(), + }) + } + } + + /// Block a contact. Accepts either LID or PN; the wire stanza always + /// carries both (`jid=LID, pn_jid=PN`) — modern WA rejects PN-only blocks. + pub async fn block(&self, jid: &Jid) -> Result<(), IqError> { + debug!(target: "Blocking", "Blocking contact"); + let (lid_jid, pn_jid) = self.resolve_lid_pn(jid.to_non_ad()).await?; self.client .execute(UpdateBlocklistSpec::block_with_pn(&lid_jid, &pn_jid)) .await?; - debug!(target: "Blocking", "Successfully blocked contact: lid={lid_jid} pn={pn_jid}"); + debug!(target: "Blocking", "Successfully blocked contact"); Ok(()) } - /// Unblock a contact. - /// - /// Modern WA exige `jid=LID` no ``. Resolve para - /// LID via mapping se o input for PN. + /// Unblock a contact. Stanza only needs the LID, but PN input is accepted + /// and resolved through the mapping. pub async fn unblock(&self, jid: &Jid) -> Result<(), IqError> { - debug!(target: "Blocking", "Unblocking contact: {}", jid); - let bare = jid.to_non_ad(); - let lid_jid = if bare.is_lid() { - bare - } else if bare.is_pn() { - let entry = self - .client - .get_lid_pn_entry(&bare) - .await - .map_err(|e| IqError::ServerError { - code: 0, - text: format!("get_lid_pn_entry: {e}"), - })? - .ok_or(IqError::ServerError { - code: 0, - text: format!("no LID↔PN mapping for {bare}"), - })?; - Jid::lid(entry.lid) - } else { - return Err(IqError::ServerError { - code: 0, - text: format!("unblock: jid {bare} is neither PN nor LID"), - }); - }; + debug!(target: "Blocking", "Unblocking contact"); + let (lid_jid, _) = self.resolve_lid_pn(jid.to_non_ad()).await?; self.client .execute(UpdateBlocklistSpec::unblock(&lid_jid)) .await?; - debug!(target: "Blocking", "Successfully unblocked contact: lid={lid_jid}"); + debug!(target: "Blocking", "Successfully unblocked contact"); Ok(()) } diff --git a/wacore/src/iq/blocklist.rs b/wacore/src/iq/blocklist.rs index 4cc956d89..3524a1924 100644 --- a/wacore/src/iq/blocklist.rs +++ b/wacore/src/iq/blocklist.rs @@ -23,13 +23,8 @@ pub enum BlocklistAction { #[wire = "unblock"] Unblock, } -/// Request node for updating blocklist. -/// -/// Modern WA exige `jid` em formato **LID** e, ao bloquear, `pn_jid` -/// adicional com o PN do contato. Sem `pn_jid` o servidor responde -/// `code=400 bad-request` (verificado contra Baileys e WA Web 2026-04). -/// -/// Wire format: `` +/// Wire requires `jid` in LID and an additional `pn_jid` (PN) when blocking; +/// servers reject PN-only blocks. #[derive(Debug, Clone, crate::ProtocolNode)] #[protocol(tag = "item")] pub struct BlocklistItemRequest { @@ -58,7 +53,7 @@ impl BlocklistItemRequest { Self::new(jid, BlocklistAction::Unblock) } - /// Block usando `lid` e `pn_jid` explícitos (formato moderno do WA). + /// Construct a block request with the LID and PN required on the wire. pub fn block_with_pn(lid: &Jid, pn_jid: &Jid) -> Self { Self { jid: lid.clone(), @@ -173,7 +168,7 @@ impl UpdateBlocklistSpec { } } - /// Block com `lid` + `pn_jid` (formato moderno exigido por WA 2026+). + /// Construct a block spec with the LID and PN required on the wire. pub fn block_with_pn(lid: &Jid, pn_jid: &Jid) -> Self { Self { request: BlocklistItemRequest::block_with_pn(lid, pn_jid), From c24d94ed805f1a38e8ca6062d4d067c07ddf48f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Mon, 27 Apr 2026 18:01:40 -0300 Subject: [PATCH 3/3] refactor(blocklist): use IqError::EncodeError for preflight, DRY block_with_pn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preflight failures (no LID↔PN mapping, jid neither PN nor LID, backend lookup error) were emitted as `IqError::ServerError { code: 0 }`, which collides with real server codes and discards the backend error. Switch to `IqError::EncodeError(anyhow::Error)` — the existing variant for "couldn't build the IQ" — and preserve the backend source. Also collapse the two LID/PN branches into a single `get_lid_pn_entry` call and have `BlocklistItemRequest::block_with_pn` delegate to `Self::new` instead of repeating the field list. --- src/features/blocking.rs | 54 ++++++++++++++------------------------ wacore/src/iq/blocklist.rs | 3 +-- 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/features/blocking.rs b/src/features/blocking.rs index 1069b52c8..d528cf698 100644 --- a/src/features/blocking.rs +++ b/src/features/blocking.rs @@ -21,42 +21,28 @@ impl<'a> Blocking<'a> { } /// Resolve `bare` (LID or PN) into the `(lid, pn)` pair the server expects - /// on blocklist stanzas. Errors stay generic to avoid leaking user IDs. + /// on blocklist stanzas. async fn resolve_lid_pn(&self, bare: Jid) -> Result<(Jid, Jid), IqError> { - if bare.is_lid() { - let entry = self - .client - .get_lid_pn_entry(&bare) - .await - .map_err(|_| IqError::ServerError { - code: 0, - text: "blocklist: LID↔PN lookup failed".to_string(), - })? - .ok_or(IqError::ServerError { - code: 0, - text: "blocklist: no LID↔PN mapping for provided jid".to_string(), - })?; - Ok((bare, Jid::pn(entry.phone_number))) - } else if bare.is_pn() { - let entry = self - .client - .get_lid_pn_entry(&bare) - .await - .map_err(|_| IqError::ServerError { - code: 0, - text: "blocklist: LID↔PN lookup failed".to_string(), - })? - .ok_or(IqError::ServerError { - code: 0, - text: "blocklist: no LID↔PN mapping for provided jid".to_string(), - })?; - Ok((Jid::lid(entry.lid), bare)) - } else { - Err(IqError::ServerError { - code: 0, - text: "blocklist: jid is neither PN nor LID".to_string(), - }) + if !(bare.is_lid() || bare.is_pn()) { + return Err(IqError::EncodeError(anyhow::anyhow!( + "blocklist: jid is neither PN nor LID" + ))); } + let entry = self + .client + .get_lid_pn_entry(&bare) + .await + .map_err(IqError::EncodeError)? + .ok_or_else(|| { + IqError::EncodeError(anyhow::anyhow!( + "blocklist: no LID↔PN mapping for provided jid" + )) + })?; + Ok(if bare.is_lid() { + (bare, Jid::pn(entry.phone_number)) + } else { + (Jid::lid(entry.lid), bare) + }) } /// Block a contact. Accepts either LID or PN; the wire stanza always diff --git a/wacore/src/iq/blocklist.rs b/wacore/src/iq/blocklist.rs index 3524a1924..795c038d4 100644 --- a/wacore/src/iq/blocklist.rs +++ b/wacore/src/iq/blocklist.rs @@ -56,9 +56,8 @@ impl BlocklistItemRequest { /// Construct a block request with the LID and PN required on the wire. pub fn block_with_pn(lid: &Jid, pn_jid: &Jid) -> Self { Self { - jid: lid.clone(), - action: BlocklistAction::Block, pn_jid: Some(pn_jid.clone()), + ..Self::new(lid, BlocklistAction::Block) } } }