Skip to main content

wacore/iq/
privacy.rs

1//! Privacy settings IQ specification.
2//!
3//! Fetches the user's privacy settings from the server.
4//!
5//! ## Wire Format
6//! ```xml
7//! <!-- Request -->
8//! <iq xmlns="privacy" type="get" to="s.whatsapp.net" id="...">
9//!   <privacy/>
10//! </iq>
11//!
12//! <!-- Response -->
13//! <iq from="s.whatsapp.net" id="..." type="result">
14//!   <privacy>
15//!     <category name="last" value="all"/>
16//!     <category name="online" value="all"/>
17//!     <category name="profile" value="contacts"/>
18//!     <category name="status" value="contacts"/>
19//!     <category name="groupadd" value="contacts"/>
20//!     ...
21//!   </privacy>
22//! </iq>
23//! ```
24//!
25//! Verified against WhatsApp Web JS (WAWebQueryPrivacy).
26
27use crate::StringEnum;
28use crate::iq::spec::IqSpec;
29use crate::request::InfoQuery;
30use wacore_binary::builder::NodeBuilder;
31use wacore_binary::jid::{Jid, SERVER_JID};
32use wacore_binary::node::{Node, NodeContent};
33
34/// IQ namespace for privacy settings.
35pub const PRIVACY_NAMESPACE: &str = "privacy";
36
37/// Privacy setting category name.
38#[derive(Debug, Clone, PartialEq, Eq, StringEnum)]
39pub enum PrivacyCategory {
40    /// Last seen visibility
41    #[str = "last"]
42    Last,
43    /// Online status visibility
44    #[str = "online"]
45    Online,
46    /// Profile photo visibility
47    #[str = "profile"]
48    Profile,
49    /// Status visibility
50    #[str = "status"]
51    Status,
52    /// Group add permissions
53    #[str = "groupadd"]
54    GroupAdd,
55    /// Read receipts
56    #[str = "readreceipts"]
57    ReadReceipts,
58    /// Other/unknown category
59    #[string_fallback]
60    Other(String),
61}
62
63/// Privacy setting value.
64#[derive(Debug, Clone, PartialEq, Eq, StringEnum)]
65pub enum PrivacyValue {
66    /// Visible to everyone
67    #[str = "all"]
68    All,
69    /// Visible only to contacts
70    #[str = "contacts"]
71    Contacts,
72    /// Not visible to anyone
73    #[str = "none"]
74    None,
75    /// Visible to contacts except specific list
76    #[str = "contact_blacklist"]
77    ContactBlacklist,
78    /// Match their settings (for online/last)
79    #[str = "match_last_seen"]
80    MatchLastSeen,
81    /// Other/unknown value
82    #[string_fallback]
83    Other(String),
84}
85
86/// A single privacy setting.
87#[derive(Debug, Clone)]
88pub struct PrivacySetting {
89    /// The category name (e.g., "last", "profile", etc.)
90    pub category: PrivacyCategory,
91    /// The privacy value (e.g., "all", "contacts", "none")
92    pub value: PrivacyValue,
93}
94
95/// Response from privacy settings query.
96#[derive(Debug, Clone, Default)]
97pub struct PrivacySettingsResponse {
98    /// The list of privacy settings.
99    pub settings: Vec<PrivacySetting>,
100}
101
102impl PrivacySettingsResponse {
103    /// Get a privacy setting by category.
104    pub fn get(&self, category: &PrivacyCategory) -> Option<&PrivacySetting> {
105        self.settings.iter().find(|s| &s.category == category)
106    }
107
108    /// Get the value for a category.
109    pub fn get_value(&self, category: &PrivacyCategory) -> Option<&PrivacyValue> {
110        self.get(category).map(|s| &s.value)
111    }
112}
113
114/// Fetches privacy settings from the server.
115#[derive(Debug, Clone, Default)]
116pub struct PrivacySettingsSpec;
117
118impl PrivacySettingsSpec {
119    /// Create a new privacy settings spec.
120    pub fn new() -> Self {
121        Self
122    }
123}
124
125impl IqSpec for PrivacySettingsSpec {
126    type Response = PrivacySettingsResponse;
127
128    fn build_iq(&self) -> InfoQuery<'static> {
129        InfoQuery::get(
130            PRIVACY_NAMESPACE,
131            Jid::new("", SERVER_JID),
132            Some(NodeContent::Nodes(vec![
133                NodeBuilder::new("privacy").build(),
134            ])),
135        )
136    }
137
138    fn parse_response(&self, response: &Node) -> Result<Self::Response, anyhow::Error> {
139        use crate::iq::node::{optional_attr, required_child};
140
141        let privacy_node = required_child(response, "privacy")?;
142
143        let mut settings = Vec::new();
144        for child in privacy_node.get_children_by_tag("category") {
145            let name = optional_attr(child, "name")
146                .ok_or_else(|| anyhow::anyhow!("missing name in category"))?;
147            let value = optional_attr(child, "value")
148                .ok_or_else(|| anyhow::anyhow!("missing value in category"))?;
149
150            settings.push(PrivacySetting {
151                category: PrivacyCategory::from(name.as_ref()),
152                value: PrivacyValue::from(value.as_ref()),
153            });
154        }
155
156        Ok(PrivacySettingsResponse { settings })
157    }
158}
159
160/// Set a single privacy setting.
161///
162/// ```xml
163/// <iq xmlns="privacy" type="set" to="s.whatsapp.net">
164///   <privacy>
165///     <category name="{category}" value="{value}"/>
166///   </privacy>
167/// </iq>
168/// ```
169#[derive(Debug, Clone)]
170pub struct SetPrivacySettingSpec {
171    pub category: String,
172    pub value: String,
173}
174
175impl SetPrivacySettingSpec {
176    pub fn new(category: impl Into<String>, value: impl Into<String>) -> Self {
177        Self {
178            category: category.into(),
179            value: value.into(),
180        }
181    }
182}
183
184impl IqSpec for SetPrivacySettingSpec {
185    type Response = ();
186
187    fn build_iq(&self) -> InfoQuery<'static> {
188        InfoQuery::set(
189            PRIVACY_NAMESPACE,
190            Jid::new("", SERVER_JID),
191            Some(NodeContent::Nodes(vec![
192                NodeBuilder::new("privacy")
193                    .children([NodeBuilder::new("category")
194                        .attr("name", &*self.category)
195                        .attr("value", &*self.value)
196                        .build()])
197                    .build(),
198            ])),
199        )
200    }
201
202    fn parse_response(&self, _response: &Node) -> Result<Self::Response, anyhow::Error> {
203        Ok(())
204    }
205}
206
207/// Set the default disappearing messages duration.
208///
209/// ```xml
210/// <iq xmlns="disappearing_mode" type="set" to="s.whatsapp.net">
211///   <disappearing_mode duration="{seconds}"/>
212/// </iq>
213/// ```
214#[derive(Debug, Clone)]
215pub struct SetDefaultDisappearingModeSpec {
216    pub duration: u32,
217}
218
219impl SetDefaultDisappearingModeSpec {
220    pub fn new(duration: u32) -> Self {
221        Self { duration }
222    }
223}
224
225impl IqSpec for SetDefaultDisappearingModeSpec {
226    type Response = ();
227
228    fn build_iq(&self) -> InfoQuery<'static> {
229        InfoQuery::set(
230            "disappearing_mode",
231            Jid::new("", SERVER_JID),
232            Some(NodeContent::Nodes(vec![
233                NodeBuilder::new("disappearing_mode")
234                    .attr("duration", self.duration.to_string())
235                    .build(),
236            ])),
237        )
238    }
239
240    fn parse_response(&self, _response: &Node) -> Result<Self::Response, anyhow::Error> {
241        Ok(())
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_privacy_settings_spec_build_iq() {
251        let spec = PrivacySettingsSpec::new();
252        let iq = spec.build_iq();
253
254        assert_eq!(iq.namespace, PRIVACY_NAMESPACE);
255        assert_eq!(iq.query_type, crate::request::InfoQueryType::Get);
256
257        if let Some(NodeContent::Nodes(nodes)) = &iq.content {
258            assert_eq!(nodes.len(), 1);
259            assert_eq!(nodes[0].tag, "privacy");
260        } else {
261            panic!("Expected NodeContent::Nodes");
262        }
263    }
264
265    #[test]
266    fn test_privacy_settings_spec_parse_response() {
267        let spec = PrivacySettingsSpec::new();
268        let response = NodeBuilder::new("iq")
269            .attr("type", "result")
270            .children([NodeBuilder::new("privacy")
271                .children([
272                    NodeBuilder::new("category")
273                        .attr("name", "last")
274                        .attr("value", "all")
275                        .build(),
276                    NodeBuilder::new("category")
277                        .attr("name", "profile")
278                        .attr("value", "contacts")
279                        .build(),
280                    NodeBuilder::new("category")
281                        .attr("name", "status")
282                        .attr("value", "none")
283                        .build(),
284                ])
285                .build()])
286            .build();
287
288        let result = spec.parse_response(&response).unwrap();
289        assert_eq!(result.settings.len(), 3);
290
291        assert_eq!(result.settings[0].category, PrivacyCategory::Last);
292        assert_eq!(result.settings[0].value, PrivacyValue::All);
293
294        assert_eq!(result.settings[1].category, PrivacyCategory::Profile);
295        assert_eq!(result.settings[1].value, PrivacyValue::Contacts);
296
297        assert_eq!(result.settings[2].category, PrivacyCategory::Status);
298        assert_eq!(result.settings[2].value, PrivacyValue::None);
299    }
300
301    #[test]
302    fn test_privacy_settings_response_get() {
303        let response = PrivacySettingsResponse {
304            settings: vec![
305                PrivacySetting {
306                    category: PrivacyCategory::Last,
307                    value: PrivacyValue::All,
308                },
309                PrivacySetting {
310                    category: PrivacyCategory::Profile,
311                    value: PrivacyValue::Contacts,
312                },
313            ],
314        };
315
316        assert_eq!(
317            response.get_value(&PrivacyCategory::Last),
318            Some(&PrivacyValue::All)
319        );
320        assert_eq!(
321            response.get_value(&PrivacyCategory::Profile),
322            Some(&PrivacyValue::Contacts)
323        );
324        assert_eq!(response.get_value(&PrivacyCategory::Online), None);
325    }
326
327    #[test]
328    fn test_privacy_category_from_str() {
329        assert_eq!(PrivacyCategory::from("last"), PrivacyCategory::Last);
330        assert_eq!(PrivacyCategory::from("online"), PrivacyCategory::Online);
331        assert_eq!(
332            PrivacyCategory::from("unknown"),
333            PrivacyCategory::Other("unknown".to_string())
334        );
335    }
336
337    #[test]
338    fn test_privacy_value_from_str() {
339        assert_eq!(PrivacyValue::from("all"), PrivacyValue::All);
340        assert_eq!(PrivacyValue::from("contacts"), PrivacyValue::Contacts);
341        assert_eq!(PrivacyValue::from("none"), PrivacyValue::None);
342        assert_eq!(
343            PrivacyValue::from("unknown"),
344            PrivacyValue::Other("unknown".to_string())
345        );
346    }
347}