//! Handling of all client2server presence stanzas use anyhow::Result; use quick_xml::events::Event; use lavina_core::room::RoomId; use proto_xmpp::bind::{Jid, Name, Resource, Server}; use proto_xmpp::client::{Message, MessageType, Presence, Subject}; use proto_xmpp::muc::{Affiliation, Delay, Role, XUser, XUserItem, XmppHistoryMessage}; use proto_xmpp::xml::{Ignore, ToXml}; use crate::XmppConnection; impl<'a> XmppConnection<'a> { #[tracing::instrument(skip(self, output, p), name = "XmppConnection::handle_presence")] pub async fn handle_presence(&mut self, output: &mut Vec>, p: Presence) -> Result<()> { match p.to { None => { self.self_presence(output, p.r#type.as_deref()).await; } Some(Jid { name: Some(name), server, // resources in MUCs are members' personas – not implemented (yet?) resource: Some(_), }) if server.0 == self.hostname_rooms => match p.r#type.as_deref() { None => { self.join_muc(output, p.id, name).await?; } Some("unavailable") => { self.leave_muc(output, p.id, name).await?; } _ => { tracing::error!("Unimplemented case") } }, _ => { // TODO other presence cases let response = Presence::<()>::default(); response.serialize(output); } } Ok(()) } async fn join_muc(&mut self, output: &mut Vec>, id: Option, name: Name) -> Result<()> { // Response presence let mut muc_presence = self.retrieve_muc_presence(&name).await?; muc_presence.id = id; muc_presence.serialize(output); // N last messages from the room history before the user joined let messages = self.retrieve_message_history(&name).await?; for message in messages { message.serialize(output) } // The subject is the last stanza sent during a MUC join process. let subject = Message::<()> { from: Some(Jid { name: Some(name.clone()), server: Server(self.hostname_rooms.clone()), resource: None, }), id: None, to: Some(Jid { name: Some(self.user.xmpp_name.clone()), server: Server(self.hostname.clone()), resource: Some(self.user.xmpp_resource.clone()), }), r#type: MessageType::Groupchat, lang: None, subject: Some(Subject(None)), body: None, custom: vec![], }; subject.serialize(output); Ok(()) } async fn leave_muc(&mut self, output: &mut Vec>, id: Option, name: Name) -> Result<()> { self.user_handle.leave_room(RoomId::try_from(name.0.clone())?).await?; let response = Presence { id, to: Some(Jid { name: Some(self.user.xmpp_name.clone()), server: Server(self.hostname.clone()), resource: Some(self.user.xmpp_resource.clone()), }), from: Some(Jid { name: Some(name.clone()), server: Server(self.hostname_rooms.clone()), resource: Some(self.user.xmpp_muc_name.clone()), }), r#type: Some("unavailable".into()), custom: vec![XUser { item: XUserItem { affiliation: Affiliation::Member, role: Role::None, jid: Jid { name: Some(self.user.xmpp_name.clone()), server: Server(self.hostname.clone()), resource: Some(self.user.xmpp_resource.clone()), }, }, self_presence: true, just_created: false, }], ..Default::default() }; response.serialize(output); Ok(()) } #[tracing::instrument(skip(self, output, r#type), name = "XmppConnection::self_presence")] async fn self_presence(&mut self, output: &mut Vec>, r#type: Option<&str>) { match r#type { Some("unavailable") => { // do not print anything } None => { let response = Presence::<()> { to: Some(Jid { name: Some(self.user.xmpp_name.clone()), server: Server(self.hostname.clone()), resource: Some(self.user.xmpp_resource.clone()), }), from: Some(Jid { name: Some(self.user.xmpp_name.clone()), server: Server(self.hostname.clone()), resource: Some(self.user.xmpp_resource.clone()), }), ..Default::default() }; response.serialize(output); } e => { tracing::error!("TODO: unknown presence type: {e:?}"); } } } #[tracing::instrument(skip(self), name = "XmppConnection::retrieve_muc_presence")] async fn retrieve_muc_presence(&mut self, name: &Name) -> Result> { let _ = self.user_handle.join_room(RoomId::try_from(name.0.clone())?).await?; // TODO handle bans let response = Presence { to: Some(Jid { name: Some(self.user.xmpp_name.clone()), server: Server(self.hostname.clone()), resource: Some(self.user.xmpp_resource.clone()), }), from: Some(Jid { name: Some(name.clone()), server: Server(self.hostname_rooms.clone()), resource: Some(self.user.xmpp_muc_name.clone()), }), custom: vec![XUser { item: XUserItem { affiliation: Affiliation::Member, role: Role::Participant, jid: Jid { name: Some(self.user.xmpp_name.clone()), server: Server(self.hostname.clone()), resource: Some(self.user.xmpp_resource.clone()), }, }, self_presence: true, just_created: false, // TODO we don't know this for sure at this point }], ..Default::default() }; Ok(response) } /// Retrieve a room's message history. The output can be serialized into a stream of XML stanzas. /// /// Example in [XmppHistoryMessage]'s docs. #[tracing::instrument(skip(self), name = "XmppConnection::retrieve_message_history")] async fn retrieve_message_history(&self, room_name: &Name) -> Result> { let room_id = RoomId::try_from(room_name.0.clone())?; let history_messages = self.user_handle.get_room_message_history(room_id).await?; let mut response = vec![]; for history_message in history_messages.into_iter() { response.push(XmppHistoryMessage { id: history_message.id.to_string(), to: Jid { name: Option::from(Name(self.user.xmpp_muc_name.0.clone().into())), server: Server(self.hostname.clone()), resource: None, }, from: Jid { name: Option::from(room_name.clone()), server: Server(self.hostname_rooms.clone()), resource: Option::from(Resource(history_message.author_name.clone().into())), }, delay: Delay { from: Jid { name: Option::from(Name(history_message.author_name.clone().into())), server: Server(self.hostname_rooms.clone()), resource: None, }, stamp: history_message.created_at.to_rfc3339(), }, body: history_message.content.clone(), }); tracing::info!( "Retrieved message: {:?} {:?}", history_message.author_name, history_message.content.clone() ); } return Ok(response); } } #[cfg(test)] mod tests { use anyhow::Result; use lavina_core::player::PlayerId; use proto_xmpp::bind::{Jid, Name, Resource, Server}; use proto_xmpp::client::Presence; use proto_xmpp::muc::{Affiliation, Role, XUser, XUserItem}; use crate::testkit::{expect_user_authenticated, TestServer}; use crate::Authenticated; #[tokio::test] async fn test_muc_joining() -> Result<()> { let server = TestServer::start().await.unwrap(); server.core.create_player(&PlayerId::from("tester")?).await?; let player_id = PlayerId::from("tester").unwrap(); let user = Authenticated { player_id, xmpp_name: Name("tester".into()), xmpp_resource: Resource("tester".into()), xmpp_muc_name: Resource("tester".into()), }; let mut player_conn = server.core.connect_to_player(&user.player_id).await; let mut conn = expect_user_authenticated(&server, &user, &mut player_conn).await.unwrap(); let muc_presence = conn.retrieve_muc_presence(&user.xmpp_name).await.unwrap(); let expected = Presence { to: Some(Jid { name: Some(conn.user.xmpp_name.clone()), server: Server(conn.hostname.clone()), resource: Some(conn.user.xmpp_resource.clone()), }), from: Some(Jid { name: Some(user.xmpp_name.clone()), server: Server(conn.hostname_rooms.clone()), resource: Some(conn.user.xmpp_muc_name.clone()), }), custom: vec![XUser { item: XUserItem { affiliation: Affiliation::Member, role: Role::Participant, jid: Jid { name: Some(conn.user.xmpp_name.clone()), server: Server(conn.hostname.clone()), resource: Some(conn.user.xmpp_resource.clone()), }, }, self_presence: true, just_created: false, }], ..Default::default() }; assert_eq!(expected, muc_presence); server.shutdown().await.unwrap(); Ok(()) } // Test that joining a room second time after a server restart, // i.e. in-memory cache of memberships is cleaned, does not cause any issues. #[tokio::test] async fn test_muc_joining_twice() -> Result<()> { let server = TestServer::start().await.unwrap(); server.core.create_player(&PlayerId::from("tester")?).await?; let player_id = PlayerId::from("tester").unwrap(); let user = Authenticated { player_id, xmpp_name: Name("tester".into()), xmpp_resource: Resource("tester".into()), xmpp_muc_name: Resource("tester".into()), }; let mut player_conn = server.core.connect_to_player(&user.player_id).await; let mut conn = expect_user_authenticated(&server, &user, &mut player_conn).await.unwrap(); let response = conn.retrieve_muc_presence(&user.xmpp_name).await.unwrap(); let expected = Presence { to: Some(Jid { name: Some(conn.user.xmpp_name.clone()), server: Server(conn.hostname.clone()), resource: Some(conn.user.xmpp_resource.clone()), }), from: Some(Jid { name: Some(user.xmpp_name.clone()), server: Server(conn.hostname_rooms.clone()), resource: Some(conn.user.xmpp_muc_name.clone()), }), custom: vec![XUser { item: XUserItem { affiliation: Affiliation::Member, role: Role::Participant, jid: Jid { name: Some(conn.user.xmpp_name.clone()), server: Server(conn.hostname.clone()), resource: Some(conn.user.xmpp_resource.clone()), }, }, self_presence: true, just_created: false, }], ..Default::default() }; assert_eq!(expected, response); drop(conn); let server = server.reboot().await.unwrap(); let mut player_conn = server.core.connect_to_player(&user.player_id).await; let mut conn = expect_user_authenticated(&server, &user, &mut player_conn).await.unwrap(); let response = conn.retrieve_muc_presence(&user.xmpp_name).await.unwrap(); assert_eq!(expected, response); server.shutdown().await.unwrap(); Ok(()) } }