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