xmpp: new Config struct for Agent, with bookmarks_autojoin for now

pep created

Signed-off-by: pep <pep@bouah.net>

Change summary

xmpp/ChangeLog         |  1 +
xmpp/src/agent.rs      | 12 +++++++++++-
xmpp/src/builder.rs    | 18 ++++++++++++++++--
xmpp/src/config.rs     | 31 +++++++++++++++++++++++++++++++
xmpp/src/lib.rs        |  2 ++
xmpp/src/pubsub/mod.rs | 38 +++++++++++++++++++++++---------------
6 files changed, 84 insertions(+), 18 deletions(-)

Detailed changes

xmpp/ChangeLog 🔗

@@ -43,6 +43,7 @@ XXXX-YY-ZZ [ RELEASER <admin@localhost> ]
         stanzas.
       - Added documentation on `Event` enum.
       - Update to edition 2024
+      - Add Config::Config struct as a user-facing Agent configuration method.
     * Fixes:
       - Use tokio::sync::RwLock not std::sync::RwLock (!432)
       - The default caps node has been shortened to https://xmpp.rs since we

xmpp/src/agent.rs 🔗

@@ -12,7 +12,7 @@ use tokio::io;
 use tokio::sync::RwLock;
 
 use crate::{
-    Error, Event, RoomNick, event_loop,
+    Config, Error, Event, RoomNick, event_loop,
     jid::{BareJid, Jid},
     message, muc,
     parsers::disco::DiscoInfoResult,
@@ -24,6 +24,7 @@ use tokio_xmpp::{Stanza, stanzastream::StanzaToken};
 
 pub struct Agent {
     pub(crate) client: TokioXmppClient,
+    pub(crate) config: Arc<RwLock<Config>>,
     pub(crate) default_nick: Arc<RwLock<RoomNick>>,
     pub(crate) lang: Arc<Vec<String>>,
     pub(crate) disco: DiscoInfoResult,
@@ -39,6 +40,7 @@ pub struct Agent {
 impl Agent {
     pub fn new(
         client: TokioXmppClient,
+        config: Config,
         default_nick: RoomNick,
         lang: Vec<String>,
         disco: DiscoInfoResult,
@@ -46,6 +48,7 @@ impl Agent {
     ) -> Agent {
         Agent {
             client,
+            config: Arc::new(RwLock::new(config)),
             default_nick: Arc::new(RwLock::new(default_nick)),
             lang: Arc::new(lang),
             disco,
@@ -58,6 +61,13 @@ impl Agent {
         }
     }
 
+    /// Reset the agent configuration to the provided Config struct
+    // TODO: refresh everything that is affected by this reset?
+    pub async fn set_config(&mut self, config: Config) {
+        let mut c = self.config.write().await;
+        *c = config;
+    }
+
     pub async fn disconnect(self) -> Result<(), Error> {
         self.client.send_end().await
     }

xmpp/src/builder.rs 🔗

@@ -9,7 +9,7 @@ use crate::tokio_xmpp::connect::{DnsConfig, StartTlsServerConnector};
 use core::str::FromStr;
 
 use crate::{
-    Agent, ClientFeature, RoomNick,
+    Agent, ClientFeature, Config, RoomNick,
     jid::{BareJid, Jid, ResourceRef},
     parsers::{
         disco::{DiscoInfoResult, Feature, Identity},
@@ -43,6 +43,7 @@ pub struct ClientBuilder<'a, C: ServerConnector> {
     jid: BareJid,
     password: &'a str,
     server_connector: C,
+    config: Config,
     website: String,
     default_nick: RoomNick,
     lang: Vec<String>,
@@ -73,6 +74,7 @@ impl<C: ServerConnector> ClientBuilder<'_, C> {
             jid,
             password,
             server_connector,
+            config: Config::default(),
             website: String::from("https://gitlab.com/xmpp-rs/tokio-xmpp"),
             default_nick: RoomNick::from_str("xmpp-rs").unwrap(),
             lang: vec![String::from("en")],
@@ -89,6 +91,11 @@ impl<C: ServerConnector> ClientBuilder<'_, C> {
         self
     }
 
+    pub fn set_config(mut self, config: Config) -> Self {
+        self.config = config;
+        self
+    }
+
     pub fn set_client(mut self, type_: ClientType, name: &str) -> Self {
         self.disco = (type_, String::from(name));
         self
@@ -169,6 +176,13 @@ impl<C: ServerConnector> ClientBuilder<'_, C> {
         let disco = self.make_disco();
         let node = self.website;
 
-        Agent::new(client, self.default_nick, self.lang, disco, node)
+        Agent::new(
+            client,
+            self.config,
+            self.default_nick,
+            self.lang,
+            disco,
+            node,
+        )
     }
 }

xmpp/src/config.rs 🔗

@@ -0,0 +1,31 @@
+// Copyright (c) 2025 Crate authors
+//
+// 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/.
+
+/// Store Agent configuration. Differs from state which is generated at runtime
+pub struct Config {
+    /// Synchronize bookmarks based on autojoin flag.
+    /// The client will join and leave based on the value of the `autojoin` flag on the (pubsub)
+    /// bookmark item.
+    /// If this `bookmarks_autojoin` attribute is set to false, `autojoin` set to false won't make
+    /// the client leave a room, neither will the removal of a bookmark item. This will only happen
+    /// after the client is restarted, as these items won't be automatically joined anymore.
+    /// <https://xmpp.org/extensions/xep-0402.html#notification>
+    pub bookmarks_autojoin: bool,
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Config {
+    fn new() -> Self {
+        Config {
+            bookmarks_autojoin: true,
+        }
+    }
+}

xmpp/src/lib.rs 🔗

@@ -65,6 +65,7 @@ use parsers::message::Id as MessageId;
 
 pub mod agent;
 pub mod builder;
+pub mod config;
 pub mod delay;
 pub mod disco;
 pub mod event;
@@ -79,6 +80,7 @@ pub mod upload;
 
 pub use agent::Agent;
 pub use builder::{ClientBuilder, ClientType};
+pub use config::Config;
 pub use event::Event;
 pub use feature::ClientFeature;
 

xmpp/src/pubsub/mod.rs 🔗

@@ -66,18 +66,22 @@ pub(crate) async fn handle_event(
                                                 status: None,
                                             })
                                             .await;
+                                    } else {
+                                        if agent.config.read().await.bookmarks_autojoin {
+                                            // So maybe another client of ours left the room... let's leave it too
+                                            agent.leave_room(LeaveRoomSettings::new(jid)).await;
+                                        }
                                     }
-                                } else {
-                                    // So maybe another client of ours left the room... let's leave it too
-                                    agent.leave_room(LeaveRoomSettings::new(jid)).await;
                                 }
                             }
                             Err(err) => println!("not bookmark: {}", err),
                         }
                     } else if let [item] = &retracted[..] {
-                        let jid = BareJid::from_str(&item.0).unwrap();
+                        if agent.config.read().await.bookmarks_autojoin {
+                            let jid = BareJid::from_str(&item.0).unwrap();
 
-                        agent.leave_room(LeaveRoomSettings::new(jid)).await;
+                            agent.leave_room(LeaveRoomSettings::new(jid)).await;
+                        }
                     } else {
                         error!("No published or retracted item in pubsub event!");
                     }
@@ -146,8 +150,10 @@ pub(crate) async fn handle_iq_result(
                                         .await;
                                 }
                             } else {
-                                // Leave the room that is no longer autojoin
-                                agent.leave_room(LeaveRoomSettings::new(jid)).await;
+                                if agent.config.read().await.bookmarks_autojoin {
+                                    // Leave the room that is no longer autojoin
+                                    agent.leave_room(LeaveRoomSettings::new(jid)).await;
+                                }
                             }
                         }
                         Err(err) => {
@@ -156,16 +162,18 @@ pub(crate) async fn handle_iq_result(
                     }
                 }
 
-                // Now we leave the rooms that are no longer in the bookmarks
-                let mut rooms_to_leave: Vec<BareJid> = Vec::new();
-                for (room, _nick) in &agent.rooms_joined {
-                    if !new_room_list.contains(&room) {
-                        rooms_to_leave.push(room.clone());
+                if agent.config.read().await.bookmarks_autojoin {
+                    // Now we leave the rooms that are no longer in the bookmarks
+                    let mut rooms_to_leave: Vec<BareJid> = Vec::new();
+                    for (room, _nick) in &agent.rooms_joined {
+                        if !new_room_list.contains(&room) {
+                            rooms_to_leave.push(room.clone());
+                        }
                     }
-                }
 
-                for room in rooms_to_leave {
-                    agent.leave_room(LeaveRoomSettings::new(room)).await;
+                    for room in rooms_to_leave {
+                        agent.leave_room(LeaveRoomSettings::new(room)).await;
+                    }
                 }
             }
             _ => unimplemented!(),