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", String::from(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", match item.jid {
334 Some(jid) => Some(String::from(jid)),
335 None => None,
336 })
337 .attr("nick", item.nick)
338 .attr("role", item.role)
339 .append(item.actor)
340 .append(item.continue_)
341 .append(item.reason)
342 .build()
343 }
344}
345
346#[derive(Debug, Clone)]
347pub struct MucUser {
348 pub status: Vec<Status>,
349 pub items: Vec<Item>,
350}
351
352impl TryFrom<Element> for MucUser {
353 type Err = Error;
354
355 fn try_from(elem: Element) -> Result<MucUser, Error> {
356 if !elem.is("x", ns::MUC_USER) {
357 return Err(Error::ParseError("This is not an x element."));
358 }
359 let mut status = vec!();
360 let mut items = vec!();
361 for child in elem.children() {
362 if child.is("status", ns::MUC_USER) {
363 status.push(Status::try_from(child.clone())?);
364 } else if child.is("item", ns::MUC_USER) {
365 items.push(Item::try_from(child.clone())?);
366 } else {
367 return Err(Error::ParseError("Unknown child in x element."));
368 }
369 }
370 for _ in elem.attrs() {
371 return Err(Error::ParseError("Unknown attribute in x element."));
372 }
373 Ok(MucUser {
374 status,
375 items,
376 })
377 }
378}
379
380impl From<MucUser> for Element {
381 fn from(muc_user: MucUser) -> Element {
382 Element::builder("x")
383 .ns(ns::MUC_USER)
384 .append(muc_user.status)
385 .build()
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use std::error::Error as StdError;
393
394 #[test]
395 fn test_simple() {
396 let elem: Element = "
397 <x xmlns='http://jabber.org/protocol/muc#user'/>
398 ".parse().unwrap();
399 MucUser::try_from(elem).unwrap();
400 }
401
402 #[test]
403 fn test_invalid_child() {
404 let elem: Element = "
405 <x xmlns='http://jabber.org/protocol/muc#user'>
406 <coucou/>
407 </x>
408 ".parse().unwrap();
409 let error = MucUser::try_from(elem).unwrap_err();
410 let message = match error {
411 Error::ParseError(string) => string,
412 _ => panic!(),
413 };
414 assert_eq!(message, "Unknown child in x element.");
415 }
416
417 #[test]
418 fn test_serialise() {
419 let elem: Element = "
420 <x xmlns='http://jabber.org/protocol/muc#user'/>
421 ".parse().unwrap();
422 let muc = MucUser { status: vec!(), items: vec!() };
423 let elem2 = muc.into();
424 assert_eq!(elem, elem2);
425 }
426
427 #[test]
428 fn test_invalid_attribute() {
429 let elem: Element = "
430 <x xmlns='http://jabber.org/protocol/muc#user' coucou=''/>
431 ".parse().unwrap();
432 let error = MucUser::try_from(elem).unwrap_err();
433 let message = match error {
434 Error::ParseError(string) => string,
435 _ => panic!(),
436 };
437 assert_eq!(message, "Unknown attribute in x element.");
438 }
439
440 #[test]
441 fn test_status_simple() {
442 let elem: Element = "
443 <status xmlns='http://jabber.org/protocol/muc#user' code='110'/>
444 ".parse().unwrap();
445 Status::try_from(elem).unwrap();
446 }
447
448 #[test]
449 fn test_status_invalid() {
450 let elem: Element = "
451 <status xmlns='http://jabber.org/protocol/muc#user'/>
452 ".parse().unwrap();
453 let error = Status::try_from(elem).unwrap_err();
454 let message = match error {
455 Error::ParseError(string) => string,
456 _ => panic!(),
457 };
458 assert_eq!(message, "Required attribute 'code' missing.");
459 }
460
461 #[test]
462 fn test_status_invalid_child() {
463 let elem: Element = "
464 <status xmlns='http://jabber.org/protocol/muc#user' code='110'>
465 <foo/>
466 </status>
467 ".parse().unwrap();
468 let error = Status::try_from(elem).unwrap_err();
469 let message = match error {
470 Error::ParseError(string) => string,
471 _ => panic!(),
472 };
473 assert_eq!(message, "Unknown child in status element.");
474 }
475
476 #[test]
477 fn test_status_simple_code() {
478 let elem: Element = "
479 <status xmlns='http://jabber.org/protocol/muc#user' code='307'/>
480 ".parse().unwrap();
481 let status = Status::try_from(elem).unwrap();
482 assert_eq!(status, Status::Kicked);
483 }
484
485 #[test]
486 fn test_status_invalid_code() {
487 let elem: Element = "
488 <status xmlns='http://jabber.org/protocol/muc#user' code='666'/>
489 ".parse().unwrap();
490 let error = Status::try_from(elem).unwrap_err();
491 let message = match error {
492 Error::ParseError(string) => string,
493 _ => panic!(),
494 };
495 assert_eq!(message, "Invalid status code.");
496 }
497
498 #[test]
499 fn test_status_invalid_code2() {
500 let elem: Element = "
501 <status xmlns='http://jabber.org/protocol/muc#user' code='coucou'/>
502 ".parse().unwrap();
503 let error = Status::try_from(elem).unwrap_err();
504 let error = match error {
505 Error::ParseIntError(error) => error,
506 _ => panic!(),
507 };
508 assert_eq!(error.description(), "invalid digit found in string");
509 }
510
511 #[test]
512 fn test_actor_required_attributes() {
513 let elem: Element = "
514 <actor xmlns='http://jabber.org/protocol/muc#user'/>
515 ".parse().unwrap();
516 let error = Actor::try_from(elem).unwrap_err();
517 let message = match error {
518 Error::ParseError(string) => string,
519 _ => panic!(),
520 };
521 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
522 }
523
524 #[test]
525 fn test_actor_required_attributes2() {
526 let elem: Element = "
527 <actor xmlns='http://jabber.org/protocol/muc#user'
528 jid='foo@bar/baz'
529 nick='baz'/>
530 ".parse().unwrap();
531 let error = Actor::try_from(elem).unwrap_err();
532 let message = match error {
533 Error::ParseError(string) => string,
534 _ => panic!(),
535 };
536 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
537 }
538
539 #[test]
540 fn test_actor_jid() {
541 let elem: Element = "
542 <actor xmlns='http://jabber.org/protocol/muc#user'
543 jid='foo@bar/baz'/>
544 ".parse().unwrap();
545 let actor = Actor::try_from(elem).unwrap();
546 let jid = match actor {
547 Actor::Jid(jid) => jid,
548 _ => panic!(),
549 };
550 assert_eq!(jid, "foo@bar/baz".parse::<Jid>().unwrap());
551 }
552
553 #[test]
554 fn test_actor_nick() {
555 let elem: Element = "
556 <actor xmlns='http://jabber.org/protocol/muc#user' nick='baz'/>
557 ".parse().unwrap();
558 let actor = Actor::try_from(elem).unwrap();
559 let nick = match actor {
560 Actor::Nick(nick) => nick,
561 _ => panic!(),
562 };
563 assert_eq!(nick, "baz".to_owned());
564 }
565
566 #[test]
567 fn test_continue_simple() {
568 let elem: Element = "
569 <continue xmlns='http://jabber.org/protocol/muc#user'/>
570 ".parse().unwrap();
571 Continue::try_from(elem).unwrap();
572 }
573
574 #[test]
575 fn test_continue_thread_attribute() {
576 let elem: Element = "
577 <continue xmlns='http://jabber.org/protocol/muc#user'
578 thread='foo'/>
579 ".parse().unwrap();
580 let continue_ = Continue::try_from(elem).unwrap();
581 assert_eq!(continue_, Continue { thread: Some("foo".to_owned()) });
582 }
583
584 #[test]
585 fn test_continue_invalid() {
586 let elem: Element = "
587 <continue xmlns='http://jabber.org/protocol/muc#user'>
588 <foobar/>
589 </continue>
590 ".parse().unwrap();
591 let continue_ = Continue::try_from(elem).unwrap_err();
592 let message = match continue_ {
593 Error::ParseError(string) => string,
594 _ => panic!(),
595 };
596 assert_eq!(message, "Unknown child in continue element.".to_owned());
597 }
598
599 #[test]
600 fn test_reason_simple() {
601 let elem: Element = "
602 <reason xmlns='http://jabber.org/protocol/muc#user'>Reason</reason>"
603 .parse().unwrap();
604 let reason = Reason::try_from(elem).unwrap();
605 assert_eq!(reason.0, "Reason".to_owned());
606 }
607
608 #[test]
609 fn test_reason_invalid_attribute() {
610 let elem: Element = "
611 <reason xmlns='http://jabber.org/protocol/muc#user' foo='bar'/>
612 ".parse().unwrap();
613 let error = Reason::try_from(elem).unwrap_err();
614 let message = match error {
615 Error::ParseError(string) => string,
616 _ => panic!(),
617 };
618 assert_eq!(message, "Unknown attribute in reason element.".to_owned());
619 }
620
621 #[test]
622 fn test_reason_invalid() {
623 let elem: Element = "
624 <reason xmlns='http://jabber.org/protocol/muc#user'>
625 <foobar/>
626 </reason>
627 ".parse().unwrap();
628 let error = Reason::try_from(elem).unwrap_err();
629 let message = match error {
630 Error::ParseError(string) => string,
631 _ => panic!(),
632 };
633 assert_eq!(message, "Unknown child in reason element.".to_owned());
634 }
635
636 #[test]
637 fn test_item_invalid_attr(){
638 let elem: Element = "
639 <item xmlns='http://jabber.org/protocol/muc#user'
640 foo='bar'/>
641 ".parse().unwrap();
642 let error = Item::try_from(elem).unwrap_err();
643 let message = match error {
644 Error::ParseError(string) => string,
645 _ => panic!(),
646 };
647 assert_eq!(message, "Unknown attribute in item element.".to_owned());
648 }
649
650 #[test]
651 fn test_item_affiliation_role_attr(){
652 let elem: Element = "
653 <item xmlns='http://jabber.org/protocol/muc#user'
654 affiliation='member'
655 role='moderator'/>
656 ".parse().unwrap();
657 Item::try_from(elem).unwrap();
658 }
659
660 #[test]
661 fn test_item_affiliation_role_invalid_attr(){
662 let elem: Element = "
663 <item xmlns='http://jabber.org/protocol/muc#user'
664 affiliation='member'/>
665 ".parse().unwrap();
666 let error = Item::try_from(elem).unwrap_err();
667 let message = match error {
668 Error::ParseError(string) => string,
669 _ => panic!(),
670 };
671 assert_eq!(message, "Required attribute 'role' missing.".to_owned());
672 }
673
674 #[test]
675 fn test_item_nick_attr(){
676 let elem: Element = "
677 <item xmlns='http://jabber.org/protocol/muc#user'
678 affiliation='member'
679 role='moderator'
680 nick='foobar'/>
681 ".parse().unwrap();
682 let item = Item::try_from(elem).unwrap();
683 match item {
684 Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())),
685 }
686 }
687
688 #[test]
689 fn test_item_affiliation_role_invalid_attr2(){
690 let elem: Element = "
691 <item xmlns='http://jabber.org/protocol/muc#user'
692 role='moderator'/>
693 ".parse().unwrap();
694 let error = Item::try_from(elem).unwrap_err();
695 let message = match error {
696 Error::ParseError(string) => string,
697 _ => panic!(),
698 };
699 assert_eq!(message, "Required attribute 'affiliation' missing.".to_owned());
700 }
701
702 #[test]
703 fn test_item_role_actor_child(){
704 let elem: Element = "
705 <item xmlns='http://jabber.org/protocol/muc#user'
706 affiliation='member'
707 role='moderator'>
708 <actor nick='foobar'/>
709 </item>
710 ".parse().unwrap();
711 let item = Item::try_from(elem).unwrap();
712 match item {
713 Item { actor, .. } =>
714 assert_eq!(actor, Some(Actor::Nick("foobar".to_owned()))),
715 }
716 }
717
718 #[test]
719 fn test_item_role_continue_child(){
720 let elem: Element = "
721 <item xmlns='http://jabber.org/protocol/muc#user'
722 affiliation='member'
723 role='moderator'>
724 <continue thread='foobar'/>
725 </item>
726 ".parse().unwrap();
727 let item = Item::try_from(elem).unwrap();
728 let continue_1 = Continue { thread: Some("foobar".to_owned()) };
729 match item {
730 Item { continue_: Some(continue_2), .. } => assert_eq!(continue_2, continue_1),
731 _ => panic!(),
732 }
733 }
734
735 #[test]
736 fn test_item_role_reason_child(){
737 let elem: Element = "
738 <item xmlns='http://jabber.org/protocol/muc#user'
739 affiliation='member'
740 role='moderator'>
741 <reason>foobar</reason>
742 </item>
743 ".parse().unwrap();
744 let item = Item::try_from(elem).unwrap();
745 match item {
746 Item { reason, .. } =>
747 assert_eq!(reason, Some(Reason("foobar".to_owned()))),
748 }
749 }
750}