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