Skip to main content

whatsapp_rust/
spam_report.rs

1//! Spam reporting feature.
2//!
3//! Types and IQ specification are defined in `wacore::iq::spam_report`.
4
5use crate::client::Client;
6use crate::request::IqError;
7use wacore::iq::spam_report::SpamReportSpec;
8
9// Re-export types from wacore
10pub use wacore::types::{SpamFlow, SpamReportRequest, SpamReportResult, build_spam_list_node};
11
12impl Client {
13    /// Send a spam report to WhatsApp.
14    ///
15    /// This sends a `spam_list` IQ stanza to report one or more messages as spam.
16    ///
17    /// # Arguments
18    /// * `request` - The spam report request containing message details
19    ///
20    /// # Returns
21    /// * `Ok(SpamReportResult)` - If the report was successfully submitted
22    /// * `Err` - If there was an error sending or processing the report
23    ///
24    /// # Example
25    /// ```rust,ignore
26    /// let result = client.send_spam_report(SpamReportRequest {
27    ///     message_id: "MSG_ID".to_string(),
28    ///     message_timestamp: 1234567890,
29    ///     from_jid: Some(sender_jid),
30    ///     spam_flow: SpamFlow::MessageMenu,
31    ///     ..Default::default()
32    /// }).await?;
33    /// ```
34    pub async fn send_spam_report(
35        &self,
36        request: SpamReportRequest,
37    ) -> Result<SpamReportResult, IqError> {
38        use wacore::iq::abprops::web;
39
40        let mut spec = SpamReportSpec::new(request);
41
42        // Attach the reported contact's tctoken so the report is accepted for a
43        // privacy-restricted account, matching WA Web's OutSpamTCTokenMixin.
44        if self
45            .ab_props()
46            .is_enabled(web::ENABLE_SPAM_REPORT_IQ_WITH_PRIVACY_TOKEN)
47            .await
48            && let Some(reported) = spec.request.from_jid.clone()
49            && let Some(token) = self.lookup_tc_token_for_jid(&reported).await
50        {
51            spec = spec.with_tc_token(token);
52        }
53
54        self.execute(spec).await
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use wacore_binary::Jid;
62
63    #[test]
64    fn test_spam_flow_as_str() {
65        assert_eq!(SpamFlow::MessageMenu.as_str(), "MessageMenu");
66        assert_eq!(
67            SpamFlow::GroupSpamBannerReport.as_str(),
68            "GroupSpamBannerReport"
69        );
70        assert_eq!(SpamFlow::ContactInfo.as_str(), "ContactInfo");
71    }
72
73    #[test]
74    fn test_build_spam_list_node_basic() {
75        let request = SpamReportRequest {
76            message_id: "TEST123".to_string(),
77            message_timestamp: 1234567890,
78            spam_flow: SpamFlow::MessageMenu,
79            ..Default::default()
80        };
81
82        let node = build_spam_list_node(&request);
83
84        assert_eq!(node.tag, "spam_list");
85        assert!(
86            node.attrs
87                .get("spam_flow")
88                .is_some_and(|v| v == "MessageMenu")
89        );
90
91        let message = node
92            .get_optional_child_by_tag(&["message"])
93            .expect("spam_list node should have message child");
94        assert!(message.attrs.get("id").is_some_and(|v| v == "TEST123"));
95        assert!(message.attrs.get("t").is_some_and(|v| v == "1234567890"));
96    }
97
98    #[test]
99    fn test_build_spam_list_node_with_raw_message() {
100        let request = SpamReportRequest {
101            message_id: "TEST456".to_string(),
102            message_timestamp: 1234567890,
103            from_jid: Some(Jid::pn("5511999887766")),
104            spam_flow: SpamFlow::MessageMenu,
105            raw_message: Some(vec![0x01, 0x02, 0x03]),
106            media_type: Some("image".to_string()),
107            ..Default::default()
108        };
109
110        let node = build_spam_list_node(&request);
111        let message = node
112            .get_optional_child_by_tag(&["message"])
113            .expect("spam_list node should have message child");
114        let raw = message
115            .get_optional_child_by_tag(&["raw"])
116            .expect("message node should have raw child");
117
118        assert!(raw.attrs.get("v").is_some_and(|v| v == "3"));
119        assert!(raw.attrs.get("mediatype").is_some_and(|v| v == "image"));
120    }
121
122    #[test]
123    fn test_build_spam_list_node_group() {
124        let request = SpamReportRequest {
125            message_id: "TEST789".to_string(),
126            message_timestamp: 1234567890,
127            group_jid: Some(Jid::group("120363025918861132")),
128            group_subject: Some("Test Group".to_string()),
129            participant_jid: Some(Jid::pn("5511999887766")),
130            spam_flow: SpamFlow::GroupInfoReport,
131            ..Default::default()
132        };
133
134        let node = build_spam_list_node(&request);
135
136        assert!(
137            node.attrs
138                .get("spam_flow")
139                .is_some_and(|v| v == "GroupInfoReport")
140        );
141        assert!(
142            node.attrs
143                .get("jid")
144                .is_some_and(|v| v == "120363025918861132@g.us")
145        );
146        assert!(node.attrs.get("subject").is_some_and(|v| v == "Test Group"));
147    }
148}