From 6c1c9d0851c439274c771f5eb2acb7a012f7515e Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Sun, 11 Jun 2017 14:48:31 +0100 Subject: [PATCH] Add a PubSub event parser and serialiser. --- src/lib.rs | 3 + src/ns.rs | 9 + src/pubsub/event.rs | 461 ++++++++++++++++++++++++++++++++++++++++++++ src/pubsub/mod.rs | 9 + 4 files changed, 482 insertions(+) create mode 100644 src/pubsub/event.rs create mode 100644 src/pubsub/mod.rs diff --git a/src/lib.rs b/src/lib.rs index b5ee57ef1fedd8c683f8580eed44c67de7fb6a16..46a2b8ef9c0c2cac2bc338c561eabc3178da839a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,6 +81,9 @@ pub mod ibb; /// XEP-0059: Result Set Management pub mod rsm; +/// XEP-0060: Publish-Subscribe +pub mod pubsub; + /// XEP-0085: Chat State Notifications pub mod chatstates; diff --git a/src/ns.rs b/src/ns.rs index 149025ac5402cd5cef9514a860c7a95345bdb4e9..88541e0e75e7c7af1e70b5b9d992508dcbf26ce4 100644 --- a/src/ns.rs +++ b/src/ns.rs @@ -30,6 +30,15 @@ pub const IBB: &'static str = "http://jabber.org/protocol/ibb"; /// XEP-0059: Result Set Management pub const RSM: &'static str = "http://jabber.org/protocol/rsm"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB: &'static str = "http://jabber.org/protocol/pubsub"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB_ERRORS: &'static str = "http://jabber.org/protocol/pubsub#errors"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB_EVENT: &'static str = "http://jabber.org/protocol/pubsub#event"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB_OWNER: &'static str = "http://jabber.org/protocol/pubsub#owner"; + /// XEP-0085: Chat State Notifications pub const CHATSTATES: &'static str = "http://jabber.org/protocol/chatstates"; diff --git a/src/pubsub/event.rs b/src/pubsub/event.rs new file mode 100644 index 0000000000000000000000000000000000000000..47d9a4699cd8033cb3a7ad979bbdc1460f295469 --- /dev/null +++ b/src/pubsub/event.rs @@ -0,0 +1,461 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// 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 std::convert::TryFrom; +use std::str::FromStr; + +use minidom::{Element, IntoElements, IntoAttributeValue, ElementEmitter}; +use jid::Jid; +use chrono::{DateTime, FixedOffset}; + +use error::Error; + +use ns; + +use data_forms::DataForm; + +#[derive(Debug, Clone)] +pub struct Item { + payload: Option, + id: Option, + node: Option, + publisher: Option, +} + +impl From for Element { + fn from(item: Item) -> Element { + Element::builder("item") + .ns(ns::PUBSUB_EVENT) + .attr("id", item.id) + .attr("node", item.node) + .attr("publisher", item.publisher.and_then(|publisher| Some(String::from(publisher)))) + .append(item.payload) + .build() + } +} + +impl IntoElements for Item { + fn into_elements(self, emitter: &mut ElementEmitter) { + emitter.append_child(self.into()); + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Subscription { + None, + Pending, + Subscribed, + Unconfigured, +} + +impl Default for Subscription { + fn default() -> Subscription { + Subscription::None + } +} + +impl FromStr for Subscription { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "none" => Subscription::None, + "pending" => Subscription::Pending, + "subscribed" => Subscription::Subscribed, + "unconfigured" => Subscription::Unconfigured, + + _ => return Err(Error::ParseError("Invalid 'subscription' attribute.")), + }) + } +} + +impl IntoAttributeValue for Subscription { + fn into_attribute_value(self) -> Option { + Some(String::from(match self { + Subscription::None => return None, + Subscription::Pending => "pending", + Subscription::Subscribed => "subscribed", + Subscription::Unconfigured => "unconfigured", + })) + } +} + +#[derive(Debug, Clone)] +pub enum PubSubEvent { + /* + Collection { + }, + */ + Configuration { + node: String, + form: Option, + }, + Delete { + node: String, + redirect: Option, + }, + EmptyItems { + node: String, + }, + PublishedItems { + node: String, + items: Vec, + }, + RetractedItems { + node: String, + items: Vec, + }, + Purge { + node: String, + }, + Subscription { + node: String, + expiry: Option>, + jid: Option, + subid: Option, + subscription: Option, + }, +} + +fn parse_items(elem: Element, node: String) -> 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::ParseError("Mix of item and retract in items element.")), + } + let mut payloads = child.children().cloned().collect::>(); + let payload = payloads.pop(); + if !payloads.is_empty() { + return Err(Error::ParseError("More than a single payload in item element.")); + } + let item = Item { + payload, + id: get_attr!(child, "id", optional), + node: get_attr!(child, "node", optional), + publisher: get_attr!(child, "publisher", optional), + }; + items.push(item); + } else if child.is("retract", ns::PUBSUB_EVENT) { + match is_retract { + None => is_retract = Some(true), + Some(true) => (), + Some(false) => return Err(Error::ParseError("Mix of item and retract in items element.")), + } + for _ in child.children() { + return Err(Error::ParseError("Unknown child in retract element.")); + } + for (attr, _) in child.attrs() { + if attr != "id" { + return Err(Error::ParseError("Unknown attribute in retract element.")); + } + } + let id = get_attr!(child, "id", required); + retracts.push(id); + } else { + return Err(Error::ParseError("Invalid child in items element.")); + } + } + Ok(match is_retract { + None => PubSubEvent::EmptyItems { node }, + Some(false) => PubSubEvent::PublishedItems { node, items }, + Some(true) => PubSubEvent::RetractedItems { node, items: retracts }, + }) +} + +impl TryFrom for PubSubEvent { + type Error = Error; + + fn try_from(elem: Element) -> Result { + if !elem.is("event", ns::PUBSUB_EVENT) { + return Err(Error::ParseError("This is not an event element.")); + } + for _ in elem.attrs() { + return Err(Error::ParseError("Unknown attribute in event element.")); + } + let mut payload = None; + for child in elem.children() { + /* + for (attr, _) in child.attrs() { + if attr != "node" { + return Err(Error::ParseError("Unknown attribute in items element.")); + } + } + */ + 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::ParseError("More than a single payload in configuration element.")); + } + 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::ParseError("More than one redirect in delete element.")); + } + let uri = get_attr!(item, "uri", required); + redirect = Some(uri); + } else { + return Err(Error::ParseError("Unknown child in delete element.")); + } + } + 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) { + for _ in child.children() { + return Err(Error::ParseError("Unknown child in purge element.")); + } + payload = Some(PubSubEvent::Purge { node }); + } else if child.is("subscription", ns::PUBSUB_EVENT) { + for _ in child.children() { + return Err(Error::ParseError("Unknown child in purge element.")); + } + payload = Some(PubSubEvent::Subscription { + node: node, + expiry: get_attr!(child, "expiry", optional), + jid: get_attr!(child, "jid", optional), + subid: get_attr!(child, "subid", optional), + subscription: get_attr!(child, "subscription", optional), + }); + } else { + return Err(Error::ParseError("Unknown child in event element.")); + } + } + Ok(payload.ok_or(Error::ParseError("No payload in event element."))?) + } +} + +impl From for Element { + fn from(event: PubSubEvent) -> Element { + let payload = match event { + PubSubEvent::Configuration { node, form } => { + Element::builder("configuration") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .append(form) + .build() + }, + PubSubEvent::Delete { node, redirect } => { + Element::builder("purge") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .append(redirect.and_then(|redirect| { + Some(Element::builder("redirect") + .ns(ns::PUBSUB_EVENT) + .attr("uri", redirect) + .build()) + })) + .build() + }, + PubSubEvent::EmptyItems { node } => { + Element::builder("items") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .build() + }, + PubSubEvent::PublishedItems { node, items } => { + Element::builder("items") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .append(items) + .build() + }, + PubSubEvent::RetractedItems { node, items } => { + Element::builder("items") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .append(items) + .build() + }, + PubSubEvent::Purge { node } => { + Element::builder("purge") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .build() + }, + PubSubEvent::Subscription { node, expiry, jid, subid, subscription } => { + Element::builder("subscription") + .ns(ns::PUBSUB_EVENT) + .attr("node", node) + .attr("expiry", expiry.and_then(|expiry| Some(expiry.to_rfc3339()))) + .attr("jid", jid.and_then(|jid| Some(String::from(jid)))) + .attr("subid", subid) + .attr("subscription", subscription) + .build() + }, + }; + Element::builder("event") + .ns(ns::PUBSUB_EVENT) + .append(payload) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::EmptyItems { node } => assert_eq!(node, String::from("coucou")), + _ => panic!(), + } + } + + #[test] + fn test_simple_items() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::PublishedItems { node, items } => { + assert_eq!(node, String::from("coucou")); + assert_eq!(items[0].id, Some(String::from("test"))); + assert_eq!(items[0].node, Some(String::from("huh?"))); + assert_eq!(items[0].publisher, Some(Jid::from_str("test@coucou").unwrap())); + assert_eq!(items[0].payload, None); + }, + _ => panic!(), + } + } + + #[test] + fn test_simple_pep() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::PublishedItems { node, items } => { + assert_eq!(node, String::from("something")); + assert_eq!(items[0].id, None); + assert_eq!(items[0].node, None); + assert_eq!(items[0].publisher, None); + match items[0].payload { + Some(ref elem) => assert!(elem.is("foreign", "example:namespace")), + _ => panic!(), + } + }, + _ => panic!(), + } + } + + #[test] + fn test_simple_retract() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::RetractedItems { node, items } => { + assert_eq!(node, String::from("something")); + assert_eq!(items[0], String::from("coucou")); + assert_eq!(items[1], String::from("test")); + }, + _ => panic!(), + } + } + + #[test] + fn test_simple_delete() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::Delete { node, redirect } => { + assert_eq!(node, String::from("coucou")); + assert_eq!(redirect, Some(String::from("hello"))); + }, + _ => panic!(), + } + } + + #[test] + fn test_simple_purge() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::Purge { node } => { + assert_eq!(node, String::from("coucou")); + }, + _ => panic!(), + } + } + + #[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: _ } => { + assert_eq!(node, String::from("coucou")); + //assert_eq!(form.type_, Result_); + }, + _ => panic!(), + } + } + + #[test] + fn test_invalid() { + let elem: Element = "".parse().unwrap(); + let error = PubSubEvent::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in event element."); + } + + #[test] + fn test_invalid_attribute() { + let elem: Element = "".parse().unwrap(); + let error = PubSubEvent::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in event element."); + } + + #[test] + fn test_ex221_subscription() { + let elem: Element = r#" + + + +"#.parse().unwrap(); + let event = PubSubEvent::try_from(elem.clone()).unwrap(); + match event.clone() { + PubSubEvent::Subscription { node, expiry, jid, subid, subscription } => { + assert_eq!(node, String::from("princely_musings")); + assert_eq!(subid, Some(String::from("ba49252aaa4f5d320c24d3766f0bdcade78c78d3"))); + assert_eq!(subscription, Some(Subscription::Subscribed)); + assert_eq!(jid, Some(Jid::from_str("francisco@denmark.lit").unwrap())); + assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap())); + }, + _ => panic!(), + } + + let elem2: Element = event.into(); + assert_eq!(elem, elem2); + } +} diff --git a/src/pubsub/mod.rs b/src/pubsub/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..9eb939442e863cabae522697569d5da6ac324f00 --- /dev/null +++ b/src/pubsub/mod.rs @@ -0,0 +1,9 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// 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/. + +pub mod event; + +pub use self::event::PubSubEvent;