Skip to main content

xmpp_parsers/
roster.rs

1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use xso::{AsXml, FromXml};
8
9use jid::BareJid;
10
11use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
12use crate::ns;
13
14generate_elem_id!(
15    /// Represents a group a contact is part of.
16    Group,
17    "group",
18    ROSTER
19);
20
21generate_attribute!(
22    /// The state of your mutual subscription with a contact.
23    Subscription, "subscription", {
24        /// The user doesn’t have any subscription to this contact’s presence,
25        /// and neither does this contact.
26        None => "none",
27
28        /// Only this contact has a subscription with you, not the opposite.
29        From => "from",
30
31        /// Only you have a subscription with this contact, not the opposite.
32        To => "to",
33
34        /// Both you and your contact are subscribed to each other’s presence.
35        Both => "both",
36
37        /// In a roster set, this asks the server to remove this contact item
38        /// from your roster.
39        Remove => "remove",
40    }, Default = None
41);
42
43generate_attribute!(
44    /// The sub-state of subscription with a contact.
45    Ask, "ask", (
46        /// Pending sub-state of the 'none' subscription state.
47        Subscribe => "subscribe"
48    )
49);
50
51/// Contact from the user’s contact list.
52#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
53#[xml(namespace = ns::ROSTER, name = "item")]
54pub struct Item {
55    /// JID of this contact.
56    #[xml(attribute)]
57    pub jid: BareJid,
58
59    /// Name of this contact.
60    #[xml(attribute(default))]
61    pub name: Option<String>,
62
63    /// Subscription status of this contact.
64    #[xml(attribute(default))]
65    pub subscription: Subscription,
66
67    /// Indicates “Pending Out” sub-states for this contact.
68    #[xml(attribute(default))]
69    pub ask: Ask,
70
71    /// Groups this contact is part of.
72    #[xml(child(n = ..))]
73    pub groups: Vec<Group>,
74
75    /// Subscription pre-approval
76    #[xml(attribute(default))]
77    pub approved: Option<bool>,
78}
79
80/// The contact list of the user.
81#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
82#[xml(namespace = ns::ROSTER, name = "query")]
83pub struct Roster {
84    /// Version of the contact list.
85    ///
86    /// This is an opaque string that should only be sent back to the server on
87    /// a new connection, if this client is storing the contact list between
88    /// connections.
89    #[xml(attribute(default))]
90    pub ver: Option<String>,
91
92    /// List of the contacts of the user.
93    #[xml(child(n = ..))]
94    pub items: Vec<Item>,
95}
96
97impl IqGetPayload for Roster {}
98impl IqSetPayload for Roster {}
99impl IqResultPayload for Roster {}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use core::str::FromStr;
105    use minidom::Element;
106    use xso::error::{Error, FromElementError};
107
108    #[cfg(target_pointer_width = "32")]
109    #[test]
110    fn test_size() {
111        assert_size!(Group, 12);
112        assert_size!(Subscription, 1);
113        assert_size!(Ask, 1);
114        assert_size!(Item, 44);
115        assert_size!(Roster, 24);
116    }
117
118    #[cfg(target_pointer_width = "64")]
119    #[test]
120    fn test_size() {
121        assert_size!(Group, 24);
122        assert_size!(Subscription, 1);
123        assert_size!(Ask, 1);
124        assert_size!(Item, 88);
125        assert_size!(Roster, 48);
126    }
127
128    #[test]
129    fn test_get() {
130        let elem: Element = "<query xmlns='jabber:iq:roster'/>".parse().unwrap();
131        let roster = Roster::try_from(elem).unwrap();
132        assert!(roster.ver.is_none());
133        assert!(roster.items.is_empty());
134    }
135
136    #[test]
137    fn test_result() {
138        let elem: Element = "<query xmlns='jabber:iq:roster' ver='ver7'><item jid='nurse@example.com'/><item jid='romeo@example.net'/></query>".parse().unwrap();
139        let roster = Roster::try_from(elem).unwrap();
140        assert_eq!(roster.ver, Some(String::from("ver7")));
141        assert_eq!(roster.items.len(), 2);
142
143        let elem: Element = "<query xmlns='jabber:iq:roster' ver='ver9'/>"
144            .parse()
145            .unwrap();
146        let roster = Roster::try_from(elem).unwrap();
147        assert_eq!(roster.ver, Some(String::from("ver9")));
148        assert!(roster.items.is_empty());
149
150        let elem: Element = r#"<query xmlns='jabber:iq:roster' ver='ver11'>
151  <item jid='romeo@example.net'
152        name='Romeo'
153        subscription='both'>
154    <group>Friends</group>
155  </item>
156  <item jid='mercutio@example.com'
157        name='Mercutio'
158        subscription='from'/>
159  <item jid='benvolio@example.net'
160        name='Benvolio'
161        subscription='both'/>
162  <item jid='contact@example.org'
163        subscription='none'
164        ask='subscribe'
165        approved='true'
166        name='MyContact'>
167      <group>MyBuddies</group>
168  </item>
169</query>
170"#
171        .parse()
172        .unwrap();
173        let roster = Roster::try_from(elem).unwrap();
174        assert_eq!(roster.ver, Some(String::from("ver11")));
175        assert_eq!(roster.items.len(), 4);
176        assert_eq!(
177            roster.items[0].jid,
178            BareJid::new("romeo@example.net").unwrap()
179        );
180        assert_eq!(roster.items[0].name, Some(String::from("Romeo")));
181        assert_eq!(roster.items[0].subscription, Subscription::Both);
182        assert_eq!(roster.items[0].ask, Ask::None);
183        assert_eq!(roster.items[0].approved, None);
184        assert_eq!(
185            roster.items[0].groups,
186            vec!(Group::from_str("Friends").unwrap())
187        );
188
189        assert_eq!(
190            roster.items[3].jid,
191            BareJid::new("contact@example.org").unwrap()
192        );
193        assert_eq!(roster.items[3].name, Some(String::from("MyContact")));
194        assert_eq!(roster.items[3].subscription, Subscription::None);
195        assert_eq!(roster.items[3].ask, Ask::Subscribe);
196        assert_eq!(roster.items[3].approved, Some(true));
197        assert_eq!(
198            roster.items[3].groups,
199            vec!(Group::from_str("MyBuddies").unwrap())
200        );
201    }
202
203    #[test]
204    fn test_multiple_groups() {
205        let elem: Element = "<query xmlns='jabber:iq:roster'><item jid='test@example.org'><group>A</group><group>B</group></item></query>"
206        .parse()
207        .unwrap();
208        let elem1 = elem.clone();
209        let roster = Roster::try_from(elem).unwrap();
210        assert!(roster.ver.is_none());
211        assert_eq!(roster.items.len(), 1);
212        assert_eq!(
213            roster.items[0].jid,
214            BareJid::new("test@example.org").unwrap()
215        );
216        assert_eq!(roster.items[0].name, None);
217        assert_eq!(roster.items[0].groups.len(), 2);
218        assert_eq!(roster.items[0].groups[0], Group::from_str("A").unwrap());
219        assert_eq!(roster.items[0].groups[1], Group::from_str("B").unwrap());
220        let elem2 = roster.into();
221        assert_eq!(elem1, elem2);
222    }
223
224    #[test]
225    fn test_set() {
226        let elem: Element =
227            "<query xmlns='jabber:iq:roster'><item jid='nurse@example.com'/></query>"
228                .parse()
229                .unwrap();
230        let roster = Roster::try_from(elem).unwrap();
231        assert!(roster.ver.is_none());
232        assert_eq!(roster.items.len(), 1);
233
234        let elem: Element = r#"<query xmlns='jabber:iq:roster'>
235  <item jid='nurse@example.com'
236        name='Nurse'>
237    <group>Servants</group>
238  </item>
239</query>"#
240            .parse()
241            .unwrap();
242        let roster = Roster::try_from(elem).unwrap();
243        assert!(roster.ver.is_none());
244        assert_eq!(roster.items.len(), 1);
245        assert_eq!(
246            roster.items[0].jid,
247            BareJid::new("nurse@example.com").unwrap()
248        );
249        assert_eq!(roster.items[0].name, Some(String::from("Nurse")));
250        assert_eq!(roster.items[0].groups.len(), 1);
251        assert_eq!(
252            roster.items[0].groups[0],
253            Group::from_str("Servants").unwrap()
254        );
255
256        let elem: Element = r#"<query xmlns='jabber:iq:roster'>
257  <item jid='nurse@example.com'
258        subscription='remove'/>
259</query>"#
260            .parse()
261            .unwrap();
262        let roster = Roster::try_from(elem).unwrap();
263        assert!(roster.ver.is_none());
264        assert_eq!(roster.items.len(), 1);
265        assert_eq!(
266            roster.items[0].jid,
267            BareJid::new("nurse@example.com").unwrap()
268        );
269        assert!(roster.items[0].name.is_none());
270        assert!(roster.items[0].groups.is_empty());
271        assert_eq!(roster.items[0].subscription, Subscription::Remove);
272    }
273
274    #[cfg(feature = "pedantic")]
275    #[test]
276    fn test_invalid() {
277        let elem: Element = "<query xmlns='jabber:iq:roster'><coucou/></query>"
278            .parse()
279            .unwrap();
280        let error = Roster::try_from(elem).unwrap_err();
281        let message = match error {
282            FromElementError::Invalid(Error::Other(string)) => string,
283            _ => panic!(),
284        };
285        assert_eq!(message, "Unknown child in Roster element.");
286
287        let elem: Element = "<query xmlns='jabber:iq:roster' coucou=''/>"
288            .parse()
289            .unwrap();
290        let error = Roster::try_from(elem).unwrap_err();
291        let message = match error {
292            FromElementError::Invalid(Error::Other(string)) => string,
293            _ => panic!(),
294        };
295        assert_eq!(message, "Unknown attribute in Roster element.");
296    }
297
298    #[test]
299    fn test_item_missing_jid() {
300        let elem: Element = "<query xmlns='jabber:iq:roster'><item/></query>"
301            .parse()
302            .unwrap();
303        let error = Roster::try_from(elem).unwrap_err();
304        let message = match error {
305            FromElementError::Invalid(Error::Other(string)) => string,
306            _ => panic!(),
307        };
308        assert_eq!(
309            message,
310            "Required attribute field 'jid' on Item element missing."
311        );
312    }
313
314    #[test]
315    fn test_item_invalid_jid() {
316        let elem: Element = "<query xmlns='jabber:iq:roster'><item jid=''/></query>"
317            .parse()
318            .unwrap();
319        let error = Roster::try_from(elem).unwrap_err();
320        assert_eq!(
321            format!("{error}"),
322            "text parse error: domain doesn’t pass idna validation"
323        );
324    }
325
326    #[test]
327    #[cfg_attr(not(feature = "pedantic"), should_panic = "Result::unwrap_err")]
328    fn test_item_unknown_child() {
329        let elem: Element =
330            "<query xmlns='jabber:iq:roster'><item jid='coucou'><coucou/></item></query>"
331                .parse()
332                .unwrap();
333        let error = Roster::try_from(elem).unwrap_err();
334        let message = match error {
335            FromElementError::Invalid(Error::Other(string)) => string,
336            _ => panic!(),
337        };
338        assert_eq!(message, "Unknown child in Item element.");
339    }
340}