Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions src/features/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,52 @@ impl<'a> Blocking<'a> {
Self { client }
}

/// Block a contact.
/// Resolve `bare` (LID or PN) into the `(lid, pn)` pair the server expects
/// on blocklist stanzas.
async fn resolve_lid_pn(&self, bare: Jid) -> Result<(Jid, Jid), IqError> {
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
/// 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: {}", jid);
self.client.execute(UpdateBlocklistSpec::block(jid)).await?;
debug!(target: "Blocking", "Successfully blocked contact: {}", jid);
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");
Ok(())
}

/// Unblock a contact.
/// 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);
debug!(target: "Blocking", "Unblocking contact");
let (lid_jid, _) = self.resolve_lid_pn(jid.to_non_ad()).await?;
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");
Ok(())
}

Expand Down
55 changes: 52 additions & 3 deletions wacore/src/iq/blocklist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,25 @@ pub enum BlocklistAction {
#[wire = "unblock"]
Unblock,
}
/// Request node for updating blocklist.
///
/// Wire format: `<item action="block|unblock" jid="...@s.whatsapp.net"/>`
/// 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 {
#[attr(name = "jid", jid)]
pub jid: Jid,
#[attr(name = "action", string_enum)]
pub action: BlocklistAction,
#[attr(name = "pn_jid", jid, optional)]
pub pn_jid: Option<Jid>,
}

impl BlocklistItemRequest {
pub fn new(jid: &Jid, action: BlocklistAction) -> Self {
Self {
jid: jid.clone(),
action,
pn_jid: None,
}
}

Expand All @@ -50,6 +52,14 @@ impl BlocklistItemRequest {
pub fn unblock(jid: &Jid) -> Self {
Self::new(jid, BlocklistAction::Unblock)
}

/// 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 {
pn_jid: Some(pn_jid.clone()),
..Self::new(lid, BlocklistAction::Block)
}
}
}
/// A single blocklist entry from the response.
///
Expand Down Expand Up @@ -156,6 +166,13 @@ impl UpdateBlocklistSpec {
request: BlocklistItemRequest::unblock(jid),
}
}

/// 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),
}
}
}

impl IqSpec for UpdateBlocklistSpec {
Expand Down Expand Up @@ -283,4 +300,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());
}
}
Loading