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
195generate_element_with_only_attributes!(Continue, "continue", ns::MUC_USER, [
196 thread: Option<String> = "thread" => optional,
197]);
198
199#[derive(Debug, Clone, PartialEq)]
200pub struct Reason(String);
201
202impl TryFrom<Element> for Reason {
203 type Err = Error;
204
205 fn try_from(elem: Element) -> Result<Reason, Error> {
206 if !elem.is("reason", ns::MUC_USER) {
207 return Err(Error::ParseError("This is not a reason element."));
208 }
209 for _ in elem.children() {
210 return Err(Error::ParseError("Unknown child in reason element."));
211 }
212 for _ in elem.attrs() {
213 return Err(Error::ParseError("Unknown attribute in reason element."));
214 }
215 Ok(Reason(elem.text()))
216 }
217}
218
219impl From<Reason> for Element {
220 fn from(reason: Reason) -> Element {
221 Element::builder("reason")
222 .ns(ns::MUC_USER)
223 .append(reason.0)
224 .build()
225 }
226}
227
228generate_attribute!(Affiliation, "affiliation", {
229 Owner => "owner",
230 Admin => "admin",
231 Member => "member",
232 Outcast => "outcast",
233 None => "none",
234}, Default = None);
235
236generate_attribute!(Role, "role", {
237 Moderator => "moderator",
238 Participant => "participant",
239 Visitor => "visitor",
240 None => "none",
241}, Default = None);
242
243#[derive(Debug, Clone)]
244pub struct Item {
245 pub affiliation: Affiliation,
246 pub jid: Option<Jid>,
247 pub nick: Option<String>,
248 pub role: Role,
249 pub actor: Option<Actor>,
250 pub continue_: Option<Continue>,
251 pub reason: Option<Reason>,
252}
253
254impl TryFrom<Element> for Item {
255 type Err = Error;
256
257 fn try_from(elem: Element) -> Result<Item, Error> {
258 if !elem.is("item", ns::MUC_USER) {
259 return Err(Error::ParseError("This is not a item element."));
260 }
261 let mut actor: Option<Actor> = None;
262 let mut continue_: Option<Continue> = None;
263 let mut reason: Option<Reason> = None;
264 for child in elem.children() {
265 if child.is("actor", ns::MUC_USER) {
266 actor = Some(child.clone().try_into()?);
267 } else if child.is("continue", ns::MUC_USER) {
268 continue_ = Some(child.clone().try_into()?);
269 } else if child.is("reason", ns::MUC_USER) {
270 reason = Some(child.clone().try_into()?);
271 } else {
272 return Err(Error::ParseError("Unknown child in item element."));
273 }
274 }
275 for (attr, _) in elem.attrs() {
276 if attr != "affiliation" && attr != "jid" &&
277 attr != "nick" && attr != "role" {
278 return Err(Error::ParseError("Unknown attribute in item element."));
279 }
280 }
281
282 let affiliation: Affiliation = get_attr!(elem, "affiliation", required);
283 let jid: Option<Jid> = get_attr!(elem, "jid", optional);
284 let nick: Option<String> = get_attr!(elem, "nick", optional);
285 let role: Role = get_attr!(elem, "role", required);
286
287 Ok(Item{
288 affiliation: affiliation,
289 jid: jid,
290 nick: nick,
291 role: role,
292 actor: actor,
293 continue_: continue_,
294 reason: reason,
295 })
296 }
297}
298
299impl From<Item> for Element {
300 fn from(item: Item) -> Element {
301 Element::builder("item")
302 .ns(ns::MUC_USER)
303 .attr("affiliation", item.affiliation)
304 .attr("jid", item.jid)
305 .attr("nick", item.nick)
306 .attr("role", item.role)
307 .append(item.actor)
308 .append(item.continue_)
309 .append(item.reason)
310 .build()
311 }
312}
313
314#[derive(Debug, Clone)]
315pub struct MucUser {
316 pub status: Vec<Status>,
317 pub items: Vec<Item>,
318}
319
320impl TryFrom<Element> for MucUser {
321 type Err = Error;
322
323 fn try_from(elem: Element) -> Result<MucUser, Error> {
324 if !elem.is("x", ns::MUC_USER) {
325 return Err(Error::ParseError("This is not an x element."));
326 }
327 let mut status = vec!();
328 let mut items = vec!();
329 for child in elem.children() {
330 if child.is("status", ns::MUC_USER) {
331 status.push(Status::try_from(child.clone())?);
332 } else if child.is("item", ns::MUC_USER) {
333 items.push(Item::try_from(child.clone())?);
334 } else {
335 return Err(Error::ParseError("Unknown child in x element."));
336 }
337 }
338 for _ in elem.attrs() {
339 return Err(Error::ParseError("Unknown attribute in x element."));
340 }
341 Ok(MucUser {
342 status,
343 items,
344 })
345 }
346}
347
348impl From<MucUser> for Element {
349 fn from(muc_user: MucUser) -> Element {
350 Element::builder("x")
351 .ns(ns::MUC_USER)
352 .append(muc_user.status)
353 .build()
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use std::error::Error as StdError;
361 use compare_elements::NamespaceAwareCompare;
362
363 #[test]
364 fn test_simple() {
365 let elem: Element = "
366 <x xmlns='http://jabber.org/protocol/muc#user'/>
367 ".parse().unwrap();
368 MucUser::try_from(elem).unwrap();
369 }
370
371 #[test]
372 fn test_invalid_child() {
373 let elem: Element = "
374 <x xmlns='http://jabber.org/protocol/muc#user'>
375 <coucou/>
376 </x>
377 ".parse().unwrap();
378 let error = MucUser::try_from(elem).unwrap_err();
379 let message = match error {
380 Error::ParseError(string) => string,
381 _ => panic!(),
382 };
383 assert_eq!(message, "Unknown child in x element.");
384 }
385
386 #[test]
387 fn test_serialise() {
388 let elem: Element = "
389 <x xmlns='http://jabber.org/protocol/muc#user'/>
390 ".parse().unwrap();
391 let muc = MucUser { status: vec!(), items: vec!() };
392 let elem2 = muc.into();
393 assert!(elem.compare_to(&elem2));
394 }
395
396 #[test]
397 fn test_invalid_attribute() {
398 let elem: Element = "
399 <x xmlns='http://jabber.org/protocol/muc#user' coucou=''/>
400 ".parse().unwrap();
401 let error = MucUser::try_from(elem).unwrap_err();
402 let message = match error {
403 Error::ParseError(string) => string,
404 _ => panic!(),
405 };
406 assert_eq!(message, "Unknown attribute in x element.");
407 }
408
409 #[test]
410 fn test_status_simple() {
411 let elem: Element = "
412 <status xmlns='http://jabber.org/protocol/muc#user' code='110'/>
413 ".parse().unwrap();
414 Status::try_from(elem).unwrap();
415 }
416
417 #[test]
418 fn test_status_invalid() {
419 let elem: Element = "
420 <status xmlns='http://jabber.org/protocol/muc#user'/>
421 ".parse().unwrap();
422 let error = Status::try_from(elem).unwrap_err();
423 let message = match error {
424 Error::ParseError(string) => string,
425 _ => panic!(),
426 };
427 assert_eq!(message, "Required attribute 'code' missing.");
428 }
429
430 #[test]
431 fn test_status_invalid_child() {
432 let elem: Element = "
433 <status xmlns='http://jabber.org/protocol/muc#user' code='110'>
434 <foo/>
435 </status>
436 ".parse().unwrap();
437 let error = Status::try_from(elem).unwrap_err();
438 let message = match error {
439 Error::ParseError(string) => string,
440 _ => panic!(),
441 };
442 assert_eq!(message, "Unknown child in status element.");
443 }
444
445 #[test]
446 fn test_status_simple_code() {
447 let elem: Element = "
448 <status xmlns='http://jabber.org/protocol/muc#user' code='307'/>
449 ".parse().unwrap();
450 let status = Status::try_from(elem).unwrap();
451 assert_eq!(status, Status::Kicked);
452 }
453
454 #[test]
455 fn test_status_invalid_code() {
456 let elem: Element = "
457 <status xmlns='http://jabber.org/protocol/muc#user' code='666'/>
458 ".parse().unwrap();
459 let error = Status::try_from(elem).unwrap_err();
460 let message = match error {
461 Error::ParseError(string) => string,
462 _ => panic!(),
463 };
464 assert_eq!(message, "Invalid status code.");
465 }
466
467 #[test]
468 fn test_status_invalid_code2() {
469 let elem: Element = "
470 <status xmlns='http://jabber.org/protocol/muc#user' code='coucou'/>
471 ".parse().unwrap();
472 let error = Status::try_from(elem).unwrap_err();
473 let error = match error {
474 Error::ParseIntError(error) => error,
475 _ => panic!(),
476 };
477 assert_eq!(error.description(), "invalid digit found in string");
478 }
479
480 #[test]
481 fn test_actor_required_attributes() {
482 let elem: Element = "
483 <actor xmlns='http://jabber.org/protocol/muc#user'/>
484 ".parse().unwrap();
485 let error = Actor::try_from(elem).unwrap_err();
486 let message = match error {
487 Error::ParseError(string) => string,
488 _ => panic!(),
489 };
490 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
491 }
492
493 #[test]
494 fn test_actor_required_attributes2() {
495 let elem: Element = "
496 <actor xmlns='http://jabber.org/protocol/muc#user'
497 jid='foo@bar/baz'
498 nick='baz'/>
499 ".parse().unwrap();
500 let error = Actor::try_from(elem).unwrap_err();
501 let message = match error {
502 Error::ParseError(string) => string,
503 _ => panic!(),
504 };
505 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
506 }
507
508 #[test]
509 fn test_actor_jid() {
510 let elem: Element = "
511 <actor xmlns='http://jabber.org/protocol/muc#user'
512 jid='foo@bar/baz'/>
513 ".parse().unwrap();
514 let actor = Actor::try_from(elem).unwrap();
515 let jid = match actor {
516 Actor::Jid(jid) => jid,
517 _ => panic!(),
518 };
519 assert_eq!(jid, "foo@bar/baz".parse::<Jid>().unwrap());
520 }
521
522 #[test]
523 fn test_actor_nick() {
524 let elem: Element = "
525 <actor xmlns='http://jabber.org/protocol/muc#user' nick='baz'/>
526 ".parse().unwrap();
527 let actor = Actor::try_from(elem).unwrap();
528 let nick = match actor {
529 Actor::Nick(nick) => nick,
530 _ => panic!(),
531 };
532 assert_eq!(nick, "baz".to_owned());
533 }
534
535 #[test]
536 fn test_continue_simple() {
537 let elem: Element = "
538 <continue xmlns='http://jabber.org/protocol/muc#user'/>
539 ".parse().unwrap();
540 Continue::try_from(elem).unwrap();
541 }
542
543 #[test]
544 fn test_continue_thread_attribute() {
545 let elem: Element = "
546 <continue xmlns='http://jabber.org/protocol/muc#user'
547 thread='foo'/>
548 ".parse().unwrap();
549 let continue_ = Continue::try_from(elem).unwrap();
550 assert_eq!(continue_.thread, Some("foo".to_owned()));
551 }
552
553 #[test]
554 fn test_continue_invalid() {
555 let elem: Element = "
556 <continue xmlns='http://jabber.org/protocol/muc#user'>
557 <foobar/>
558 </continue>
559 ".parse().unwrap();
560 let continue_ = Continue::try_from(elem).unwrap_err();
561 let message = match continue_ {
562 Error::ParseError(string) => string,
563 _ => panic!(),
564 };
565 assert_eq!(message, "Unknown child in continue element.".to_owned());
566 }
567
568 #[test]
569 fn test_reason_simple() {
570 let elem: Element = "
571 <reason xmlns='http://jabber.org/protocol/muc#user'>Reason</reason>"
572 .parse().unwrap();
573 let reason = Reason::try_from(elem).unwrap();
574 assert_eq!(reason.0, "Reason".to_owned());
575 }
576
577 #[test]
578 fn test_reason_invalid_attribute() {
579 let elem: Element = "
580 <reason xmlns='http://jabber.org/protocol/muc#user' foo='bar'/>
581 ".parse().unwrap();
582 let error = Reason::try_from(elem).unwrap_err();
583 let message = match error {
584 Error::ParseError(string) => string,
585 _ => panic!(),
586 };
587 assert_eq!(message, "Unknown attribute in reason element.".to_owned());
588 }
589
590 #[test]
591 fn test_reason_invalid() {
592 let elem: Element = "
593 <reason xmlns='http://jabber.org/protocol/muc#user'>
594 <foobar/>
595 </reason>
596 ".parse().unwrap();
597 let error = Reason::try_from(elem).unwrap_err();
598 let message = match error {
599 Error::ParseError(string) => string,
600 _ => panic!(),
601 };
602 assert_eq!(message, "Unknown child in reason element.".to_owned());
603 }
604
605 #[test]
606 fn test_item_invalid_attr(){
607 let elem: Element = "
608 <item xmlns='http://jabber.org/protocol/muc#user'
609 foo='bar'/>
610 ".parse().unwrap();
611 let error = Item::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 item element.".to_owned());
617 }
618
619 #[test]
620 fn test_item_affiliation_role_attr(){
621 let elem: Element = "
622 <item xmlns='http://jabber.org/protocol/muc#user'
623 affiliation='member'
624 role='moderator'/>
625 ".parse().unwrap();
626 Item::try_from(elem).unwrap();
627 }
628
629 #[test]
630 fn test_item_affiliation_role_invalid_attr(){
631 let elem: Element = "
632 <item xmlns='http://jabber.org/protocol/muc#user'
633 affiliation='member'/>
634 ".parse().unwrap();
635 let error = Item::try_from(elem).unwrap_err();
636 let message = match error {
637 Error::ParseError(string) => string,
638 _ => panic!(),
639 };
640 assert_eq!(message, "Required attribute 'role' missing.".to_owned());
641 }
642
643 #[test]
644 fn test_item_nick_attr(){
645 let elem: Element = "
646 <item xmlns='http://jabber.org/protocol/muc#user'
647 affiliation='member'
648 role='moderator'
649 nick='foobar'/>
650 ".parse().unwrap();
651 let item = Item::try_from(elem).unwrap();
652 match item {
653 Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())),
654 }
655 }
656
657 #[test]
658 fn test_item_affiliation_role_invalid_attr2(){
659 let elem: Element = "
660 <item xmlns='http://jabber.org/protocol/muc#user'
661 role='moderator'/>
662 ".parse().unwrap();
663 let error = Item::try_from(elem).unwrap_err();
664 let message = match error {
665 Error::ParseError(string) => string,
666 _ => panic!(),
667 };
668 assert_eq!(message, "Required attribute 'affiliation' missing.".to_owned());
669 }
670
671 #[test]
672 fn test_item_role_actor_child(){
673 let elem: Element = "
674 <item xmlns='http://jabber.org/protocol/muc#user'
675 affiliation='member'
676 role='moderator'>
677 <actor nick='foobar'/>
678 </item>
679 ".parse().unwrap();
680 let item = Item::try_from(elem).unwrap();
681 match item {
682 Item { actor, .. } =>
683 assert_eq!(actor, Some(Actor::Nick("foobar".to_owned()))),
684 }
685 }
686
687 #[test]
688 fn test_item_role_continue_child(){
689 let elem: Element = "
690 <item xmlns='http://jabber.org/protocol/muc#user'
691 affiliation='member'
692 role='moderator'>
693 <continue thread='foobar'/>
694 </item>
695 ".parse().unwrap();
696 let item = Item::try_from(elem).unwrap();
697 let continue_1 = Continue { thread: Some("foobar".to_owned()) };
698 match item {
699 Item { continue_: Some(continue_2), .. } => assert_eq!(continue_2.thread, continue_1.thread),
700 _ => panic!(),
701 }
702 }
703
704 #[test]
705 fn test_item_role_reason_child(){
706 let elem: Element = "
707 <item xmlns='http://jabber.org/protocol/muc#user'
708 affiliation='member'
709 role='moderator'>
710 <reason>foobar</reason>
711 </item>
712 ".parse().unwrap();
713 let item = Item::try_from(elem).unwrap();
714 match item {
715 Item { reason, .. } =>
716 assert_eq!(reason, Some(Reason("foobar".to_owned()))),
717 }
718 }
719}