From 6b7b11c5ebba3df3ab283b953fa11d875f05773b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Thu, 4 Jun 2026 08:09:47 -0300 Subject: [PATCH 1/2] fix(blocking): resolve LID/PN before is_blocked compares block()/unblock() store blocks keyed by LID, so GetBlocklist returns LID-keyed entries. is_blocked compared the raw input .user with no resolution, so a PN-input query never matched a LID-keyed block and returned a false negative. Resolve the queried JID to its LID/PN pair (same mapping block() uses) and match each entry against the raw user plus the resolved LID and PN, via a pure blocklist_contains() helper so the resolution is unit-testable. --- src/features/blocking.rs | 66 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/features/blocking.rs b/src/features/blocking.rs index d528cf698..0ea787dbb 100644 --- a/src/features/blocking.rs +++ b/src/features/blocking.rs @@ -79,17 +79,77 @@ impl<'a> Blocking<'a> { /// Check if a contact is blocked. /// - /// Compares only the user part of the JID, ignoring device ID, - /// since blocking applies to the entire user account, not individual devices. + /// Compares only the user part of the JID, ignoring device ID, since blocking + /// applies to the entire user account, not individual devices. pub async fn is_blocked(&self, jid: &Jid) -> anyhow::Result { let blocklist = self.get_blocklist().await?; - Ok(blocklist.iter().any(|e| e.jid.user == jid.user)) + let bare = jid.to_non_ad(); + + // Blocks are stored keyed by LID (block() always resolves the input to a LID), so a + // PN-input query must resolve to its LID before comparing or it never matches a + // LID-keyed entry. Match against the raw user plus the resolved LID and PN; fall back + // to the raw user alone when no mapping exists. + let mapping = self.client.get_lid_pn_entry(&bare).await.ok().flatten(); + let mut users: Vec<&str> = vec![bare.user.as_str()]; + if let Some(entry) = mapping.as_ref() { + users.push(entry.lid.as_str()); + users.push(entry.phone_number.as_str()); + } + + Ok(blocklist_contains(&blocklist, &users)) } } +/// Whether any blocklist entry's user part matches one of `candidate_users`. +/// +/// Blocks are stored keyed by LID, so the caller resolves the queried JID to its +/// LID/PN pair and passes all of them (raw, LID, PN) to catch a LID-keyed entry from +/// a PN-input query (and the reverse). +fn blocklist_contains(blocklist: &[BlocklistEntry], candidate_users: &[&str]) -> bool { + blocklist + .iter() + .any(|e| candidate_users.contains(&e.jid.user.as_str())) +} + impl Client { /// Access blocking operations. pub fn blocking(&self) -> Blocking<'_> { Blocking::new(self) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn lid_entry(user: &str) -> BlocklistEntry { + BlocklistEntry { + jid: Jid::lid(user.to_string()), + timestamp: None, + } + } + + #[test] + fn pn_query_matches_lid_keyed_block_only_when_resolved() { + // A block stored under the LID (modern WA) must be found once the PN query is + // resolved to that LID. Without resolution the LID-keyed block is missed (the bug). + let blocklist = vec![lid_entry("100000012345678")]; + + assert!( + blocklist_contains(&blocklist, &["559980000001", "100000012345678"]), + "resolved PN->LID candidate matches the LID-keyed block" + ); + assert!( + !blocklist_contains(&blocklist, &["559980000001"]), + "raw PN alone misses the LID-keyed block (the false negative)" + ); + assert!( + blocklist_contains(&blocklist, &["100000012345678"]), + "a LID query matches directly" + ); + assert!( + !blocklist_contains(&blocklist, &["559981111111", "100000099999999"]), + "an unrelated contact is not blocked" + ); + } +} From 9d0b23fa90c3757ba1ae8a299a60f67d60c7b9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Thu, 4 Jun 2026 08:20:30 -0300 Subject: [PATCH 2/2] fix(blocking): propagate get_lid_pn_entry errors in is_blocked Swallowing the resolution error with .ok().flatten() would fall back to the raw user and re-introduce the false negative this PR fixes when the backend is failing. Propagate Err; only Ok(None) (genuine absence) falls back to the raw user. --- src/features/blocking.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/blocking.rs b/src/features/blocking.rs index 0ea787dbb..b22fd8137 100644 --- a/src/features/blocking.rs +++ b/src/features/blocking.rs @@ -87,9 +87,10 @@ impl<'a> Blocking<'a> { // Blocks are stored keyed by LID (block() always resolves the input to a LID), so a // PN-input query must resolve to its LID before comparing or it never matches a - // LID-keyed entry. Match against the raw user plus the resolved LID and PN; fall back - // to the raw user alone when no mapping exists. - let mapping = self.client.get_lid_pn_entry(&bare).await.ok().flatten(); + // LID-keyed entry. Match against the raw user plus the resolved LID and PN. Propagate a + // backend failure (swallowing it would fall back to the raw user and re-introduce the + // false negative); a genuine absence (Ok(None), incl. a non-LID/PN input) falls back. + let mapping = self.client.get_lid_pn_entry(&bare).await?; let mut users: Vec<&str> = vec![bare.user.as_str()]; if let Some(entry) = mapping.as_ref() { users.push(entry.lid.as_str());