1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use crate::data_forms::DataForm;
8use crate::date::DateTime;
9use crate::message::MessagePayload;
10use crate::ns;
11use crate::pubsub::{Item as PubSubItem, ItemId, NodeName, Subscription, SubscriptionId};
12use crate::util::error::Error;
13use crate::Element;
14use jid::Jid;
15use std::convert::TryFrom;
16
17/// Event wrapper for a PubSub `<item/>`.
18#[derive(Debug, Clone)]
19pub struct Item(pub PubSubItem);
20
21impl_pubsub_item!(Item, PUBSUB_EVENT);
22
23/// Represents an event happening to a PubSub node.
24#[derive(Debug, Clone)]
25pub enum PubSubEvent {
26 /*
27 Collection {
28 },
29 */
30 /// This node’s configuration changed.
31 Configuration {
32 /// The node affected.
33 node: NodeName,
34
35 /// The new configuration of this node.
36 form: Option<DataForm>,
37 },
38
39 /// This node has been deleted, with an optional redirect to another node.
40 Delete {
41 /// The node affected.
42 node: NodeName,
43
44 /// The xmpp: URI of another node replacing this one.
45 redirect: Option<String>,
46 },
47
48 /// Some items have been published on this node.
49 PublishedItems {
50 /// The node affected.
51 node: NodeName,
52
53 /// The list of published items.
54 items: Vec<Item>,
55 },
56
57 /// Some items have been removed from this node.
58 RetractedItems {
59 /// The node affected.
60 node: NodeName,
61
62 /// The list of retracted items.
63 items: Vec<ItemId>,
64 },
65
66 /// All items of this node just got removed at once.
67 Purge {
68 /// The node affected.
69 node: NodeName,
70 },
71
72 /// The user’s subscription to this node has changed.
73 Subscription {
74 /// The node affected.
75 node: NodeName,
76
77 /// The time at which this subscription will expire.
78 expiry: Option<DateTime>,
79
80 /// The JID of the user affected.
81 jid: Option<Jid>,
82
83 /// An identifier for this subscription.
84 subid: Option<SubscriptionId>,
85
86 /// The state of this subscription.
87 subscription: Option<Subscription>,
88 },
89}
90
91fn parse_items(elem: Element, node: NodeName) -> Result<PubSubEvent, Error> {
92 let mut is_retract = None;
93 let mut items = vec![];
94 let mut retracts = vec![];
95 for child in elem.children() {
96 if child.is("item", ns::PUBSUB_EVENT) {
97 match is_retract {
98 None => is_retract = Some(false),
99 Some(false) => (),
100 Some(true) => {
101 return Err(Error::ParseError(
102 "Mix of item and retract in items element.",
103 ));
104 }
105 }
106 items.push(Item::try_from(child.clone())?);
107 } else if child.is("retract", ns::PUBSUB_EVENT) {
108 match is_retract {
109 None => is_retract = Some(true),
110 Some(true) => (),
111 Some(false) => {
112 return Err(Error::ParseError(
113 "Mix of item and retract in items element.",
114 ));
115 }
116 }
117 check_no_children!(child, "retract");
118 check_no_unknown_attributes!(child, "retract", ["id"]);
119 let id = get_attr!(child, "id", Required);
120 retracts.push(id);
121 } else {
122 return Err(Error::ParseError("Invalid child in items element."));
123 }
124 }
125 Ok(match is_retract {
126 Some(false) => PubSubEvent::PublishedItems { node, items },
127 Some(true) => PubSubEvent::RetractedItems {
128 node,
129 items: retracts,
130 },
131 None => return Err(Error::ParseError("Missing children in items element.")),
132 })
133}
134
135impl TryFrom<Element> for PubSubEvent {
136 type Error = Error;
137
138 fn try_from(elem: Element) -> Result<PubSubEvent, Error> {
139 check_self!(elem, "event", PUBSUB_EVENT);
140 check_no_attributes!(elem, "event");
141
142 let mut payload = None;
143 for child in elem.children() {
144 let node = get_attr!(child, "node", Required);
145 if child.is("configuration", ns::PUBSUB_EVENT) {
146 let mut payloads = child.children().cloned().collect::<Vec<_>>();
147 let item = payloads.pop();
148 if !payloads.is_empty() {
149 return Err(Error::ParseError(
150 "More than a single payload in configuration element.",
151 ));
152 }
153 let form = match item {
154 None => None,
155 Some(payload) => Some(DataForm::try_from(payload)?),
156 };
157 payload = Some(PubSubEvent::Configuration { node, form });
158 } else if child.is("delete", ns::PUBSUB_EVENT) {
159 let mut redirect = None;
160 for item in child.children() {
161 if item.is("redirect", ns::PUBSUB_EVENT) {
162 if redirect.is_some() {
163 return Err(Error::ParseError(
164 "More than one redirect in delete element.",
165 ));
166 }
167 let uri = get_attr!(item, "uri", Required);
168 redirect = Some(uri);
169 } else {
170 return Err(Error::ParseError("Unknown child in delete element."));
171 }
172 }
173 payload = Some(PubSubEvent::Delete { node, redirect });
174 } else if child.is("items", ns::PUBSUB_EVENT) {
175 payload = Some(parse_items(child.clone(), node)?);
176 } else if child.is("purge", ns::PUBSUB_EVENT) {
177 check_no_children!(child, "purge");
178 payload = Some(PubSubEvent::Purge { node });
179 } else if child.is("subscription", ns::PUBSUB_EVENT) {
180 check_no_children!(child, "subscription");
181 payload = Some(PubSubEvent::Subscription {
182 node,
183 expiry: get_attr!(child, "expiry", Option),
184 jid: get_attr!(child, "jid", Option),
185 subid: get_attr!(child, "subid", Option),
186 subscription: get_attr!(child, "subscription", Option),
187 });
188 } else {
189 return Err(Error::ParseError("Unknown child in event element."));
190 }
191 }
192 payload.ok_or(Error::ParseError("No payload in event element."))
193 }
194}
195
196impl From<PubSubEvent> for Element {
197 fn from(event: PubSubEvent) -> Element {
198 let payload = match event {
199 PubSubEvent::Configuration { node, form } => {
200 Element::builder("configuration", ns::PUBSUB_EVENT)
201 .attr("node", node)
202 .append_all(form.map(Element::from))
203 }
204 PubSubEvent::Delete { node, redirect } => Element::builder("purge", ns::PUBSUB_EVENT)
205 .attr("node", node)
206 .append_all(redirect.map(|redirect| {
207 Element::builder("redirect", ns::PUBSUB_EVENT).attr("uri", redirect)
208 })),
209 PubSubEvent::PublishedItems { node, items } => {
210 Element::builder("items", ns::PUBSUB_EVENT)
211 .attr("node", node)
212 .append_all(items.into_iter())
213 }
214 PubSubEvent::RetractedItems { node, items } => {
215 Element::builder("items", ns::PUBSUB_EVENT)
216 .attr("node", node)
217 .append_all(
218 items
219 .into_iter()
220 .map(|id| Element::builder("retract", ns::PUBSUB_EVENT).attr("id", id)),
221 )
222 }
223 PubSubEvent::Purge { node } => {
224 Element::builder("purge", ns::PUBSUB_EVENT).attr("node", node)
225 }
226 PubSubEvent::Subscription {
227 node,
228 expiry,
229 jid,
230 subid,
231 subscription,
232 } => Element::builder("subscription", ns::PUBSUB_EVENT)
233 .attr("node", node)
234 .attr("expiry", expiry)
235 .attr("jid", jid)
236 .attr("subid", subid)
237 .attr("subscription", subscription),
238 };
239 Element::builder("event", ns::PUBSUB_EVENT)
240 .append(payload)
241 .build()
242 }
243}
244
245impl MessagePayload for PubSubEvent {}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use jid::BareJid;
251
252 #[test]
253 fn missing_items() {
254 let elem: Element =
255 "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'/></event>"
256 .parse()
257 .unwrap();
258 let error = PubSubEvent::try_from(elem).unwrap_err();
259 let message = match error {
260 Error::ParseError(string) => string,
261 _ => panic!(),
262 };
263 assert_eq!(message, "Missing children in items element.");
264 }
265
266 #[test]
267 fn test_simple_items() {
268 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'><item id='test' publisher='test@coucou'/></items></event>".parse().unwrap();
269 let event = PubSubEvent::try_from(elem).unwrap();
270 match event {
271 PubSubEvent::PublishedItems { node, items } => {
272 assert_eq!(node, NodeName(String::from("coucou")));
273 assert_eq!(items[0].id, Some(ItemId(String::from("test"))));
274 assert_eq!(
275 items[0].publisher.clone().unwrap(),
276 BareJid::new("test", "coucou")
277 );
278 assert_eq!(items[0].payload, None);
279 }
280 _ => panic!(),
281 }
282 }
283
284 #[test]
285 fn test_simple_pep() {
286 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><item><foreign xmlns='example:namespace'/></item></items></event>".parse().unwrap();
287 let event = PubSubEvent::try_from(elem).unwrap();
288 match event {
289 PubSubEvent::PublishedItems { node, items } => {
290 assert_eq!(node, NodeName(String::from("something")));
291 assert_eq!(items[0].id, None);
292 assert_eq!(items[0].publisher, None);
293 match items[0].payload {
294 Some(ref elem) => assert!(elem.is("foreign", "example:namespace")),
295 _ => panic!(),
296 }
297 }
298 _ => panic!(),
299 }
300 }
301
302 #[test]
303 fn test_simple_retract() {
304 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><retract id='coucou'/><retract id='test'/></items></event>".parse().unwrap();
305 let event = PubSubEvent::try_from(elem).unwrap();
306 match event {
307 PubSubEvent::RetractedItems { node, items } => {
308 assert_eq!(node, NodeName(String::from("something")));
309 assert_eq!(items[0], ItemId(String::from("coucou")));
310 assert_eq!(items[1], ItemId(String::from("test")));
311 }
312 _ => panic!(),
313 }
314 }
315
316 #[test]
317 fn test_simple_delete() {
318 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><delete node='coucou'><redirect uri='hello'/></delete></event>".parse().unwrap();
319 let event = PubSubEvent::try_from(elem).unwrap();
320 match event {
321 PubSubEvent::Delete { node, redirect } => {
322 assert_eq!(node, NodeName(String::from("coucou")));
323 assert_eq!(redirect, Some(String::from("hello")));
324 }
325 _ => panic!(),
326 }
327 }
328
329 #[test]
330 fn test_simple_purge() {
331 let elem: Element =
332 "<event xmlns='http://jabber.org/protocol/pubsub#event'><purge node='coucou'/></event>"
333 .parse()
334 .unwrap();
335 let event = PubSubEvent::try_from(elem).unwrap();
336 match event {
337 PubSubEvent::Purge { node } => {
338 assert_eq!(node, NodeName(String::from("coucou")));
339 }
340 _ => panic!(),
341 }
342 }
343
344 #[test]
345 fn test_simple_configure() {
346 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();
347 let event = PubSubEvent::try_from(elem).unwrap();
348 match event {
349 PubSubEvent::Configuration { node, form: _ } => {
350 assert_eq!(node, NodeName(String::from("coucou")));
351 //assert_eq!(form.type_, Result_);
352 }
353 _ => panic!(),
354 }
355 }
356
357 #[test]
358 fn test_invalid() {
359 let elem: Element =
360 "<event xmlns='http://jabber.org/protocol/pubsub#event'><coucou node='test'/></event>"
361 .parse()
362 .unwrap();
363 let error = PubSubEvent::try_from(elem).unwrap_err();
364 let message = match error {
365 Error::ParseError(string) => string,
366 _ => panic!(),
367 };
368 assert_eq!(message, "Unknown child in event element.");
369 }
370
371 #[cfg(not(feature = "disable-validation"))]
372 #[test]
373 fn test_invalid_attribute() {
374 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event' coucou=''/>"
375 .parse()
376 .unwrap();
377 let error = PubSubEvent::try_from(elem).unwrap_err();
378 let message = match error {
379 Error::ParseError(string) => string,
380 _ => panic!(),
381 };
382 assert_eq!(message, "Unknown attribute in event element.");
383 }
384
385 #[test]
386 fn test_ex221_subscription() {
387 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>"
388 .parse()
389 .unwrap();
390 let event = PubSubEvent::try_from(elem.clone()).unwrap();
391 match event.clone() {
392 PubSubEvent::Subscription {
393 node,
394 expiry,
395 jid,
396 subid,
397 subscription,
398 } => {
399 assert_eq!(node, NodeName(String::from("princely_musings")));
400 assert_eq!(
401 subid,
402 Some(SubscriptionId(String::from(
403 "ba49252aaa4f5d320c24d3766f0bdcade78c78d3"
404 )))
405 );
406 assert_eq!(subscription, Some(Subscription::Subscribed));
407 assert_eq!(jid.unwrap(), BareJid::new("francisco", "denmark.lit"));
408 assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap()));
409 }
410 _ => panic!(),
411 }
412
413 let elem2: Element = event.into();
414 assert_eq!(elem, elem2);
415 }
416}