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};
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/// Possesses a 'jid' and a 'nick' attribute, so that an action can be attributed either to a real
84/// JID or to a roomnick. -- CHANGELOG 1.25 (2012-02-08)
85#[derive(Debug, Clone, PartialEq)]
86pub enum Actor {
87 Jid(Jid),
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(_))
103 | (None, None) =>
104 return Err(Error::ParseError("Either 'jid' or 'nick' attribute is required.")),
105 (Some(jid), _) => Ok(Actor::Jid(jid)),
106 (_, Some(nick)) => Ok(Actor::Nick(nick)),
107 }
108 }
109}
110
111impl From<Actor> for Element {
112 fn from(actor: Actor) -> Element {
113 let elem = Element::builder("actor").ns(ns::MUC_USER);
114
115 (match actor {
116 Actor::Jid(jid) => elem.attr("jid", jid),
117 Actor::Nick(nick) => elem.attr("nick", nick),
118 }).build()
119 }
120}
121
122generate_element_with_only_attributes!(Continue, "continue", MUC_USER, [
123 thread: Option<String> = "thread" => optional,
124]);
125
126generate_elem_id!(Reason, "reason", MUC_USER);
127
128generate_attribute!(Affiliation, "affiliation", {
129 Owner => "owner",
130 Admin => "admin",
131 Member => "member",
132 Outcast => "outcast",
133 None => "none",
134}, Default = None);
135
136generate_attribute!(Role, "role", {
137 Moderator => "moderator",
138 Participant => "participant",
139 Visitor => "visitor",
140 None => "none",
141}, Default = None);
142
143#[derive(Debug, Clone)]
144pub struct Item {
145 pub affiliation: Affiliation,
146 pub jid: Option<Jid>,
147 pub nick: Option<String>,
148 pub role: Role,
149 pub actor: Option<Actor>,
150 pub continue_: Option<Continue>,
151 pub reason: Option<Reason>,
152}
153
154impl TryFrom<Element> for Item {
155 type Err = Error;
156
157 fn try_from(elem: Element) -> Result<Item, Error> {
158 check_self!(elem, "item", MUC_USER);
159 check_no_unknown_attributes!(elem, "item", ["affiliation", "jid", "nick", "role"]);
160 let mut actor: Option<Actor> = None;
161 let mut continue_: Option<Continue> = None;
162 let mut reason: Option<Reason> = None;
163 for child in elem.children() {
164 if child.is("actor", ns::MUC_USER) {
165 actor = Some(child.clone().try_into()?);
166 } else if child.is("continue", ns::MUC_USER) {
167 continue_ = Some(child.clone().try_into()?);
168 } else if child.is("reason", ns::MUC_USER) {
169 reason = Some(child.clone().try_into()?);
170 } else {
171 return Err(Error::ParseError("Unknown child in item element."));
172 }
173 }
174
175 let affiliation: Affiliation = get_attr!(elem, "affiliation", required);
176 let jid: Option<Jid> = get_attr!(elem, "jid", optional);
177 let nick: Option<String> = get_attr!(elem, "nick", optional);
178 let role: Role = get_attr!(elem, "role", required);
179
180 Ok(Item{
181 affiliation: affiliation,
182 jid: jid,
183 nick: nick,
184 role: role,
185 actor: actor,
186 continue_: continue_,
187 reason: reason,
188 })
189 }
190}
191
192impl From<Item> for Element {
193 fn from(item: Item) -> Element {
194 Element::builder("item")
195 .ns(ns::MUC_USER)
196 .attr("affiliation", item.affiliation)
197 .attr("jid", item.jid)
198 .attr("nick", item.nick)
199 .attr("role", item.role)
200 .append(item.actor)
201 .append(item.continue_)
202 .append(item.reason)
203 .build()
204 }
205}
206
207#[derive(Debug, Clone)]
208pub struct MucUser {
209 pub status: Vec<Status>,
210 pub items: Vec<Item>,
211}
212
213impl TryFrom<Element> for MucUser {
214 type Err = Error;
215
216 fn try_from(elem: Element) -> Result<MucUser, Error> {
217 check_self!(elem, "x", MUC_USER);
218 check_no_attributes!(elem, "x");
219 let mut status = vec!();
220 let mut items = vec!();
221 for child in elem.children() {
222 if child.is("status", ns::MUC_USER) {
223 status.push(Status::try_from(child.clone())?);
224 } else if child.is("item", ns::MUC_USER) {
225 items.push(Item::try_from(child.clone())?);
226 } else {
227 return Err(Error::ParseError("Unknown child in x element."));
228 }
229 }
230 Ok(MucUser {
231 status,
232 items,
233 })
234 }
235}
236
237impl From<MucUser> for Element {
238 fn from(muc_user: MucUser) -> Element {
239 Element::builder("x")
240 .ns(ns::MUC_USER)
241 .append(muc_user.status)
242 .build()
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use std::error::Error as StdError;
250 use compare_elements::NamespaceAwareCompare;
251
252 #[test]
253 fn test_simple() {
254 let elem: Element = "
255 <x xmlns='http://jabber.org/protocol/muc#user'/>
256 ".parse().unwrap();
257 MucUser::try_from(elem).unwrap();
258 }
259
260 #[test]
261 fn test_invalid_child() {
262 let elem: Element = "
263 <x xmlns='http://jabber.org/protocol/muc#user'>
264 <coucou/>
265 </x>
266 ".parse().unwrap();
267 let error = MucUser::try_from(elem).unwrap_err();
268 let message = match error {
269 Error::ParseError(string) => string,
270 _ => panic!(),
271 };
272 assert_eq!(message, "Unknown child in x element.");
273 }
274
275 #[test]
276 fn test_serialise() {
277 let elem: Element = "
278 <x xmlns='http://jabber.org/protocol/muc#user'/>
279 ".parse().unwrap();
280 let muc = MucUser { status: vec!(), items: vec!() };
281 let elem2 = muc.into();
282 assert!(elem.compare_to(&elem2));
283 }
284
285 #[test]
286 fn test_invalid_attribute() {
287 let elem: Element = "
288 <x xmlns='http://jabber.org/protocol/muc#user' coucou=''/>
289 ".parse().unwrap();
290 let error = MucUser::try_from(elem).unwrap_err();
291 let message = match error {
292 Error::ParseError(string) => string,
293 _ => panic!(),
294 };
295 assert_eq!(message, "Unknown attribute in x element.");
296 }
297
298 #[test]
299 fn test_status_simple() {
300 let elem: Element = "
301 <status xmlns='http://jabber.org/protocol/muc#user' code='110'/>
302 ".parse().unwrap();
303 Status::try_from(elem).unwrap();
304 }
305
306 #[test]
307 fn test_status_invalid() {
308 let elem: Element = "
309 <status xmlns='http://jabber.org/protocol/muc#user'/>
310 ".parse().unwrap();
311 let error = Status::try_from(elem).unwrap_err();
312 let message = match error {
313 Error::ParseError(string) => string,
314 _ => panic!(),
315 };
316 assert_eq!(message, "Required attribute 'code' missing.");
317 }
318
319 #[test]
320 fn test_status_invalid_child() {
321 let elem: Element = "
322 <status xmlns='http://jabber.org/protocol/muc#user' code='110'>
323 <foo/>
324 </status>
325 ".parse().unwrap();
326 let error = Status::try_from(elem).unwrap_err();
327 let message = match error {
328 Error::ParseError(string) => string,
329 _ => panic!(),
330 };
331 assert_eq!(message, "Unknown child in status element.");
332 }
333
334 #[test]
335 fn test_status_simple_code() {
336 let elem: Element = "
337 <status xmlns='http://jabber.org/protocol/muc#user' code='307'/>
338 ".parse().unwrap();
339 let status = Status::try_from(elem).unwrap();
340 assert_eq!(status, Status::Kicked);
341 }
342
343 #[test]
344 fn test_status_invalid_code() {
345 let elem: Element = "
346 <status xmlns='http://jabber.org/protocol/muc#user' code='666'/>
347 ".parse().unwrap();
348 let error = Status::try_from(elem).unwrap_err();
349 let message = match error {
350 Error::ParseError(string) => string,
351 _ => panic!(),
352 };
353 assert_eq!(message, "Invalid status code value.");
354 }
355
356 #[test]
357 fn test_status_invalid_code2() {
358 let elem: Element = "
359 <status xmlns='http://jabber.org/protocol/muc#user' code='coucou'/>
360 ".parse().unwrap();
361 let error = Status::try_from(elem).unwrap_err();
362 let error = match error {
363 Error::ParseIntError(error) => error,
364 _ => panic!(),
365 };
366 assert_eq!(error.description(), "invalid digit found in string");
367 }
368
369 #[test]
370 fn test_actor_required_attributes() {
371 let elem: Element = "
372 <actor xmlns='http://jabber.org/protocol/muc#user'/>
373 ".parse().unwrap();
374 let error = Actor::try_from(elem).unwrap_err();
375 let message = match error {
376 Error::ParseError(string) => string,
377 _ => panic!(),
378 };
379 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
380 }
381
382 #[test]
383 fn test_actor_required_attributes2() {
384 let elem: Element = "
385 <actor xmlns='http://jabber.org/protocol/muc#user'
386 jid='foo@bar/baz'
387 nick='baz'/>
388 ".parse().unwrap();
389 let error = Actor::try_from(elem).unwrap_err();
390 let message = match error {
391 Error::ParseError(string) => string,
392 _ => panic!(),
393 };
394 assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
395 }
396
397 #[test]
398 fn test_actor_jid() {
399 let elem: Element = "
400 <actor xmlns='http://jabber.org/protocol/muc#user'
401 jid='foo@bar/baz'/>
402 ".parse().unwrap();
403 let actor = Actor::try_from(elem).unwrap();
404 let jid = match actor {
405 Actor::Jid(jid) => jid,
406 _ => panic!(),
407 };
408 assert_eq!(jid, "foo@bar/baz".parse::<Jid>().unwrap());
409 }
410
411 #[test]
412 fn test_actor_nick() {
413 let elem: Element = "
414 <actor xmlns='http://jabber.org/protocol/muc#user' nick='baz'/>
415 ".parse().unwrap();
416 let actor = Actor::try_from(elem).unwrap();
417 let nick = match actor {
418 Actor::Nick(nick) => nick,
419 _ => panic!(),
420 };
421 assert_eq!(nick, "baz".to_owned());
422 }
423
424 #[test]
425 fn test_continue_simple() {
426 let elem: Element = "
427 <continue xmlns='http://jabber.org/protocol/muc#user'/>
428 ".parse().unwrap();
429 Continue::try_from(elem).unwrap();
430 }
431
432 #[test]
433 fn test_continue_thread_attribute() {
434 let elem: Element = "
435 <continue xmlns='http://jabber.org/protocol/muc#user'
436 thread='foo'/>
437 ".parse().unwrap();
438 let continue_ = Continue::try_from(elem).unwrap();
439 assert_eq!(continue_.thread, Some("foo".to_owned()));
440 }
441
442 #[test]
443 fn test_continue_invalid() {
444 let elem: Element = "
445 <continue xmlns='http://jabber.org/protocol/muc#user'>
446 <foobar/>
447 </continue>
448 ".parse().unwrap();
449 let continue_ = Continue::try_from(elem).unwrap_err();
450 let message = match continue_ {
451 Error::ParseError(string) => string,
452 _ => panic!(),
453 };
454 assert_eq!(message, "Unknown child in continue element.".to_owned());
455 }
456
457 #[test]
458 fn test_reason_simple() {
459 let elem: Element = "
460 <reason xmlns='http://jabber.org/protocol/muc#user'>Reason</reason>"
461 .parse().unwrap();
462 let reason = Reason::try_from(elem).unwrap();
463 assert_eq!(reason.0, "Reason".to_owned());
464 }
465
466 #[test]
467 fn test_reason_invalid_attribute() {
468 let elem: Element = "
469 <reason xmlns='http://jabber.org/protocol/muc#user' foo='bar'/>
470 ".parse().unwrap();
471 let error = Reason::try_from(elem).unwrap_err();
472 let message = match error {
473 Error::ParseError(string) => string,
474 _ => panic!(),
475 };
476 assert_eq!(message, "Unknown attribute in reason element.".to_owned());
477 }
478
479 #[test]
480 fn test_reason_invalid() {
481 let elem: Element = "
482 <reason xmlns='http://jabber.org/protocol/muc#user'>
483 <foobar/>
484 </reason>
485 ".parse().unwrap();
486 let error = Reason::try_from(elem).unwrap_err();
487 let message = match error {
488 Error::ParseError(string) => string,
489 _ => panic!(),
490 };
491 assert_eq!(message, "Unknown child in reason element.".to_owned());
492 }
493
494 #[test]
495 fn test_item_invalid_attr(){
496 let elem: Element = "
497 <item xmlns='http://jabber.org/protocol/muc#user'
498 foo='bar'/>
499 ".parse().unwrap();
500 let error = Item::try_from(elem).unwrap_err();
501 let message = match error {
502 Error::ParseError(string) => string,
503 _ => panic!(),
504 };
505 assert_eq!(message, "Unknown attribute in item element.".to_owned());
506 }
507
508 #[test]
509 fn test_item_affiliation_role_attr(){
510 let elem: Element = "
511 <item xmlns='http://jabber.org/protocol/muc#user'
512 affiliation='member'
513 role='moderator'/>
514 ".parse().unwrap();
515 Item::try_from(elem).unwrap();
516 }
517
518 #[test]
519 fn test_item_affiliation_role_invalid_attr(){
520 let elem: Element = "
521 <item xmlns='http://jabber.org/protocol/muc#user'
522 affiliation='member'/>
523 ".parse().unwrap();
524 let error = Item::try_from(elem).unwrap_err();
525 let message = match error {
526 Error::ParseError(string) => string,
527 _ => panic!(),
528 };
529 assert_eq!(message, "Required attribute 'role' missing.".to_owned());
530 }
531
532 #[test]
533 fn test_item_nick_attr(){
534 let elem: Element = "
535 <item xmlns='http://jabber.org/protocol/muc#user'
536 affiliation='member'
537 role='moderator'
538 nick='foobar'/>
539 ".parse().unwrap();
540 let item = Item::try_from(elem).unwrap();
541 match item {
542 Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())),
543 }
544 }
545
546 #[test]
547 fn test_item_affiliation_role_invalid_attr2(){
548 let elem: Element = "
549 <item xmlns='http://jabber.org/protocol/muc#user'
550 role='moderator'/>
551 ".parse().unwrap();
552 let error = Item::try_from(elem).unwrap_err();
553 let message = match error {
554 Error::ParseError(string) => string,
555 _ => panic!(),
556 };
557 assert_eq!(message, "Required attribute 'affiliation' missing.".to_owned());
558 }
559
560 #[test]
561 fn test_item_role_actor_child(){
562 let elem: Element = "
563 <item xmlns='http://jabber.org/protocol/muc#user'
564 affiliation='member'
565 role='moderator'>
566 <actor nick='foobar'/>
567 </item>
568 ".parse().unwrap();
569 let item = Item::try_from(elem).unwrap();
570 match item {
571 Item { actor, .. } =>
572 assert_eq!(actor, Some(Actor::Nick("foobar".to_owned()))),
573 }
574 }
575
576 #[test]
577 fn test_item_role_continue_child(){
578 let elem: Element = "
579 <item xmlns='http://jabber.org/protocol/muc#user'
580 affiliation='member'
581 role='moderator'>
582 <continue thread='foobar'/>
583 </item>
584 ".parse().unwrap();
585 let item = Item::try_from(elem).unwrap();
586 let continue_1 = Continue { thread: Some("foobar".to_owned()) };
587 match item {
588 Item { continue_: Some(continue_2), .. } => assert_eq!(continue_2.thread, continue_1.thread),
589 _ => panic!(),
590 }
591 }
592
593 #[test]
594 fn test_item_role_reason_child(){
595 let elem: Element = "
596 <item xmlns='http://jabber.org/protocol/muc#user'
597 affiliation='member'
598 role='moderator'>
599 <reason>foobar</reason>
600 </item>
601 ".parse().unwrap();
602 let item = Item::try_from(elem).unwrap();
603 match item {
604 Item { reason, .. } =>
605 assert_eq!(reason, Some(Reason("foobar".to_owned()))),
606 }
607 }
608}