Rename the xmpp-parsers directory to parsers

This doesn’t change anything to the name of the crate, just makes
autocompletion easier by not sharing the same prefix as the xmpp crate.
This commit is contained in:
Emmanuel Gil Peyrot 2021-10-11 08:24:14 +02:00
commit 9410849d7a
84 changed files with 2 additions and 2 deletions

416
parsers/src/pubsub/event.rs Normal file
View file

@ -0,0 +1,416 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::DataForm;
use crate::date::DateTime;
use crate::message::MessagePayload;
use crate::ns;
use crate::pubsub::{Item as PubSubItem, ItemId, NodeName, Subscription, SubscriptionId};
use crate::util::error::Error;
use crate::Element;
use jid::Jid;
use std::convert::TryFrom;
/// Event wrapper for a PubSub `<item/>`.
#[derive(Debug, Clone)]
pub struct Item(pub PubSubItem);
impl_pubsub_item!(Item, PUBSUB_EVENT);
/// Represents an event happening to a PubSub node.
#[derive(Debug, Clone)]
pub enum PubSubEvent {
/*
Collection {
},
*/
/// This nodes configuration changed.
Configuration {
/// The node affected.
node: NodeName,
/// The new configuration of this node.
form: Option<DataForm>,
},
/// This node has been deleted, with an optional redirect to another node.
Delete {
/// The node affected.
node: NodeName,
/// The xmpp: URI of another node replacing this one.
redirect: Option<String>,
},
/// Some items have been published on this node.
PublishedItems {
/// The node affected.
node: NodeName,
/// The list of published items.
items: Vec<Item>,
},
/// Some items have been removed from this node.
RetractedItems {
/// The node affected.
node: NodeName,
/// The list of retracted items.
items: Vec<ItemId>,
},
/// All items of this node just got removed at once.
Purge {
/// The node affected.
node: NodeName,
},
/// The users subscription to this node has changed.
Subscription {
/// The node affected.
node: NodeName,
/// The time at which this subscription will expire.
expiry: Option<DateTime>,
/// The JID of the user affected.
jid: Option<Jid>,
/// An identifier for this subscription.
subid: Option<SubscriptionId>,
/// The state of this subscription.
subscription: Option<Subscription>,
},
}
fn parse_items(elem: Element, node: NodeName) -> Result<PubSubEvent, Error> {
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.",
));
}
}
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::ParseError(
"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::ParseError("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::ParseError("Missing children in items element.")),
})
}
impl TryFrom<Element> for PubSubEvent {
type Error = Error;
fn try_from(elem: Element) -> Result<PubSubEvent, Error> {
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::<Vec<_>>();
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) {
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::ParseError("Unknown child in event element."));
}
}
Ok(payload.ok_or(Error::ParseError("No payload in event element."))?)
}
}
impl From<PubSubEvent> 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.into_iter())
}
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 MessagePayload for PubSubEvent {}
#[cfg(test)]
mod tests {
use super::*;
use jid::BareJid;
#[test]
fn missing_items() {
let elem: Element =
"<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'/></event>"
.parse()
.unwrap();
let error = PubSubEvent::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Missing children in items element.");
}
#[test]
fn test_simple_items() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'><item id='test' publisher='test@coucou'/></items></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::PublishedItems { node, items } => {
assert_eq!(node, NodeName(String::from("coucou")));
assert_eq!(items[0].id, Some(ItemId(String::from("test"))));
assert_eq!(
items[0].publisher.clone().unwrap(),
BareJid::new("test", "coucou")
);
assert_eq!(items[0].payload, None);
}
_ => panic!(),
}
}
#[test]
fn test_simple_pep() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><item><foreign xmlns='example:namespace'/></item></items></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::PublishedItems { node, items } => {
assert_eq!(node, NodeName(String::from("something")));
assert_eq!(items[0].id, 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 = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><retract id='coucou'/><retract id='test'/></items></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::RetractedItems { node, items } => {
assert_eq!(node, NodeName(String::from("something")));
assert_eq!(items[0], ItemId(String::from("coucou")));
assert_eq!(items[1], ItemId(String::from("test")));
}
_ => panic!(),
}
}
#[test]
fn test_simple_delete() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><delete node='coucou'><redirect uri='hello'/></delete></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::Delete { node, redirect } => {
assert_eq!(node, NodeName(String::from("coucou")));
assert_eq!(redirect, Some(String::from("hello")));
}
_ => panic!(),
}
}
#[test]
fn test_simple_purge() {
let elem: Element =
"<event xmlns='http://jabber.org/protocol/pubsub#event'><purge node='coucou'/></event>"
.parse()
.unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::Purge { node } => {
assert_eq!(node, NodeName(String::from("coucou")));
}
_ => panic!(),
}
}
#[test]
fn test_simple_configure() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><configuration node='coucou'><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field></x></configuration></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::Configuration { node, form: _ } => {
assert_eq!(node, NodeName(String::from("coucou")));
//assert_eq!(form.type_, Result_);
}
_ => panic!(),
}
}
#[test]
fn test_invalid() {
let elem: Element =
"<event xmlns='http://jabber.org/protocol/pubsub#event'><coucou node='test'/></event>"
.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.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event' coucou=''/>"
.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 = "<event xmlns='http://jabber.org/protocol/pubsub#event'><subscription expiry='2006-02-28T23:59:59+00:00' jid='francisco@denmark.lit' node='princely_musings' subid='ba49252aaa4f5d320c24d3766f0bdcade78c78d3' subscription='subscribed'/></event>"
.parse()
.unwrap();
let event = PubSubEvent::try_from(elem.clone()).unwrap();
match event.clone() {
PubSubEvent::Subscription {
node,
expiry,
jid,
subid,
subscription,
} => {
assert_eq!(node, NodeName(String::from("princely_musings")));
assert_eq!(
subid,
Some(SubscriptionId(String::from(
"ba49252aaa4f5d320c24d3766f0bdcade78c78d3"
)))
);
assert_eq!(subscription, Some(Subscription::Subscribed));
assert_eq!(jid.unwrap(), BareJid::new("francisco", "denmark.lit"));
assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap()));
}
_ => panic!(),
}
let elem2: Element = event.into();
assert_eq!(elem, elem2);
}
}

107
parsers/src/pubsub/mod.rs Normal file
View file

@ -0,0 +1,107 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
/// The `http://jabber.org/protocol/pubsub#event` protocol.
pub mod event;
/// The `http://jabber.org/protocol/pubsub#owner` protocol.
pub mod owner;
/// The `http://jabber.org/protocol/pubsub` protocol.
pub mod pubsub;
pub use self::event::PubSubEvent;
pub use self::owner::PubSubOwner;
pub use self::pubsub::PubSub;
use crate::{Element, Jid};
generate_id!(
/// The name of a PubSub node, used to identify it on a JID.
NodeName
);
generate_id!(
/// The identifier of an item, which is unique per node.
ItemId
);
generate_id!(
/// The identifier of a subscription to a PubSub node.
SubscriptionId
);
generate_attribute!(
/// The state of a subscription to a node.
Subscription, "subscription", {
/// The user is not subscribed to this node.
None => "none",
/// The users subscription to this node is still pending.
Pending => "pending",
/// The user is subscribed to this node.
Subscribed => "subscribed",
/// The users subscription to this node will only be valid once
/// configured.
Unconfigured => "unconfigured",
}, Default = None
);
generate_attribute!(
/// A list of possible affiliations to a node.
AffiliationAttribute, "affiliation", {
/// You are a member of this node, you can subscribe and retrieve items.
Member => "member",
/// You dont have a specific affiliation with this node, you can only subscribe to it.
None => "none",
/// You are banned from this node.
Outcast => "outcast",
/// You are an owner of this node, and can do anything with it.
Owner => "owner",
/// You are a publisher on this node, you can publish and retract items to it.
Publisher => "publisher",
/// You can publish and retract items on this node, but not subscribe or retrieve items.
PublishOnly => "publish-only",
}
);
/// An item from a PubSub node.
#[derive(Debug, Clone, PartialEq)]
pub struct Item {
/// The identifier for this item, unique per node.
pub id: Option<ItemId>,
/// The JID of the entity who published this item.
pub publisher: Option<Jid>,
/// The payload of this item, in an arbitrary namespace.
pub payload: Option<Element>,
}
impl Item {
/// Create a new item, accepting only payloads implementing `PubSubPayload`.
pub fn new<P: PubSubPayload>(
id: Option<ItemId>,
publisher: Option<Jid>,
payload: Option<P>,
) -> Item {
Item {
id,
publisher,
payload: payload.map(Into::into),
}
}
}
/// This trait should be implemented on any element which can be included as a PubSub payload.
pub trait PubSubPayload: ::std::convert::TryFrom<crate::Element> + Into<crate::Element> {}

364
parsers/src/pubsub/owner.rs Normal file
View file

@ -0,0 +1,364 @@
// Copyright (c) 2020 Paul Fariello <paul@fariello.eu>
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::DataForm;
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
use crate::ns;
use crate::pubsub::{AffiliationAttribute, NodeName, Subscription};
use crate::util::error::Error;
use crate::Element;
use jid::Jid;
use std::convert::TryFrom;
generate_element!(
/// A list of affiliations you have on a service, or on a node.
Affiliations, "affiliations", PUBSUB_OWNER,
attributes: [
/// The node name this request pertains to.
node: Required<NodeName> = "node",
],
children: [
/// The actual list of affiliation elements.
affiliations: Vec<Affiliation> = ("affiliation", PUBSUB_OWNER) => Affiliation
]
);
generate_element!(
/// An affiliation element.
Affiliation, "affiliation", PUBSUB_OWNER,
attributes: [
/// The node this affiliation pertains to.
jid: Required<Jid> = "jid",
/// The affiliation you currently have on this node.
affiliation: Required<AffiliationAttribute> = "affiliation",
]
);
generate_element!(
/// Request to configure a node.
Configure, "configure", PUBSUB_OWNER,
attributes: [
/// The node to be configured.
node: Option<NodeName> = "node",
],
children: [
/// The form to configure it.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
]
);
generate_element!(
/// Request to change default configuration.
Default, "default", PUBSUB_OWNER,
children: [
/// The form to configure it.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
]
);
generate_element!(
/// Request to delete a node.
Delete, "delete", PUBSUB_OWNER,
attributes: [
/// The node to be configured.
node: Required<NodeName> = "node",
],
children: [
/// Redirection to replace the deleted node.
redirect: Option<Redirect> = ("redirect", PUBSUB_OWNER) => Redirect
]
);
generate_element!(
/// A redirect element.
Redirect, "redirect", PUBSUB_OWNER,
attributes: [
/// The node this node will be redirected to.
uri: Required<String> = "uri",
]
);
generate_element!(
/// Request to delete a node.
Purge, "purge", PUBSUB_OWNER,
attributes: [
/// The node to be configured.
node: Required<NodeName> = "node",
]
);
generate_element!(
/// A request for current subscriptions.
Subscriptions, "subscriptions", PUBSUB_OWNER,
attributes: [
/// The node to query.
node: Required<NodeName> = "node",
],
children: [
/// The list of subscription elements returned.
subscriptions: Vec<SubscriptionElem> = ("subscription", PUBSUB_OWNER) => SubscriptionElem
]
);
generate_element!(
/// A subscription element, describing the state of a subscription.
SubscriptionElem, "subscription", PUBSUB_OWNER,
attributes: [
/// The JID affected by this subscription.
jid: Required<Jid> = "jid",
/// The state of the subscription.
subscription: Required<Subscription> = "subscription",
/// Subscription unique id.
subid: Option<String> = "subid",
]
);
/// Main payload used to communicate with a PubSubOwner service.
///
/// `<pubsub xmlns="http://jabber.org/protocol/pubsub#owner"/>`
#[derive(Debug, Clone)]
pub enum PubSubOwner {
/// Manage the affiliations of a node.
Affiliations(Affiliations),
/// Request to configure a node, with optional suggested name and suggested configuration.
Configure(Configure),
/// Request the default node configuration.
Default(Default),
/// Delete a node.
Delete(Delete),
/// Purge all items from node.
Purge(Purge),
/// Request subscriptions of a node.
Subscriptions(Subscriptions),
}
impl IqGetPayload for PubSubOwner {}
impl IqSetPayload for PubSubOwner {}
impl IqResultPayload for PubSubOwner {}
impl TryFrom<Element> for PubSubOwner {
type Error = Error;
fn try_from(elem: Element) -> Result<PubSubOwner, Error> {
check_self!(elem, "pubsub", PUBSUB_OWNER);
check_no_attributes!(elem, "pubsub");
let mut payload = None;
for child in elem.children() {
if child.is("configure", ns::PUBSUB_OWNER) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub owner element.",
));
}
let configure = Configure::try_from(child.clone())?;
payload = Some(PubSubOwner::Configure(configure));
} else {
return Err(Error::ParseError("Unknown child in pubsub element."));
}
}
Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?)
}
}
impl From<PubSubOwner> for Element {
fn from(pubsub: PubSubOwner) -> Element {
Element::builder("pubsub", ns::PUBSUB_OWNER)
.append_all(match pubsub {
PubSubOwner::Affiliations(affiliations) => vec![Element::from(affiliations)],
PubSubOwner::Configure(configure) => vec![Element::from(configure)],
PubSubOwner::Default(default) => vec![Element::from(default)],
PubSubOwner::Delete(delete) => vec![Element::from(delete)],
PubSubOwner::Purge(purge) => vec![Element::from(purge)],
PubSubOwner::Subscriptions(subscriptions) => vec![Element::from(subscriptions)],
})
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
use jid::BareJid;
use std::str::FromStr;
#[test]
fn affiliations() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><affiliations node='foo'><affiliation jid='hamlet@denmark.lit' affiliation='owner'/><affiliation jid='polonius@denmark.lit' affiliation='outcast'/></affiliations></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSubOwner::Affiliations(Affiliations {
node: NodeName(String::from("foo")),
affiliations: vec![
Affiliation {
jid: Jid::Bare(BareJid::from_str("hamlet@denmark.lit").unwrap()),
affiliation: AffiliationAttribute::Owner,
},
Affiliation {
jid: Jid::Bare(BareJid::from_str("polonius@denmark.lit").unwrap()),
affiliation: AffiliationAttribute::Outcast,
},
],
});
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn configure() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><configure node='foo'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model' type='list-single'><value>whitelist</value></field></x></configure></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSubOwner::Configure(Configure {
node: Some(NodeName(String::from("foo"))),
form: Some(DataForm {
type_: DataFormType::Submit,
form_type: Some(String::from(ns::PUBSUB_CONFIGURE)),
title: None,
instructions: None,
fields: vec![Field {
var: String::from("pubsub#access_model"),
type_: FieldType::ListSingle,
label: None,
required: false,
options: vec![],
values: vec![String::from("whitelist")],
media: vec![],
}],
}),
});
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn test_serialize_configure() {
let reference: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><configure node='foo'><x xmlns='jabber:x:data' type='submit'/></configure></pubsub>"
.parse()
.unwrap();
let elem: Element = "<x xmlns='jabber:x:data' type='submit'/>".parse().unwrap();
let form = DataForm::try_from(elem).unwrap();
let configure = PubSubOwner::Configure(Configure {
node: Some(NodeName(String::from("foo"))),
form: Some(form),
});
let serialized: Element = configure.into();
assert_eq!(serialized, reference);
}
#[test]
fn default() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><default><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model' type='list-single'><value>whitelist</value></field></x></default></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSubOwner::Default(Default {
form: Some(DataForm {
type_: DataFormType::Submit,
form_type: Some(String::from(ns::PUBSUB_CONFIGURE)),
title: None,
instructions: None,
fields: vec![Field {
var: String::from("pubsub#access_model"),
type_: FieldType::ListSingle,
label: None,
required: false,
options: vec![],
values: vec![String::from("whitelist")],
media: vec![],
}],
}),
});
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn delete() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><delete node='foo'><redirect uri='xmpp:hamlet@denmark.lit?;node=blog'/></delete></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSubOwner::Delete(Delete {
node: NodeName(String::from("foo")),
redirect: Some(Redirect {
uri: String::from("xmpp:hamlet@denmark.lit?;node=blog"),
}),
});
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn purge() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><purge node='foo'></purge></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSubOwner::Purge(Purge {
node: NodeName(String::from("foo")),
});
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn subscriptions() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><subscriptions node='foo'><subscription jid='hamlet@denmark.lit' subscription='subscribed'/><subscription jid='polonius@denmark.lit' subscription='unconfigured'/><subscription jid='bernardo@denmark.lit' subscription='subscribed' subid='123-abc'/><subscription jid='bernardo@denmark.lit' subscription='subscribed' subid='004-yyy'/></subscriptions></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSubOwner::Subscriptions(Subscriptions {
node: NodeName(String::from("foo")),
subscriptions: vec![
SubscriptionElem {
jid: Jid::Bare(BareJid::from_str("hamlet@denmark.lit").unwrap()),
subscription: Subscription::Subscribed,
subid: None,
},
SubscriptionElem {
jid: Jid::Bare(BareJid::from_str("polonius@denmark.lit").unwrap()),
subscription: Subscription::Unconfigured,
subid: None,
},
SubscriptionElem {
jid: Jid::Bare(BareJid::from_str("bernardo@denmark.lit").unwrap()),
subscription: Subscription::Subscribed,
subid: Some(String::from("123-abc")),
},
SubscriptionElem {
jid: Jid::Bare(BareJid::from_str("bernardo@denmark.lit").unwrap()),
subscription: Subscription::Subscribed,
subid: Some(String::from("004-yyy")),
},
],
});
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
}

View file

@ -0,0 +1,772 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::DataForm;
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
use crate::ns;
use crate::pubsub::{
AffiliationAttribute, Item as PubSubItem, NodeName, Subscription, SubscriptionId,
};
use crate::util::error::Error;
use crate::Element;
use jid::Jid;
use std::convert::TryFrom;
// TODO: a better solution would be to split this into a query and a result elements, like for
// XEP-0030.
generate_element!(
/// A list of affiliations you have on a service, or on a node.
Affiliations, "affiliations", PUBSUB,
attributes: [
/// The optional node name this request pertains to.
node: Option<NodeName> = "node",
],
children: [
/// The actual list of affiliation elements.
affiliations: Vec<Affiliation> = ("affiliation", PUBSUB) => Affiliation
]
);
generate_element!(
/// An affiliation element.
Affiliation, "affiliation", PUBSUB,
attributes: [
/// The node this affiliation pertains to.
node: Required<NodeName> = "node",
/// The affiliation you currently have on this node.
affiliation: Required<AffiliationAttribute> = "affiliation",
]
);
generate_element!(
/// Request to configure a new node.
Configure, "configure", PUBSUB,
children: [
/// The form to configure it.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
]
);
generate_element!(
/// Request to create a new node.
Create, "create", PUBSUB,
attributes: [
/// The node name to create, if `None` the service will generate one.
node: Option<NodeName> = "node",
]
);
generate_element!(
/// Request for a default node configuration.
Default, "default", PUBSUB,
attributes: [
/// The node targeted by this request, otherwise the entire service.
node: Option<NodeName> = "node",
// TODO: do we really want to support collection nodes?
// type: Option<String> = "type",
]
);
generate_element!(
/// A request for a list of items.
Items, "items", PUBSUB,
attributes: [
// TODO: should be an xs:positiveInteger, that is, an unbounded int ≥ 1.
/// Maximum number of items returned.
max_items: Option<u32> = "max_items",
/// The node queried by this request.
node: Required<NodeName> = "node",
/// The subscription identifier related to this request.
subid: Option<SubscriptionId> = "subid",
],
children: [
/// The actual list of items returned.
items: Vec<Item> = ("item", PUBSUB) => Item
]
);
impl Items {
/// Create a new items request.
pub fn new(node: &str) -> Items {
Items {
node: NodeName(String::from(node)),
max_items: None,
subid: None,
items: Vec::new(),
}
}
}
/// Response wrapper for a PubSub `<item/>`.
#[derive(Debug, Clone, PartialEq)]
pub struct Item(pub PubSubItem);
impl_pubsub_item!(Item, PUBSUB);
generate_element!(
/// The options associated to a subscription request.
Options, "options", PUBSUB,
attributes: [
/// The JID affected by this request.
jid: Required<Jid> = "jid",
/// The node affected by this request.
node: Option<NodeName> = "node",
/// The subscription identifier affected by this request.
subid: Option<SubscriptionId> = "subid",
],
children: [
/// The form describing the subscription.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
]
);
generate_element!(
/// Request to publish items to a node.
Publish, "publish", PUBSUB,
attributes: [
/// The target node for this operation.
node: Required<NodeName> = "node",
],
children: [
/// The items you want to publish.
items: Vec<Item> = ("item", PUBSUB) => Item
]
);
generate_element!(
/// The options associated to a publish request.
PublishOptions, "publish-options", PUBSUB,
children: [
/// The form describing these options.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
]
);
generate_attribute!(
/// Whether a retract request should notify subscribers or not.
Notify,
"notify",
bool
);
generate_element!(
/// A request to retract some items from a node.
Retract, "retract", PUBSUB,
attributes: [
/// The node affected by this request.
node: Required<NodeName> = "node",
/// Whether a retract request should notify subscribers or not.
notify: Default<Notify> = "notify",
],
children: [
/// The items affected by this request.
items: Vec<Item> = ("item", PUBSUB) => Item
]
);
/// Indicate that the subscription can be configured.
#[derive(Debug, Clone, PartialEq)]
pub struct SubscribeOptions {
/// If `true`, the configuration is actually required.
required: bool,
}
impl TryFrom<Element> for SubscribeOptions {
type Error = Error;
fn try_from(elem: Element) -> Result<Self, Error> {
check_self!(elem, "subscribe-options", PUBSUB);
check_no_attributes!(elem, "subscribe-options");
let mut required = false;
for child in elem.children() {
if child.is("required", ns::PUBSUB) {
if required {
return Err(Error::ParseError(
"More than one required element in subscribe-options.",
));
}
required = true;
} else {
return Err(Error::ParseError(
"Unknown child in subscribe-options element.",
));
}
}
Ok(SubscribeOptions { required })
}
}
impl From<SubscribeOptions> for Element {
fn from(subscribe_options: SubscribeOptions) -> Element {
Element::builder("subscribe-options", ns::PUBSUB)
.append_all(if subscribe_options.required {
Some(Element::builder("required", ns::PUBSUB))
} else {
None
})
.build()
}
}
generate_element!(
/// A request to subscribe a JID to a node.
Subscribe, "subscribe", PUBSUB,
attributes: [
/// The JID being subscribed.
jid: Required<Jid> = "jid",
/// The node to subscribe to.
node: Option<NodeName> = "node",
]
);
generate_element!(
/// A request for current subscriptions.
Subscriptions, "subscriptions", PUBSUB,
attributes: [
/// The node to query.
node: Option<NodeName> = "node",
],
children: [
/// The list of subscription elements returned.
subscription: Vec<SubscriptionElem> = ("subscription", PUBSUB) => SubscriptionElem
]
);
generate_element!(
/// A subscription element, describing the state of a subscription.
SubscriptionElem, "subscription", PUBSUB,
attributes: [
/// The JID affected by this subscription.
jid: Required<Jid> = "jid",
/// The node affected by this subscription.
node: Option<NodeName> = "node",
/// The subscription identifier for this subscription.
subid: Option<SubscriptionId> = "subid",
/// The state of the subscription.
subscription: Option<Subscription> = "subscription",
],
children: [
/// The options related to this subscription.
subscribe_options: Option<SubscribeOptions> = ("subscribe-options", PUBSUB) => SubscribeOptions
]
);
generate_element!(
/// An unsubscribe request.
Unsubscribe, "unsubscribe", PUBSUB,
attributes: [
/// The JID affected by this request.
jid: Required<Jid> = "jid",
/// The node affected by this request.
node: Option<NodeName> = "node",
/// The subscription identifier for this subscription.
subid: Option<SubscriptionId> = "subid",
]
);
/// Main payload used to communicate with a PubSub service.
///
/// `<pubsub xmlns="http://jabber.org/protocol/pubsub"/>`
#[derive(Debug, Clone, PartialEq)]
pub enum PubSub {
/// Request to create a new node, with optional suggested name and suggested configuration.
Create {
/// The create request.
create: Create,
/// The configure request for the new node.
configure: Option<Configure>,
},
/// A subcribe request.
Subscribe {
/// The subscribe request.
subscribe: Option<Subscribe>,
/// The options related to this subscribe request.
options: Option<Options>,
},
/// Request to publish items to a node, with optional options.
Publish {
/// The publish request.
publish: Publish,
/// The options related to this publish request.
publish_options: Option<PublishOptions>,
},
/// A list of affiliations you have on a service, or on a node.
Affiliations(Affiliations),
/// Request for a default node configuration.
Default(Default),
/// A request for a list of items.
Items(Items),
/// A request to retract some items from a node.
Retract(Retract),
/// A request about a subscription.
Subscription(SubscriptionElem),
/// A request for current subscriptions.
Subscriptions(Subscriptions),
/// An unsubscribe request.
Unsubscribe(Unsubscribe),
}
impl IqGetPayload for PubSub {}
impl IqSetPayload for PubSub {}
impl IqResultPayload for PubSub {}
impl TryFrom<Element> for PubSub {
type Error = Error;
fn try_from(elem: Element) -> Result<PubSub, Error> {
check_self!(elem, "pubsub", PUBSUB);
check_no_attributes!(elem, "pubsub");
let mut payload = None;
for child in elem.children() {
if child.is("create", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let create = Create::try_from(child.clone())?;
payload = Some(PubSub::Create {
create,
configure: None,
});
} else if child.is("subscribe", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let subscribe = Subscribe::try_from(child.clone())?;
payload = Some(PubSub::Subscribe {
subscribe: Some(subscribe),
options: None,
});
} else if child.is("options", ns::PUBSUB) {
if let Some(PubSub::Subscribe { subscribe, options }) = payload {
if options.is_some() {
return Err(Error::ParseError(
"Options is already defined in pubsub element.",
));
}
let options = Some(Options::try_from(child.clone())?);
payload = Some(PubSub::Subscribe { subscribe, options });
} else if payload.is_none() {
let options = Options::try_from(child.clone())?;
payload = Some(PubSub::Subscribe {
subscribe: None,
options: Some(options),
});
} else {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
} else if child.is("configure", ns::PUBSUB) {
if let Some(PubSub::Create { create, configure }) = payload {
if configure.is_some() {
return Err(Error::ParseError(
"Configure is already defined in pubsub element.",
));
}
let configure = Some(Configure::try_from(child.clone())?);
payload = Some(PubSub::Create { create, configure });
} else {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
} else if child.is("publish", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let publish = Publish::try_from(child.clone())?;
payload = Some(PubSub::Publish {
publish,
publish_options: None,
});
} else if child.is("publish-options", ns::PUBSUB) {
if let Some(PubSub::Publish {
publish,
publish_options,
}) = payload
{
if publish_options.is_some() {
return Err(Error::ParseError(
"Publish-options are already defined in pubsub element.",
));
}
let publish_options = Some(PublishOptions::try_from(child.clone())?);
payload = Some(PubSub::Publish {
publish,
publish_options,
});
} else {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
} else if child.is("affiliations", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let affiliations = Affiliations::try_from(child.clone())?;
payload = Some(PubSub::Affiliations(affiliations));
} else if child.is("default", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let default = Default::try_from(child.clone())?;
payload = Some(PubSub::Default(default));
} else if child.is("items", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let items = Items::try_from(child.clone())?;
payload = Some(PubSub::Items(items));
} else if child.is("retract", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let retract = Retract::try_from(child.clone())?;
payload = Some(PubSub::Retract(retract));
} else if child.is("subscription", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let subscription = SubscriptionElem::try_from(child.clone())?;
payload = Some(PubSub::Subscription(subscription));
} else if child.is("subscriptions", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let subscriptions = Subscriptions::try_from(child.clone())?;
payload = Some(PubSub::Subscriptions(subscriptions));
} else if child.is("unsubscribe", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let unsubscribe = Unsubscribe::try_from(child.clone())?;
payload = Some(PubSub::Unsubscribe(unsubscribe));
} else {
return Err(Error::ParseError("Unknown child in pubsub element."));
}
}
Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?)
}
}
impl From<PubSub> for Element {
fn from(pubsub: PubSub) -> Element {
Element::builder("pubsub", ns::PUBSUB)
.append_all(match pubsub {
PubSub::Create { create, configure } => {
let mut elems = vec![Element::from(create)];
if let Some(configure) = configure {
elems.push(Element::from(configure));
}
elems
}
PubSub::Subscribe { subscribe, options } => {
let mut elems = vec![];
if let Some(subscribe) = subscribe {
elems.push(Element::from(subscribe));
}
if let Some(options) = options {
elems.push(Element::from(options));
}
elems
}
PubSub::Publish {
publish,
publish_options,
} => {
let mut elems = vec![Element::from(publish)];
if let Some(publish_options) = publish_options {
elems.push(Element::from(publish_options));
}
elems
}
PubSub::Affiliations(affiliations) => vec![Element::from(affiliations)],
PubSub::Default(default) => vec![Element::from(default)],
PubSub::Items(items) => vec![Element::from(items)],
PubSub::Retract(retract) => vec![Element::from(retract)],
PubSub::Subscription(subscription) => vec![Element::from(subscription)],
PubSub::Subscriptions(subscriptions) => vec![Element::from(subscriptions)],
PubSub::Unsubscribe(unsubscribe) => vec![Element::from(unsubscribe)],
})
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
use jid::FullJid;
#[test]
fn create() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Create { create, configure } => {
assert!(create.node.is_none());
assert!(configure.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
let elem: Element =
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><create node='coucou'/></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Create { create, configure } => {
assert_eq!(&create.node.unwrap().0, "coucou");
assert!(configure.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn create_and_configure_empty() {
let elem: Element =
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/><configure/></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Create { create, configure } => {
assert!(create.node.is_none());
assert!(configure.unwrap().form.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn create_and_configure_simple() {
// XXX: Do we want xmpp-parsers to always specify the field type in the output Element?
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create node='foo'/><configure><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model' type='list-single'><value>whitelist</value></field></x></configure></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::Create {
create: Create {
node: Some(NodeName(String::from("foo"))),
},
configure: Some(Configure {
form: Some(DataForm {
type_: DataFormType::Submit,
form_type: Some(String::from(ns::PUBSUB_CONFIGURE)),
title: None,
instructions: None,
fields: vec![Field {
var: String::from("pubsub#access_model"),
type_: FieldType::ListSingle,
label: None,
required: false,
options: vec![],
values: vec![String::from("whitelist")],
media: vec![],
}],
}),
}),
};
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn publish() {
let elem: Element =
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Publish {
publish,
publish_options,
} => {
assert_eq!(&publish.node.0, "coucou");
assert!(publish_options.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn publish_with_publish_options() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/><publish-options/></pubsub>".parse().unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Publish {
publish,
publish_options,
} => {
assert_eq!(&publish.node.0, "coucou");
assert!(publish_options.unwrap().form.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn invalid_empty_pubsub() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'/>"
.parse()
.unwrap();
let error = PubSub::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "No payload in pubsub element.");
}
#[test]
fn publish_option() {
let elem: Element = "<publish-options xmlns='http://jabber.org/protocol/pubsub'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#publish-options</value></field></x></publish-options>".parse().unwrap();
let publish_options = PublishOptions::try_from(elem).unwrap();
assert_eq!(
&publish_options.form.unwrap().form_type.unwrap(),
"http://jabber.org/protocol/pubsub#publish-options"
);
}
#[test]
fn subscribe_options() {
let elem1: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'/>"
.parse()
.unwrap();
let subscribe_options1 = SubscribeOptions::try_from(elem1).unwrap();
assert_eq!(subscribe_options1.required, false);
let elem2: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'><required/></subscribe-options>".parse().unwrap();
let subscribe_options2 = SubscribeOptions::try_from(elem2).unwrap();
assert_eq!(subscribe_options2.required, true);
}
#[test]
fn test_options_without_subscribe() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><options xmlns='http://jabber.org/protocol/pubsub' jid='juliet@capulet.lit/balcony'><x xmlns='jabber:x:data' type='submit'/></options></pubsub>".parse().unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Subscribe { subscribe, options } => {
assert!(subscribe.is_none());
assert!(options.is_some());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert_eq!(elem1, elem2);
}
#[test]
fn test_serialize_options() {
let reference: Element = "<options xmlns='http://jabber.org/protocol/pubsub' jid='juliet@capulet.lit/balcony'><x xmlns='jabber:x:data' type='submit'/></options>"
.parse()
.unwrap();
let elem: Element = "<x xmlns='jabber:x:data' type='submit'/>".parse().unwrap();
let form = DataForm::try_from(elem).unwrap();
let options = Options {
jid: Jid::Full(FullJid::new("juliet", "capulet.lit", "balcony")),
node: None,
subid: None,
form: Some(form),
};
let serialized: Element = options.into();
assert_eq!(serialized, reference);
}
#[test]
fn test_serialize_publish_options() {
let reference: Element = "<publish-options xmlns='http://jabber.org/protocol/pubsub'><x xmlns='jabber:x:data' type='submit'/></publish-options>"
.parse()
.unwrap();
let elem: Element = "<x xmlns='jabber:x:data' type='submit'/>".parse().unwrap();
let form = DataForm::try_from(elem).unwrap();
let options = PublishOptions { form: Some(form) };
let serialized: Element = options.into();
assert_eq!(serialized, reference);
}
}