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)] pub 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}