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