bookmarks.rs

  1// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7//!
  8//! Chatroom bookmarks from [XEP-0048](https://xmpp.org/extensions/attic/xep-0048-1.0.html). You should never use this, but use
  9//! [`bookmarks2`][`crate::bookmarks2`], or [`private::Query`][`crate::private::Query`] for legacy servers which do not advertise
 10//! `urn:xmpp:bookmarks:1#compat` on the user's BareJID in a disco info request.
 11//!
 12//! See [ModernXMPP docs](https://docs.modernxmpp.org/client/groupchat/#bookmarks) on how to handle all historic
 13//! and newer specifications for your clients.
 14//!
 15//! The [`Conference`][crate::bookmarks::Conference] struct used in [`private::Query`][`crate::private::Query`] is the one from this module. Only the querying mechanism changes from a legacy PubSub implementation here, to a legacy Private XML Query implementation in that other module. The [`Conference`][crate::bookmarks2::Conference] element from the [`bookmarks2`][crate::bookmarks2] module is a different structure, but conversion is possible from [`bookmarks::Conference`][crate::bookmarks::Conference] to [`bookmarks2::Conference`][crate::bookmarks2::Conference] via the [`Conference::into_bookmarks2`][crate::bookmarks::Conference::into_bookmarks2] method.
 16
 17use xso::{AsXml, FromXml};
 18
 19use jid::BareJid;
 20
 21pub use crate::bookmarks2;
 22use crate::jid::ResourcePart;
 23use crate::ns;
 24
 25/// A conference bookmark.
 26#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
 27#[xml(namespace = ns::BOOKMARKS, name = "conference")]
 28pub struct Conference {
 29    /// Whether a conference bookmark should be joined automatically.
 30    #[xml(attribute(default))]
 31    pub autojoin: bool,
 32
 33    /// The JID of the conference.
 34    #[xml(attribute)]
 35    pub jid: BareJid,
 36
 37    /// A user-defined name for this conference.
 38    #[xml(attribute(default))]
 39    pub name: Option<String>,
 40
 41    /// The nick the user will use to join this conference.
 42    #[xml(extract(default, fields(text(type_ = ResourcePart))))]
 43    pub nick: Option<ResourcePart>,
 44
 45    /// The password required to join this conference.
 46    #[xml(extract(default, fields(text(type_ = String))))]
 47    pub password: Option<String>,
 48}
 49
 50impl Conference {
 51    /// Turns a XEP-0048 Conference element into a XEP-0402 "Bookmarks2" Conference element, in a
 52    /// tuple with the room JID.
 53    pub fn into_bookmarks2(self) -> (BareJid, bookmarks2::Conference) {
 54        (
 55            self.jid,
 56            bookmarks2::Conference {
 57                autojoin: self.autojoin,
 58                name: self.name,
 59                nick: self.nick,
 60                password: self.password,
 61                extensions: None,
 62            },
 63        )
 64    }
 65}
 66
 67/// An URL bookmark.
 68#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
 69#[xml(namespace = ns::BOOKMARKS, name = "url")]
 70pub struct Url {
 71    /// A user-defined name for this URL.
 72    #[xml(attribute(default))]
 73    pub name: Option<String>,
 74
 75    /// The URL of this bookmark.
 76    #[xml(attribute)]
 77    pub url: String,
 78}
 79
 80/// Container element for multiple bookmarks.
 81#[derive(FromXml, AsXml, PartialEq, Debug, Clone, Default)]
 82#[xml(namespace = ns::BOOKMARKS, name = "storage")]
 83pub struct Storage {
 84    /// Conferences the user has expressed an interest in.
 85    #[xml(child(n = ..))]
 86    pub conferences: Vec<Conference>,
 87
 88    /// URLs the user is interested in.
 89    #[xml(child(n = ..))]
 90    pub urls: Vec<Url>,
 91}
 92
 93impl Storage {
 94    /// Create an empty bookmarks storage.
 95    pub fn new() -> Storage {
 96        Storage::default()
 97    }
 98}
 99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use minidom::Element;
104
105    #[cfg(target_pointer_width = "32")]
106    #[test]
107    fn test_size() {
108        assert_size!(Conference, 56);
109        assert_size!(Url, 24);
110        assert_size!(Storage, 24);
111    }
112
113    #[cfg(target_pointer_width = "64")]
114    #[test]
115    fn test_size() {
116        assert_size!(Conference, 112);
117        assert_size!(Url, 48);
118        assert_size!(Storage, 48);
119    }
120
121    #[test]
122    fn empty() {
123        let elem: Element = "<storage xmlns='storage:bookmarks'/>".parse().unwrap();
124        let elem1 = elem.clone();
125        let storage = Storage::try_from(elem).unwrap();
126        assert_eq!(storage.conferences.len(), 0);
127        assert_eq!(storage.urls.len(), 0);
128
129        let elem2 = Element::from(Storage::new());
130        assert_eq!(elem1, elem2);
131    }
132
133    #[test]
134    fn wrong_resource() {
135        // This emoji is not valid according to Resource prep
136        let elem: Element = "<storage xmlns='storage:bookmarks'><url name='Example' url='https://example.com/'/><conference autojoin='true' jid='foo@muc.localhost' name='TEST'><nick>Whatever\u{1F469}\u{1F3FE}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F469}\u{1F3FC}</nick></conference></storage>".parse().unwrap();
137        let res = Storage::try_from(elem);
138        assert!(res.is_err());
139        assert_eq!(
140            res.unwrap_err().to_string().as_str(),
141            "text parse error: resource doesn’t pass resourceprep validation"
142        );
143    }
144
145    #[test]
146    fn complete() {
147        let elem: Element = "<storage xmlns='storage:bookmarks'><url name='Example' url='https://example.org/'/><conference autojoin='true' jid='test-muc@muc.localhost' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></storage>".parse().unwrap();
148        let storage = Storage::try_from(elem).unwrap();
149        assert_eq!(storage.urls.len(), 1);
150        assert_eq!(storage.urls[0].clone().name.unwrap(), "Example");
151        assert_eq!(storage.urls[0].url, "https://example.org/");
152        assert_eq!(storage.conferences.len(), 1);
153        assert_eq!(storage.conferences[0].autojoin, true);
154        assert_eq!(
155            storage.conferences[0].jid,
156            BareJid::new("test-muc@muc.localhost").unwrap()
157        );
158        assert_eq!(storage.conferences[0].clone().name.unwrap(), "Test MUC");
159        assert_eq!(
160            storage.conferences[0].clone().nick.unwrap().as_str(),
161            "Coucou"
162        );
163        assert_eq!(storage.conferences[0].clone().password.unwrap(), "secret");
164    }
165}