From 580923814b74ba7e7f6867ef96414a323dbfdce4 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Wed, 22 May 2024 09:29:44 +0000 Subject: [PATCH] xmpp: send user-item and empty room subject on muc join (#69) Co-authored-by: Mikhail Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/69 --- crates/projection-xmpp/src/message.rs | 6 +- crates/projection-xmpp/src/presence.rs | 74 ++++++++++++++++-- crates/proto-xmpp/src/client.rs | 79 ++++++++++++------- crates/proto-xmpp/src/muc/mod.rs | 101 ++++++++++++++++++++++++- 4 files changed, 220 insertions(+), 40 deletions(-) diff --git a/crates/projection-xmpp/src/message.rs b/crates/projection-xmpp/src/message.rs index 15a3e0d..f490ace 100644 --- a/crates/projection-xmpp/src/message.rs +++ b/crates/projection-xmpp/src/message.rs @@ -20,7 +20,8 @@ impl<'a> XmppConnection<'a> { }) = m.to { if server.0.as_ref() == &*self.hostname_rooms && m.r#type == MessageType::Groupchat { - self.user_handle.send_message(RoomId::from(name.0.clone())?, m.body.clone().into()).await?; + let Some(body) = &m.body else { return Ok(()) }; + self.user_handle.send_message(RoomId::from(name.0.clone())?, body.clone()).await?; Message::<()> { to: Some(Jid { name: Some(self.user.xmpp_name.clone()), @@ -42,7 +43,8 @@ impl<'a> XmppConnection<'a> { .serialize(output); Ok(()) } else if server.0.as_ref() == &*self.hostname && m.r#type == MessageType::Chat { - self.user_handle.send_dialog_message(PlayerId::from(name.0.clone())?, m.body.clone()).await?; + let Some(body) = &m.body else { return Ok(()) }; + self.user_handle.send_dialog_message(PlayerId::from(name.0.clone())?, body.clone()).await?; Ok(()) } else { todo!() diff --git a/crates/projection-xmpp/src/presence.rs b/crates/projection-xmpp/src/presence.rs index 3712baa..8527e2e 100644 --- a/crates/projection-xmpp/src/presence.rs +++ b/crates/projection-xmpp/src/presence.rs @@ -5,8 +5,8 @@ use quick_xml::events::Event; use lavina_core::room::RoomId; use proto_xmpp::bind::{Jid, Name, Server}; -use proto_xmpp::client::Presence; -use proto_xmpp::muc::XUser; +use proto_xmpp::client::{Message, MessageType, Presence, Subject}; +use proto_xmpp::muc::{Affiliation, Role, XUser, XUserItem}; use proto_xmpp::xml::{Ignore, ToXml}; use crate::XmppConnection; @@ -23,8 +23,28 @@ impl<'a> XmppConnection<'a> { // resources in MUCs are members' personas – not implemented (yet?) resource: Some(_), }) if server.0 == self.hostname_rooms => { - let response = self.muc_presence(&name).await?; + let mut response = self.muc_presence(&name).await?; + response.id = p.id; + let subject = Message::<()> { + from: Some(Jid { + name: Some(name), + 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![], + }; response.serialize(output); + subject.serialize(output); } _ => { // TODO other presence cases @@ -56,7 +76,9 @@ impl<'a> XmppConnection<'a> { }; response.serialize(output); } - _ => todo!(), + e => { + tracing::error!("TODO: unknown presence type: {e:?}"); + } } } @@ -75,7 +97,19 @@ impl<'a> XmppConnection<'a> { server: Server(self.hostname_rooms.clone()), resource: Some(self.user.xmpp_muc_name.clone()), }), - custom: vec![XUser], + 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) @@ -93,7 +127,7 @@ mod tests { use lavina_core::player::PlayerId; use proto_xmpp::bind::{Jid, Name, Resource, Server}; use proto_xmpp::client::Presence; - use proto_xmpp::muc::XUser; + use proto_xmpp::muc::{Affiliation, Role, XUser, XUserItem}; #[tokio::test] async fn test_muc_joining() -> Result<()> { @@ -124,7 +158,19 @@ mod tests { server: Server(conn.hostname_rooms.clone()), resource: Some(conn.user.xmpp_muc_name.clone()), }), - custom: vec![XUser], + 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); @@ -164,7 +210,19 @@ mod tests { server: Server(conn.hostname_rooms.clone()), resource: Some(conn.user.xmpp_muc_name.clone()), }), - custom: vec![XUser], + 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); diff --git a/crates/proto-xmpp/src/client.rs b/crates/proto-xmpp/src/client.rs index 7b96507..36d3b55 100644 --- a/crates/proto-xmpp/src/client.rs +++ b/crates/proto-xmpp/src/client.rs @@ -20,8 +20,8 @@ pub struct Message { // default is Normal pub r#type: MessageType, pub lang: Option, - pub subject: Option, - pub body: Str, + pub subject: Option, + pub body: Option, pub custom: Vec, } @@ -38,6 +38,20 @@ impl FromXml for Message { } } +#[derive(PartialEq, Eq, Debug)] +pub struct Subject(pub Option); +impl ToXml for Subject { + fn serialize(&self, events: &mut Vec>) { + if let Some(ref s) = self.0 { + events.push(Event::Start(BytesStart::new("subject"))); + events.push(Event::Text(BytesText::new(s).into_owned())); + events.push(Event::End(BytesEnd::new("subject"))); + } else { + events.push(Event::Empty(BytesStart::new("subject"))); + } + } +} + #[derive(From)] struct MessageParser(MessageParserInner); @@ -57,7 +71,7 @@ struct MessageParserState { to: Option, r#type: MessageType, lang: Option, - subject: Option, + subject: Option, body: Option, custom: Vec, } @@ -121,22 +135,16 @@ impl Parser for MessageParser { } } } - Event::End(_) => { - if let Some(body) = state.body { - Continuation::Final(Ok(Message { - from: state.from, - id: state.id, - to: state.to, - r#type: state.r#type, - lang: state.lang, - subject: state.subject, - body, - custom: state.custom, - })) - } else { - Continuation::Final(Err(ffail!("Body not found"))) - } - } + Event::End(_) => Continuation::Final(Ok(Message { + from: state.from, + id: state.id, + to: state.to, + r#type: state.r#type, + lang: state.lang, + subject: state.subject, + body: state.body, + custom: state.custom, + })), Event::Empty(_) => { let parser = T::parse(); match parser.consume(namespace, event) { @@ -153,7 +161,7 @@ impl Parser for MessageParser { InSubject(mut state) => match event { Event::Text(ref bytes) => { let subject = fail_fast!(std::str::from_utf8(&*bytes)); - state.subject = Some(subject.into()); + state.subject = Some(Subject(Some(subject.into()))); Continuation::Continue(InSubject(state).into()) } Event::End(_) => Continuation::Continue(Outer(state).into()), @@ -208,9 +216,14 @@ impl ToXml for Message { value: self.r#type.as_str().as_bytes().into(), }); events.push(Event::Start(bytes)); - events.push(Event::Start(BytesStart::new("body"))); - events.push(Event::Text(BytesText::new(&self.body).into_owned())); - events.push(Event::End(BytesEnd::new("body"))); + if let Some(subject) = &self.subject { + subject.serialize(events); + } + if let Some(body) = &self.body { + events.push(Event::Start(BytesStart::new("body"))); + events.push(Event::Text(BytesText::new(body).into_owned())); + events.push(Event::End(BytesEnd::new("body"))); + } events.push(Event::End(BytesEnd::new("message"))); } } @@ -487,6 +500,7 @@ impl ToXml for Iq { #[derive(PartialEq, Eq, Debug)] pub struct Presence { + pub id: Option, pub to: Option, pub from: Option, pub priority: Option, @@ -499,6 +513,7 @@ pub struct Presence { impl Default for Presence { fn default() -> Self { Self { + id: Default::default(), to: Default::default(), from: Default::default(), priority: Default::default(), @@ -573,6 +588,10 @@ impl FromXml for Presence { let s = std::str::from_utf8(&attr.value)?; p.r#type = Some(s.into()); } + b"id" => { + let s = std::str::from_utf8(&attr.value)?; + p.id = Option::from(s.to_string()); + } _ => {} } } @@ -660,6 +679,12 @@ impl ToXml for Presence { value: from.to_string().as_bytes().into(), }]); } + if let Some(ref id) = self.id { + start.extend_attributes([Attribute { + key: QName(b"id"), + value: id.to_string().as_bytes().into(), + }]); + } events.push(Event::Start(start)); if let Some(ref priority) = self.priority { let s = priority.0.to_string(); @@ -698,8 +723,8 @@ mod tests { }), r#type: MessageType::Chat, lang: None, - subject: Some("daa".into()), - body: "bbb".into(), + subject: Some(Subject(Some("daa".into()))), + body: Some("bbb".into()), custom: vec![Ignore], } ) @@ -721,8 +746,8 @@ mod tests { }), r#type: MessageType::Chat, lang: None, - subject: Some("daa".into()), - body: "bbb".into(), + subject: Some(Subject(Some("daa".into()))), + body: Some("bbb".into()), custom: vec![Ignore], } ) diff --git a/crates/proto-xmpp/src/muc/mod.rs b/crates/proto-xmpp/src/muc/mod.rs index 61cf933..2fcfe02 100644 --- a/crates/proto-xmpp/src/muc/mod.rs +++ b/crates/proto-xmpp/src/muc/mod.rs @@ -1,8 +1,9 @@ #![allow(unused_variables)] -use quick_xml::events::{BytesStart, Event}; +use quick_xml::events::{BytesEnd, BytesStart, Event}; use quick_xml::name::ResolveResult; +use crate::bind::Jid; use crate::xml::*; use anyhow::{anyhow, Result}; @@ -144,13 +145,107 @@ impl FromXml for X { } } +/// Information about an MUC member. May contain [MUC status codes](https://xmpp.org/registrar/mucstatus.html). #[derive(Debug, PartialEq, Eq)] -pub struct XUser; +pub struct XUser { + pub item: XUserItem, + /// Code 110. The receiver is the user referred to in the presence stanza. + pub self_presence: bool, + /// Code 201. The room from which the presence stanza was sent was just created. + pub just_created: bool, +} impl ToXml for XUser { fn serialize(&self, output: &mut Vec>) { let mut tag = BytesStart::new("x"); tag.push_attribute(("xmlns", XMLNS_USER)); - output.push(Event::Empty(tag)); + output.push(Event::Start(tag)); + self.item.serialize(output); + if self.self_presence { + let mut meg = BytesStart::new("status"); + meg.push_attribute(("code", "110")); + output.push(Event::Empty(meg)); + } + if self.just_created { + let mut meg = BytesStart::new("status"); + meg.push_attribute(("code", "201")); + output.push(Event::Empty(meg)); + } + output.push(Event::End(BytesEnd::new("x"))); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct XUserItem { + pub affiliation: Affiliation, + pub jid: Jid, + pub role: Role, +} +impl ToXml for XUserItem { + fn serialize(&self, output: &mut Vec>) { + let mut meg = BytesStart::new("item"); + meg.push_attribute(("affiliation", self.affiliation.to_str())); + meg.push_attribute(("role", self.role.to_str())); + meg.push_attribute(("jid", self.jid.to_string().as_str())); + output.push(Event::Empty(meg)); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Affiliation { + Owner, + Admin, + Member, + Outcast, + None, +} +impl Affiliation { + pub fn from_str(s: &str) -> Option { + match s { + "owner" => Some(Self::Owner), + "admin" => Some(Self::Admin), + "member" => Some(Self::Member), + "outcast" => Some(Self::Outcast), + "none" => Some(Self::None), + _ => None, + } + } + + pub fn to_str(&self) -> &str { + match self { + Self::Owner => "owner", + Self::Admin => "admin", + Self::Member => "member", + Self::Outcast => "outcast", + Self::None => "none", + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Role { + Moderator, + Participant, + Visitor, + None, +} +impl Role { + pub fn from_str(s: &str) -> Option { + match s { + "moderator" => Some(Self::Moderator), + "participant" => Some(Self::Participant), + "visitor" => Some(Self::Visitor), + "none" => Some(Self::None), + _ => None, + } + } + + pub fn to_str(&self) -> &str { + match self { + Self::Moderator => "moderator", + Self::Participant => "participant", + Self::Visitor => "visitor", + Self::None => "none", + } } }