xmpp_parsers/
presence.rs

1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
2// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
8use xso::{error::Error, AsOptionalXmlText, AsXml, AsXmlText, FromXml, FromXmlText};
9
10use crate::message::Lang;
11use crate::ns;
12use alloc::{borrow::Cow, collections::BTreeMap};
13use jid::Jid;
14use minidom::Element;
15
16/// Should be implemented on every known payload of a `<presence/>`.
17pub trait PresencePayload: TryFrom<Element> + Into<Element> {}
18
19/// Specifies the availability of an entity or resource.
20#[derive(Debug, Clone, PartialEq)]
21pub enum Show {
22    /// The entity or resource is temporarily away.
23    Away,
24
25    /// The entity or resource is actively interested in chatting.
26    Chat,
27
28    /// The entity or resource is busy (dnd = "Do Not Disturb").
29    Dnd,
30
31    /// The entity or resource is away for an extended period (xa = "eXtended
32    /// Away").
33    Xa,
34}
35
36impl FromXmlText for Show {
37    fn from_xml_text(s: String) -> Result<Show, Error> {
38        Ok(match s.as_ref() {
39            "away" => Show::Away,
40            "chat" => Show::Chat,
41            "dnd" => Show::Dnd,
42            "xa" => Show::Xa,
43
44            _ => return Err(Error::Other("Invalid value for show.")),
45        })
46    }
47}
48
49impl AsXmlText for Show {
50    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
51        Ok(Cow::Borrowed(match self {
52            Show::Away => "away",
53            Show::Chat => "chat",
54            Show::Dnd => "dnd",
55            Show::Xa => "xa",
56        }))
57    }
58}
59
60type Status = String;
61
62/// Priority of this presence.  This value can go from -128 to 127, defaults to
63/// 0, and any negative value will prevent this resource from receiving
64/// messages addressed to the bare JID.
65#[derive(FromXml, AsXml, Debug, Default, Clone, PartialEq)]
66#[xml(namespace = ns::DEFAULT_NS, name = "priority")]
67pub struct Priority(#[xml(text)] i8);
68
69/// Accepted values for the 'type' attribute of a presence.
70#[derive(Debug, Default, Clone, PartialEq)]
71pub enum Type {
72    /// This value is not an acceptable 'type' attribute, it is only used
73    /// internally to signal the absence of 'type'.
74    #[default]
75    None,
76
77    /// An error has occurred regarding processing of a previously sent
78    /// presence stanza; if the presence stanza is of type "error", it MUST
79    /// include an \<error/\> child element (refer to
80    /// [XMPP‑CORE](https://xmpp.org/rfcs/rfc6120.html)).
81    Error,
82
83    /// A request for an entity's current presence; SHOULD be generated only by
84    /// a server on behalf of a user.
85    Probe,
86
87    /// The sender wishes to subscribe to the recipient's presence.
88    Subscribe,
89
90    /// The sender has allowed the recipient to receive their presence.
91    Subscribed,
92
93    /// The sender is no longer available for communication.
94    Unavailable,
95
96    /// The sender is unsubscribing from the receiver's presence.
97    Unsubscribe,
98
99    /// The subscription request has been denied or a previously granted
100    /// subscription has been canceled.
101    Unsubscribed,
102}
103
104impl FromXmlText for Type {
105    fn from_xml_text(s: String) -> Result<Type, Error> {
106        Ok(match s.as_ref() {
107            "error" => Type::Error,
108            "probe" => Type::Probe,
109            "subscribe" => Type::Subscribe,
110            "subscribed" => Type::Subscribed,
111            "unavailable" => Type::Unavailable,
112            "unsubscribe" => Type::Unsubscribe,
113            "unsubscribed" => Type::Unsubscribed,
114
115            _ => {
116                return Err(Error::Other(
117                    "Invalid 'type' attribute on presence element.",
118                ));
119            }
120        })
121    }
122}
123
124impl AsOptionalXmlText for Type {
125    fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, Error> {
126        Ok(Some(Cow::Borrowed(match self {
127            Type::None => return Ok(None),
128
129            Type::Error => "error",
130            Type::Probe => "probe",
131            Type::Subscribe => "subscribe",
132            Type::Subscribed => "subscribed",
133            Type::Unavailable => "unavailable",
134            Type::Unsubscribe => "unsubscribe",
135            Type::Unsubscribed => "unsubscribed",
136        })))
137    }
138}
139
140/// The main structure representing the `<presence/>` stanza.
141#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
142#[xml(namespace = ns::DEFAULT_NS, name = "presence", discard(attribute = "xml:lang"))]
143pub struct Presence {
144    /// The sender of this presence.
145    #[xml(attribute(default))]
146    pub from: Option<Jid>,
147
148    /// The recipient of this presence.
149    #[xml(attribute(default))]
150    pub to: Option<Jid>,
151
152    /// The identifier, unique on this stream, of this stanza.
153    #[xml(attribute(default))]
154    pub id: Option<String>,
155
156    /// The type of this presence stanza.
157    #[xml(attribute(default, name = "type"))]
158    pub type_: Type,
159
160    /// The availability of the sender of this presence.
161    #[xml(extract(name = "show", default, fields(text(type_ = Show))))]
162    pub show: Option<Show>,
163
164    /// A localised list of statuses defined in this presence.
165    #[xml(extract(n = .., name = "status", fields(
166        attribute(type_ = Lang, name = "xml:lang", default),
167        text(type_ = String),
168    )))]
169    pub statuses: BTreeMap<Lang, Status>,
170
171    /// The sender’s resource priority, if negative it won’t receive messages
172    /// that haven’t been directed to it.
173    #[xml(child(default))]
174    pub priority: Priority,
175
176    /// A list of payloads contained in this presence.
177    #[xml(element(n = ..))]
178    pub payloads: Vec<Element>,
179}
180
181impl Presence {
182    /// Create a new presence of this type.
183    pub fn new(type_: Type) -> Presence {
184        Presence {
185            from: None,
186            to: None,
187            id: None,
188            type_,
189            show: None,
190            statuses: BTreeMap::new(),
191            priority: Priority(0i8),
192            payloads: vec![],
193        }
194    }
195
196    /// Create a presence without a type, which means available
197    pub fn available() -> Presence {
198        Self::new(Type::None)
199    }
200
201    /// Builds a presence of type Error
202    pub fn error() -> Presence {
203        Self::new(Type::Error)
204    }
205
206    /// Builds a presence of type Probe
207    pub fn probe() -> Presence {
208        Self::new(Type::Probe)
209    }
210
211    /// Builds a presence of type Subscribe
212    pub fn subscribe() -> Presence {
213        Self::new(Type::Subscribe)
214    }
215
216    /// Builds a presence of type Subscribed
217    pub fn subscribed() -> Presence {
218        Self::new(Type::Subscribed)
219    }
220
221    /// Builds a presence of type Unavailable
222    pub fn unavailable() -> Presence {
223        Self::new(Type::Unavailable)
224    }
225
226    /// Builds a presence of type Unsubscribe
227    pub fn unsubscribe() -> Presence {
228        Self::new(Type::Unsubscribe)
229    }
230
231    /// Set the emitter of this presence, this should only be useful for
232    /// servers and components, as clients can only send presences from their
233    /// own resource (which is implicit).
234    pub fn with_from<J: Into<Jid>>(mut self, from: J) -> Presence {
235        self.from = Some(from.into());
236        self
237    }
238
239    /// Set the recipient of this presence, this is only useful for directed
240    /// presences.
241    pub fn with_to<J: Into<Jid>>(mut self, to: J) -> Presence {
242        self.to = Some(to.into());
243        self
244    }
245
246    /// Set the identifier for this presence.
247    pub fn with_id(mut self, id: String) -> Presence {
248        self.id = Some(id);
249        self
250    }
251
252    /// Set the availability information of this presence.
253    pub fn with_show(mut self, show: Show) -> Presence {
254        self.show = Some(show);
255        self
256    }
257
258    /// Set the priority of this presence.
259    pub fn with_priority(mut self, priority: i8) -> Presence {
260        self.priority = Priority(priority);
261        self
262    }
263
264    /// Set a payload inside this presence.
265    pub fn with_payload<P: PresencePayload>(mut self, payload: P) -> Presence {
266        self.payloads.push(payload.into());
267        self
268    }
269
270    /// Set the payloads of this presence.
271    pub fn with_payloads(mut self, payloads: Vec<Element>) -> Presence {
272        self.payloads = payloads;
273        self
274    }
275
276    /// Set the availability information of this presence.
277    pub fn set_status<L, S>(&mut self, lang: L, status: S)
278    where
279        L: Into<Lang>,
280        S: Into<Status>,
281    {
282        self.statuses.insert(lang.into(), status.into());
283    }
284
285    /// Add a payload to this presence.
286    pub fn add_payload<P: PresencePayload>(&mut self, payload: P) {
287        self.payloads.push(payload.into());
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use jid::{BareJid, FullJid};
295    use xso::error::FromElementError;
296
297    #[cfg(target_pointer_width = "32")]
298    #[test]
299    fn test_size() {
300        assert_size!(Show, 1);
301        assert_size!(Type, 1);
302        assert_size!(Presence, 72);
303    }
304
305    #[cfg(target_pointer_width = "64")]
306    #[test]
307    fn test_size() {
308        assert_size!(Show, 1);
309        assert_size!(Type, 1);
310        assert_size!(Presence, 144);
311    }
312
313    #[test]
314    fn test_simple() {
315        #[cfg(not(feature = "component"))]
316        let elem: Element = "<presence xmlns='jabber:client'/>".parse().unwrap();
317        #[cfg(feature = "component")]
318        let elem: Element = "<presence xmlns='jabber:component:accept'/>"
319            .parse()
320            .unwrap();
321        let presence = Presence::try_from(elem).unwrap();
322        assert_eq!(presence.from, None);
323        assert_eq!(presence.to, None);
324        assert_eq!(presence.id, None);
325        assert_eq!(presence.type_, Type::None);
326        assert!(presence.payloads.is_empty());
327    }
328
329    // TODO: This test is currently ignored because it serializes <priority/>
330    // always, so let’s implement that in xso first.  The only downside to
331    // having it included is some more bytes on the wire, we can live with that
332    // for now.
333    #[test]
334    #[ignore]
335    fn test_serialise() {
336        #[cfg(not(feature = "component"))]
337        let elem: Element = "<presence xmlns='jabber:client' type='unavailable'/>"
338            .parse()
339            .unwrap();
340        #[cfg(feature = "component")]
341        let elem: Element = "<presence xmlns='jabber:component:accept' type='unavailable'/>"
342            .parse()
343            .unwrap();
344        let presence = Presence::new(Type::Unavailable);
345        let elem2 = presence.into();
346        assert_eq!(elem, elem2);
347    }
348
349    #[test]
350    fn test_show() {
351        #[cfg(not(feature = "component"))]
352        let elem: Element = "<presence xmlns='jabber:client'><show>chat</show></presence>"
353            .parse()
354            .unwrap();
355        #[cfg(feature = "component")]
356        let elem: Element =
357            "<presence xmlns='jabber:component:accept'><show>chat</show></presence>"
358                .parse()
359                .unwrap();
360        let presence = Presence::try_from(elem).unwrap();
361        assert_eq!(presence.payloads.len(), 0);
362        assert_eq!(presence.show, Some(Show::Chat));
363    }
364
365    #[test]
366    fn test_empty_show_value() {
367        #[cfg(not(feature = "component"))]
368        let elem: Element = "<presence xmlns='jabber:client'/>".parse().unwrap();
369        #[cfg(feature = "component")]
370        let elem: Element = "<presence xmlns='jabber:component:accept'/>"
371            .parse()
372            .unwrap();
373        let presence = Presence::try_from(elem).unwrap();
374        assert_eq!(presence.show, None);
375    }
376
377    #[test]
378    fn test_missing_show_value() {
379        #[cfg(not(feature = "component"))]
380        let elem: Element = "<presence xmlns='jabber:client'><show/></presence>"
381            .parse()
382            .unwrap();
383        #[cfg(feature = "component")]
384        let elem: Element = "<presence xmlns='jabber:component:accept'><show/></presence>"
385            .parse()
386            .unwrap();
387        let error = Presence::try_from(elem).unwrap_err();
388        let message = match error {
389            FromElementError::Invalid(Error::Other(string)) => string,
390            _ => panic!(),
391        };
392        assert_eq!(message, "Invalid value for show.");
393    }
394
395    #[test]
396    fn test_invalid_show() {
397        // "online" used to be a pretty common mistake.
398        #[cfg(not(feature = "component"))]
399        let elem: Element = "<presence xmlns='jabber:client'><show>online</show></presence>"
400            .parse()
401            .unwrap();
402        #[cfg(feature = "component")]
403        let elem: Element =
404            "<presence xmlns='jabber:component:accept'><show>online</show></presence>"
405                .parse()
406                .unwrap();
407        let error = Presence::try_from(elem).unwrap_err();
408        let message = match error {
409            FromElementError::Invalid(Error::Other(string)) => string,
410            _ => panic!(),
411        };
412        assert_eq!(message, "Invalid value for show.");
413    }
414
415    #[test]
416    fn test_empty_status() {
417        #[cfg(not(feature = "component"))]
418        let elem: Element = "<presence xmlns='jabber:client'><status/></presence>"
419            .parse()
420            .unwrap();
421        #[cfg(feature = "component")]
422        let elem: Element = "<presence xmlns='jabber:component:accept'><status/></presence>"
423            .parse()
424            .unwrap();
425        let presence = Presence::try_from(elem).unwrap();
426        assert_eq!(presence.payloads.len(), 0);
427        assert_eq!(presence.statuses.len(), 1);
428        assert_eq!(presence.statuses[""], "");
429    }
430
431    #[test]
432    fn test_status() {
433        #[cfg(not(feature = "component"))]
434        let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status></presence>"
435            .parse()
436            .unwrap();
437        #[cfg(feature = "component")]
438        let elem: Element =
439            "<presence xmlns='jabber:component:accept'><status>Here!</status></presence>"
440                .parse()
441                .unwrap();
442        let presence = Presence::try_from(elem).unwrap();
443        assert_eq!(presence.payloads.len(), 0);
444        assert_eq!(presence.statuses.len(), 1);
445        assert_eq!(presence.statuses[""], "Here!");
446    }
447
448    #[test]
449    fn test_multiple_statuses() {
450        #[cfg(not(feature = "component"))]
451        let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
452        #[cfg(feature = "component")]
453        let elem: Element = "<presence xmlns='jabber:component:accept'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
454        let presence = Presence::try_from(elem).unwrap();
455        assert_eq!(presence.payloads.len(), 0);
456        assert_eq!(presence.statuses.len(), 2);
457        assert_eq!(presence.statuses[""], "Here!");
458        assert_eq!(presence.statuses["fr"], "Là!");
459    }
460
461    // TODO: Enable that test again once xso supports rejecting multiple
462    // identical xml:lang versions.
463    #[test]
464    #[ignore]
465    fn test_invalid_multiple_statuses() {
466        #[cfg(not(feature = "component"))]
467        let elem: Element = "<presence xmlns='jabber:client'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
468        #[cfg(feature = "component")]
469        let elem: Element = "<presence xmlns='jabber:component:accept'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
470        let error = Presence::try_from(elem).unwrap_err();
471        let message = match error {
472            FromElementError::Invalid(Error::Other(string)) => string,
473            _ => panic!(),
474        };
475        assert_eq!(
476            message,
477            "Status element present twice for the same xml:lang."
478        );
479    }
480
481    #[test]
482    fn test_priority() {
483        #[cfg(not(feature = "component"))]
484        let elem: Element = "<presence xmlns='jabber:client'><priority>-1</priority></presence>"
485            .parse()
486            .unwrap();
487        #[cfg(feature = "component")]
488        let elem: Element =
489            "<presence xmlns='jabber:component:accept'><priority>-1</priority></presence>"
490                .parse()
491                .unwrap();
492        let presence = Presence::try_from(elem).unwrap();
493        assert_eq!(presence.payloads.len(), 0);
494        assert_eq!(presence.priority, Priority(-1i8));
495    }
496
497    #[test]
498    fn test_invalid_priority() {
499        #[cfg(not(feature = "component"))]
500        let elem: Element = "<presence xmlns='jabber:client'><priority>128</priority></presence>"
501            .parse()
502            .unwrap();
503        #[cfg(feature = "component")]
504        let elem: Element =
505            "<presence xmlns='jabber:component:accept'><priority>128</priority></presence>"
506                .parse()
507                .unwrap();
508        let error = Presence::try_from(elem).unwrap_err();
509        match error {
510            FromElementError::Invalid(Error::TextParseError(e))
511                if e.is::<core::num::ParseIntError>() =>
512            {
513                ()
514            }
515            _ => panic!(),
516        };
517    }
518
519    #[test]
520    fn test_unknown_child() {
521        #[cfg(not(feature = "component"))]
522        let elem: Element = "<presence xmlns='jabber:client'><test xmlns='invalid'/></presence>"
523            .parse()
524            .unwrap();
525        #[cfg(feature = "component")]
526        let elem: Element =
527            "<presence xmlns='jabber:component:accept'><test xmlns='invalid'/></presence>"
528                .parse()
529                .unwrap();
530        let presence = Presence::try_from(elem).unwrap();
531        let payload = &presence.payloads[0];
532        assert!(payload.is("test", "invalid"));
533    }
534
535    #[cfg(not(feature = "disable-validation"))]
536    #[test]
537    fn test_invalid_status_child() {
538        #[cfg(not(feature = "component"))]
539        let elem: Element = "<presence xmlns='jabber:client'><status><coucou/></status></presence>"
540            .parse()
541            .unwrap();
542        #[cfg(feature = "component")]
543        let elem: Element =
544            "<presence xmlns='jabber:component:accept'><status><coucou/></status></presence>"
545                .parse()
546                .unwrap();
547        let error = Presence::try_from(elem).unwrap_err();
548        let message = match error {
549            FromElementError::Invalid(Error::Other(string)) => string,
550            _ => panic!(),
551        };
552        assert_eq!(
553            message,
554            "Unknown child in extraction for field 'statuses' in Presence element."
555        );
556    }
557
558    #[cfg(not(feature = "disable-validation"))]
559    #[test]
560    fn test_invalid_attribute() {
561        #[cfg(not(feature = "component"))]
562        let elem: Element = "<presence xmlns='jabber:client'><status coucou=''/></presence>"
563            .parse()
564            .unwrap();
565        #[cfg(feature = "component")]
566        let elem: Element =
567            "<presence xmlns='jabber:component:accept'><status coucou=''/></presence>"
568                .parse()
569                .unwrap();
570        let error = Presence::try_from(elem).unwrap_err();
571        let message = match error {
572            FromElementError::Invalid(Error::Other(string)) => string,
573            _ => panic!(),
574        };
575        assert_eq!(
576            message,
577            "Unknown attribute in extraction for field 'statuses' in Presence element."
578        );
579    }
580
581    #[test]
582    fn test_serialise_status() {
583        let status = Status::from("Hello world!");
584        let mut presence = Presence::new(Type::Unavailable);
585        presence.statuses.insert(Lang::from(""), status);
586        let elem: Element = presence.into();
587        assert!(elem.is("presence", ns::DEFAULT_NS));
588        assert!(elem.children().next().unwrap().is("status", ns::DEFAULT_NS));
589    }
590
591    #[test]
592    fn test_serialise_priority() {
593        let presence = Presence::new(Type::None).with_priority(42);
594        let elem: Element = presence.into();
595        assert!(elem.is("presence", ns::DEFAULT_NS));
596        let priority = elem.children().next().unwrap();
597        assert!(priority.is("priority", ns::DEFAULT_NS));
598        assert_eq!(priority.text(), "42");
599    }
600
601    #[test]
602    fn presence_with_to() {
603        let presence = Presence::new(Type::None);
604        let elem: Element = presence.into();
605        assert_eq!(elem.attr("to"), None);
606
607        let presence = Presence::new(Type::None).with_to(Jid::new("localhost").unwrap());
608        let elem: Element = presence.into();
609        assert_eq!(elem.attr("to"), Some("localhost"));
610
611        let presence = Presence::new(Type::None).with_to(BareJid::new("localhost").unwrap());
612        let elem: Element = presence.into();
613        assert_eq!(elem.attr("to"), Some("localhost"));
614
615        let presence =
616            Presence::new(Type::None).with_to(Jid::new("test@localhost/coucou").unwrap());
617        let elem: Element = presence.into();
618        assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
619
620        let presence =
621            Presence::new(Type::None).with_to(FullJid::new("test@localhost/coucou").unwrap());
622        let elem: Element = presence.into();
623        assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
624    }
625
626    #[test]
627    fn test_xml_lang() {
628        #[cfg(not(feature = "component"))]
629        let elem: Element = "<presence xmlns='jabber:client' xml:lang='fr'/>"
630            .parse()
631            .unwrap();
632        #[cfg(feature = "component")]
633        let elem: Element = "<presence xmlns='jabber:component:accept' xml:lang='fr'/>"
634            .parse()
635            .unwrap();
636        let presence = Presence::try_from(elem).unwrap();
637        assert_eq!(presence.from, None);
638        assert_eq!(presence.to, None);
639        assert_eq!(presence.id, None);
640        assert_eq!(presence.type_, Type::None);
641        assert!(presence.payloads.is_empty());
642    }
643}