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")]
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        lang(type_ = 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    use xso::exports::rxml;
297
298    #[cfg(target_pointer_width = "32")]
299    #[test]
300    fn test_size() {
301        assert_size!(Show, 1);
302        assert_size!(Type, 1);
303        assert_size!(Presence, 72);
304    }
305
306    #[cfg(target_pointer_width = "64")]
307    #[test]
308    fn test_size() {
309        assert_size!(Show, 1);
310        assert_size!(Type, 1);
311        assert_size!(Presence, 144);
312    }
313
314    #[test]
315    fn test_simple() {
316        #[cfg(not(feature = "component"))]
317        let elem: Element = "<presence xmlns='jabber:client'/>".parse().unwrap();
318        #[cfg(feature = "component")]
319        let elem: Element = "<presence xmlns='jabber:component:accept'/>"
320            .parse()
321            .unwrap();
322        let presence = Presence::try_from(elem).unwrap();
323        assert_eq!(presence.from, None);
324        assert_eq!(presence.to, None);
325        assert_eq!(presence.id, None);
326        assert_eq!(presence.type_, Type::None);
327        assert!(presence.payloads.is_empty());
328    }
329
330    // TODO: This test is currently ignored because it serializes <priority/>
331    // always, so let’s implement that in xso first.  The only downside to
332    // having it included is some more bytes on the wire, we can live with that
333    // for now.
334    #[test]
335    #[ignore]
336    fn test_serialise() {
337        #[cfg(not(feature = "component"))]
338        let elem: Element = "<presence xmlns='jabber:client' type='unavailable'/>"
339            .parse()
340            .unwrap();
341        #[cfg(feature = "component")]
342        let elem: Element = "<presence xmlns='jabber:component:accept' type='unavailable'/>"
343            .parse()
344            .unwrap();
345        let presence = Presence::new(Type::Unavailable);
346        let elem2 = presence.into();
347        assert_eq!(elem, elem2);
348    }
349
350    #[test]
351    fn test_show() {
352        #[cfg(not(feature = "component"))]
353        let elem: Element = "<presence xmlns='jabber:client'><show>chat</show></presence>"
354            .parse()
355            .unwrap();
356        #[cfg(feature = "component")]
357        let elem: Element =
358            "<presence xmlns='jabber:component:accept'><show>chat</show></presence>"
359                .parse()
360                .unwrap();
361        let presence = Presence::try_from(elem).unwrap();
362        assert_eq!(presence.payloads.len(), 0);
363        assert_eq!(presence.show, Some(Show::Chat));
364    }
365
366    #[test]
367    fn test_empty_show_value() {
368        #[cfg(not(feature = "component"))]
369        let elem: Element = "<presence xmlns='jabber:client'/>".parse().unwrap();
370        #[cfg(feature = "component")]
371        let elem: Element = "<presence xmlns='jabber:component:accept'/>"
372            .parse()
373            .unwrap();
374        let presence = Presence::try_from(elem).unwrap();
375        assert_eq!(presence.show, None);
376    }
377
378    #[test]
379    fn test_missing_show_value() {
380        #[cfg(not(feature = "component"))]
381        let elem: Element = "<presence xmlns='jabber:client'><show/></presence>"
382            .parse()
383            .unwrap();
384        #[cfg(feature = "component")]
385        let elem: Element = "<presence xmlns='jabber:component:accept'><show/></presence>"
386            .parse()
387            .unwrap();
388        let error = Presence::try_from(elem).unwrap_err();
389        let message = match error {
390            FromElementError::Invalid(Error::Other(string)) => string,
391            _ => panic!(),
392        };
393        assert_eq!(message, "Invalid value for show.");
394    }
395
396    #[test]
397    fn test_invalid_show() {
398        // "online" used to be a pretty common mistake.
399        #[cfg(not(feature = "component"))]
400        let elem: Element = "<presence xmlns='jabber:client'><show>online</show></presence>"
401            .parse()
402            .unwrap();
403        #[cfg(feature = "component")]
404        let elem: Element =
405            "<presence xmlns='jabber:component:accept'><show>online</show></presence>"
406                .parse()
407                .unwrap();
408        let error = Presence::try_from(elem).unwrap_err();
409        let message = match error {
410            FromElementError::Invalid(Error::Other(string)) => string,
411            _ => panic!(),
412        };
413        assert_eq!(message, "Invalid value for show.");
414    }
415
416    #[test]
417    fn test_empty_status() {
418        #[cfg(not(feature = "component"))]
419        let elem: Element = "<presence xmlns='jabber:client'><status/></presence>"
420            .parse()
421            .unwrap();
422        #[cfg(feature = "component")]
423        let elem: Element = "<presence xmlns='jabber:component:accept'><status/></presence>"
424            .parse()
425            .unwrap();
426        let presence = Presence::try_from(elem).unwrap();
427        assert_eq!(presence.payloads.len(), 0);
428        assert_eq!(presence.statuses.len(), 1);
429        assert_eq!(presence.statuses[""], "");
430    }
431
432    #[test]
433    fn test_status() {
434        #[cfg(not(feature = "component"))]
435        let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status></presence>"
436            .parse()
437            .unwrap();
438        #[cfg(feature = "component")]
439        let elem: Element =
440            "<presence xmlns='jabber:component:accept'><status>Here!</status></presence>"
441                .parse()
442                .unwrap();
443        let presence = Presence::try_from(elem).unwrap();
444        assert_eq!(presence.payloads.len(), 0);
445        assert_eq!(presence.statuses.len(), 1);
446        assert_eq!(presence.statuses[""], "Here!");
447    }
448
449    #[test]
450    fn test_multiple_statuses() {
451        #[cfg(not(feature = "component"))]
452        let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
453        #[cfg(feature = "component")]
454        let elem: Element = "<presence xmlns='jabber:component:accept'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
455        let presence = Presence::try_from(elem).unwrap();
456        assert_eq!(presence.payloads.len(), 0);
457        assert_eq!(presence.statuses.len(), 2);
458        assert_eq!(presence.statuses[""], "Here!");
459        assert_eq!(presence.statuses["fr"], "Là!");
460    }
461
462    // TODO: Enable that test again once xso supports rejecting multiple
463    // identical xml:lang versions.
464    #[test]
465    #[ignore]
466    fn test_invalid_multiple_statuses() {
467        #[cfg(not(feature = "component"))]
468        let elem: Element = "<presence xmlns='jabber:client'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
469        #[cfg(feature = "component")]
470        let elem: Element = "<presence xmlns='jabber:component:accept'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
471        let error = Presence::try_from(elem).unwrap_err();
472        let message = match error {
473            FromElementError::Invalid(Error::Other(string)) => string,
474            _ => panic!(),
475        };
476        assert_eq!(
477            message,
478            "Status element present twice for the same xml:lang."
479        );
480    }
481
482    #[test]
483    fn test_priority() {
484        #[cfg(not(feature = "component"))]
485        let elem: Element = "<presence xmlns='jabber:client'><priority>-1</priority></presence>"
486            .parse()
487            .unwrap();
488        #[cfg(feature = "component")]
489        let elem: Element =
490            "<presence xmlns='jabber:component:accept'><priority>-1</priority></presence>"
491                .parse()
492                .unwrap();
493        let presence = Presence::try_from(elem).unwrap();
494        assert_eq!(presence.payloads.len(), 0);
495        assert_eq!(presence.priority, Priority(-1i8));
496    }
497
498    #[test]
499    fn test_invalid_priority() {
500        #[cfg(not(feature = "component"))]
501        let elem: Element = "<presence xmlns='jabber:client'><priority>128</priority></presence>"
502            .parse()
503            .unwrap();
504        #[cfg(feature = "component")]
505        let elem: Element =
506            "<presence xmlns='jabber:component:accept'><priority>128</priority></presence>"
507                .parse()
508                .unwrap();
509        let error = Presence::try_from(elem).unwrap_err();
510        match error {
511            FromElementError::Invalid(Error::TextParseError(e))
512                if e.is::<core::num::ParseIntError>() =>
513            {
514                ()
515            }
516            _ => panic!(),
517        };
518    }
519
520    #[test]
521    fn test_unknown_child() {
522        #[cfg(not(feature = "component"))]
523        let elem: Element = "<presence xmlns='jabber:client'><test xmlns='invalid'/></presence>"
524            .parse()
525            .unwrap();
526        #[cfg(feature = "component")]
527        let elem: Element =
528            "<presence xmlns='jabber:component:accept'><test xmlns='invalid'/></presence>"
529                .parse()
530                .unwrap();
531        let presence = Presence::try_from(elem).unwrap();
532        let payload = &presence.payloads[0];
533        assert!(payload.is("test", "invalid"));
534    }
535
536    #[cfg(not(feature = "disable-validation"))]
537    #[test]
538    fn test_invalid_status_child() {
539        #[cfg(not(feature = "component"))]
540        let elem: Element = "<presence xmlns='jabber:client'><status><coucou/></status></presence>"
541            .parse()
542            .unwrap();
543        #[cfg(feature = "component")]
544        let elem: Element =
545            "<presence xmlns='jabber:component:accept'><status><coucou/></status></presence>"
546                .parse()
547                .unwrap();
548        let error = Presence::try_from(elem).unwrap_err();
549        let message = match error {
550            FromElementError::Invalid(Error::Other(string)) => string,
551            _ => panic!(),
552        };
553        assert_eq!(
554            message,
555            "Unknown child in extraction for field 'statuses' in Presence element."
556        );
557    }
558
559    #[cfg(not(feature = "disable-validation"))]
560    #[test]
561    fn test_invalid_attribute() {
562        #[cfg(not(feature = "component"))]
563        let elem: Element = "<presence xmlns='jabber:client'><status coucou=''/></presence>"
564            .parse()
565            .unwrap();
566        #[cfg(feature = "component")]
567        let elem: Element =
568            "<presence xmlns='jabber:component:accept'><status coucou=''/></presence>"
569                .parse()
570                .unwrap();
571        let error = Presence::try_from(elem).unwrap_err();
572        let message = match error {
573            FromElementError::Invalid(Error::Other(string)) => string,
574            _ => panic!(),
575        };
576        assert_eq!(
577            message,
578            "Unknown attribute in extraction for field 'statuses' in Presence element."
579        );
580    }
581
582    #[test]
583    fn test_serialise_status() {
584        let status = Status::from("Hello world!");
585        let mut presence = Presence::new(Type::Unavailable);
586        presence.statuses.insert(Lang::from(""), status);
587        let elem: Element = presence.into();
588        assert!(elem.is("presence", ns::DEFAULT_NS));
589        assert!(elem.children().next().unwrap().is("status", ns::DEFAULT_NS));
590    }
591
592    #[test]
593    fn test_serialise_priority() {
594        let presence = Presence::new(Type::None).with_priority(42);
595        let elem: Element = presence.into();
596        assert!(elem.is("presence", ns::DEFAULT_NS));
597        let priority = elem.children().next().unwrap();
598        assert!(priority.is("priority", ns::DEFAULT_NS));
599        assert_eq!(priority.text(), "42");
600    }
601
602    #[test]
603    fn presence_with_to() {
604        let presence = Presence::new(Type::None);
605        let elem: Element = presence.into();
606        assert_eq!(elem.attr(rxml::xml_ncname!("to")), None);
607
608        let presence = Presence::new(Type::None).with_to(Jid::new("localhost").unwrap());
609        let elem: Element = presence.into();
610        assert_eq!(elem.attr(rxml::xml_ncname!("to")), Some("localhost"));
611
612        let presence = Presence::new(Type::None).with_to(BareJid::new("localhost").unwrap());
613        let elem: Element = presence.into();
614        assert_eq!(elem.attr(rxml::xml_ncname!("to")), Some("localhost"));
615
616        let presence =
617            Presence::new(Type::None).with_to(Jid::new("test@localhost/coucou").unwrap());
618        let elem: Element = presence.into();
619        assert_eq!(
620            elem.attr(rxml::xml_ncname!("to")),
621            Some("test@localhost/coucou")
622        );
623
624        let presence =
625            Presence::new(Type::None).with_to(FullJid::new("test@localhost/coucou").unwrap());
626        let elem: Element = presence.into();
627        assert_eq!(
628            elem.attr(rxml::xml_ncname!("to")),
629            Some("test@localhost/coucou")
630        );
631    }
632
633    #[test]
634    fn test_xml_lang() {
635        #[cfg(not(feature = "component"))]
636        let elem: Element = "<presence xmlns='jabber:client' xml:lang='fr'/>"
637            .parse()
638            .unwrap();
639        #[cfg(feature = "component")]
640        let elem: Element = "<presence xmlns='jabber:component:accept' xml:lang='fr'/>"
641            .parse()
642            .unwrap();
643        let presence = Presence::try_from(elem).unwrap();
644        assert_eq!(presence.from, None);
645        assert_eq!(presence.to, None);
646        assert_eq!(presence.id, None);
647        assert_eq!(presence.type_, Type::None);
648        assert!(presence.payloads.is_empty());
649    }
650}