Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
61 changes: 53 additions & 8 deletions src/features/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,66 @@ 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. 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(|_| 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(),
})
}
}

/// 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
56 changes: 53 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,15 @@ 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 {
jid: lid.clone(),
action: BlocklistAction::Block,
pn_jid: Some(pn_jid.clone()),
}
}
}
/// A single blocklist entry from the response.
///
Expand Down Expand Up @@ -156,6 +167,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 +301,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