xmpp-rs/parsers/src/bookmarks2.rs
Emmanuel Gil Peyrot 7b66de1166 xmpp-parsers: Stop reexporting extra symbols
Let’s continue reexporting jid and minidom, but not their inner pub
items, users of this crate can go one level deeper if they need that.

Only xso::error::Error is still useful to reexport, as this is part of
the public API of all of our parsers.
2024-07-25 14:36:09 +00:00

204 lines
8.1 KiB
Rust

// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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<String>,
/// The nick the user will use to join this conference.
pub nick: Option<String>,
/// The password required to join this conference.
pub password: Option<String>,
/// Extensions elements.
pub extensions: Vec<Element>,
}
impl Conference {
/// Create a new conference.
pub fn new() -> Conference {
Conference::default()
}
}
impl TryFrom<Element> for Conference {
type Error = FromElementError;
fn try_from(root: Element) -> Result<Conference, FromElementError> {
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<Conference> 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 = "<conference xmlns='urn:xmpp:bookmarks:1'/>"
.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 = "<conference xmlns='urn:xmpp:bookmarks:1' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password><extensions><test xmlns='urn:xmpp:unknown' /></extensions></conference>".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 = "<item xmlns='http://jabber.org/protocol/pubsub' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:1' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></item>".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 = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='urn:xmpp:bookmarks:1'><item xmlns='http://jabber.org/protocol/pubsub#event' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:1' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></item></items></event>".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");
}
}