forked from lavina/lavina
342 lines
13 KiB
Rust
342 lines
13 KiB
Rust
//! 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<Event<'static>>, p: Presence<Ignore>) -> 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<Event<'static>>, id: Option<String>, 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<Event<'static>>, id: Option<String>, 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<Event<'static>>, 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<Presence<XUser>> {
|
||
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<Vec<XmppHistoryMessage>> {
|
||
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(())
|
||
}
|
||
}
|