1// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
2// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
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 crate::ns;
9use crate::presence::PresencePayload;
10use crate::util::error::Error;
11use crate::Element;
12
13use jid::FullJid;
14use std::convert::TryFrom;
15
16generate_attribute_enum!(
17/// Lists all of the possible status codes used in MUC presences.
18Status, "status", MUC_USER, "code", {
19 /// 100: Inform user that any occupant is allowed to see the user's full JID
20 NonAnonymousRoom => 100,
21
22 /// 101: Inform user that his or her affiliation changed while not in the room
23 AffiliationChange => 101,
24
25 /// 102: Inform occupants that room now shows unavailable members
26 ConfigShowsUnavailableMembers => 102,
27
28 /// 103: Inform occupants that room now does not show unavailable members
29 ConfigHidesUnavailableMembers => 103,
30
31 /// 104: Inform occupants that a non-privacy-related room configuration change has occurred
32 ConfigNonPrivacyRelated => 104,
33
34 /// 110: Inform user that presence refers to itself
35 SelfPresence => 110,
36
37 /// 170: Inform occupants that room logging is now enabled
38 ConfigRoomLoggingEnabled => 170,
39
40 /// 171: Inform occupants that room logging is now disabled
41 ConfigRoomLoggingDisabled => 171,
42
43 /// 172: Inform occupants that the room is now non-anonymous
44 ConfigRoomNonAnonymous => 172,
45
46 /// 173: Inform occupants that the room is now semi-anonymous
47 ConfigRoomSemiAnonymous => 173,
48
49 /// 201: Inform user that a new room has been created
50 RoomHasBeenCreated => 201,
51
52 /// 210: Inform user that service has assigned or modified occupant's roomnick
53 AssignedNick => 210,
54
55 /// 301: Inform user that they have been banned from the room
56 Banned => 301,
57
58 /// 303: Inform all occupants of new room nickname
59 NewNick => 303,
60
61 /// 307: Inform user that they have been kicked from the room
62 Kicked => 307,
63
64 /// 321: Inform user that they are being removed from the room
65 /// because of an affiliation change
66 RemovalFromRoom => 321,
67
68 /// 322: Inform user that they are being removed from the room
69 /// because the room has been changed to members-only and the
70 /// user is not a member
71 ConfigMembersOnly => 322,
72
73 /// 332: Inform user that they are being removed from the room
74 /// because the MUC service is being shut down
75 ServiceShutdown => 332,
76
77 /// 333: Inform user that they are being removed from the room for technical reasons
78 ServiceErrorKick => 333,
79});
80
81/// Optional \<actor/\> element used in \<item/\> elements inside presence stanzas of type
82/// "unavailable" that are sent to users who are kick or banned, as well as within IQs for tracking
83/// purposes. -- CHANGELOG 0.17 (2002-10-23)
84///
85/// Possesses a 'jid' and a 'nick' attribute, so that an action can be attributed either to a real
86/// JID or to a roomnick. -- CHANGELOG 1.25 (2012-02-08)
87#[derive(Debug, Clone, PartialEq)]
88pub enum Actor {
89 /// The full JID associated with this user.
90 Jid(FullJid),
91
92 /// The nickname of this user.
93 Nick(String),
94}
95
96impl TryFrom<Element> for Actor {
97 type Error = Error;
98
99 fn try_from(elem: Element) -> Result<Actor, Error> {
100 check_self!(elem, "actor", MUC_USER);
101 check_no_unknown_attributes!(elem, "actor", ["jid", "nick"]);
102 check_no_children!(elem, "actor");
103 let jid: Option<FullJid> = get_attr!(elem, "jid", Option);
104 let nick = get_attr!(elem, "nick", Option);
105
106 match (jid, nick) {
107 (Some(_), Some(_)) | (None, None) => Err(Error::ParseError(
108 "Either 'jid' or 'nick' attribute is required.",
109 )),
110 (Some(jid), _) => Ok(Actor::Jid(jid)),
111 (_, Some(nick)) => Ok(Actor::Nick(nick)),
112 }
113 }
114}
115
116impl From<Actor> for Element {
117 fn from(actor: Actor) -> Element {
118 let elem = Element::builder("actor", ns::MUC_USER);
119
120 (match actor {
121 Actor::Jid(jid) => elem.attr("jid", jid),
122 Actor::Nick(nick) => elem.attr("nick", nick),
123 })
124 .build()
125 }
126}
127
128generate_element!(
129 /// Used to continue a one-to-one discussion in a room, with more than one
130 /// participant.
131 Continue, "continue", MUC_USER,
132 attributes: [
133 /// The thread to continue in this room.
134 thread: Option<String> = "thread",
135 ]
136);
137
138generate_elem_id!(
139 /// A reason for inviting, declining, etc. a request.
140 Reason,
141 "reason",
142 MUC_USER
143);
144
145generate_attribute!(
146 /// The affiliation of an entity with a room, which isn’t tied to its
147 /// presence in it.
148 Affiliation, "affiliation", {
149 /// The user who created the room, or who got appointed by its creator
150 /// to be their equal.
151 Owner => "owner",
152
153 /// A user who has been empowered by an owner to do administrative
154 /// operations.
155 Admin => "admin",
156
157 /// A user who is whitelisted to speak in moderated rooms, or to join a
158 /// member-only room.
159 Member => "member",
160
161 /// A user who has been banned from this room.
162 Outcast => "outcast",
163
164 /// A normal participant.
165 None => "none",
166 }, Default = None
167);
168
169generate_attribute!(
170 /// The current role of an entity in a room, it can be changed by an owner
171 /// or an administrator but will be lost once they leave the room.
172 Role, "role", {
173 /// This user can kick other participants, as well as grant and revoke
174 /// them voice.
175 Moderator => "moderator",
176
177 /// A user who can speak in this room.
178 Participant => "participant",
179
180 /// A user who cannot speak in this room, and must request voice before
181 /// doing so.
182 Visitor => "visitor",
183
184 /// A user who is absent from the room.
185 None => "none",
186 }, Default = None
187);
188
189generate_element!(
190 /// An item representing a user in a room.
191 Item, "item", MUC_USER, attributes: [
192 /// The affiliation of this user with the room.
193 affiliation: Required<Affiliation> = "affiliation",
194
195 /// The real JID of this user, if you are allowed to see it.
196 jid: Option<FullJid> = "jid",
197
198 /// The current nickname of this user.
199 nick: Option<String> = "nick",
200
201 /// The current role of this user.
202 role: Required<Role> = "role",
203 ], children: [
204 /// The actor affected by this item.
205 actor: Option<Actor> = ("actor", MUC_USER) => Actor,
206
207 /// Whether this continues a one-to-one discussion.
208 continue_: Option<Continue> = ("continue", MUC_USER) => Continue,
209
210 /// A reason for this item.
211 reason: Option<Reason> = ("reason", MUC_USER) => Reason
212 ]
213);
214
215impl Item {
216 /// Creates a new item with the given affiliation and role.
217 pub fn new(affiliation: Affiliation, role: Role) -> Item {
218 Item {
219 affiliation,
220 role,
221 jid: None,
222 nick: None,
223 actor: None,
224 continue_: None,
225 reason: None,
226 }
227 }
228
229 /// Set a jid for this Item
230 pub fn with_jid(mut self, jid: FullJid) -> Item {
231 self.jid = Some(jid);
232 self
233 }
234
235 /// Set a nick for this Item
236 pub fn with_nick<S: Into<String>>(mut self, nick: S) -> Item {
237 self.nick = Some(nick.into());
238 self
239 }
240
241 /// Set an actor for this Item
242 pub fn with_actor(mut self, actor: Actor) -> Item {
243 self.actor = Some(actor);
244 self
245 }
246
247 /// Set a continue value for this Item
248 pub fn with_continue<S: Into<String>>(mut self, continue_: S) -> Item {
249 self.continue_ = Some(Continue {
250 thread: Some(continue_.into()),
251 });
252 self
253 }
254
255 /// Set a reason for this Item
256 pub fn with_reason<S: Into<String>>(mut self, reason: S) -> Item {
257 self.reason = Some(Reason(reason.into()));
258 self
259 }
260}
261
262generate_element!(
263 /// The main muc#user element.
264 MucUser, "x", MUC_USER, children: [
265 /// List of statuses applying to this item.
266 status: Vec<Status> = ("status", MUC_USER) => Status,
267
268 /// List of items.
269 items: Vec<Item> = ("item", MUC_USER) => Item
270 ]
271);
272
273impl PresencePayload for MucUser {}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::presence::{Presence, Type as PresenceType};
279
280 #[test]
281 fn test_simple() {
282 let elem: Element = "<x xmlns='http://jabber.org/protocol/muc#user'/>"
283 .parse()
284 .unwrap();
285 MucUser::try_from(elem).unwrap();
286 }
287
288 #[test]
289 fn statuses_and_items() {
290 let elem: Element = "<x xmlns='http://jabber.org/protocol/muc#user'>
291 <status code='101'/>
292 <status code='102'/>
293 <item affiliation='member' role='moderator'/>
294 </x>"
295 .parse()
296 .unwrap();
297 let muc_user = MucUser::try_from(elem).unwrap();
298 assert_eq!(muc_user.status.len(), 2);
299 assert_eq!(muc_user.status[0], Status::AffiliationChange);
300 assert_eq!(muc_user.status[1], Status::ConfigShowsUnavailableMembers);
301 assert_eq!(muc_user.items.len(), 1);
302 assert_eq!(muc_user.items[0].affiliation, Affiliation::Member);
303 assert_eq!(muc_user.items[0].role, Role::Moderator);
304 }
305
306 #[test]
307 fn test_invalid_child() {
308 let elem: Element = "<x xmlns='http://jabber.org/protocol/muc#user'>
309 <coucou/>
310 </x>"
311 .parse()
312 .unwrap();
313 let error = MucUser::try_from(elem).unwrap_err();
314 let message = match error {
315 Error::ParseError(string) => string,
316 _ => panic!(),
317 };
318 assert_eq!(message, "Unknown child in x element.");
319 }
320
321 #[test]
322 fn test_serialise() {
323 let elem: Element = "<x xmlns='http://jabber.org/protocol/muc#user'/>"
324 .parse()
325 .unwrap();
326 let muc = MucUser {
327 status: vec![],
328 items: vec![],
329 };
330 let elem2 = muc.into();
331 assert_eq!(elem, elem2);
332 }
333
334 #[cfg(not(feature = "disable-validation"))]
335 #[test]
336 fn test_invalid_attribute() {
337 let elem: Element = "<x xmlns='http://jabber.org/protocol/muc#user' coucou=''/>"
338 .parse()
339 .unwrap();
340 let error = MucUser::try_from(elem).unwrap_err();
341 let message = match error {
342 Error::ParseError(string) => string,
343 _ => panic!(),
344 };
345 assert_eq!(message, "Unknown attribute in x element.");
346 }
347
348 #[test]
349 fn test_status_simple() {
350 let elem: Element = "<status xmlns='http://jabber.org/protocol/muc#user' code='110'/>"
351 .parse()
352 .unwrap();
353 Status::try_from(elem).unwrap();
354 }
355
356 #[test]
357 fn test_status_invalid() {
358 let elem: Element = "<status xmlns='http://jabber.org/protocol/muc#user'/>"
359 .parse()
360 .unwrap();
361 let error = Status::try_from(elem).unwrap_err();
362 let message = match error {
363 Error::ParseError(string) => string,
364 _ => panic!(),
365 };
366 assert_eq!(message, "Required attribute 'code' missing.");
367 }
368
369 #[cfg(not(feature = "disable-validation"))]
370 #[test]
371 fn test_status_invalid_child() {
372 let elem: Element = "<status xmlns='http://jabber.org/protocol/muc#user' code='110'>
373 <foo/>
374 </status>"
375 .parse()
376 .unwrap();
377 let error = Status::try_from(elem).unwrap_err();
378 let message = match error {
379 Error::ParseError(string) => string,
380 _ => panic!(),
381 };
382 assert_eq!(message, "Unknown child in status element.");
383 }
384
385 #[test]
386 fn test_status_simple_code() {
387 let elem: Element = "<status xmlns='http://jabber.org/protocol/muc#user' code='307'/>"
388 .parse()
389 .unwrap();
390 let status = Status::try_from(elem).unwrap();
391 assert_eq!(status, Status::Kicked);
392 }
393
394 #[test]
395 fn test_status_invalid_code() {
396 let elem: Element = "<status xmlns='http://jabber.org/protocol/muc#user' code='666'/>"
397 .parse()
398 .unwrap();
399 let error = Status::try_from(elem).unwrap_err();
400 let message = match error {
401 Error::ParseError(string) => string,
402 _ => panic!(),
403 };
404 assert_eq!(message, "Invalid status code value.");
405 }
406
407 #[test]
408 fn test_status_invalid_code2() {
409 let elem: Element = "<status xmlns='http://jabber.org/protocol/muc#user' code='coucou'/>"
410 .parse()
411 .unwrap();
412 let error = Status::try_from(elem).unwrap_err();
413 let error = match error {
414 Error::ParseIntError(error) => error,
415 _ => panic!(),
416 };
417 assert_eq!(error.to_string(), "invalid digit found in string");
418 }
419
420 #[test]
421 fn test_actor_required_attributes() {
422 let elem: Element = "<actor xmlns='http://jabber.org/protocol/muc#user'/>"
423 .parse()
424 .unwrap();
425 let error = Actor::try_from(elem).unwrap_err();
426 let message = match error {
427 Error::ParseError(string) => string,
428 _ => panic!(),
429 };
430 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
431 }
432
433 #[test]
434 fn test_actor_required_attributes2() {
435 let elem: Element = "<actor xmlns='http://jabber.org/protocol/muc#user'
436 jid='foo@bar/baz'
437 nick='baz'/>"
438 .parse()
439 .unwrap();
440 let error = Actor::try_from(elem).unwrap_err();
441 let message = match error {
442 Error::ParseError(string) => string,
443 _ => panic!(),
444 };
445 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
446 }
447
448 #[test]
449 fn test_actor_jid() {
450 let elem: Element = "<actor xmlns='http://jabber.org/protocol/muc#user'
451 jid='foo@bar/baz'/>"
452 .parse()
453 .unwrap();
454 let actor = Actor::try_from(elem).unwrap();
455 let jid = match actor {
456 Actor::Jid(jid) => jid,
457 _ => panic!(),
458 };
459 assert_eq!(jid, "foo@bar/baz".parse::<FullJid>().unwrap());
460 }
461
462 #[test]
463 fn test_actor_nick() {
464 let elem: Element = "<actor xmlns='http://jabber.org/protocol/muc#user' nick='baz'/>"
465 .parse()
466 .unwrap();
467 let actor = Actor::try_from(elem).unwrap();
468 let nick = match actor {
469 Actor::Nick(nick) => nick,
470 _ => panic!(),
471 };
472 assert_eq!(nick, "baz".to_owned());
473 }
474
475 #[test]
476 fn test_continue_simple() {
477 let elem: Element = "<continue xmlns='http://jabber.org/protocol/muc#user'/>"
478 .parse()
479 .unwrap();
480 Continue::try_from(elem).unwrap();
481 }
482
483 #[test]
484 fn test_continue_thread_attribute() {
485 let elem: Element = "<continue xmlns='http://jabber.org/protocol/muc#user'
486 thread='foo'/>"
487 .parse()
488 .unwrap();
489 let continue_ = Continue::try_from(elem).unwrap();
490 assert_eq!(continue_.thread, Some("foo".to_owned()));
491 }
492
493 #[test]
494 fn test_continue_invalid() {
495 let elem: Element = "<continue xmlns='http://jabber.org/protocol/muc#user'>
496 <foobar/>
497 </continue>"
498 .parse()
499 .unwrap();
500 let continue_ = Continue::try_from(elem).unwrap_err();
501 let message = match continue_ {
502 Error::ParseError(string) => string,
503 _ => panic!(),
504 };
505 assert_eq!(message, "Unknown child in continue element.".to_owned());
506 }
507
508 #[test]
509 fn test_reason_simple() {
510 let elem: Element = "<reason xmlns='http://jabber.org/protocol/muc#user'>Reason</reason>"
511 .parse()
512 .unwrap();
513 let elem2 = elem.clone();
514 let reason = Reason::try_from(elem).unwrap();
515 assert_eq!(reason.0, "Reason".to_owned());
516
517 let elem3 = reason.into();
518 assert_eq!(elem2, elem3);
519 }
520
521 #[cfg(not(feature = "disable-validation"))]
522 #[test]
523 fn test_reason_invalid_attribute() {
524 let elem: Element = "<reason xmlns='http://jabber.org/protocol/muc#user' foo='bar'/>"
525 .parse()
526 .unwrap();
527 let error = Reason::try_from(elem).unwrap_err();
528 let message = match error {
529 Error::ParseError(string) => string,
530 _ => panic!(),
531 };
532 assert_eq!(message, "Unknown attribute in reason element.".to_owned());
533 }
534
535 #[cfg(not(feature = "disable-validation"))]
536 #[test]
537 fn test_reason_invalid() {
538 let elem: Element = "<reason xmlns='http://jabber.org/protocol/muc#user'>
539 <foobar/>
540 </reason>"
541 .parse()
542 .unwrap();
543 let error = Reason::try_from(elem).unwrap_err();
544 let message = match error {
545 Error::ParseError(string) => string,
546 _ => panic!(),
547 };
548 assert_eq!(message, "Unknown child in reason element.".to_owned());
549 }
550
551 #[cfg(not(feature = "disable-validation"))]
552 #[test]
553 fn test_item_invalid_attr() {
554 let elem: Element = "<item xmlns='http://jabber.org/protocol/muc#user'
555 foo='bar'/>"
556 .parse()
557 .unwrap();
558 let error = Item::try_from(elem).unwrap_err();
559 let message = match error {
560 Error::ParseError(string) => string,
561 _ => panic!(),
562 };
563 assert_eq!(message, "Unknown attribute in item element.".to_owned());
564 }
565
566 #[test]
567 fn test_item_affiliation_role_attr() {
568 let elem: Element = "<item xmlns='http://jabber.org/protocol/muc#user'
569 affiliation='member'
570 role='moderator'/>"
571 .parse()
572 .unwrap();
573 Item::try_from(elem).unwrap();
574 }
575
576 #[test]
577 fn test_item_affiliation_role_invalid_attr() {
578 let elem: Element = "<item xmlns='http://jabber.org/protocol/muc#user'
579 affiliation='member'/>"
580 .parse()
581 .unwrap();
582 let error = Item::try_from(elem).unwrap_err();
583 let message = match error {
584 Error::ParseError(string) => string,
585 _ => panic!(),
586 };
587 assert_eq!(message, "Required attribute 'role' missing.".to_owned());
588 }
589
590 #[test]
591 fn test_item_nick_attr() {
592 let elem: Element = "<item xmlns='http://jabber.org/protocol/muc#user'
593 affiliation='member'
594 role='moderator'
595 nick='foobar'/>"
596 .parse()
597 .unwrap();
598 let item = Item::try_from(elem).unwrap();
599 match item {
600 Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())),
601 }
602 }
603
604 #[test]
605 fn test_item_affiliation_role_invalid_attr2() {
606 let elem: Element = "<item xmlns='http://jabber.org/protocol/muc#user'
607 role='moderator'/>"
608 .parse()
609 .unwrap();
610 let error = Item::try_from(elem).unwrap_err();
611 let message = match error {
612 Error::ParseError(string) => string,
613 _ => panic!(),
614 };
615 assert_eq!(
616 message,
617 "Required attribute 'affiliation' missing.".to_owned()
618 );
619 }
620
621 #[test]
622 fn test_item_role_actor_child() {
623 let elem: Element = "<item xmlns='http://jabber.org/protocol/muc#user'
624 affiliation='member'
625 role='moderator'>
626 <actor nick='foobar'/>
627 </item>"
628 .parse()
629 .unwrap();
630 let item = Item::try_from(elem).unwrap();
631 match item {
632 Item { actor, .. } => assert_eq!(actor, Some(Actor::Nick("foobar".to_owned()))),
633 }
634 }
635
636 #[test]
637 fn test_item_role_continue_child() {
638 let elem: Element = "<item xmlns='http://jabber.org/protocol/muc#user'
639 affiliation='member'
640 role='moderator'>
641 <continue thread='foobar'/>
642 </item>"
643 .parse()
644 .unwrap();
645 let item = Item::try_from(elem).unwrap();
646 let continue_1 = Continue {
647 thread: Some("foobar".to_owned()),
648 };
649 match item {
650 Item {
651 continue_: Some(continue_2),
652 ..
653 } => assert_eq!(continue_2.thread, continue_1.thread),
654 _ => panic!(),
655 }
656 }
657
658 #[test]
659 fn test_item_role_reason_child() {
660 let elem: Element = "<item xmlns='http://jabber.org/protocol/muc#user'
661 affiliation='member'
662 role='moderator'>
663 <reason>foobar</reason>
664 </item>"
665 .parse()
666 .unwrap();
667 let item = Item::try_from(elem).unwrap();
668 match item {
669 Item { reason, .. } => assert_eq!(reason, Some(Reason("foobar".to_owned()))),
670 }
671 }
672
673 #[test]
674 fn test_serialize_item() {
675 let reference: Element = "<item xmlns='http://jabber.org/protocol/muc#user' affiliation='member' role='moderator'><actor nick='foobar'/><continue thread='foobar'/><reason>foobar</reason></item>"
676 .parse()
677 .unwrap();
678
679 let elem: Element = "<actor xmlns='http://jabber.org/protocol/muc#user' nick='foobar'/>"
680 .parse()
681 .unwrap();
682 let actor = Actor::try_from(elem).unwrap();
683
684 let elem: Element =
685 "<continue xmlns='http://jabber.org/protocol/muc#user' thread='foobar'/>"
686 .parse()
687 .unwrap();
688 let continue_ = Continue::try_from(elem).unwrap();
689
690 let elem: Element = "<reason xmlns='http://jabber.org/protocol/muc#user'>foobar</reason>"
691 .parse()
692 .unwrap();
693 let reason = Reason::try_from(elem).unwrap();
694
695 let item = Item {
696 affiliation: Affiliation::Member,
697 role: Role::Moderator,
698 jid: None,
699 nick: None,
700 actor: Some(actor),
701 reason: Some(reason),
702 continue_: Some(continue_),
703 };
704
705 let serialized: Element = item.into();
706 assert_eq!(serialized, reference);
707 }
708
709 #[test]
710 fn presence_payload() {
711 let elem: Element = "<x xmlns='http://jabber.org/protocol/muc#user'/>"
712 .parse()
713 .unwrap();
714 let presence = Presence::new(PresenceType::None).with_payloads(vec![elem]);
715 assert_eq!(presence.payloads.len(), 1);
716 }
717}