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