pubsub.rs

  1// Copyright (c) 2018 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
  7#![deny(missing_docs)]
  8
  9use try_from::TryFrom;
 10
 11use minidom::Element;
 12use jid::Jid;
 13
 14use error::Error;
 15
 16use ns;
 17
 18use iq::{IqGetPayload, IqSetPayload, IqResultPayload};
 19use data_forms::DataForm;
 20
 21use pubsub::{NodeName, ItemId, Subscription, SubscriptionId};
 22
 23// TODO: a better solution would be to split this into a query and a result elements, like for
 24// XEP-0030.
 25generate_element_with_children!(
 26    /// A list of affiliations you have on a service, or on a node.
 27    Affiliations, "affiliations", PUBSUB,
 28    attributes: [
 29        /// The optional node name this request pertains to.
 30        node: Option<NodeName> = "node" => optional,
 31    ],
 32    children: [
 33        /// The actual list of affiliation elements.
 34        affiliations: Vec<Affiliation> = ("affiliation", PUBSUB) => Affiliation
 35    ]
 36);
 37
 38generate_attribute!(
 39    /// A list of possible affiliations to a node.
 40    AffiliationAttribute, "affiliation", {
 41        /// You are a member of this node, you can subscribe and retrieve items.
 42        Member => "member",
 43
 44        /// You don’t have a specific affiliation with this node, you can only subscribe to it.
 45        None => "none",
 46
 47        /// You are banned from this node.
 48        Outcast => "outcast",
 49
 50        /// You are an owner of this node, and can do anything with it.
 51        Owner => "owner",
 52
 53        /// You are a publisher on this node, you can publish and retract items to it.
 54        Publisher => "publisher",
 55
 56        /// You can publish and retract items on this node, but not subscribe or retrive items.
 57        PublishOnly => "publish-only",
 58    }
 59);
 60
 61generate_element_with_only_attributes!(
 62    /// An affiliation element.
 63    Affiliation, "affiliation", PUBSUB, [
 64        /// The node this affiliation pertains to.
 65        node: NodeName = "node" => required,
 66
 67        /// The affiliation you currently have on this node.
 68        affiliation: AffiliationAttribute = "affiliation" => required,
 69    ]
 70);
 71
 72generate_element_with_children!(
 73    /// Request to configure a new node.
 74    Configure, "configure", PUBSUB,
 75    child: (
 76        /// The form to configure it.
 77        form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
 78    )
 79);
 80
 81generate_element_with_only_attributes!(
 82    /// Request to create a new node.
 83    Create, "create", PUBSUB, [
 84        /// The node name to create, if `None` the service will generate one.
 85        node: Option<NodeName> = "node" => optional,
 86    ]
 87);
 88
 89generate_element_with_only_attributes!(
 90    /// Request for a default node configuration.
 91    Default, "default", PUBSUB, [
 92        /// The node targetted by this request, otherwise the entire service.
 93        node: Option<NodeName> = "node" => optional,
 94
 95        // TODO: do we really want to support collection nodes?
 96        // type: String = "type" => optional,
 97    ]
 98);
 99
100generate_element_with_children!(
101    /// A request for a list of items.
102    Items, "items", PUBSUB,
103    attributes: [
104        // TODO: should be an xs:positiveInteger, that is, an unbounded int ≥ 1.
105        /// Maximum number of items returned.
106        max_items: Option<u32> = "max_items" => optional,
107
108        /// The node queried by this request.
109        node: NodeName = "node" => required,
110
111        /// The subscription identifier related to this request.
112        subid: Option<SubscriptionId> = "subid" => optional,
113    ],
114    children: [
115        /// The actual list of items returned.
116        items: Vec<Item> = ("item", PUBSUB) => Item
117    ]
118);
119
120/// An item from a PubSub node.
121#[derive(Debug, Clone)]
122pub struct Item {
123    /// The payload of this item, in an arbitrary namespace.
124    pub payload: Option<Element>,
125
126    /// The 'id' attribute of this item.
127    pub id: Option<ItemId>,
128}
129
130impl TryFrom<Element> for Item {
131    type Err = Error;
132
133    fn try_from(elem: Element) -> Result<Item, Error> {
134        check_self!(elem, "item", PUBSUB);
135        check_no_unknown_attributes!(elem, "item", ["id"]);
136        let mut payloads = elem.children().cloned().collect::<Vec<_>>();
137        let payload = payloads.pop();
138        if !payloads.is_empty() {
139            return Err(Error::ParseError("More than a single payload in item element."));
140        }
141        Ok(Item {
142            payload,
143            id: get_attr!(elem, "id", optional),
144        })
145    }
146}
147
148impl From<Item> for Element {
149    fn from(item: Item) -> Element {
150        Element::builder("item")
151                .ns(ns::PUBSUB)
152                .attr("id", item.id)
153                .append(item.payload)
154                .build()
155    }
156}
157
158generate_element_with_children!(
159    /// The options associated to a subscription request.
160    Options, "options", PUBSUB,
161    attributes: [
162        /// The JID affected by this request.
163        jid: Jid = "jid" => required,
164
165        /// The node affected by this request.
166        node: Option<NodeName> = "node" => optional,
167
168        /// The subscription identifier affected by this request.
169        subid: Option<SubscriptionId> = "subid" => optional,
170    ],
171    child: (
172        /// The form describing the subscription.
173        form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
174    )
175);
176
177generate_element_with_children!(
178    /// Request to publish items to a node.
179    Publish, "publish", PUBSUB,
180    attributes: [
181        /// The target node for this operation.
182        node: NodeName = "node" => required,
183    ],
184    children: [
185        /// The items you want to publish.
186        items: Vec<Item> = ("item", PUBSUB) => Item
187    ]
188);
189
190generate_element_with_children!(
191    /// The options associated to a publish request.
192    PublishOptions, "publish-options", PUBSUB,
193    child: (
194        /// The form describing these options.
195        form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
196    )
197);
198
199generate_attribute!(
200    /// Whether a retract request should notify subscribers or not.
201    Notify, "notify", bool
202);
203
204generate_element_with_children!(
205    /// A request to retract some items from a node.
206    Retract, "retract", PUBSUB,
207    attributes: [
208        /// The node affected by this request.
209        node: NodeName = "node" => required,
210
211        /// Whether a retract request should notify subscribers or not.
212        notify: Notify = "notify" => default,
213    ],
214    children: [
215        /// The items affected by this request.
216        items: Vec<Item> = ("item", PUBSUB) => Item
217    ]
218);
219
220/// Indicate that the subscription can be configured.
221#[derive(Debug, Clone)]
222pub struct SubscribeOptions {
223    /// If `true`, the configuration is actually required.
224    required: bool,
225}
226
227impl TryFrom<Element> for SubscribeOptions {
228    type Err = Error;
229
230    fn try_from(elem: Element) -> Result<Self, Error> {
231        check_self!(elem, "subscribe-options", PUBSUB);
232        check_no_attributes!(elem, "subscribe-options");
233        let mut required = false;
234        for child in elem.children() {
235            if child.is("required", ns::PUBSUB) {
236                if required {
237                    return Err(Error::ParseError("More than one required element in subscribe-options."));
238                }
239                required = true;
240            } else {
241                return Err(Error::ParseError("Unknown child in subscribe-options element."));
242            }
243        }
244        Ok(SubscribeOptions { required })
245    }
246}
247
248impl From<SubscribeOptions> for Element {
249    fn from(subscribe_options: SubscribeOptions) -> Element {
250        Element::builder("subscribe-options")
251            .ns(ns::PUBSUB)
252            .append(if subscribe_options.required {
253                 vec!(Element::builder("required")
254                     .ns(ns::PUBSUB)
255                     .build())
256             } else {
257                 vec!()
258             })
259            .build()
260    }
261}
262
263generate_element_with_only_attributes!(
264    /// A request to subscribe a JID to a node.
265    Subscribe, "subscribe", PUBSUB, [
266        /// The JID being subscribed.
267        jid: Jid = "jid" => required,
268
269        /// The node to subscribe to.
270        node: Option<NodeName> = "node" => optional,
271    ]
272);
273
274generate_element_with_children!(
275    /// A request for current subscriptions.
276    Subscriptions, "subscriptions", PUBSUB,
277    attributes: [
278        /// The node to query.
279        node: Option<NodeName> = "node" => optional,
280    ],
281    children: [
282        /// The list of subscription elements returned.
283        subscription: Vec<SubscriptionElem> = ("subscription", PUBSUB) => SubscriptionElem
284    ]
285);
286
287generate_element_with_children!(
288    /// A subscription element, describing the state of a subscription.
289    SubscriptionElem, "subscription", PUBSUB,
290    attributes: [
291        /// The JID affected by this subscription.
292        jid: Jid = "jid" => required,
293
294        /// The node affected by this subscription.
295        node: Option<NodeName> = "node" => optional,
296
297        /// The subscription identifier for this subscription.
298        subid: Option<SubscriptionId> = "subid" => optional,
299
300        /// The state of the subscription.
301        subscription: Option<Subscription> = "subscription" => optional,
302    ],
303    child: (
304        /// The options related to this subscription.
305        subscribe_options: Option<SubscribeOptions> = ("subscribe-options", PUBSUB) => SubscribeOptions
306    )
307);
308
309generate_element_with_only_attributes!(
310    /// An unsubscribe request.
311    Unsubscribe, "unsubscribe", PUBSUB, [
312        /// The JID affected by this request.
313        jid: Jid = "jid" => required,
314
315        /// The node affected by this request.
316        node: Option<NodeName> = "node" => optional,
317
318        /// The subscription identifier for this subscription.
319        subid: Option<SubscriptionId> = "subid" => optional,
320    ]
321);
322
323/// Main payload used to communicate with a PubSub service.
324///
325/// `<pubsub xmlns="http://jabber.org/protocol/pubsub"/>`
326#[derive(Debug, Clone)]
327pub enum PubSub {
328    /// Request to create a new node, with optional suggested name and suggested configuration.
329    Create {
330        /// The create request.
331        create: Create,
332
333        /// The configure request for the new node.
334        configure: Option<Configure>
335    },
336
337    /// Request to publish items to a node, with optional options.
338    Publish {
339        /// The publish request.
340        publish: Publish,
341
342        /// The options related to this publish request.
343        publish_options: Option<PublishOptions>
344    },
345
346    /// A list of affiliations you have on a service, or on a node.
347    Affiliations(Affiliations),
348
349    /// Request for a default node configuration.
350    Default(Default),
351
352    /// A request for a list of items.
353    Items(Items),
354
355    /// A request to retract some items from a node.
356    Retract(Retract),
357
358    /// A request about a subscription.
359    Subscription(SubscriptionElem),
360
361    /// A request for current subscriptions.
362    Subscriptions(Subscriptions),
363
364    /// An unsubscribe request.
365    Unsubscribe(Unsubscribe),
366}
367
368impl IqGetPayload for PubSub {}
369impl IqSetPayload for PubSub {}
370impl IqResultPayload for PubSub {}
371
372impl TryFrom<Element> for PubSub {
373    type Err = Error;
374
375    fn try_from(elem: Element) -> Result<PubSub, Error> {
376        check_self!(elem, "pubsub", PUBSUB);
377        check_no_attributes!(elem, "pubsub");
378
379        let mut payload = None;
380        for child in elem.children() {
381            if child.is("create", ns::PUBSUB) {
382                if payload.is_some() {
383                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
384                }
385                let create = Create::try_from(child.clone())?;
386                payload = Some(PubSub::Create { create, configure: None });
387            } else if child.is("configure", ns::PUBSUB) {
388                if let Some(PubSub::Create { create, configure }) = payload {
389                    if configure.is_some() {
390                        return Err(Error::ParseError("Configure is already defined in pubsub element."));
391                    }
392                    let configure = Some(Configure::try_from(child.clone())?);
393                    payload = Some(PubSub::Create { create, configure });
394                } else {
395                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
396                }
397            } else if child.is("publish", ns::PUBSUB) {
398                if payload.is_some() {
399                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
400                }
401                let publish = Publish::try_from(child.clone())?;
402                payload = Some(PubSub::Publish { publish, publish_options: None });
403            } else if child.is("publish-options", ns::PUBSUB) {
404                if let Some(PubSub::Publish { publish, publish_options }) = payload {
405                    if publish_options.is_some() {
406                        return Err(Error::ParseError("Publish-options are already defined in pubsub element."));
407                    }
408                    let publish_options = Some(PublishOptions::try_from(child.clone())?);
409                    payload = Some(PubSub::Publish { publish, publish_options });
410                } else {
411                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
412                }
413            } else if child.is("affiliations", ns::PUBSUB) {
414                if payload.is_some() {
415                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
416                }
417                let affiliations = Affiliations::try_from(child.clone())?;
418                payload = Some(PubSub::Affiliations(affiliations));
419            } else if child.is("default", ns::PUBSUB) {
420                if payload.is_some() {
421                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
422                }
423                let default = Default::try_from(child.clone())?;
424                payload = Some(PubSub::Default(default));
425            } else if child.is("items", ns::PUBSUB) {
426                if payload.is_some() {
427                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
428                }
429                let items = Items::try_from(child.clone())?;
430                payload = Some(PubSub::Items(items));
431            } else if child.is("retract", ns::PUBSUB) {
432                if payload.is_some() {
433                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
434                }
435                let retract = Retract::try_from(child.clone())?;
436                payload = Some(PubSub::Retract(retract));
437            } else if child.is("subscription", ns::PUBSUB) {
438                if payload.is_some() {
439                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
440                }
441                let subscription = SubscriptionElem::try_from(child.clone())?;
442                payload = Some(PubSub::Subscription(subscription));
443            } else if child.is("subscriptions", ns::PUBSUB) {
444                if payload.is_some() {
445                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
446                }
447                let subscriptions = Subscriptions::try_from(child.clone())?;
448                payload = Some(PubSub::Subscriptions(subscriptions));
449            } else if child.is("unsubscribe", ns::PUBSUB) {
450                if payload.is_some() {
451                    return Err(Error::ParseError("Payload is already defined in pubsub element."));
452                }
453                let unsubscribe = Unsubscribe::try_from(child.clone())?;
454                payload = Some(PubSub::Unsubscribe(unsubscribe));
455            } else {
456                return Err(Error::ParseError("Unknown child in pubsub element."));
457            }
458        }
459        Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?)
460    }
461}
462
463impl From<PubSub> for Element {
464    fn from(pubsub: PubSub) -> Element {
465        Element::builder("pubsub")
466            .ns(ns::PUBSUB)
467            .append(match pubsub {
468                 PubSub::Create { create, configure } => {
469                     let mut elems = vec!(Element::from(create));
470                     if let Some(configure) = configure {
471                         elems.push(Element::from(configure));
472                     }
473                     elems
474                 },
475                 PubSub::Publish { publish, publish_options } => {
476                     let mut elems = vec!(Element::from(publish));
477                     if let Some(publish_options) = publish_options {
478                         elems.push(Element::from(publish_options));
479                     }
480                     elems
481                 },
482                 PubSub::Affiliations(affiliations) => vec!(Element::from(affiliations)),
483                 PubSub::Default(default) => vec!(Element::from(default)),
484                 PubSub::Items(items) => vec!(Element::from(items)),
485                 PubSub::Retract(retract) => vec!(Element::from(retract)),
486                 PubSub::Subscription(subscription) => vec!(Element::from(subscription)),
487                 PubSub::Subscriptions(subscriptions) => vec!(Element::from(subscriptions)),
488                 PubSub::Unsubscribe(unsubscribe) => vec!(Element::from(unsubscribe)),
489             })
490            .build()
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use compare_elements::NamespaceAwareCompare;
498
499    #[test]
500    fn create() {
501        let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/></pubsub>".parse().unwrap();
502        let elem1 = elem.clone();
503        let pubsub = PubSub::try_from(elem).unwrap();
504        match pubsub.clone() {
505            PubSub::Create { create, configure } => {
506                assert!(create.node.is_none());
507                assert!(configure.is_none());
508            }
509            _ => panic!(),
510        }
511
512        let elem2 = Element::from(pubsub);
513        assert!(elem1.compare_to(&elem2));
514
515        let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create node='coucou'/></pubsub>".parse().unwrap();
516        let elem1 = elem.clone();
517        let pubsub = PubSub::try_from(elem).unwrap();
518        match pubsub.clone() {
519            PubSub::Create { create, configure } => {
520                assert_eq!(&create.node.unwrap().0, "coucou");
521                assert!(configure.is_none());
522            }
523            _ => panic!(),
524        }
525
526        let elem2 = Element::from(pubsub);
527        assert!(elem1.compare_to(&elem2));
528    }
529
530    #[test]
531    fn create_and_configure() {
532        let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/><configure/></pubsub>".parse().unwrap();
533        let elem1 = elem.clone();
534        let pubsub = PubSub::try_from(elem).unwrap();
535        match pubsub.clone() {
536            PubSub::Create { create, configure } => {
537                assert!(create.node.is_none());
538                assert!(configure.unwrap().form.is_none());
539            }
540            _ => panic!(),
541        }
542
543        let elem2 = Element::from(pubsub);
544        assert!(elem1.compare_to(&elem2));
545    }
546
547    #[test]
548    fn publish() {
549        let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/></pubsub>".parse().unwrap();
550        let elem1 = elem.clone();
551        let pubsub = PubSub::try_from(elem).unwrap();
552        match pubsub.clone() {
553            PubSub::Publish { publish, publish_options } => {
554                assert_eq!(&publish.node.0, "coucou");
555                assert!(publish_options.is_none());
556            }
557            _ => panic!(),
558        }
559
560        let elem2 = Element::from(pubsub);
561        assert!(elem1.compare_to(&elem2));
562    }
563
564    #[test]
565    fn publish_with_publish_options() {
566        let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/><publish-options/></pubsub>".parse().unwrap();
567        let elem1 = elem.clone();
568        let pubsub = PubSub::try_from(elem).unwrap();
569        match pubsub.clone() {
570            PubSub::Publish { publish, publish_options } => {
571                assert_eq!(&publish.node.0, "coucou");
572                assert!(publish_options.unwrap().form.is_none());
573            }
574            _ => panic!(),
575        }
576
577        let elem2 = Element::from(pubsub);
578        assert!(elem1.compare_to(&elem2));
579    }
580
581    #[test]
582    fn invalid_empty_pubsub() {
583        let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'/>".parse().unwrap();
584        let error = PubSub::try_from(elem).unwrap_err();
585        let message = match error {
586            Error::ParseError(string) => string,
587            _ => panic!(),
588        };
589        assert_eq!(message, "No payload in pubsub element.");
590    }
591
592    #[test]
593    fn publish_option() {
594        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();
595        let publish_options = PublishOptions::try_from(elem).unwrap();
596        assert_eq!(&publish_options.form.unwrap().form_type.unwrap(), "http://jabber.org/protocol/pubsub#publish-options");
597    }
598
599    #[test]
600    fn subscribe_options() {
601        let elem1: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'/>".parse().unwrap();
602        let subscribe_options1 = SubscribeOptions::try_from(elem1).unwrap();
603        assert_eq!(subscribe_options1.required, false);
604
605        let elem2: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'><required/></subscribe-options>".parse().unwrap();
606        let subscribe_options2 = SubscribeOptions::try_from(elem2).unwrap();
607        assert_eq!(subscribe_options2.required, true);
608    }
609}