parsers/xmpp: MUC bookmarks have nickname typed as ResourcePart

xmppftw created

Change summary

parsers/ChangeLog            |  2 ++
parsers/src/bookmarks.rs     | 22 +++++++++++++++++++---
parsers/src/bookmarks2.rs    | 23 ++++++++++++++++++-----
xmpp/ChangeLog               |  3 +++
xmpp/examples/hello_bot.rs   |  6 ++++--
xmpp/src/agent.rs            | 10 +++++-----
xmpp/src/builder.rs          | 10 +++++-----
xmpp/src/lib.rs              |  5 +++--
xmpp/src/muc/room.rs         | 10 +++++-----
xmpp/src/presence/receive.rs |  2 +-
10 files changed, 65 insertions(+), 28 deletions(-)

Detailed changes

parsers/ChangeLog πŸ”—

@@ -34,6 +34,8 @@ XXXX-YY-ZZ RELEASER <admin@example.com>
         use them just like normal booleans. The following structs are deprecated:
         pubsub::pubsub::Notify, bookmarks2::Autojoin, extdisco::Restricted,
         fast::Tls0Rtt, legacy_omemo::IsPreKey, mam::Complete, sm::ResumeAttr (!476)
+      - bookmarks::Conference and bookmarks2::Conference use ResourcePart to store
+        the optional nickname instead of a String (!485)
     * Improvements:
       - Keep unsupported vCard elements as `minidom::Element`, so that they
         get serialized back instead of being dropped.  We now also test for

parsers/src/bookmarks.rs πŸ”—

@@ -19,6 +19,7 @@ use xso::{AsXml, FromXml};
 use jid::BareJid;
 
 pub use crate::bookmarks2;
+use crate::jid::ResourcePart;
 use crate::ns;
 
 /// A conference bookmark.
@@ -38,8 +39,8 @@ pub struct Conference {
     pub name: Option<String>,
 
     /// The nick the user will use to join this conference.
-    #[xml(extract(default, fields(text(type_ = String))))]
-    pub nick: Option<String>,
+    #[xml(extract(default, fields(text(type_ = ResourcePart))))]
+    pub nick: Option<ResourcePart>,
 
     /// The password required to join this conference.
     #[xml(extract(default, fields(text(type_ = String))))]
@@ -129,6 +130,18 @@ mod tests {
         assert_eq!(elem1, elem2);
     }
 
+    #[test]
+    fn wrong_resource() {
+        // This emoji is not valid according to Resource prep
+        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();
+        let res = Storage::try_from(elem);
+        assert!(res.is_err());
+        assert_eq!(
+            res.unwrap_err().to_string().as_str(),
+            "text parse error: resource doesn’t pass resourceprep validation"
+        );
+    }
+
     #[test]
     fn complete() {
         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();
@@ -143,7 +156,10 @@ mod tests {
             BareJid::new("test-muc@muc.localhost").unwrap()
         );
         assert_eq!(storage.conferences[0].clone().name.unwrap(), "Test MUC");
-        assert_eq!(storage.conferences[0].clone().nick.unwrap(), "Coucou");
+        assert_eq!(
+            storage.conferences[0].clone().nick.unwrap().as_str(),
+            "Coucou"
+        );
         assert_eq!(storage.conferences[0].clone().password.unwrap(), "secret");
     }
 }

parsers/src/bookmarks2.rs πŸ”—

@@ -14,6 +14,7 @@
 
 use xso::{AsXml, FromXml};
 
+use crate::jid::ResourcePart;
 use crate::ns;
 use minidom::Element;
 
@@ -39,8 +40,8 @@ pub struct Conference {
     pub name: Option<String>,
 
     /// The nick the user will use to join this conference.
-    #[xml(extract(default, fields(text(type_ = String))))]
-    pub nick: Option<String>,
+    #[xml(extract(default, fields(text(type_ = ResourcePart))))]
+    pub nick: Option<ResourcePart>,
 
     /// The password required to join this conference.
     #[xml(extract(default, fields(text(type_ = String))))]
@@ -91,13 +92,25 @@ mod tests {
         assert_eq!(elem1, elem2);
     }
 
+    #[test]
+    fn wrong_resource() {
+        // This emoji is not valid according to Resource prep
+        let elem: Element = "<conference xmlns='urn:xmpp:bookmarks:1' autojoin='true'><nick>Whatever\u{1F469}\u{1F3FE}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F469}\u{1F3FC}</nick></conference>".parse().unwrap();
+        let res = Conference::try_from(elem);
+        assert!(res.is_err());
+        assert_eq!(
+            res.unwrap_err().to_string().as_str(),
+            "text parse error: resource doesn’t pass resourceprep validation"
+        );
+    }
+
     #[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, true);
         assert_eq!(conference.name, Some(String::from("Test MUC")));
-        assert_eq!(conference.clone().nick.unwrap(), "Coucou");
+        assert_eq!(conference.clone().nick.unwrap().as_str(), "Coucou");
         assert_eq!(conference.clone().password.unwrap(), "secret");
         let payloads = conference.clone().extensions.unwrap().payloads;
         assert_eq!(payloads.len(), 1);
@@ -115,7 +128,7 @@ mod tests {
         println!("FOO: conference: {:?}", conference);
         assert_eq!(conference.autojoin, true);
         assert_eq!(conference.name, Some(String::from("Test MUC")));
-        assert_eq!(conference.clone().nick.unwrap(), "Coucou");
+        assert_eq!(conference.clone().nick.unwrap().as_str(), "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();
@@ -132,7 +145,7 @@ mod tests {
         let conference = Conference::try_from(payload).unwrap();
         assert_eq!(conference.autojoin, true);
         assert_eq!(conference.name, Some(String::from("Test MUC")));
-        assert_eq!(conference.clone().nick.unwrap(), "Coucou");
+        assert_eq!(conference.clone().nick.unwrap().as_str(), "Coucou");
         assert_eq!(conference.clone().password.unwrap(), "secret");
     }
 }

xmpp/ChangeLog πŸ”—

@@ -5,6 +5,9 @@ XXXX-YY-ZZ [ RELEASER <admin@localhost> ]
         Agent now handles MUC connection states internally. (!481)
       - Agent::leave_room now takes LeaveRoomSettings argument (!483)
       - Agent::join_room now takes JoinRoomSettings argument (!483)
+      - builder::ClientBuilder::set_default_nick no longer takes a &str, but
+        any type that implements AsRef<jid::ResourceRef>, such as produced
+        by ResourcePart::new (!485)
     * Added:
       - Agent::send_room_message takes RoomMessageSettings argument (!483)
     * Fixes:

xmpp/examples/hello_bot.rs πŸ”—

@@ -6,7 +6,7 @@
 
 use std::env::args;
 use std::str::FromStr;
-use tokio_xmpp::jid::BareJid;
+use tokio_xmpp::jid::{BareJid, ResourcePart};
 use xmpp::muc::room::{JoinRoomSettings, RoomMessageSettings};
 use xmpp::{ClientBuilder, ClientFeature, ClientType, Event};
 
@@ -39,11 +39,13 @@ async fn main() -> Result<(), Option<()>> {
         }
     }
 
+    let nick = ResourcePart::new("bot").unwrap();
+
     // Client instance
     let mut client = ClientBuilder::new(jid, password)
         .set_client(ClientType::Bot, "xmpp-rs")
         .set_website("https://gitlab.com/xmpp-rs/xmpp-rs")
-        .set_default_nick("bot")
+        .set_default_nick(nick)
         .enable_feature(ClientFeature::ContactList)
         .enable_feature(ClientFeature::JoinRooms)
         .build();

xmpp/src/agent.rs πŸ”—

@@ -11,7 +11,7 @@ use tokio::sync::RwLock;
 pub use tokio_xmpp::parsers;
 use tokio_xmpp::parsers::{disco::DiscoInfoResult, message::MessageType};
 pub use tokio_xmpp::{
-    jid::{BareJid, FullJid, Jid},
+    jid::{BareJid, FullJid, Jid, ResourcePart},
     minidom::Element,
     Client as TokioXmppClient,
 };
@@ -20,16 +20,16 @@ use crate::{event_loop, message, muc, upload, Error, Event, RoomNick};
 
 pub struct Agent {
     pub(crate) client: TokioXmppClient,
-    pub(crate) default_nick: Arc<RwLock<String>>,
+    pub(crate) default_nick: Arc<RwLock<ResourcePart>>,
     pub(crate) lang: Arc<Vec<String>>,
     pub(crate) disco: DiscoInfoResult,
     pub(crate) node: String,
     pub(crate) uploads: Vec<(String, Jid, PathBuf)>,
     pub(crate) awaiting_disco_bookmarks_type: bool,
     // Mapping of room->nick
-    pub(crate) rooms_joined: HashMap<BareJid, String>,
-    pub(crate) rooms_joining: HashMap<BareJid, String>,
-    pub(crate) rooms_leaving: HashMap<BareJid, String>,
+    pub(crate) rooms_joined: HashMap<BareJid, ResourcePart>,
+    pub(crate) rooms_joining: HashMap<BareJid, ResourcePart>,
+    pub(crate) rooms_leaving: HashMap<BareJid, ResourcePart>,
 }
 
 impl Agent {

xmpp/src/builder.rs πŸ”—

@@ -11,7 +11,7 @@ use tokio::sync::RwLock;
 use tokio_xmpp::connect::{DnsConfig, StartTlsServerConnector};
 use tokio_xmpp::{
     connect::ServerConnector,
-    jid::{BareJid, Jid},
+    jid::{BareJid, Jid, ResourcePart, ResourceRef},
     parsers::{
         disco::{DiscoInfoResult, Feature, Identity},
         ns,
@@ -48,7 +48,7 @@ pub struct ClientBuilder<'a, C: ServerConnector> {
     password: &'a str,
     server_connector: C,
     website: String,
-    default_nick: String,
+    default_nick: ResourcePart,
     lang: Vec<String>,
     disco: (ClientType, String),
     features: Vec<ClientFeature>,
@@ -78,7 +78,7 @@ impl<C: ServerConnector> ClientBuilder<'_, C> {
             password,
             server_connector,
             website: String::from("https://gitlab.com/xmpp-rs/tokio-xmpp"),
-            default_nick: String::from("xmpp-rs"),
+            default_nick: ResourcePart::new("xmpp-rs").unwrap().into(),
             lang: vec![String::from("en")],
             disco: (ClientType::default(), String::from("tokio-xmpp")),
             features: vec![],
@@ -103,8 +103,8 @@ impl<C: ServerConnector> ClientBuilder<'_, C> {
         self
     }
 
-    pub fn set_default_nick(mut self, nick: &str) -> Self {
-        self.default_nick = String::from(nick);
+    pub fn set_default_nick(mut self, nick: impl AsRef<ResourceRef>) -> Self {
+        self.default_nick = nick.as_ref().to_owned();
         self
     }
 

xmpp/src/lib.rs πŸ”—

@@ -44,7 +44,7 @@ pub type RoomNick = String;
 /*
 #[cfg(all(test, any(feature = "starttls-rust", feature = "starttls-native")))]
 mod tests {
-    use super::jid::BareJid;
+    use super::jid::{BareJid, ResourcePart};
     use super::{ClientBuilder, ClientFeature, ClientType, Event};
     use std::str::FromStr;
     use tokio_xmpp::Client as TokioXmppClient;
@@ -52,6 +52,7 @@ mod tests {
     #[tokio::test]
     async fn test_simple() {
         let jid = BareJid::from_str("foo@bar").unwrap();
+        let nick = ResourcePart::new("bot").unwrap();
 
         let client = TokioXmppClient::new(jid.clone(), "meh");
 
@@ -59,7 +60,7 @@ mod tests {
         let client_builder = ClientBuilder::new(jid, "meh")
             .set_client(ClientType::Bot, "xmpp-rs")
             .set_website("https://gitlab.com/xmpp-rs/xmpp-rs")
-            .set_default_nick("bot")
+            .set_default_nick(nick)
             .enable_feature(ClientFeature::ContactList);
 
         #[cfg(feature = "avatars")]

xmpp/src/muc/room.rs πŸ”—

@@ -6,7 +6,7 @@
 
 use crate::parsers::message::MessageType;
 use tokio_xmpp::{
-    jid::BareJid,
+    jid::{BareJid, ResourcePart, ResourceRef},
     parsers::{
         muc::Muc,
         presence::{Presence, Type as PresenceType},
@@ -18,7 +18,7 @@ use crate::Agent;
 #[derive(Clone, Debug)]
 pub struct JoinRoomSettings<'a> {
     pub room: BareJid,
-    pub nick: Option<String>,
+    pub nick: Option<ResourcePart>,
     pub password: Option<String>,
     pub status: Option<(&'a str, &'a str)>,
 }
@@ -33,7 +33,7 @@ impl<'a> JoinRoomSettings<'a> {
         }
     }
 
-    pub fn with_nick(mut self, nick: impl AsRef<str>) -> Self {
+    pub fn with_nick(mut self, nick: impl AsRef<ResourceRef>) -> Self {
         self.nick = Some(nick.as_ref().into());
         self
     }
@@ -81,7 +81,7 @@ pub async fn join_room<'a>(agent: &mut Agent, settings: JoinRoomSettings<'a>) {
         agent.default_nick.read().await.clone()
     };
 
-    let room_jid = room.with_resource_str(&nick).unwrap();
+    let room_jid = room.with_resource(&nick);
     let mut presence = Presence::new(PresenceType::None).with_to(room_jid);
     presence.add_payload(muc);
 
@@ -156,7 +156,7 @@ pub async fn leave_room<'a>(agent: &mut Agent, settings: LeaveRoomSettings<'a>)
         error!("Failed to send leave room presence: {}", e);
     }
 
-    agent.rooms_leaving.insert(room, nickname.to_string());
+    agent.rooms_leaving.insert(room, nickname.clone());
 }
 
 #[derive(Clone, Debug)]

xmpp/src/presence/receive.rs πŸ”—

@@ -35,7 +35,7 @@ pub async fn handle_presence(agent: &mut Agent, presence: Presence) -> Vec<Event
                 PresenceType::None => {
                     // According to https://xmpp.org/extensions/xep-0045.html#enter-pres, no type should be seen as "available".
                     if let Some(nick) = agent.rooms_joining.get(&from) {
-                        agent.rooms_joined.insert(from.clone(), nick.to_string());
+                        agent.rooms_joined.insert(from.clone(), nick.clone());
                         agent.rooms_joining.remove(&from);
                     } else {
                         warn!("Received self-presence from {} while the room was not marked as joining.", presence.from.unwrap());