diff --git a/xmpp/ChangeLog b/xmpp/ChangeLog index 5f9091ceec174bae0d4a52bcb5aff83f4d19aa6f..7e842f26d9b68de596b24cfbc202cf9f3de1ef9e 100644 --- a/xmpp/ChangeLog +++ b/xmpp/ChangeLog @@ -1,8 +1,12 @@ Version NEXT XXXX-YY-ZZ [ RELEASER ] - * 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 and sets inner tokio_xmpp Client diff --git a/xmpp/examples/hello_bot.rs b/xmpp/examples/hello_bot.rs index 1dac75aa1c0d4890dc0faee5b7fe51781f2af748..6812f434c64cb6033b89f7a8edf4603afe9ff659 100644 --- a/xmpp/examples/hello_bot.rs +++ b/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); } - _ => (), } } } diff --git a/xmpp/src/agent.rs b/xmpp/src/agent.rs index 9a63deb2536d41950fd8055b37580a8fc496b616..5f5421d2745c841207d9a5394952221059c49007 100644 --- a/xmpp/src/agent.rs +++ b/xmpp/src/agent.rs @@ -38,35 +38,16 @@ impl Agent { self.client.send_end().await } - pub async fn join_room( - &mut self, - room: BareJid, - nick: Option, - password: Option, - 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, - status: impl Into, - ) { - 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 Agent { 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, diff --git a/xmpp/src/iq/result.rs b/xmpp/src/iq/result.rs index a67035b4cb5e6bd056a06ebd50eb2b6bc658563b..0f6f3a59d65d9d964211aa74746f1f15adc36647 100644 --- a/xmpp/src/iq/result.rs +++ b/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( agent: &mut Agent, @@ -39,7 +39,14 @@ pub async fn handle_iq_result( 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) => { diff --git a/xmpp/src/muc/room.rs b/xmpp/src/muc/room.rs index 659f07183362bcf68701b58d4055493bd2fe1281..d957df183d25c7deff18c85bc7089dafd3d20be8 100644 --- a/xmpp/src/muc/room.rs +++ b/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( +#[derive(Clone, Debug)] +pub struct JoinRoomSettings<'a> { + pub room: BareJid, + pub nick: Option, + pub password: Option, + 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) -> Self { + self.nick = Some(nick.as_ref().into()); + self + } + + pub fn with_password(mut self, password: impl AsRef) -> 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, - room: BareJid, - nick: Option, - password: Option, - 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( 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( /// /// 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( +/// TODO: this method should set autojoin false on bookmark +pub async fn leave_room<'a, C: ServerConnector>( agent: &mut Agent, - room: BareJid, - lang: impl Into, - status: impl Into, + 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( // 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 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( 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, + settings: RoomMessageSettings<'a>, +) { + let RoomMessageSettings { + room, + message, + lang, + } = settings; + + agent + .send_message( + room.into(), + MessageType::Groupchat, + lang.unwrap_or(""), + message, + ) + .await; +} diff --git a/xmpp/src/pubsub/mod.rs b/xmpp/src/pubsub/mod.rs index 980158f45e5ccac5f91dfaf1645506d86deee5e9..f514baecc18dae864dbce20e353c8d598be61a0d 100644 --- a/xmpp/src/pubsub/mod.rs +++ b/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( 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( 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( 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( } for room in rooms_to_leave { - agent.leave_room(room, "", "").await; + agent.leave_room(LeaveRoomSettings::new(room)).await; } } _ => unimplemented!(),