From fa61f421bbbae08c0cf60ec5fd012f7d1fd050ff Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Mon, 27 Jan 2025 04:24:02 +0100 Subject: [PATCH] xmpp-parsers: Use xso for pubsub events Also split the PubSubEvent enum into Event and Payload. --- parsers/ChangeLog | 3 + parsers/src/bookmarks2.rs | 14 +- parsers/src/message.rs | 4 +- parsers/src/pubsub/event.rs | 293 ++++++++---------------- parsers/src/pubsub/mod.rs | 2 +- tokio-xmpp/examples/download_avatars.rs | 13 +- xmpp/src/pubsub/mod.rs | 79 ++++--- 7 files changed, 156 insertions(+), 252 deletions(-) diff --git a/parsers/ChangeLog b/parsers/ChangeLog index 155b76a847ff65d67bb8a90192eddde0ffdd577f..bc5e5e2d3be2ebfbef54c11d665642ecb8889d0d 100644 --- a/parsers/ChangeLog +++ b/parsers/ChangeLog @@ -51,6 +51,9 @@ XXXX-YY-ZZ RELEASER - http_upload’s Header type has been split into HeaderName, an enum containing only the name of the header, and the Header struct with both a name and a value(!530) + - pubsub::Event is now the wrapper for the pubsub::event::Payload enum, + and the PublishedItems and RetractedItems have been merged into the + Items sub-struct. These replace the previous PubSubEvent enum (!531) * New parsers/serialisers: - Stream Features (RFC 6120) (!400) - Spam Reporting (XEP-0377) (!506) diff --git a/parsers/src/bookmarks2.rs b/parsers/src/bookmarks2.rs index 2848757cbae04559d6a2fe036514f04203e2235f..52dfe7d06abf19c73be799dbf6ad059afebcfbdf 100644 --- a/parsers/src/bookmarks2.rs +++ b/parsers/src/bookmarks2.rs @@ -62,7 +62,7 @@ impl Conference { #[cfg(test)] mod tests { use super::*; - use crate::pubsub::{pubsub::Item as PubSubItem, PubSubEvent}; + use crate::pubsub::{self, pubsub::Item as PubSubItem}; #[cfg(target_pointer_width = "32")] #[test] @@ -132,10 +132,16 @@ mod tests { assert_eq!(conference.clone().password.unwrap(), "secret"); let elem: Element = "Coucousecret".parse().unwrap(); - let mut items = match PubSubEvent::try_from(elem) { - Ok(PubSubEvent::PublishedItems { node, items }) => { + let event = pubsub::Event::try_from(elem).unwrap(); + let mut items = match event.payload { + pubsub::event::Payload::Items { + node, + published, + retracted, + } => { assert_eq!(&node.0, ns::BOOKMARKS2); - items + assert_eq!(retracted.len(), 0); + published } _ => panic!(), }; diff --git a/parsers/src/message.rs b/parsers/src/message.rs index 0c96cbc973854ac9fef1594c0857c8f3dd9786b8..b1dbbbb4ed4b2a2a704b1a03b2a32e2e48ed88da 100644 --- a/parsers/src/message.rs +++ b/parsers/src/message.rs @@ -582,7 +582,7 @@ mod tests { #[test] fn test_extract_payload() { use super::super::attention::Attention; - use super::super::pubsub::event::PubSubEvent; + use super::super::pubsub; #[cfg(not(feature = "component"))] let elem: Element = "".parse().unwrap(); @@ -590,7 +590,7 @@ mod tests { let elem: Element = "".parse().unwrap(); let mut message = Message::try_from(elem).unwrap(); assert_eq!(message.payloads.len(), 1); - match message.extract_payload::() { + match message.extract_payload::() { Ok(None) => (), other => panic!("unexpected result: {:?}", other), }; diff --git a/parsers/src/pubsub/event.rs b/parsers/src/pubsub/event.rs index 47fbc77457a1e8947e0e846a8c1a6480900287bc..273c27ff848a86bd216915b2e8179023d303a03b 100644 --- a/parsers/src/pubsub/event.rs +++ b/parsers/src/pubsub/event.rs @@ -13,7 +13,6 @@ use crate::ns; use crate::pubsub::{ItemId, NodeName, Subscription, SubscriptionId}; use jid::Jid; use minidom::Element; -use xso::error::{Error, FromElementError}; /// An event item from a PubSub node. #[derive(FromXml, AsXml, Debug, Clone, PartialEq)] @@ -33,231 +32,103 @@ pub struct Item { } /// Represents an event happening to a PubSub node. -#[derive(Debug, Clone, PartialEq)] -pub enum PubSubEvent { +#[derive(FromXml, AsXml, Debug, Clone, PartialEq)] +#[xml(namespace = ns::PUBSUB_EVENT, name = "event")] +pub struct Event { + /// The inner child of this event. + #[xml(child)] + pub payload: Payload, +} + +impl MessagePayload for Event {} + +/// Represents an event happening to a PubSub node. +#[derive(FromXml, AsXml, Debug, Clone, PartialEq)] +#[xml(namespace = ns::PUBSUB_EVENT, exhaustive)] +pub enum Payload { /* Collection { }, */ /// This node’s configuration changed. + #[xml(name = "configuration")] Configuration { /// The node affected. + #[xml(attribute)] node: NodeName, /// The new configuration of this node. + #[xml(child(default))] form: Option, }, /// This node has been deleted, with an optional redirect to another node. + #[xml(name = "delete")] Delete { /// The node affected. + #[xml(attribute)] node: NodeName, /// The xmpp: URI of another node replacing this one. + #[xml(extract(default, fields(attribute(default, name = "uri"))))] redirect: Option, }, - /// Some items have been published on this node. - PublishedItems { + /// Some items have been published or retracted on this node. + #[xml(name = "items")] + Items { /// The node affected. + #[xml(attribute)] node: NodeName, /// The list of published items. - items: Vec, - }, - - /// Some items have been removed from this node. - RetractedItems { - /// The node affected. - node: NodeName, + #[xml(child(n = ..))] + published: Vec, /// The list of retracted items. - items: Vec, + #[xml(extract(n = .., name = "retract", fields(attribute(name = "id", type_ = ItemId))))] + retracted: Vec, }, /// All items of this node just got removed at once. + #[xml(name = "purge")] Purge { /// The node affected. + #[xml(attribute)] node: NodeName, }, /// The user’s subscription to this node has changed. + #[xml(name = "subscription")] Subscription { /// The node affected. + #[xml(attribute)] node: NodeName, /// The time at which this subscription will expire. + #[xml(attribute(default))] expiry: Option, /// The JID of the user affected. + #[xml(attribute(default))] jid: Option, /// An identifier for this subscription. + #[xml(attribute(default))] subid: Option, /// The state of this subscription. + #[xml(attribute(default))] subscription: Option, }, } -fn parse_items(elem: Element, node: NodeName) -> Result { - let mut is_retract = None; - let mut items = vec![]; - let mut retracts = vec![]; - for child in elem.children() { - if child.is("item", ns::PUBSUB_EVENT) { - match is_retract { - None => is_retract = Some(false), - Some(false) => (), - Some(true) => { - return Err(Error::Other("Mix of item and retract in items element.")); - } - } - items.push(Item::try_from(child.clone())?); - } else if child.is("retract", ns::PUBSUB_EVENT) { - match is_retract { - None => is_retract = Some(true), - Some(true) => (), - Some(false) => { - return Err(Error::Other("Mix of item and retract in items element.")); - } - } - check_no_children!(child, "retract"); - check_no_unknown_attributes!(child, "retract", ["id"]); - let id = get_attr!(child, "id", Required); - retracts.push(id); - } else { - return Err(Error::Other("Invalid child in items element.")); - } - } - Ok(match is_retract { - Some(false) => PubSubEvent::PublishedItems { node, items }, - Some(true) => PubSubEvent::RetractedItems { - node, - items: retracts, - }, - None => return Err(Error::Other("Missing children in items element.")), - }) -} - -impl TryFrom for PubSubEvent { - type Error = FromElementError; - - fn try_from(elem: Element) -> Result { - check_self!(elem, "event", PUBSUB_EVENT); - check_no_attributes!(elem, "event"); - - let mut payload = None; - for child in elem.children() { - let node = get_attr!(child, "node", Required); - if child.is("configuration", ns::PUBSUB_EVENT) { - let mut payloads = child.children().cloned().collect::>(); - let item = payloads.pop(); - if !payloads.is_empty() { - return Err(Error::Other( - "More than a single payload in configuration element.", - ) - .into()); - } - let form = match item { - None => None, - Some(payload) => Some(DataForm::try_from(payload)?), - }; - payload = Some(PubSubEvent::Configuration { node, form }); - } else if child.is("delete", ns::PUBSUB_EVENT) { - let mut redirect = None; - for item in child.children() { - if item.is("redirect", ns::PUBSUB_EVENT) { - if redirect.is_some() { - return Err( - Error::Other("More than one redirect in delete element.").into() - ); - } - let uri = get_attr!(item, "uri", Required); - redirect = Some(uri); - } else { - return Err(Error::Other("Unknown child in delete element.").into()); - } - } - payload = Some(PubSubEvent::Delete { node, redirect }); - } else if child.is("items", ns::PUBSUB_EVENT) { - payload = Some(parse_items(child.clone(), node)?); - } else if child.is("purge", ns::PUBSUB_EVENT) { - check_no_children!(child, "purge"); - payload = Some(PubSubEvent::Purge { node }); - } else if child.is("subscription", ns::PUBSUB_EVENT) { - check_no_children!(child, "subscription"); - payload = Some(PubSubEvent::Subscription { - node, - expiry: get_attr!(child, "expiry", Option), - jid: get_attr!(child, "jid", Option), - subid: get_attr!(child, "subid", Option), - subscription: get_attr!(child, "subscription", Option), - }); - } else { - return Err(Error::Other("Unknown child in event element.").into()); - } - } - payload.ok_or(Error::Other("No payload in event element.").into()) - } -} - -impl From for Element { - fn from(event: PubSubEvent) -> Element { - let payload = match event { - PubSubEvent::Configuration { node, form } => { - Element::builder("configuration", ns::PUBSUB_EVENT) - .attr("node", node) - .append_all(form.map(Element::from)) - } - PubSubEvent::Delete { node, redirect } => Element::builder("purge", ns::PUBSUB_EVENT) - .attr("node", node) - .append_all(redirect.map(|redirect| { - Element::builder("redirect", ns::PUBSUB_EVENT).attr("uri", redirect) - })), - PubSubEvent::PublishedItems { node, items } => { - Element::builder("items", ns::PUBSUB_EVENT) - .attr("node", node) - .append_all(items) - } - PubSubEvent::RetractedItems { node, items } => { - Element::builder("items", ns::PUBSUB_EVENT) - .attr("node", node) - .append_all( - items - .into_iter() - .map(|id| Element::builder("retract", ns::PUBSUB_EVENT).attr("id", id)), - ) - } - PubSubEvent::Purge { node } => { - Element::builder("purge", ns::PUBSUB_EVENT).attr("node", node) - } - PubSubEvent::Subscription { - node, - expiry, - jid, - subid, - subscription, - } => Element::builder("subscription", ns::PUBSUB_EVENT) - .attr("node", node) - .attr("expiry", expiry) - .attr("jid", jid) - .attr("subid", subid) - .attr("subscription", subscription), - }; - Element::builder("event", ns::PUBSUB_EVENT) - .append(payload) - .build() - } -} - -impl PubSubEvent { +impl Payload { /// Return the name of the node to which this event is related. pub fn node_name(&self) -> &NodeName { match self { Self::Purge { node, .. } => node, - Self::PublishedItems { node, .. } => node, - Self::RetractedItems { node, .. } => node, + Self::Items { node, .. } => node, Self::Subscription { node, .. } => node, Self::Delete { node, .. } => node, Self::Configuration { node, .. } => node, @@ -265,20 +136,21 @@ impl PubSubEvent { } } -impl MessagePayload for PubSubEvent {} - #[cfg(test)] mod tests { use super::*; use jid::BareJid; + use xso::error::{Error, FromElementError}; + // TODO: Reenable this test once we support asserting that a Vec isn’t empty. #[test] + #[ignore] fn missing_items() { let elem: Element = "" .parse() .unwrap(); - let error = PubSubEvent::try_from(elem).unwrap_err(); + let error = Event::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), @@ -289,16 +161,21 @@ mod tests { #[test] fn test_simple_items() { let elem: Element = "".parse().unwrap(); - let event = PubSubEvent::try_from(elem).unwrap(); - match event { - PubSubEvent::PublishedItems { node, items } => { + let event = Event::try_from(elem).unwrap(); + match event.payload { + Payload::Items { + node, + published, + retracted, + } => { assert_eq!(node, NodeName(String::from("coucou"))); - assert_eq!(items[0].id, Some(ItemId(String::from("test")))); + assert_eq!(retracted.len(), 0); + assert_eq!(published[0].id, Some(ItemId(String::from("test")))); assert_eq!( - items[0].publisher.clone().unwrap(), + published[0].publisher.clone().unwrap(), BareJid::new("test@coucou").unwrap() ); - assert_eq!(items[0].payload, None); + assert_eq!(published[0].payload, None); } _ => panic!(), } @@ -307,13 +184,18 @@ mod tests { #[test] fn test_simple_pep() { let elem: Element = "".parse().unwrap(); - let event = PubSubEvent::try_from(elem).unwrap(); - match event { - PubSubEvent::PublishedItems { node, items } => { + let event = Event::try_from(elem).unwrap(); + match event.payload { + Payload::Items { + node, + published, + retracted, + } => { assert_eq!(node, NodeName(String::from("something"))); - assert_eq!(items[0].id, None); - assert_eq!(items[0].publisher, None); - match items[0].payload { + assert_eq!(retracted.len(), 0); + assert_eq!(published[0].id, None); + assert_eq!(published[0].publisher, None); + match published[0].payload { Some(ref elem) => assert!(elem.is("foreign", "example:namespace")), _ => panic!(), } @@ -325,12 +207,17 @@ mod tests { #[test] fn test_simple_retract() { let elem: Element = "".parse().unwrap(); - let event = PubSubEvent::try_from(elem).unwrap(); - match event { - PubSubEvent::RetractedItems { node, items } => { + let event = Event::try_from(elem).unwrap(); + match event.payload { + Payload::Items { + node, + published, + retracted, + } => { assert_eq!(node, NodeName(String::from("something"))); - assert_eq!(items[0], ItemId(String::from("coucou"))); - assert_eq!(items[1], ItemId(String::from("test"))); + assert_eq!(published.len(), 0); + assert_eq!(retracted[0], ItemId(String::from("coucou"))); + assert_eq!(retracted[1], ItemId(String::from("test"))); } _ => panic!(), } @@ -339,9 +226,9 @@ mod tests { #[test] fn test_simple_delete() { let elem: Element = "".parse().unwrap(); - let event = PubSubEvent::try_from(elem).unwrap(); - match event { - PubSubEvent::Delete { node, redirect } => { + let event = Event::try_from(elem).unwrap(); + match event.payload { + Payload::Delete { node, redirect } => { assert_eq!(node, NodeName(String::from("coucou"))); assert_eq!(redirect, Some(String::from("hello"))); } @@ -355,9 +242,9 @@ mod tests { "" .parse() .unwrap(); - let event = PubSubEvent::try_from(elem).unwrap(); - match event { - PubSubEvent::Purge { node } => { + let event = Event::try_from(elem).unwrap(); + match event.payload { + Payload::Purge { node } => { assert_eq!(node, NodeName(String::from("coucou"))); } _ => panic!(), @@ -367,9 +254,9 @@ mod tests { #[test] fn test_simple_configure() { let elem: Element = "http://jabber.org/protocol/pubsub#node_config".parse().unwrap(); - let event = PubSubEvent::try_from(elem).unwrap(); - match event { - PubSubEvent::Configuration { node, form: _ } => { + let event = Event::try_from(elem).unwrap(); + match event.payload { + Payload::Configuration { node, form: _ } => { assert_eq!(node, NodeName(String::from("coucou"))); //assert_eq!(form.type_, Result_); } @@ -383,12 +270,12 @@ mod tests { "" .parse() .unwrap(); - let error = PubSubEvent::try_from(elem).unwrap_err(); + let error = Event::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown child in event element."); + assert_eq!(message, "This is not a Payload element."); } #[cfg(not(feature = "disable-validation"))] @@ -397,12 +284,12 @@ mod tests { let elem: Element = "" .parse() .unwrap(); - let error = PubSubEvent::try_from(elem).unwrap_err(); + let error = Event::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown attribute in event element."); + assert_eq!(message, "Unknown attribute in Event element."); } #[test] @@ -410,9 +297,9 @@ mod tests { let elem: Element = "" .parse() .unwrap(); - let event = PubSubEvent::try_from(elem.clone()).unwrap(); - match event.clone() { - PubSubEvent::Subscription { + let event = Event::try_from(elem.clone()).unwrap(); + match event.payload.clone() { + Payload::Subscription { node, expiry, jid, diff --git a/parsers/src/pubsub/mod.rs b/parsers/src/pubsub/mod.rs index 923dbc3f888e26b7b5f5befce49903f585fc3fc4..e4beae2092696e4e33327fc097a3e418e4fb090e 100644 --- a/parsers/src/pubsub/mod.rs +++ b/parsers/src/pubsub/mod.rs @@ -14,7 +14,7 @@ pub mod owner; #[allow(clippy::module_inception)] pub mod pubsub; -pub use self::event::PubSubEvent; +pub use self::event::Event; pub use self::owner::PubSubOwner; pub use self::pubsub::PubSub; diff --git a/tokio-xmpp/examples/download_avatars.rs b/tokio-xmpp/examples/download_avatars.rs index bedae361fdd508d51172d73c179483030c100f82..cd595383741ed58f4287db5af007571249de8b0a 100644 --- a/tokio-xmpp/examples/download_avatars.rs +++ b/tokio-xmpp/examples/download_avatars.rs @@ -15,7 +15,7 @@ use xmpp_parsers::{ ns, presence::{Presence, Type as PresenceType}, pubsub::{ - event::PubSubEvent, + self, pubsub::{Items, PubSub}, NodeName, }, @@ -126,10 +126,15 @@ async fn main() { } for child in message.payloads { if child.is("event", ns::PUBSUB_EVENT) { - let event = PubSubEvent::try_from(child).unwrap(); - if let PubSubEvent::PublishedItems { node, items } = event { + let event = pubsub::Event::try_from(child).unwrap(); + if let pubsub::event::Payload::Items { + node, + published, + retracted: _, + } = event.payload + { if node.0 == ns::AVATAR_METADATA { - for item in items.into_iter() { + for item in published.into_iter() { let payload = item.payload.clone().unwrap(); if payload.is("metadata", ns::AVATAR_METADATA) { // TODO: do something with these metadata. diff --git a/xmpp/src/pubsub/mod.rs b/xmpp/src/pubsub/mod.rs index e7fcdbac1866e68897662b002a53fe9edf71b8db..fb0c7770ee2bedd4928ad47f6d640d84b404c9c0 100644 --- a/xmpp/src/pubsub/mod.rs +++ b/xmpp/src/pubsub/mod.rs @@ -10,7 +10,7 @@ use crate::{ muc::room::{JoinRoomSettings, LeaveRoomSettings}, parsers::{ bookmarks2, ns, - pubsub::{event::PubSubEvent, pubsub::PubSub}, + pubsub::{self, pubsub::PubSub}, }, Agent, Event, RoomNick, }; @@ -30,61 +30,64 @@ pub(crate) async fn handle_event( #[allow(unused_mut)] let mut events = Vec::new(); - let event = PubSubEvent::try_from(elem); + let event = pubsub::Event::try_from(elem); trace!("PubSub event: {:#?}", event); match event { - Ok(PubSubEvent::PublishedItems { node, items }) => { + Ok(pubsub::Event { + payload: + pubsub::event::Payload::Items { + node, + published, + retracted, + }, + }) => { match node.0 { #[cfg(feature = "avatars")] ref node if node == ns::AVATAR_METADATA => { + // TODO: Also handle retracted! let new_events = - avatar::handle_metadata_pubsub_event(&from, agent, items).await; + avatar::handle_metadata_pubsub_event(&from, agent, published).await; events.extend(new_events); } ref node if node == ns::BOOKMARKS2 => { // TODO: Check that our bare JID is the sender. - assert_eq!(items.len(), 1); - let item = items.clone().pop().unwrap(); - let jid = BareJid::from_str(&item.id.clone().unwrap().0).unwrap(); - let payload = item.payload.clone().unwrap(); - match bookmarks2::Conference::try_from(payload) { - Ok(conference) => { - if conference.autojoin { - if !agent.rooms_joined.contains_key(&jid) { - agent - .join_room(JoinRoomSettings { - room: jid, - nick: conference.nick.map(RoomNick::new), - password: conference.password, - status: None, - }) - .await; + if let [item] = &published[..] { + let jid = BareJid::from_str(&item.id.clone().unwrap().0).unwrap(); + let payload = item.payload.clone().unwrap(); + match bookmarks2::Conference::try_from(payload) { + Ok(conference) => { + if conference.autojoin { + if !agent.rooms_joined.contains_key(&jid) { + agent + .join_room(JoinRoomSettings { + room: jid, + nick: conference.nick.map(RoomNick::new), + password: conference.password, + status: None, + }) + .await; + } + } else { + // 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), } - Err(err) => println!("not bookmark: {}", err), - } - } - ref node => unimplemented!("node {}", node), - } - } - Ok(PubSubEvent::RetractedItems { node, items }) => { - match node.0 { - ref node if node == ns::BOOKMARKS2 => { - // TODO: Check that our bare JID is the sender. - assert_eq!(items.len(), 1); - let item = items.clone().pop().unwrap(); - let jid = BareJid::from_str(&item.0).unwrap(); + } else if let [item] = &retracted[..] { + 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!"); + } } ref node => unimplemented!("node {}", node), } } - Ok(PubSubEvent::Purge { node }) => match node.0 { + Ok(pubsub::Event { + payload: pubsub::event::Payload::Purge { node }, + }) => match node.0 { ref node if node == ns::BOOKMARKS2 => { warn!("The bookmarks2 PEP node was deleted!"); }