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
7#![allow(missing_docs)]
8
9use try_from::TryFrom;
10use std::collections::BTreeMap;
11
12use minidom::Element;
13
14use jid::Jid;
15
16use error::Error;
17
18use ns;
19
20/// Should be implemented on every known payload of a `<message/>`.
21pub trait MessagePayload: TryFrom<Element> + Into<Element> {}
22
23generate_attribute!(
24 /// The type of a message.
25 MessageType, "type", {
26 /// Standard instant messaging message.
27 Chat => "chat",
28
29 /// Notifies that an error happened.
30 Error => "error",
31
32 /// Standard group instant messaging message.
33 Groupchat => "groupchat",
34
35 /// Used by servers to notify users when things happen.
36 Headline => "headline",
37
38 /// This is an email-like message, it usually contains a
39 /// [subject](struct.Subject.html).
40 Normal => "normal",
41 }, Default = Normal
42);
43
44type Lang = String;
45
46generate_elem_id!(
47 /// Represents one `<body/>` element, that is the free form text content of
48 /// a message.
49 Body, "body", DEFAULT_NS
50);
51
52generate_elem_id!(
53 /// Defines the subject of a room, or of an email-like normal message.
54 Subject, "subject", DEFAULT_NS
55);
56
57generate_elem_id!(
58 /// A thread identifier, so that other people can specify to which message
59 /// they are replying.
60 Thread, "thread", DEFAULT_NS
61);
62
63/// The main structure representing the `<message/>` stanza.
64#[derive(Debug, Clone)]
65pub struct Message {
66 /// The JID emitting this stanza.
67 pub from: Option<Jid>,
68
69 /// The recipient of this stanza.
70 pub to: Option<Jid>,
71
72 /// The @id attribute of this stanza, which is required in order to match a
73 /// request with its response.
74 pub id: Option<String>,
75
76 /// The type of this message.
77 pub type_: MessageType,
78
79 /// A list of bodies, sorted per language. Use
80 /// [get_best_body()](#method.get_best_body) to access them on reception.
81 pub bodies: BTreeMap<Lang, Body>,
82
83 /// A list of subjects, sorted per language. Use
84 /// [get_best_subject()](#method.get_best_subject) to access them on
85 /// reception.
86 pub subjects: BTreeMap<Lang, Subject>,
87
88 /// An optional thread identifier, so that other people can reply directly
89 /// to this message.
90 pub thread: Option<Thread>,
91
92 /// A list of the extension payloads contained in this stanza.
93 pub payloads: Vec<Element>,
94}
95
96impl Message {
97 /// Creates a new `<message/>` stanza for the given recipient.
98 pub fn new(to: Option<Jid>) -> Message {
99 Message {
100 from: None,
101 to: to,
102 id: None,
103 type_: MessageType::Chat,
104 bodies: BTreeMap::new(),
105 subjects: BTreeMap::new(),
106 thread: None,
107 payloads: vec!(),
108 }
109 }
110
111 fn get_best<'a, T>(map: &'a BTreeMap<Lang, T>, preferred_langs: Vec<&str>) -> Option<(Lang, &'a T)> {
112 if map.is_empty() {
113 return None;
114 }
115 for lang in preferred_langs {
116 if let Some(value) = map.get(lang) {
117 return Some((Lang::from(lang), value));
118 }
119 }
120 if let Some(value) = map.get("") {
121 return Some((Lang::new(), value));
122 }
123 map.iter().map(|(lang, value)| (lang.clone(), value)).next()
124 }
125
126 /// Returns the best matching body from a list of languages.
127 ///
128 /// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English
129 /// body without an xml:lang attribute, and you pass ["fr", "en"] as your preferred languages,
130 /// `Some(("fr", the_second_body))` will be returned.
131 ///
132 /// If no body matches, an undefined body will be returned.
133 pub fn get_best_body(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Body)> {
134 Message::get_best::<Body>(&self.bodies, preferred_langs)
135 }
136
137 /// Returns the best matching subject from a list of languages.
138 ///
139 /// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English
140 /// subject without an xml:lang attribute, and you pass ["fr", "en"] as your preferred
141 /// languages, `Some(("fr", the_second_subject))` will be returned.
142 ///
143 /// If no subject matches, an undefined subject will be returned.
144 pub fn get_best_subject(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Subject)> {
145 Message::get_best::<Subject>(&self.subjects, preferred_langs)
146 }
147}
148
149impl TryFrom<Element> for Message {
150 type Err = Error;
151
152 fn try_from(root: Element) -> Result<Message, Error> {
153 check_self!(root, "message", DEFAULT_NS);
154 let from = get_attr!(root, "from", optional);
155 let to = get_attr!(root, "to", optional);
156 let id = get_attr!(root, "id", optional);
157 let type_ = get_attr!(root, "type", default);
158 let mut bodies = BTreeMap::new();
159 let mut subjects = BTreeMap::new();
160 let mut thread = None;
161 let mut payloads = vec!();
162 for elem in root.children() {
163 if elem.is("body", ns::DEFAULT_NS) {
164 check_no_children!(elem, "body");
165 let lang = get_attr!(elem, "xml:lang", default);
166 let body = Body(elem.text());
167 if bodies.insert(lang, body).is_some() {
168 return Err(Error::ParseError("Body element present twice for the same xml:lang."));
169 }
170 } else if elem.is("subject", ns::DEFAULT_NS) {
171 check_no_children!(elem, "subject");
172 let lang = get_attr!(elem, "xml:lang", default);
173 let subject = Subject(elem.text());
174 if subjects.insert(lang, subject).is_some() {
175 return Err(Error::ParseError("Subject element present twice for the same xml:lang."));
176 }
177 } else if elem.is("thread", ns::DEFAULT_NS) {
178 if thread.is_some() {
179 return Err(Error::ParseError("Thread element present twice."));
180 }
181 check_no_children!(elem, "thread");
182 thread = Some(Thread(elem.text()));
183 } else {
184 payloads.push(elem.clone())
185 }
186 }
187 Ok(Message {
188 from: from,
189 to: to,
190 id: id,
191 type_: type_,
192 bodies: bodies,
193 subjects: subjects,
194 thread: thread,
195 payloads: payloads,
196 })
197 }
198}
199
200impl From<Message> for Element {
201 fn from(message: Message) -> Element {
202 Element::builder("message")
203 .ns(ns::DEFAULT_NS)
204 .attr("from", message.from)
205 .attr("to", message.to)
206 .attr("id", message.id)
207 .attr("type", message.type_)
208 .append(message.subjects.into_iter()
209 .map(|(lang, subject)| {
210 let mut subject = Element::from(subject);
211 subject.set_attr("xml:lang", match lang.as_ref() {
212 "" => None,
213 lang => Some(lang),
214 });
215 subject
216 })
217 .collect::<Vec<_>>())
218 .append(message.bodies.into_iter()
219 .map(|(lang, body)| {
220 let mut body = Element::from(body);
221 body.set_attr("xml:lang", match lang.as_ref() {
222 "" => None,
223 lang => Some(lang),
224 });
225 body
226 })
227 .collect::<Vec<_>>())
228 .append(message.payloads)
229 .build()
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use std::str::FromStr;
237 use compare_elements::NamespaceAwareCompare;
238
239 #[test]
240 fn test_simple() {
241 #[cfg(not(feature = "component"))]
242 let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
243 #[cfg(feature = "component")]
244 let elem: Element = "<message xmlns='jabber:component:accept'/>".parse().unwrap();
245 let message = Message::try_from(elem).unwrap();
246 assert_eq!(message.from, None);
247 assert_eq!(message.to, None);
248 assert_eq!(message.id, None);
249 assert_eq!(message.type_, MessageType::Normal);
250 assert!(message.payloads.is_empty());
251 }
252
253 #[test]
254 fn test_serialise() {
255 #[cfg(not(feature = "component"))]
256 let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
257 #[cfg(feature = "component")]
258 let elem: Element = "<message xmlns='jabber:component:accept'/>".parse().unwrap();
259 let mut message = Message::new(None);
260 message.type_ = MessageType::Normal;
261 let elem2 = message.into();
262 assert_eq!(elem, elem2);
263 }
264
265 #[test]
266 fn test_body() {
267 #[cfg(not(feature = "component"))]
268 let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
269 #[cfg(feature = "component")]
270 let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
271 let elem1 = elem.clone();
272 let message = Message::try_from(elem).unwrap();
273 assert_eq!(message.bodies[""], Body::from_str("Hello world!").unwrap());
274
275 {
276 let (lang, body) = message.get_best_body(vec!("en")).unwrap();
277 assert_eq!(lang, "");
278 assert_eq!(body, &Body::from_str("Hello world!").unwrap());
279 }
280
281 let elem2 = message.into();
282 assert!(elem1.compare_to(&elem2));
283 }
284
285 #[test]
286 fn test_serialise_body() {
287 #[cfg(not(feature = "component"))]
288 let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
289 #[cfg(feature = "component")]
290 let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
291 let mut message = Message::new(Some(Jid::from_str("coucou@example.org").unwrap()));
292 message.bodies.insert(String::from(""), Body::from_str("Hello world!").unwrap());
293 let elem2 = message.into();
294 assert!(elem.compare_to(&elem2));
295 }
296
297 #[test]
298 fn test_subject() {
299 #[cfg(not(feature = "component"))]
300 let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><subject>Hello world!</subject></message>".parse().unwrap();
301 #[cfg(feature = "component")]
302 let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><subject>Hello world!</subject></message>".parse().unwrap();
303 let elem1 = elem.clone();
304 let message = Message::try_from(elem).unwrap();
305 assert_eq!(message.subjects[""], Subject::from_str("Hello world!").unwrap());
306
307 {
308 let (lang, subject) = message.get_best_subject(vec!("en")).unwrap();
309 assert_eq!(lang, "");
310 assert_eq!(subject, &Subject::from_str("Hello world!").unwrap());
311 }
312
313 let elem2 = message.into();
314 assert!(elem1.compare_to(&elem2));
315 }
316
317 #[test]
318 fn get_best_body() {
319 #[cfg(not(feature = "component"))]
320 let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body xml:lang='de'>Hallo Welt!</body><body xml:lang='fr'>Salut le monde !</body><body>Hello world!</body></message>".parse().unwrap();
321 #[cfg(feature = "component")]
322 let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
323 let message = Message::try_from(elem).unwrap();
324
325 // Tests basic feature.
326 {
327 let (lang, body) = message.get_best_body(vec!("fr")).unwrap();
328 assert_eq!(lang, "fr");
329 assert_eq!(body, &Body::from_str("Salut le monde !").unwrap());
330 }
331
332 // Tests order.
333 {
334 let (lang, body) = message.get_best_body(vec!("en", "de")).unwrap();
335 assert_eq!(lang, "de");
336 assert_eq!(body, &Body::from_str("Hallo Welt!").unwrap());
337 }
338
339 // Tests fallback.
340 {
341 let (lang, body) = message.get_best_body(vec!()).unwrap();
342 assert_eq!(lang, "");
343 assert_eq!(body, &Body::from_str("Hello world!").unwrap());
344 }
345
346 // Tests fallback.
347 {
348 let (lang, body) = message.get_best_body(vec!("ja")).unwrap();
349 assert_eq!(lang, "");
350 assert_eq!(body, &Body::from_str("Hello world!").unwrap());
351 }
352
353 let message = Message::new(None);
354
355 // Tests without a body.
356 assert_eq!(message.get_best_body(vec!("ja")), None);
357 }
358
359 #[test]
360 fn test_attention() {
361 #[cfg(not(feature = "component"))]
362 let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
363 #[cfg(feature = "component")]
364 let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
365 let elem1 = elem.clone();
366 let message = Message::try_from(elem).unwrap();
367 let elem2 = message.into();
368 assert_eq!(elem1, elem2);
369 }
370}