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::util::error::Error;
9use crate::ns;
10use jid::FullJid;
11use minidom::Element;
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) => {
103 Err(Error::ParseError(
104 "Either 'jid' or 'nick' attribute is required.",
105 ))
106 }
107 (Some(jid), _) => Ok(Actor::Jid(jid)),
108 (_, Some(nick)) => Ok(Actor::Nick(nick)),
109 }
110 }
111}
112
113impl From<Actor> for Element {
114 fn from(actor: Actor) -> Element {
115 let elem = Element::builder("actor").ns(ns::MUC_USER);
116
117 (match actor {
118 Actor::Jid(jid) => elem.attr("jid", jid),
119 Actor::Nick(nick) => elem.attr("nick", nick),
120 })
121 .build()
122 }
123}
124
125generate_element!(
126 /// Used to continue a one-to-one discussion in a room, with more than one
127 /// participant.
128 Continue, "continue", MUC_USER,
129 attributes: [
130 /// The thread to continue in this room.
131 thread: Option<String> = "thread",
132 ]
133);
134
135generate_elem_id!(
136 /// A reason for inviting, declining, etc. a request.
137 Reason,
138 "reason",
139 MUC_USER
140);
141
142generate_attribute!(
143 /// The affiliation of an entity with a room, which isn’t tied to its
144 /// presence in it.
145 Affiliation, "affiliation", {
146 /// The user who created the room, or who got appointed by its creator
147 /// to be their equal.
148 Owner => "owner",
149
150 /// A user who has been empowered by an owner to do administrative
151 /// operations.
152 Admin => "admin",
153
154 /// A user who is whitelisted to speak in moderated rooms, or to join a
155 /// member-only room.
156 Member => "member",
157
158 /// A user who has been banned from this room.
159 Outcast => "outcast",
160
161 /// A normal participant.
162 None => "none",
163 }, Default = None
164);
165
166generate_attribute!(
167 /// The current role of an entity in a room, it can be changed by an owner
168 /// or an administrator but will be lost once they leave the room.
169 Role, "role", {
170 /// This user can kick other participants, as well as grant and revoke
171 /// them voice.
172 Moderator => "moderator",
173
174 /// A user who can speak in this room.
175 Participant => "participant",
176
177 /// A user who cannot speak in this room, and must request voice before
178 /// doing so.
179 Visitor => "visitor",
180
181 /// A user who is absent from the room.
182 None => "none",
183 }, Default = None
184);
185
186generate_element!(
187 /// An item representing a user in a room.
188 Item, "item", MUC_USER, attributes: [
189 /// The affiliation of this user with the room.
190 affiliation: Required<Affiliation> = "affiliation",
191
192 /// The real JID of this user, if you are allowed to see it.
193 jid: Option<FullJid> = "jid",
194
195 /// The current nickname of this user.
196 nick: Option<String> = "nick",
197
198 /// The current role of this user.
199 role: Required<Role> = "role",
200 ], children: [
201 /// The actor affected by this item.
202 actor: Option<Actor> = ("actor", MUC_USER) => Actor,
203
204 /// Whether this continues a one-to-one discussion.
205 continue_: Option<Continue> = ("continue", MUC_USER) => Continue,
206
207 /// A reason for this item.
208 reason: Option<Reason> = ("reason", MUC_USER) => Reason
209 ]
210);
211
212impl Item {
213 /// Creates a new item with the given affiliation and role.
214 pub fn new(affiliation: Affiliation, role: Role) -> Item {
215 Item {
216 affiliation,
217 role,
218 jid: None,
219 nick: None,
220 actor: None,
221 continue_: None,
222 reason: None,
223 }
224 }
225}
226
227generate_element!(
228 /// The main muc#user element.
229 MucUser, "x", MUC_USER, children: [
230 /// List of statuses applying to this item.
231 status: Vec<Status> = ("status", MUC_USER) => Status,
232
233 /// List of items.
234 items: Vec<Item> = ("item", MUC_USER) => Item
235 ]
236);
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::util::compare_elements::NamespaceAwareCompare;
242 use std::error::Error as StdError;
243
244 #[test]
245 fn test_simple() {
246 let elem: Element = "
247 <x xmlns='http://jabber.org/protocol/muc#user'/>
248 "
249 .parse()
250 .unwrap();
251 MucUser::try_from(elem).unwrap();
252 }
253
254 #[test]
255 fn statuses_and_items() {
256 let elem: Element = "
257 <x xmlns='http://jabber.org/protocol/muc#user'>
258 <status code='101'/>
259 <status code='102'/>
260 <item affiliation='member' role='moderator'/>
261 </x>
262 "
263 .parse()
264 .unwrap();
265 let muc_user = MucUser::try_from(elem).unwrap();
266 assert_eq!(muc_user.status.len(), 2);
267 assert_eq!(muc_user.status[0], Status::AffiliationChange);
268 assert_eq!(muc_user.status[1], Status::ConfigShowsUnavailableMembers);
269 assert_eq!(muc_user.items.len(), 1);
270 assert_eq!(muc_user.items[0].affiliation, Affiliation::Member);
271 assert_eq!(muc_user.items[0].role, Role::Moderator);
272 }
273
274 #[test]
275 fn test_invalid_child() {
276 let elem: Element = "
277 <x xmlns='http://jabber.org/protocol/muc#user'>
278 <coucou/>
279 </x>
280 "
281 .parse()
282 .unwrap();
283 let error = MucUser::try_from(elem).unwrap_err();
284 let message = match error {
285 Error::ParseError(string) => string,
286 _ => panic!(),
287 };
288 assert_eq!(message, "Unknown child in x element.");
289 }
290
291 #[test]
292 fn test_serialise() {
293 let elem: Element = "
294 <x xmlns='http://jabber.org/protocol/muc#user'/>
295 "
296 .parse()
297 .unwrap();
298 let muc = MucUser {
299 status: vec![],
300 items: vec![],
301 };
302 let elem2 = muc.into();
303 assert!(elem.compare_to(&elem2));
304 }
305
306 #[cfg(not(feature = "disable-validation"))]
307 #[test]
308 fn test_invalid_attribute() {
309 let elem: Element = "
310 <x xmlns='http://jabber.org/protocol/muc#user' coucou=''/>
311 "
312 .parse()
313 .unwrap();
314 let error = MucUser::try_from(elem).unwrap_err();
315 let message = match error {
316 Error::ParseError(string) => string,
317 _ => panic!(),
318 };
319 assert_eq!(message, "Unknown attribute in x element.");
320 }
321
322 #[test]
323 fn test_status_simple() {
324 let elem: Element = "
325 <status xmlns='http://jabber.org/protocol/muc#user' code='110'/>
326 "
327 .parse()
328 .unwrap();
329 Status::try_from(elem).unwrap();
330 }
331
332 #[test]
333 fn test_status_invalid() {
334 let elem: Element = "
335 <status xmlns='http://jabber.org/protocol/muc#user'/>
336 "
337 .parse()
338 .unwrap();
339 let error = Status::try_from(elem).unwrap_err();
340 let message = match error {
341 Error::ParseError(string) => string,
342 _ => panic!(),
343 };
344 assert_eq!(message, "Required attribute 'code' missing.");
345 }
346
347 #[cfg(not(feature = "disable-validation"))]
348 #[test]
349 fn test_status_invalid_child() {
350 let elem: Element = "
351 <status xmlns='http://jabber.org/protocol/muc#user' code='110'>
352 <foo/>
353 </status>
354 "
355 .parse()
356 .unwrap();
357 let error = Status::try_from(elem).unwrap_err();
358 let message = match error {
359 Error::ParseError(string) => string,
360 _ => panic!(),
361 };
362 assert_eq!(message, "Unknown child in status element.");
363 }
364
365 #[test]
366 fn test_status_simple_code() {
367 let elem: Element = "
368 <status xmlns='http://jabber.org/protocol/muc#user' code='307'/>
369 "
370 .parse()
371 .unwrap();
372 let status = Status::try_from(elem).unwrap();
373 assert_eq!(status, Status::Kicked);
374 }
375
376 #[test]
377 fn test_status_invalid_code() {
378 let elem: Element = "
379 <status xmlns='http://jabber.org/protocol/muc#user' code='666'/>
380 "
381 .parse()
382 .unwrap();
383 let error = Status::try_from(elem).unwrap_err();
384 let message = match error {
385 Error::ParseError(string) => string,
386 _ => panic!(),
387 };
388 assert_eq!(message, "Invalid status code value.");
389 }
390
391 #[test]
392 fn test_status_invalid_code2() {
393 let elem: Element = "
394 <status xmlns='http://jabber.org/protocol/muc#user' code='coucou'/>
395 "
396 .parse()
397 .unwrap();
398 let error = Status::try_from(elem).unwrap_err();
399 let error = match error {
400 Error::ParseIntError(error) => error,
401 _ => panic!(),
402 };
403 assert_eq!(error.description(), "invalid digit found in string");
404 }
405
406 #[test]
407 fn test_actor_required_attributes() {
408 let elem: Element = "
409 <actor xmlns='http://jabber.org/protocol/muc#user'/>
410 "
411 .parse()
412 .unwrap();
413 let error = Actor::try_from(elem).unwrap_err();
414 let message = match error {
415 Error::ParseError(string) => string,
416 _ => panic!(),
417 };
418 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
419 }
420
421 #[test]
422 fn test_actor_required_attributes2() {
423 let elem: Element = "
424 <actor xmlns='http://jabber.org/protocol/muc#user'
425 jid='foo@bar/baz'
426 nick='baz'/>
427 "
428 .parse()
429 .unwrap();
430 let error = Actor::try_from(elem).unwrap_err();
431 let message = match error {
432 Error::ParseError(string) => string,
433 _ => panic!(),
434 };
435 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
436 }
437
438 #[test]
439 fn test_actor_jid() {
440 let elem: Element = "
441 <actor xmlns='http://jabber.org/protocol/muc#user'
442 jid='foo@bar/baz'/>
443 "
444 .parse()
445 .unwrap();
446 let actor = Actor::try_from(elem).unwrap();
447 let jid = match actor {
448 Actor::Jid(jid) => jid,
449 _ => panic!(),
450 };
451 assert_eq!(jid, "foo@bar/baz".parse::<FullJid>().unwrap());
452 }
453
454 #[test]
455 fn test_actor_nick() {
456 let elem: Element = "
457 <actor xmlns='http://jabber.org/protocol/muc#user' nick='baz'/>
458 "
459 .parse()
460 .unwrap();
461 let actor = Actor::try_from(elem).unwrap();
462 let nick = match actor {
463 Actor::Nick(nick) => nick,
464 _ => panic!(),
465 };
466 assert_eq!(nick, "baz".to_owned());
467 }
468
469 #[test]
470 fn test_continue_simple() {
471 let elem: Element = "
472 <continue xmlns='http://jabber.org/protocol/muc#user'/>
473 "
474 .parse()
475 .unwrap();
476 Continue::try_from(elem).unwrap();
477 }
478
479 #[test]
480 fn test_continue_thread_attribute() {
481 let elem: Element = "
482 <continue xmlns='http://jabber.org/protocol/muc#user'
483 thread='foo'/>
484 "
485 .parse()
486 .unwrap();
487 let continue_ = Continue::try_from(elem).unwrap();
488 assert_eq!(continue_.thread, Some("foo".to_owned()));
489 }
490
491 #[test]
492 fn test_continue_invalid() {
493 let elem: Element = "
494 <continue xmlns='http://jabber.org/protocol/muc#user'>
495 <foobar/>
496 </continue>
497 "
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 = "
511 <reason xmlns='http://jabber.org/protocol/muc#user'>Reason</reason>"
512 .parse()
513 .unwrap();
514 let reason = Reason::try_from(elem).unwrap();
515 assert_eq!(reason.0, "Reason".to_owned());
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}