use crate::WireEnum;
use crate::iq::node::optional_child;
use crate::iq::spec::IqSpec;
use crate::protocol::ProtocolNode;
use crate::request::InfoQuery;
use anyhow::Result;
use log::warn;
use wacore_binary::builder::NodeBuilder;
use wacore_binary::{Jid, Server};
use wacore_binary::{Node, NodeContent, NodeRef};
pub const BLOCKLIST_IQ_NAMESPACE: &str = "blocklist";
#[derive(Debug, Clone, Copy, PartialEq, Eq, WireEnum)]
pub enum BlocklistAction {
#[wire = "block"]
Block,
#[wire = "unblock"]
Unblock,
}
#[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,
}
}
pub fn block(jid: &Jid) -> Self {
Self::new(jid, BlocklistAction::Block)
}
pub fn unblock(jid: &Jid) -> Self {
Self::new(jid, BlocklistAction::Unblock)
}
pub fn block_with_pn(lid: &Jid, pn_jid: &Jid) -> Self {
Self {
pn_jid: Some(pn_jid.clone()),
..Self::new(lid, BlocklistAction::Block)
}
}
}
#[derive(Debug, Clone, crate::ProtocolNode)]
#[protocol(tag = "item")]
pub struct BlocklistEntry {
#[attr(name = "jid", jid)]
pub jid: Jid,
#[attr(name = "t", u64)]
pub timestamp: Option<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct BlocklistResponse {
pub entries: Vec<BlocklistEntry>,
}
impl ProtocolNode for BlocklistResponse {
fn tag(&self) -> &'static str {
"list"
}
fn into_node(self) -> Node {
let children: Vec<Node> = self.entries.into_iter().map(|e| e.into_node()).collect();
NodeBuilder::new("list").children(children).build()
}
fn try_from_node_ref(node: &NodeRef<'_>) -> Result<Self> {
let entries = if let Some(list) = optional_child(node, "list") {
list.get_children_by_tag("item")
} else {
node.get_children_by_tag("item")
}
.filter_map(|item| match BlocklistEntry::try_from_node_ref(item) {
Ok(entry) => Some(entry),
Err(e) => {
warn!(target: "blocklist", "Failed to parse blocklist entry: {e}");
None
}
})
.collect();
Ok(Self { entries })
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct GetBlocklistSpec;
impl IqSpec for GetBlocklistSpec {
type Response = Vec<BlocklistEntry>;
fn build_iq(&self) -> InfoQuery<'static> {
InfoQuery::get(BLOCKLIST_IQ_NAMESPACE, Jid::new("", Server::Pn), None)
}
fn parse_response(&self, response: &NodeRef<'_>) -> Result<Self::Response> {
let entries = if let Some(list) = response.get_optional_child("list") {
list.get_children_by_tag("item")
} else {
response.get_children_by_tag("item")
}
.filter_map(|item| match BlocklistEntry::try_from_node_ref(item) {
Ok(entry) => Some(entry),
Err(e) => {
warn!(target: "blocklist", "Failed to parse blocklist entry: {e}");
None
}
})
.collect();
Ok(entries)
}
}
#[derive(Debug, Clone)]
pub struct UpdateBlocklistSpec {
request: BlocklistItemRequest,
}
impl UpdateBlocklistSpec {
pub fn new(jid: &Jid, action: BlocklistAction) -> Self {
Self {
request: BlocklistItemRequest::new(jid, action),
}
}
pub fn block(jid: &Jid) -> Self {
Self {
request: BlocklistItemRequest::block(jid),
}
}
pub fn unblock(jid: &Jid) -> Self {
Self {
request: BlocklistItemRequest::unblock(jid),
}
}
pub fn block_with_pn(lid: &Jid, pn_jid: &Jid) -> Self {
Self {
request: BlocklistItemRequest::block_with_pn(lid, pn_jid),
}
}
}
impl IqSpec for UpdateBlocklistSpec {
type Response = ();
fn build_iq(&self) -> InfoQuery<'static> {
InfoQuery::set(
BLOCKLIST_IQ_NAMESPACE,
Jid::new("", Server::Pn),
Some(NodeContent::Nodes(vec![self.request.clone().into_node()])),
)
}
fn parse_response(&self, _response: &NodeRef<'_>) -> Result<Self::Response> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blocklist_action_string_enum() {
assert_eq!(BlocklistAction::Block.as_str(), "block");
assert_eq!(BlocklistAction::Unblock.as_str(), "unblock");
assert_eq!(
BlocklistAction::try_from("block").unwrap(),
BlocklistAction::Block
);
assert_eq!(
BlocklistAction::try_from("unblock").unwrap(),
BlocklistAction::Unblock
);
}
#[test]
fn test_blocklist_item_request_into_node() {
let jid: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
let request = BlocklistItemRequest::block(&jid);
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 == "1234567890@s.whatsapp.net")
);
}
#[test]
fn test_blocklist_entry_into_node() {
let jid: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
let entry = BlocklistEntry {
jid: jid.clone(),
timestamp: Some(1234567890),
};
let node = entry.into_node();
assert_eq!(node.tag, "item");
assert!(
node.attrs
.get("jid")
.is_some_and(|v| v == "1234567890@s.whatsapp.net")
);
assert!(node.attrs.get("t").is_some_and(|v| v == "1234567890"));
}
#[test]
fn test_blocklist_entry_try_from_node() {
let node = NodeBuilder::new("item")
.attr("jid", "1234567890@s.whatsapp.net")
.attr("t", "1234567890")
.build();
let entry = BlocklistEntry::try_from_node(&node).unwrap();
assert_eq!(entry.jid.user, "1234567890");
assert_eq!(entry.timestamp, Some(1234567890));
}
#[test]
fn test_blocklist_response_with_list_wrapper() {
let list_node = NodeBuilder::new("list")
.children([
NodeBuilder::new("item")
.attr("jid", "111@s.whatsapp.net")
.build(),
NodeBuilder::new("item")
.attr("jid", "222@s.whatsapp.net")
.build(),
])
.build();
let response_node = NodeBuilder::new("response").children([list_node]).build();
let response = BlocklistResponse::try_from_node(&response_node).unwrap();
assert_eq!(response.entries.len(), 2);
assert_eq!(response.entries[0].jid.user, "111");
assert_eq!(response.entries[1].jid.user, "222");
}
#[test]
fn test_blocklist_response_direct_items() {
let response_node = NodeBuilder::new("response")
.children([
NodeBuilder::new("item")
.attr("jid", "111@s.whatsapp.net")
.build(),
NodeBuilder::new("item")
.attr("jid", "222@s.whatsapp.net")
.build(),
])
.build();
let response = BlocklistResponse::try_from_node(&response_node).unwrap();
assert_eq!(response.entries.len(), 2);
}
#[test]
fn test_update_blocklist_spec_convenience_methods() {
let jid: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
let block_spec = UpdateBlocklistSpec::block(&jid);
assert_eq!(block_spec.request.action, BlocklistAction::Block);
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());
}
}