1// Copyright (c) 2017 Maxime “pep” Buquet <pep+code@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::error::Error;
9use crate::ns;
10use jid::Jid;
11use minidom::Element;
12use try_from::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(Jid),
86
87 /// The nickname of this user.
88 Nick(String),
89}
90
91impl TryFrom<Element> for Actor {
92 type Err = 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<Jid> = get_attr!(elem, "jid", optional);
99 let nick = get_attr!(elem, "nick", optional);
100
101 match (jid, nick) {
102 (Some(_), Some(_)) | (None, None) => {
103 return 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" => optional,
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: Affiliation = "affiliation" => required,
191
192 /// The real JID of this user, if you are allowed to see it.
193 jid: Option<Jid> = "jid" => optional,
194
195 /// The current nickname of this user.
196 nick: Option<String> = "nick" => optional,
197
198 /// The current role of this user.
199 role: Role = "role" => required
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::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 #[test]
307 fn test_invalid_attribute() {
308 let elem: Element = "
309 <x xmlns='http://jabber.org/protocol/muc#user' coucou=''/>
310 "
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 attribute in x element.");
319 }
320
321 #[test]
322 fn test_status_simple() {
323 let elem: Element = "
324 <status xmlns='http://jabber.org/protocol/muc#user' code='110'/>
325 "
326 .parse()
327 .unwrap();
328 Status::try_from(elem).unwrap();
329 }
330
331 #[test]
332 fn test_status_invalid() {
333 let elem: Element = "
334 <status xmlns='http://jabber.org/protocol/muc#user'/>
335 "
336 .parse()
337 .unwrap();
338 let error = Status::try_from(elem).unwrap_err();
339 let message = match error {
340 Error::ParseError(string) => string,
341 _ => panic!(),
342 };
343 assert_eq!(message, "Required attribute 'code' missing.");
344 }
345
346 #[test]
347 fn test_status_invalid_child() {
348 let elem: Element = "
349 <status xmlns='http://jabber.org/protocol/muc#user' code='110'>
350 <foo/>
351 </status>
352 "
353 .parse()
354 .unwrap();
355 let error = Status::try_from(elem).unwrap_err();
356 let message = match error {
357 Error::ParseError(string) => string,
358 _ => panic!(),
359 };
360 assert_eq!(message, "Unknown child in status element.");
361 }
362
363 #[test]
364 fn test_status_simple_code() {
365 let elem: Element = "
366 <status xmlns='http://jabber.org/protocol/muc#user' code='307'/>
367 "
368 .parse()
369 .unwrap();
370 let status = Status::try_from(elem).unwrap();
371 assert_eq!(status, Status::Kicked);
372 }
373
374 #[test]
375 fn test_status_invalid_code() {
376 let elem: Element = "
377 <status xmlns='http://jabber.org/protocol/muc#user' code='666'/>
378 "
379 .parse()
380 .unwrap();
381 let error = Status::try_from(elem).unwrap_err();
382 let message = match error {
383 Error::ParseError(string) => string,
384 _ => panic!(),
385 };
386 assert_eq!(message, "Invalid status code value.");
387 }
388
389 #[test]
390 fn test_status_invalid_code2() {
391 let elem: Element = "
392 <status xmlns='http://jabber.org/protocol/muc#user' code='coucou'/>
393 "
394 .parse()
395 .unwrap();
396 let error = Status::try_from(elem).unwrap_err();
397 let error = match error {
398 Error::ParseIntError(error) => error,
399 _ => panic!(),
400 };
401 assert_eq!(error.description(), "invalid digit found in string");
402 }
403
404 #[test]
405 fn test_actor_required_attributes() {
406 let elem: Element = "
407 <actor xmlns='http://jabber.org/protocol/muc#user'/>
408 "
409 .parse()
410 .unwrap();
411 let error = Actor::try_from(elem).unwrap_err();
412 let message = match error {
413 Error::ParseError(string) => string,
414 _ => panic!(),
415 };
416 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
417 }
418
419 #[test]
420 fn test_actor_required_attributes2() {
421 let elem: Element = "
422 <actor xmlns='http://jabber.org/protocol/muc#user'
423 jid='foo@bar/baz'
424 nick='baz'/>
425 "
426 .parse()
427 .unwrap();
428 let error = Actor::try_from(elem).unwrap_err();
429 let message = match error {
430 Error::ParseError(string) => string,
431 _ => panic!(),
432 };
433 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
434 }
435
436 #[test]
437 fn test_actor_jid() {
438 let elem: Element = "
439 <actor xmlns='http://jabber.org/protocol/muc#user'
440 jid='foo@bar/baz'/>
441 "
442 .parse()
443 .unwrap();
444 let actor = Actor::try_from(elem).unwrap();
445 let jid = match actor {
446 Actor::Jid(jid) => jid,
447 _ => panic!(),
448 };
449 assert_eq!(jid, "foo@bar/baz".parse::<Jid>().unwrap());
450 }
451
452 #[test]
453 fn test_actor_nick() {
454 let elem: Element = "
455 <actor xmlns='http://jabber.org/protocol/muc#user' nick='baz'/>
456 "
457 .parse()
458 .unwrap();
459 let actor = Actor::try_from(elem).unwrap();
460 let nick = match actor {
461 Actor::Nick(nick) => nick,
462 _ => panic!(),
463 };
464 assert_eq!(nick, "baz".to_owned());
465 }
466
467 #[test]
468 fn test_continue_simple() {
469 let elem: Element = "
470 <continue xmlns='http://jabber.org/protocol/muc#user'/>
471 "
472 .parse()
473 .unwrap();
474 Continue::try_from(elem).unwrap();
475 }
476
477 #[test]
478 fn test_continue_thread_attribute() {
479 let elem: Element = "
480 <continue xmlns='http://jabber.org/protocol/muc#user'
481 thread='foo'/>
482 "
483 .parse()
484 .unwrap();
485 let continue_ = Continue::try_from(elem).unwrap();
486 assert_eq!(continue_.thread, Some("foo".to_owned()));
487 }
488
489 #[test]
490 fn test_continue_invalid() {
491 let elem: Element = "
492 <continue xmlns='http://jabber.org/protocol/muc#user'>
493 <foobar/>
494 </continue>
495 "
496 .parse()
497 .unwrap();
498 let continue_ = Continue::try_from(elem).unwrap_err();
499 let message = match continue_ {
500 Error::ParseError(string) => string,
501 _ => panic!(),
502 };
503 assert_eq!(message, "Unknown child in continue element.".to_owned());
504 }
505
506 #[test]
507 fn test_reason_simple() {
508 let elem: Element = "
509 <reason xmlns='http://jabber.org/protocol/muc#user'>Reason</reason>"
510 .parse()
511 .unwrap();
512 let reason = Reason::try_from(elem).unwrap();
513 assert_eq!(reason.0, "Reason".to_owned());
514 }
515
516 #[test]
517 fn test_reason_invalid_attribute() {
518 let elem: Element = "
519 <reason xmlns='http://jabber.org/protocol/muc#user' foo='bar'/>
520 "
521 .parse()
522 .unwrap();
523 let error = Reason::try_from(elem).unwrap_err();
524 let message = match error {
525 Error::ParseError(string) => string,
526 _ => panic!(),
527 };
528 assert_eq!(message, "Unknown attribute in reason element.".to_owned());
529 }
530
531 #[test]
532 fn test_reason_invalid() {
533 let elem: Element = "
534 <reason xmlns='http://jabber.org/protocol/muc#user'>
535 <foobar/>
536 </reason>
537 "
538 .parse()
539 .unwrap();
540 let error = Reason::try_from(elem).unwrap_err();
541 let message = match error {
542 Error::ParseError(string) => string,
543 _ => panic!(),
544 };
545 assert_eq!(message, "Unknown child in reason element.".to_owned());
546 }
547
548 #[test]
549 fn test_item_invalid_attr() {
550 let elem: Element = "
551 <item xmlns='http://jabber.org/protocol/muc#user'
552 foo='bar'/>
553 "
554 .parse()
555 .unwrap();
556 let error = Item::try_from(elem).unwrap_err();
557 let message = match error {
558 Error::ParseError(string) => string,
559 _ => panic!(),
560 };
561 assert_eq!(message, "Unknown attribute in item element.".to_owned());
562 }
563
564 #[test]
565 fn test_item_affiliation_role_attr() {
566 let elem: Element = "
567 <item xmlns='http://jabber.org/protocol/muc#user'
568 affiliation='member'
569 role='moderator'/>
570 "
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 = "
579 <item xmlns='http://jabber.org/protocol/muc#user'
580 affiliation='member'/>
581 "
582 .parse()
583 .unwrap();
584 let error = Item::try_from(elem).unwrap_err();
585 let message = match error {
586 Error::ParseError(string) => string,
587 _ => panic!(),
588 };
589 assert_eq!(message, "Required attribute 'role' missing.".to_owned());
590 }
591
592 #[test]
593 fn test_item_nick_attr() {
594 let elem: Element = "
595 <item xmlns='http://jabber.org/protocol/muc#user'
596 affiliation='member'
597 role='moderator'
598 nick='foobar'/>
599 "
600 .parse()
601 .unwrap();
602 let item = Item::try_from(elem).unwrap();
603 match item {
604 Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())),
605 }
606 }
607
608 #[test]
609 fn test_item_affiliation_role_invalid_attr2() {
610 let elem: Element = "
611 <item xmlns='http://jabber.org/protocol/muc#user'
612 role='moderator'/>
613 "
614 .parse()
615 .unwrap();
616 let error = Item::try_from(elem).unwrap_err();
617 let message = match error {
618 Error::ParseError(string) => string,
619 _ => panic!(),
620 };
621 assert_eq!(
622 message,
623 "Required attribute 'affiliation' missing.".to_owned()
624 );
625 }
626
627 #[test]
628 fn test_item_role_actor_child() {
629 let elem: Element = "
630 <item xmlns='http://jabber.org/protocol/muc#user'
631 affiliation='member'
632 role='moderator'>
633 <actor nick='foobar'/>
634 </item>
635 "
636 .parse()
637 .unwrap();
638 let item = Item::try_from(elem).unwrap();
639 match item {
640 Item { actor, .. } => assert_eq!(actor, Some(Actor::Nick("foobar".to_owned()))),
641 }
642 }
643
644 #[test]
645 fn test_item_role_continue_child() {
646 let elem: Element = "
647 <item xmlns='http://jabber.org/protocol/muc#user'
648 affiliation='member'
649 role='moderator'>
650 <continue thread='foobar'/>
651 </item>
652 "
653 .parse()
654 .unwrap();
655 let item = Item::try_from(elem).unwrap();
656 let continue_1 = Continue {
657 thread: Some("foobar".to_owned()),
658 };
659 match item {
660 Item {
661 continue_: Some(continue_2),
662 ..
663 } => assert_eq!(continue_2.thread, continue_1.thread),
664 _ => panic!(),
665 }
666 }
667
668 #[test]
669 fn test_item_role_reason_child() {
670 let elem: Element = "
671 <item xmlns='http://jabber.org/protocol/muc#user'
672 affiliation='member'
673 role='moderator'>
674 <reason>foobar</reason>
675 </item>
676 "
677 .parse()
678 .unwrap();
679 let item = Item::try_from(elem).unwrap();
680 match item {
681 Item { reason, .. } => assert_eq!(reason, Some(Reason("foobar".to_owned()))),
682 }
683 }
684}