Add JoinRoomSettings, LeaveRoomSettings, and RoomMessageSettings

xmppftw created

Change summary

xmpp/ChangeLog             |   6 +
xmpp/examples/hello_bot.rs |  38 ++++-------
xmpp/src/agent.rs          |  31 ++------
xmpp/src/iq/result.rs      |  11 ++
xmpp/src/muc/room.rs       | 134 +++++++++++++++++++++++++++++++++------
xmpp/src/pubsub/mod.rs     |  39 +++++-----
6 files changed, 167 insertions(+), 92 deletions(-)

Detailed changes

xmpp/ChangeLog 🔗

@@ -1,8 +1,12 @@
 Version NEXT
 XXXX-YY-ZZ [ RELEASER <admin@localhost> ]
-    * Breaking chagnes:
+    * Breaking:
       - Event::LeaveRoom, Event::LeaveAllRooms, and Event::JoinRooms have been removed.
         Agent now handles MUC connection states internally. (!481)
+      - Agent::leave_room now takes LeaveRoomSettings argument (!483)
+      - Agent::join_room now takes JoinRoomSettings argument (!483)
+    * Added:
+      - Agent::send_room_message takes RoomMessageSettings argument (!483)
     * Fixes:
       - Use tokio::sync::RwLock not std::sync::RwLock (!432)
       - Agent::wait_for_events now return Vec<Event> and sets inner tokio_xmpp Client

xmpp/examples/hello_bot.rs 🔗

@@ -6,8 +6,8 @@
 
 use std::env::args;
 use std::str::FromStr;
-use tokio_xmpp::jid::{BareJid, Jid};
-use tokio_xmpp::parsers::message::MessageType;
+use tokio_xmpp::jid::BareJid;
+use xmpp::muc::room::RoomMessageSettings;
 use xmpp::{ClientBuilder, ClientFeature, ClientType, Event};
 
 #[tokio::main]
@@ -28,7 +28,6 @@ async fn main() -> Result<(), Option<()>> {
         .set_client(ClientType::Bot, "xmpp-rs")
         .set_website("https://gitlab.com/xmpp-rs/xmpp-rs")
         .set_default_nick("bot")
-        .enable_feature(ClientFeature::Avatars)
         .enable_feature(ClientFeature::ContactList)
         .enable_feature(ClientFeature::JoinRooms)
         .build();
@@ -37,43 +36,34 @@ async fn main() -> Result<(), Option<()>> {
         for event in client.wait_for_events().await {
             match event {
                 Event::Online => {
-                    println!("Online.");
+                    log::info!("Online.");
                 }
                 Event::Disconnected(e) => {
-                    println!("Disconnected because of {}.", e);
-                    return Err(None);
-                }
-                Event::ContactAdded(contact) => {
-                    println!("Contact {} added.", contact.jid);
-                }
-                Event::ContactRemoved(contact) => {
-                    println!("Contact {} removed.", contact.jid);
-                }
-                Event::ContactChanged(contact) => {
-                    println!("Contact {} changed.", contact.jid);
+                    log::info!("Disconnected: {}.", e);
                 }
                 Event::ChatMessage(_id, jid, body, time_info) => {
-                    println!("Message from {} at {}: {}", jid, time_info.received, body.0);
+                    log::info!(
+                        "{} {}: {}",
+                        time_info.received.time().format("%H:%M"),
+                        jid,
+                        body.0
+                    );
                 }
                 Event::RoomJoined(jid) => {
-                    println!("Joined room {}.", jid);
+                    log::info!("Joined room {}.", jid);
                     client
-                        .send_message(Jid::from(jid), MessageType::Groupchat, "en", "Hello world!")
+                        .send_room_message(RoomMessageSettings::new(jid, "Hello world!"))
                         .await;
                 }
-                Event::RoomLeft(jid) => {
-                    println!("Left room {}.", jid);
-                }
                 Event::RoomMessage(_id, jid, nick, body, time_info) => {
                     println!(
                         "Message in room {} from {} at {}: {}",
                         jid, nick, time_info.received, body.0
                     );
                 }
-                Event::AvatarRetrieved(jid, path) => {
-                    println!("Received avatar for {} in {}.", jid, path);
+                _ => {
+                    log::debug!("Unimplemented event:\n{:#?}", event);
                 }
-                _ => (),
             }
         }
     }

xmpp/src/agent.rs 🔗

@@ -38,35 +38,16 @@ impl<C: ServerConnector> Agent<C> {
         self.client.send_end().await
     }
 
-    pub async fn join_room(
-        &mut self,
-        room: BareJid,
-        nick: Option<String>,
-        password: Option<String>,
-        lang: &str,
-        status: &str,
-    ) {
-        muc::room::join_room(self, room, nick, password, lang, status).await
+    pub async fn join_room<'a>(&mut self, settings: muc::room::JoinRoomSettings<'a>) {
+        muc::room::join_room(self, settings).await
     }
 
     /// Request to leave a chatroom.
     ///
     /// If successful, an [Event::RoomLeft] event will be produced. This method does not remove the room
     /// from bookmarks nor remove the autojoin flag. See [muc::room::leave_room] for more information.
-    ///
-    /// # Arguments
-    ///
-    /// * `room_jid`: The JID of the room to leave.
-    /// * `nickname`: The nickname to use in the room.
-    /// * `lang`: The language of the status message (empty string when unknown).
-    /// * `status`: The status message to send.
-    pub async fn leave_room(
-        &mut self,
-        room_jid: BareJid,
-        lang: impl Into<String>,
-        status: impl Into<String>,
-    ) {
-        muc::room::leave_room(self, room_jid, lang, status).await
+    pub async fn leave_room<'a>(&mut self, settings: muc::room::LeaveRoomSettings<'a>) {
+        muc::room::leave_room(self, settings).await
     }
 
     pub async fn send_message(
@@ -79,6 +60,10 @@ impl<C: ServerConnector> Agent<C> {
         message::send::send_message(self, recipient, type_, lang, text).await
     }
 
+    pub async fn send_room_message<'a>(&mut self, settings: muc::room::RoomMessageSettings<'a>) {
+        muc::room::send_room_message(self, settings).await
+    }
+
     pub async fn send_room_private_message(
         &mut self,
         room: BareJid,

xmpp/src/iq/result.rs 🔗

@@ -11,7 +11,7 @@ use tokio_xmpp::{
     parsers::{disco::DiscoInfoResult, ns, private::Query as PrivateXMLQuery, roster::Roster},
 };
 
-use crate::{disco, pubsub, upload, Agent, Event};
+use crate::{disco, muc::room::JoinRoomSettings, pubsub, upload, Agent, Event};
 
 pub async fn handle_iq_result<C: ServerConnector>(
     agent: &mut Agent<C>,
@@ -39,7 +39,14 @@ pub async fn handle_iq_result<C: ServerConnector>(
             Ok(query) => {
                 for conf in query.storage.conferences {
                     let (jid, room) = conf.into_bookmarks2();
-                    agent.join_room(jid, room.nick, room.password, "", "").await;
+                    agent
+                        .join_room(JoinRoomSettings {
+                            room: jid,
+                            nick: room.nick,
+                            password: room.password,
+                            status: None,
+                        })
+                        .await;
                 }
             }
             Err(e) => {

xmpp/src/muc/room.rs 🔗

@@ -4,6 +4,7 @@
 // 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/.
 
+use crate::parsers::message::MessageType;
 use tokio_xmpp::connect::ServerConnector;
 use tokio_xmpp::{
     jid::BareJid,
@@ -15,14 +16,52 @@ use tokio_xmpp::{
 
 use crate::Agent;
 
-pub async fn join_room<C: ServerConnector>(
+#[derive(Clone, Debug)]
+pub struct JoinRoomSettings<'a> {
+    pub room: BareJid,
+    pub nick: Option<String>,
+    pub password: Option<String>,
+    pub status: Option<(&'a str, &'a str)>,
+}
+
+impl<'a> JoinRoomSettings<'a> {
+    pub fn new(room: BareJid) -> Self {
+        Self {
+            room,
+            nick: None,
+            password: None,
+            status: None,
+        }
+    }
+
+    pub fn with_nick(mut self, nick: impl AsRef<str>) -> Self {
+        self.nick = Some(nick.as_ref().into());
+        self
+    }
+
+    pub fn with_password(mut self, password: impl AsRef<str>) -> Self {
+        self.password = Some(password.as_ref().into());
+        self
+    }
+
+    pub fn with_status(mut self, lang: &'a str, content: &'a str) -> Self {
+        self.status = Some((lang, content));
+        self
+    }
+}
+
+/// TODO: this method should add bookmark and ensure autojoin is true
+pub async fn join_room<'a, C: ServerConnector>(
     agent: &mut Agent<C>,
-    room: BareJid,
-    nick: Option<String>,
-    password: Option<String>,
-    lang: &str,
-    status: &str,
+    settings: JoinRoomSettings<'a>,
 ) {
+    let JoinRoomSettings {
+        room,
+        nick,
+        password,
+        status,
+    } = settings;
+
     if agent.rooms_joining.contains_key(&room) {
         // We are already joining
         warn!("Requesting to join again room {room} which is already joining...");
@@ -49,12 +88,32 @@ pub async fn join_room<C: ServerConnector>(
     let room_jid = room.with_resource_str(&nick).unwrap();
     let mut presence = Presence::new(PresenceType::None).with_to(room_jid);
     presence.add_payload(muc);
+
+    let (lang, status) = status.unwrap_or(("", ""));
     presence.set_status(String::from(lang), String::from(status));
+
     let _ = agent.client.send_stanza(presence.into()).await;
 
     agent.rooms_joining.insert(room, nick);
 }
 
+#[derive(Clone, Debug)]
+pub struct LeaveRoomSettings<'a> {
+    pub room: BareJid,
+    pub status: Option<(&'a str, &'a str)>,
+}
+
+impl<'a> LeaveRoomSettings<'a> {
+    pub fn new(room: BareJid) -> Self {
+        Self { room, status: None }
+    }
+
+    pub fn with_status(mut self, lang: &'a str, content: &'a str) -> Self {
+        self.status = Some((lang, content));
+        self
+    }
+}
+
 /// Send a "leave room" request to the server (specifically, an "unavailable" presence stanza).
 ///
 /// The returned future will resolve when the request has been sent,
@@ -62,23 +121,13 @@ pub async fn join_room<C: ServerConnector>(
 ///
 /// If successful, a `RoomLeft` event should be received later as a confirmation. See [XEP-0045](https://xmpp.org/extensions/xep-0045.html#exit).
 ///
-/// Note that this method does NOT remove the room from the auto-join list; the latter
-/// is more a list of bookmarks that the account knows about and that have a flag set
-/// to indicate that they should be joined automatically after connecting (see the JoinRoom event).
-///
-/// Regarding the latter, see the these [ModernXMPP minutes about auto-join behavior](https://docs.modernxmpp.org/meetings/2019-01-brussels/#bookmarks).
-///
-/// # Arguments
-///
-/// * `room_jid`: The JID of the room to leave.
-/// * `lang`: The language of the status message.
-/// * `status`: The status message to send.
-pub async fn leave_room<C: ServerConnector>(
+/// TODO: this method should set autojoin false on bookmark
+pub async fn leave_room<'a, C: ServerConnector>(
     agent: &mut Agent<C>,
-    room: BareJid,
-    lang: impl Into<String>,
-    status: impl Into<String>,
+    settings: LeaveRoomSettings<'a>,
 ) {
+    let LeaveRoomSettings { room, status } = settings;
+
     if agent.rooms_leaving.contains_key(&room) {
         // We are already leaving
         warn!("Requesting to leave again room {room} which is already leaving...");
@@ -97,14 +146,16 @@ pub async fn leave_room<C: ServerConnector>(
     // XEP-0045 specifies that, to leave a room, the client must send a presence stanza
     // with type="unavailable".
     let mut presence = Presence::new(PresenceType::Unavailable).with_to(
-        room.with_resource_str(nickname)
+        room.with_resource_str(nickname.as_str())
             .expect("Invalid room JID after adding resource part."),
     );
 
     // Optionally, the client may include a status message in the presence stanza.
     // TODO: Should this be optional? The XEP says "MAY", but the method signature requires the arguments.
     // XEP-0045: "The occupant MAY include normal <status/> information in the unavailable presence stanzas"
-    presence.set_status(lang, status);
+    if let Some((lang, content)) = status {
+        presence.set_status(lang, content);
+    }
 
     // Send the presence stanza.
     if let Err(e) = agent.client.send_stanza(presence.into()).await {
@@ -114,3 +165,40 @@ pub async fn leave_room<C: ServerConnector>(
 
     agent.rooms_leaving.insert(room, nickname.to_string());
 }
+
+#[derive(Clone, Debug)]
+pub struct RoomMessageSettings<'a> {
+    pub room: BareJid,
+    pub message: &'a str,
+    pub lang: Option<&'a str>,
+}
+
+impl<'a> RoomMessageSettings<'a> {
+    pub fn new(room: BareJid, message: &'a str) -> Self {
+        Self {
+            room,
+            message,
+            lang: None,
+        }
+    }
+}
+
+pub async fn send_room_message<'a, C: ServerConnector>(
+    agent: &mut Agent<C>,
+    settings: RoomMessageSettings<'a>,
+) {
+    let RoomMessageSettings {
+        room,
+        message,
+        lang,
+    } = settings;
+
+    agent
+        .send_message(
+            room.into(),
+            MessageType::Groupchat,
+            lang.unwrap_or(""),
+            message,
+        )
+        .await;
+}

xmpp/src/pubsub/mod.rs 🔗

@@ -5,7 +5,10 @@
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 use super::Agent;
-use crate::Event;
+use crate::{
+    muc::room::{JoinRoomSettings, LeaveRoomSettings},
+    Event,
+};
 use std::str::FromStr;
 use tokio_xmpp::{
     connect::ServerConnector,
@@ -52,18 +55,17 @@ pub(crate) async fn handle_event<C: ServerConnector>(
                             if conference.autojoin {
                                 if !agent.rooms_joined.contains_key(&jid) {
                                     agent
-                                        .join_room(
-                                            jid.clone(),
-                                            conference.nick,
-                                            conference.password,
-                                            "",
-                                            "",
-                                        )
+                                        .join_room(JoinRoomSettings {
+                                            room: jid,
+                                            nick: conference.nick,
+                                            password: conference.password,
+                                            status: None,
+                                        })
                                         .await;
                                 }
                             } else {
                                 // So maybe another client of ours left the room... let's leave it too
-                                agent.leave_room(jid.clone(), "", "").await;
+                                agent.leave_room(LeaveRoomSettings::new(jid)).await;
                             }
                         }
                         Err(err) => println!("not bookmark: {}", err),
@@ -80,7 +82,7 @@ pub(crate) async fn handle_event<C: ServerConnector>(
                     let item = items.clone().pop().unwrap();
                     let jid = BareJid::from_str(&item.0).unwrap();
 
-                    agent.leave_room(jid.clone(), "", "").await;
+                    agent.leave_room(LeaveRoomSettings::new(jid)).await;
                 }
                 ref node => unimplemented!("node {}", node),
             }
@@ -136,18 +138,17 @@ pub(crate) async fn handle_iq_result<C: ServerConnector>(
                             if conference.autojoin {
                                 if !agent.rooms_joined.contains_key(&jid) {
                                     agent
-                                        .join_room(
-                                            jid.clone(),
-                                            conference.nick,
-                                            conference.password,
-                                            "",
-                                            "",
-                                        )
+                                        .join_room(JoinRoomSettings {
+                                            room: jid,
+                                            nick: conference.nick,
+                                            password: conference.password,
+                                            status: None,
+                                        })
                                         .await;
                                 }
                             } else {
                                 // Leave the room that is no longer autojoin
-                                agent.leave_room(jid.clone(), "", "").await;
+                                agent.leave_room(LeaveRoomSettings::new(jid)).await;
                             }
                         }
                         Err(err) => {
@@ -165,7 +166,7 @@ pub(crate) async fn handle_iq_result<C: ServerConnector>(
                 }
 
                 for room in rooms_to_leave {
-                    agent.leave_room(room, "", "").await;
+                    agent.leave_room(LeaveRoomSettings::new(room)).await;
                 }
             }
             _ => unimplemented!(),