// Copyright (c) 2019 Emmanuel Gil Peyrot // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. //! //! Chatroom bookmarks from [XEP-0402](https://xmpp.org/extensions/xep-0402.html) for newer servers //! which advertise `urn:xmpp:bookmarks:1#compat` on the user's BareJID in a disco info request. //! On legacy non-compliant servers, use the [`private`][crate::private] module instead. //! //! See [ModernXMPP docs](https://docs.modernxmpp.org/client/groupchat/#bookmarks) on how to handle all historic //! and newer specifications for your clients. //! //! This module exposes the [`Autojoin`][crate::bookmarks2::Autojoin] boolean flag, the [`Conference`][crate::bookmarks2::Conference] chatroom element, and the [BOOKMARKS2][crate::ns::BOOKMARKS2] XML namespace. use crate::ns; use minidom::Element; use xso::error::{Error, FromElementError}; generate_attribute!( /// Whether a conference bookmark should be joined automatically. Autojoin, "autojoin", bool ); /// A conference bookmark. #[derive(Debug, Clone, Default)] pub struct Conference { /// Whether a conference bookmark should be joined automatically. pub autojoin: Autojoin, /// A user-defined name for this conference. pub name: Option, /// The nick the user will use to join this conference. pub nick: Option, /// The password required to join this conference. pub password: Option, /// Extensions elements. pub extensions: Vec, } impl Conference { /// Create a new conference. pub fn new() -> Conference { Conference::default() } } impl TryFrom for Conference { type Error = FromElementError; fn try_from(root: Element) -> Result { check_self!(root, "conference", BOOKMARKS2, "Conference"); check_no_unknown_attributes!(root, "Conference", ["autojoin", "name"]); let mut conference = Conference { autojoin: get_attr!(root, "autojoin", Default), name: get_attr!(root, "name", Option), nick: None, password: None, extensions: Vec::new(), }; for child in root.children() { if child.is("nick", ns::BOOKMARKS2) { if conference.nick.is_some() { return Err(Error::Other("Conference must not have more than one nick.").into()); } check_no_children!(child, "nick"); check_no_attributes!(child, "nick"); conference.nick = Some(child.text()); } else if child.is("password", ns::BOOKMARKS2) { if conference.password.is_some() { return Err( Error::Other("Conference must not have more than one password.").into(), ); } check_no_children!(child, "password"); check_no_attributes!(child, "password"); conference.password = Some(child.text()); } else if child.is("extensions", ns::BOOKMARKS2) { if !conference.extensions.is_empty() { return Err(Error::Other( "Conference must not have more than one extensions element.", ) .into()); } conference.extensions.extend(child.children().cloned()); } else { return Err(Error::Other("Unknown element in bookmarks2 conference").into()); } } Ok(conference) } } impl From for Element { fn from(conference: Conference) -> Element { Element::builder("conference", ns::BOOKMARKS2) .attr("autojoin", conference.autojoin) .attr("name", conference.name) .append_all( conference .nick .map(|nick| Element::builder("nick", ns::BOOKMARKS2).append(nick)), ) .append_all( conference .password .map(|password| Element::builder("password", ns::BOOKMARKS2).append(password)), ) .append_all(match conference.extensions { empty if empty.is_empty() => None, extensions => { Some(Element::builder("extensions", ns::BOOKMARKS2).append_all(extensions)) } }) .build() } } #[cfg(test)] mod tests { use super::*; use crate::pubsub::{pubsub::Item as PubSubItem, PubSubEvent}; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(Conference, 52); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(Conference, 104); } #[test] fn simple() { let elem: Element = "" .parse() .unwrap(); let elem1 = elem.clone(); let conference = Conference::try_from(elem).unwrap(); assert_eq!(conference.autojoin, Autojoin::False); assert_eq!(conference.name, None); assert_eq!(conference.nick, None); assert_eq!(conference.password, None); let elem2 = Element::from(Conference::new()); assert_eq!(elem1, elem2); } #[test] fn complete() { let elem: Element = "Coucousecret".parse().unwrap(); let conference = Conference::try_from(elem).unwrap(); assert_eq!(conference.autojoin, Autojoin::True); assert_eq!(conference.name, Some(String::from("Test MUC"))); assert_eq!(conference.clone().nick.unwrap(), "Coucou"); assert_eq!(conference.clone().password.unwrap(), "secret"); assert_eq!(conference.clone().extensions.len(), 1); assert!(conference.clone().extensions[0].is("test", "urn:xmpp:unknown")); } #[test] fn wrapped() { let elem: Element = "Coucousecret".parse().unwrap(); let item = PubSubItem::try_from(elem).unwrap(); let payload = item.payload.clone().unwrap(); println!("FOO: payload: {:?}", payload); // let conference = Conference::try_from(payload).unwrap(); let conference = Conference::try_from(payload).unwrap(); println!("FOO: conference: {:?}", conference); assert_eq!(conference.autojoin, Autojoin::True); assert_eq!(conference.name, Some(String::from("Test MUC"))); assert_eq!(conference.clone().nick.unwrap(), "Coucou"); assert_eq!(conference.clone().password.unwrap(), "secret"); let elem: Element = "Coucousecret".parse().unwrap(); let mut items = match PubSubEvent::try_from(elem) { Ok(PubSubEvent::PublishedItems { node, items }) => { assert_eq!(&node.0, ns::BOOKMARKS2); items } _ => panic!(), }; assert_eq!(items.len(), 1); let item = items.pop().unwrap(); let payload = item.payload.clone().unwrap(); let conference = Conference::try_from(payload).unwrap(); assert_eq!(conference.autojoin, Autojoin::True); assert_eq!(conference.name, Some(String::from("Test MUC"))); assert_eq!(conference.clone().nick.unwrap(), "Coucou"); assert_eq!(conference.clone().password.unwrap(), "secret"); } }