whatsapp_rust/features/
blocking.rs1use crate::client::Client;
7use crate::request::IqError;
8use log::debug;
9use thiserror::Error;
10pub use wacore::iq::blocklist::BlocklistEntry;
11use wacore::iq::blocklist::{GetBlocklistSpec, UpdateBlocklistSpec};
12use wacore_binary::Jid;
13
14#[derive(Debug, Error)]
16#[non_exhaustive]
17pub enum BlockingError {
18 #[error("{0}")]
20 Iq(#[from] IqError),
21 #[error("invalid blocklist target: {0}")]
24 InvalidJid(String),
25 #[error("{0}")]
27 Internal(#[from] anyhow::Error),
28}
29
30pub struct Blocking<'a> {
32 client: &'a Client,
33}
34
35impl<'a> Blocking<'a> {
36 pub(crate) fn new(client: &'a Client) -> Self {
37 Self { client }
38 }
39
40 async fn resolve_lid_pn(&self, bare: Jid) -> Result<(Jid, Jid), BlockingError> {
43 if !(bare.is_lid() || bare.is_pn()) {
44 return Err(BlockingError::InvalidJid(
45 "jid is neither PN nor LID".into(),
46 ));
47 }
48 let entry = self.client.get_lid_pn_entry(&bare).await?.ok_or_else(|| {
49 BlockingError::InvalidJid("no LID↔PN mapping for provided jid".into())
50 })?;
51 Ok(if bare.is_lid() {
52 (bare, Jid::pn(&*entry.phone_number))
53 } else {
54 (Jid::lid(&*entry.lid), bare)
55 })
56 }
57
58 pub async fn block(&self, jid: &Jid) -> Result<(), BlockingError> {
61 debug!(target: "Blocking", "Blocking contact");
62 let (lid_jid, pn_jid) = self.resolve_lid_pn(jid.to_non_ad()).await?;
63 self.client
64 .execute(UpdateBlocklistSpec::block_with_pn(&lid_jid, &pn_jid))
65 .await?;
66 debug!(target: "Blocking", "Successfully blocked contact");
67 Ok(())
68 }
69
70 pub async fn unblock(&self, jid: &Jid) -> Result<(), BlockingError> {
73 debug!(target: "Blocking", "Unblocking contact");
74 let bare = jid.to_non_ad();
77 let lid_jid = if bare.is_lid() {
78 bare
79 } else {
80 self.resolve_lid_pn(bare).await?.0
81 };
82 self.client
83 .execute(UpdateBlocklistSpec::unblock(&lid_jid))
84 .await?;
85 debug!(target: "Blocking", "Successfully unblocked contact");
86 Ok(())
87 }
88
89 pub async fn get_blocklist(&self) -> Result<Vec<BlocklistEntry>, BlockingError> {
91 debug!(target: "Blocking", "Fetching blocklist...");
92 let entries = self.client.execute(GetBlocklistSpec).await?;
93 debug!(target: "Blocking", "Fetched {} blocked contacts", entries.len());
94 Ok(entries)
95 }
96
97 pub async fn is_blocked(&self, jid: &Jid) -> Result<bool, BlockingError> {
102 let blocklist = self.get_blocklist().await?;
103 let bare = jid.to_non_ad();
104
105 let mapping = self.client.get_lid_pn_entry(&bare).await?;
111 let mut users: Vec<&str> = vec![bare.user.as_str()];
112 if let Some(entry) = mapping.as_ref() {
113 users.push(&*entry.lid);
114 users.push(&*entry.phone_number);
115 }
116
117 Ok(blocklist_contains(&blocklist, &users))
118 }
119}
120
121fn blocklist_contains(blocklist: &[BlocklistEntry], candidate_users: &[&str]) -> bool {
127 blocklist
128 .iter()
129 .any(|e| candidate_users.contains(&e.jid.user.as_str()))
130}
131
132impl Client {
133 pub fn blocking(&self) -> Blocking<'_> {
135 Blocking::new(self)
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn lid_entry(user: &str) -> BlocklistEntry {
144 BlocklistEntry {
145 jid: Jid::lid(user.to_string()),
146 timestamp: None,
147 }
148 }
149
150 #[test]
151 fn pn_query_matches_lid_keyed_block_only_when_resolved() {
152 let blocklist = vec![lid_entry("100000012345678")];
155
156 assert!(
157 blocklist_contains(&blocklist, &["559980000001", "100000012345678"]),
158 "resolved PN->LID candidate matches the LID-keyed block"
159 );
160 assert!(
161 !blocklist_contains(&blocklist, &["559980000001"]),
162 "raw PN alone misses the LID-keyed block (the false negative)"
163 );
164 assert!(
165 blocklist_contains(&blocklist, &["100000012345678"]),
166 "a LID query matches directly"
167 );
168 assert!(
169 !blocklist_contains(&blocklist, &["559981111111", "100000099999999"]),
170 "an unrelated contact is not blocked"
171 );
172 }
173}