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 xso::{AsXml, FromXml};
8
9use crate::data_forms::DataForm;
10use crate::date::DateTime;
11use crate::message::MessagePayload;
12use crate::ns;
13use crate::pubsub::{ItemId, NodeName, Subscription, SubscriptionId};
14use jid::Jid;
15use minidom::Element;
16
17/// An event item from a PubSub node.
18#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
19#[xml(namespace = ns::PUBSUB_EVENT, name = "item")]
20pub struct Item {
21 /// The identifier for this item, unique per node.
22 #[xml(attribute(default))]
23 pub id: Option<ItemId>,
24
25 /// The JID of the entity who published this item.
26 #[xml(attribute(default))]
27 pub publisher: Option<Jid>,
28
29 /// The payload of this item, in an arbitrary namespace.
30 #[xml(element(default))]
31 pub payload: Option<Element>,
32}
33
34/// Represents an event happening to a PubSub node.
35#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
36#[xml(namespace = ns::PUBSUB_EVENT, name = "event")]
37pub struct Event {
38 /// The inner child of this event.
39 #[xml(child)]
40 pub payload: Payload,
41}
42
43impl MessagePayload for Event {}
44
45/// Represents an event happening to a PubSub node.
46#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
47#[xml(namespace = ns::PUBSUB_EVENT, exhaustive)]
48pub enum Payload {
49 /*
50 Collection {
51 },
52 */
53 /// This node’s configuration changed.
54 #[xml(name = "configuration")]
55 Configuration {
56 /// The node affected.
57 #[xml(attribute)]
58 node: NodeName,
59
60 /// The new configuration of this node.
61 #[xml(child(default))]
62 form: Option<DataForm>,
63 },
64
65 /// This node has been deleted, with an optional redirect to another node.
66 #[xml(name = "delete")]
67 Delete {
68 /// The node affected.
69 #[xml(attribute)]
70 node: NodeName,
71
72 /// The xmpp: URI of another node replacing this one.
73 #[xml(extract(default, fields(attribute(default, name = "uri"))))]
74 redirect: Option<String>,
75 },
76
77 /// Some items have been published or retracted on this node.
78 #[xml(name = "items")]
79 Items {
80 /// The node affected.
81 #[xml(attribute)]
82 node: NodeName,
83
84 /// The list of published items.
85 #[xml(child(n = ..))]
86 published: Vec<Item>,
87
88 /// The list of retracted items.
89 #[xml(extract(n = .., name = "retract", fields(attribute(name = "id", type_ = ItemId))))]
90 retracted: Vec<ItemId>,
91 },
92
93 /// All items of this node just got removed at once.
94 #[xml(name = "purge")]
95 Purge {
96 /// The node affected.
97 #[xml(attribute)]
98 node: NodeName,
99 },
100
101 /// The user’s subscription to this node has changed.
102 #[xml(name = "subscription")]
103 Subscription {
104 /// The node affected.
105 #[xml(attribute)]
106 node: NodeName,
107
108 /// The time at which this subscription will expire.
109 #[xml(attribute(default))]
110 expiry: Option<DateTime>,
111
112 /// The JID of the user affected.
113 #[xml(attribute(default))]
114 jid: Option<Jid>,
115
116 /// An identifier for this subscription.
117 #[xml(attribute(default))]
118 subid: Option<SubscriptionId>,
119
120 /// The state of this subscription.
121 #[xml(attribute(default))]
122 subscription: Option<Subscription>,
123 },
124}
125
126impl Payload {
127 /// Return the name of the node to which this event is related.
128 pub fn node_name(&self) -> &NodeName {
129 match self {
130 Self::Purge { node, .. } => node,
131 Self::Items { node, .. } => node,
132 Self::Subscription { node, .. } => node,
133 Self::Delete { node, .. } => node,
134 Self::Configuration { node, .. } => node,
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use jid::BareJid;
143 use xso::error::{Error, FromElementError};
144
145 // TODO: Reenable this test once we support asserting that a Vec isn’t empty.
146 #[test]
147 #[ignore]
148 fn missing_items() {
149 let elem: Element =
150 "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'/></event>"
151 .parse()
152 .unwrap();
153 let error = Event::try_from(elem).unwrap_err();
154 let message = match error {
155 FromElementError::Invalid(Error::Other(string)) => string,
156 _ => panic!(),
157 };
158 assert_eq!(message, "Missing children in items element.");
159 }
160
161 #[test]
162 fn test_simple_items() {
163 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'><item id='test' publisher='test@coucou'/></items></event>".parse().unwrap();
164 let event = Event::try_from(elem).unwrap();
165 match event.payload {
166 Payload::Items {
167 node,
168 published,
169 retracted,
170 } => {
171 assert_eq!(node, NodeName(String::from("coucou")));
172 assert_eq!(retracted.len(), 0);
173 assert_eq!(published[0].id, Some(ItemId(String::from("test"))));
174 assert_eq!(
175 published[0].publisher.clone().unwrap(),
176 BareJid::new("test@coucou").unwrap()
177 );
178 assert_eq!(published[0].payload, None);
179 }
180 _ => panic!(),
181 }
182 }
183
184 #[test]
185 fn test_simple_pep() {
186 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><item><foreign xmlns='example:namespace'/></item></items></event>".parse().unwrap();
187 let event = Event::try_from(elem).unwrap();
188 match event.payload {
189 Payload::Items {
190 node,
191 published,
192 retracted,
193 } => {
194 assert_eq!(node, NodeName(String::from("something")));
195 assert_eq!(retracted.len(), 0);
196 assert_eq!(published[0].id, None);
197 assert_eq!(published[0].publisher, None);
198 match published[0].payload {
199 Some(ref elem) => assert!(elem.is("foreign", "example:namespace")),
200 _ => panic!(),
201 }
202 }
203 _ => panic!(),
204 }
205 }
206
207 #[test]
208 fn test_simple_retract() {
209 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><retract id='coucou'/><retract id='test'/></items></event>".parse().unwrap();
210 let event = Event::try_from(elem).unwrap();
211 match event.payload {
212 Payload::Items {
213 node,
214 published,
215 retracted,
216 } => {
217 assert_eq!(node, NodeName(String::from("something")));
218 assert_eq!(published.len(), 0);
219 assert_eq!(retracted[0], ItemId(String::from("coucou")));
220 assert_eq!(retracted[1], ItemId(String::from("test")));
221 }
222 _ => panic!(),
223 }
224 }
225
226 #[test]
227 fn test_simple_delete() {
228 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><delete node='coucou'><redirect uri='hello'/></delete></event>".parse().unwrap();
229 let event = Event::try_from(elem).unwrap();
230 match event.payload {
231 Payload::Delete { node, redirect } => {
232 assert_eq!(node, NodeName(String::from("coucou")));
233 assert_eq!(redirect, Some(String::from("hello")));
234 }
235 _ => panic!(),
236 }
237 }
238
239 #[test]
240 fn test_simple_purge() {
241 let elem: Element =
242 "<event xmlns='http://jabber.org/protocol/pubsub#event'><purge node='coucou'/></event>"
243 .parse()
244 .unwrap();
245 let event = Event::try_from(elem).unwrap();
246 match event.payload {
247 Payload::Purge { node } => {
248 assert_eq!(node, NodeName(String::from("coucou")));
249 }
250 _ => panic!(),
251 }
252 }
253
254 #[test]
255 fn test_simple_configure() {
256 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();
257 let event = Event::try_from(elem).unwrap();
258 match event.payload {
259 Payload::Configuration { node, form: _ } => {
260 assert_eq!(node, NodeName(String::from("coucou")));
261 //assert_eq!(form.type_, Result_);
262 }
263 _ => panic!(),
264 }
265 }
266
267 #[test]
268 fn test_invalid() {
269 let elem: Element =
270 "<event xmlns='http://jabber.org/protocol/pubsub#event'><coucou node='test'/></event>"
271 .parse()
272 .unwrap();
273 let error = Event::try_from(elem).unwrap_err();
274 let message = match error {
275 FromElementError::Invalid(Error::Other(string)) => string,
276 _ => panic!(),
277 };
278 assert_eq!(message, "This is not a Payload element.");
279 }
280
281 #[cfg(not(feature = "disable-validation"))]
282 #[test]
283 fn test_invalid_attribute() {
284 let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event' coucou=''/>"
285 .parse()
286 .unwrap();
287 let error = Event::try_from(elem).unwrap_err();
288 let message = match error {
289 FromElementError::Invalid(Error::Other(string)) => string,
290 _ => panic!(),
291 };
292 assert_eq!(message, "Unknown attribute in Event element.");
293 }
294
295 #[test]
296 fn test_ex221_subscription() {
297 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>"
298 .parse()
299 .unwrap();
300 let event = Event::try_from(elem.clone()).unwrap();
301 match event.payload.clone() {
302 Payload::Subscription {
303 node,
304 expiry,
305 jid,
306 subid,
307 subscription,
308 } => {
309 assert_eq!(node, NodeName(String::from("princely_musings")));
310 assert_eq!(
311 subid,
312 Some(SubscriptionId(String::from(
313 "ba49252aaa4f5d320c24d3766f0bdcade78c78d3"
314 )))
315 );
316 assert_eq!(subscription, Some(Subscription::Subscribed));
317 assert_eq!(jid.unwrap(), BareJid::new("francisco@denmark.lit").unwrap());
318 assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap()));
319 }
320 _ => panic!(),
321 }
322
323 let elem2: Element = event.into();
324 assert_eq!(elem, elem2);
325 }
326}