1// Copyright (c) 2020 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// TODO: validate nicks by applying the “nickname” profile of the PRECIS OpaqueString class, as
8// defined in RFC 7700.
9
10use crate::iq::{IqResultPayload, IqSetPayload};
11use crate::message::MessagePayload;
12use crate::pubsub::{NodeName, PubSubPayload};
13use jid::BareJid;
14
15generate_id!(
16 /// The identifier a participant receives when joining a channel.
17 ParticipantId
18);
19
20impl ParticipantId {
21 /// Create a new ParticipantId.
22 pub fn new<P: Into<String>>(participant: P) -> ParticipantId {
23 ParticipantId(participant.into())
24 }
25}
26
27generate_id!(
28 /// A MIX channel identifier.
29 ChannelId
30);
31
32generate_element!(
33 /// Represents a participant in a MIX channel, usually returned on the
34 /// urn:xmpp:mix:nodes:participants PubSub node.
35 Participant, "participant", MIX_CORE,
36 children: [
37 /// The nick of this participant.
38 nick: Required<String> = ("nick", MIX_CORE) => String,
39
40 /// The bare JID of this participant.
41 // TODO: should be a BareJid!
42 jid: Required<String> = ("jid", MIX_CORE) => String
43 ]
44);
45
46impl PubSubPayload for Participant {}
47
48impl Participant {
49 /// Create a new MIX participant.
50 pub fn new<J: Into<String>, N: Into<String>>(jid: J, nick: N) -> Participant {
51 Participant {
52 nick: nick.into(),
53 jid: jid.into(),
54 }
55 }
56}
57
58generate_element!(
59 /// A node to subscribe to.
60 Subscribe, "subscribe", MIX_CORE,
61 attributes: [
62 /// The PubSub node to subscribe to.
63 node: Required<NodeName> = "node",
64 ]
65);
66
67impl Subscribe {
68 /// Create a new Subscribe element.
69 pub fn new<N: Into<String>>(node: N) -> Subscribe {
70 Subscribe {
71 node: NodeName(node.into()),
72 }
73 }
74}
75
76generate_element!(
77 /// A request from a user’s server to join a MIX channel.
78 Join, "join", MIX_CORE,
79 attributes: [
80 /// The participant identifier returned by the MIX service on successful join.
81 id: Option<ParticipantId> = "id",
82 ],
83 children: [
84 /// The nick requested by the user or set by the service.
85 nick: Required<String> = ("nick", MIX_CORE) => String,
86
87 /// Which MIX nodes to subscribe to.
88 subscribes: Vec<Subscribe> = ("subscribe", MIX_CORE) => Subscribe
89 ]
90);
91
92impl IqSetPayload for Join {}
93impl IqResultPayload for Join {}
94
95impl Join {
96 /// Create a new Join element.
97 pub fn from_nick_and_nodes<N: Into<String>>(nick: N, nodes: &[&str]) -> Join {
98 let subscribes = nodes.iter().cloned().map(Subscribe::new).collect();
99 Join {
100 id: None,
101 nick: nick.into(),
102 subscribes,
103 }
104 }
105
106 /// Sets the JID on this update-subscription.
107 pub fn with_id<I: Into<String>>(mut self, id: I) -> Self {
108 self.id = Some(ParticipantId(id.into()));
109 self
110 }
111}
112
113generate_element!(
114 /// Update a given subscription.
115 UpdateSubscription, "update-subscription", MIX_CORE,
116 attributes: [
117 /// The JID of the user to be affected.
118 // TODO: why is it not a participant id instead?
119 jid: Option<BareJid> = "jid",
120 ],
121 children: [
122 /// The list of additional nodes to subscribe to.
123 // TODO: what happens when we are already subscribed? Also, how do we unsubscribe from
124 // just one?
125 subscribes: Vec<Subscribe> = ("subscribe", MIX_CORE) => Subscribe
126 ]
127);
128
129impl IqSetPayload for UpdateSubscription {}
130impl IqResultPayload for UpdateSubscription {}
131
132impl UpdateSubscription {
133 /// Create a new UpdateSubscription element.
134 pub fn from_nodes(nodes: &[&str]) -> UpdateSubscription {
135 let subscribes = nodes.iter().cloned().map(Subscribe::new).collect();
136 UpdateSubscription {
137 jid: None,
138 subscribes,
139 }
140 }
141
142 /// Sets the JID on this update-subscription.
143 pub fn with_jid(mut self, jid: BareJid) -> Self {
144 self.jid = Some(jid);
145 self
146 }
147}
148
149generate_empty_element!(
150 /// Request to leave a given MIX channel. It will automatically unsubscribe the user from all
151 /// nodes on this channel.
152 Leave,
153 "leave",
154 MIX_CORE
155);
156
157impl IqSetPayload for Leave {}
158impl IqResultPayload for Leave {}
159
160generate_element!(
161 /// A request to change the user’s nick.
162 SetNick, "setnick", MIX_CORE,
163 children: [
164 /// The new requested nick.
165 nick: Required<String> = ("nick", MIX_CORE) => String
166 ]
167);
168
169impl IqSetPayload for SetNick {}
170impl IqResultPayload for SetNick {}
171
172impl SetNick {
173 /// Create a new SetNick element.
174 pub fn new<N: Into<String>>(nick: N) -> SetNick {
175 SetNick { nick: nick.into() }
176 }
177}
178
179generate_element!(
180 /// Message payload describing who actually sent the message, since unlike in MUC, all messages
181 /// are sent from the channel’s JID.
182 Mix, "mix", MIX_CORE,
183 children: [
184 /// The nick of the user who said something.
185 nick: Required<String> = ("nick", MIX_CORE) => String,
186
187 /// The JID of the user who said something.
188 // TODO: should be a BareJid!
189 jid: Required<String> = ("jid", MIX_CORE) => String
190 ]
191);
192
193impl MessagePayload for Mix {}
194
195impl Mix {
196 /// Create a new Mix element.
197 pub fn new<N: Into<String>, J: Into<String>>(nick: N, jid: J) -> Mix {
198 Mix {
199 nick: nick.into(),
200 jid: jid.into(),
201 }
202 }
203}
204
205generate_element!(
206 /// Create a new MIX channel.
207 #[derive(Default)]
208 Create, "create", MIX_CORE,
209 attributes: [
210 /// The requested channel identifier.
211 channel: Option<ChannelId> = "channel",
212 ]
213);
214
215impl IqSetPayload for Create {}
216impl IqResultPayload for Create {}
217
218impl Create {
219 /// Create a new ad-hoc Create element.
220 pub fn new() -> Create {
221 Create::default()
222 }
223
224 /// Create a new Create element with a channel identifier.
225 pub fn from_channel_id<C: Into<String>>(channel: C) -> Create {
226 Create {
227 channel: Some(ChannelId(channel.into())),
228 }
229 }
230}
231
232generate_element!(
233 /// Destroy a given MIX channel.
234 Destroy, "destroy", MIX_CORE,
235 attributes: [
236 /// The channel identifier to be destroyed.
237 channel: Required<ChannelId> = "channel",
238 ]
239);
240
241// TODO: section 7.3.4, example 33, doesn’t mirror the <destroy/> in the iq result unlike every
242// other section so far.
243impl IqSetPayload for Destroy {}
244
245impl Destroy {
246 /// Create a new Destroy element.
247 pub fn new<C: Into<String>>(channel: C) -> Destroy {
248 Destroy {
249 channel: ChannelId(channel.into()),
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::Element;
258 use std::convert::TryFrom;
259
260 #[test]
261 fn participant() {
262 let elem: Element = "<participant xmlns='urn:xmpp:mix:core:1'><jid>foo@bar</jid><nick>coucou</nick></participant>"
263 .parse()
264 .unwrap();
265 let participant = Participant::try_from(elem).unwrap();
266 assert_eq!(participant.nick, "coucou");
267 assert_eq!(participant.jid, "foo@bar");
268 }
269
270 #[test]
271 fn join() {
272 let elem: Element = "<join xmlns='urn:xmpp:mix:core:1'><subscribe node='urn:xmpp:mix:nodes:messages'/><subscribe node='urn:xmpp:mix:nodes:info'/><nick>coucou</nick></join>"
273 .parse()
274 .unwrap();
275 let join = Join::try_from(elem).unwrap();
276 assert_eq!(join.nick, "coucou");
277 assert_eq!(join.id, None);
278 assert_eq!(join.subscribes.len(), 2);
279 assert_eq!(join.subscribes[0].node.0, "urn:xmpp:mix:nodes:messages");
280 assert_eq!(join.subscribes[1].node.0, "urn:xmpp:mix:nodes:info");
281 }
282
283 #[test]
284 fn update_subscription() {
285 let elem: Element = "<update-subscription xmlns='urn:xmpp:mix:core:1'><subscribe node='urn:xmpp:mix:nodes:participants'/></update-subscription>"
286 .parse()
287 .unwrap();
288 let update_subscription = UpdateSubscription::try_from(elem).unwrap();
289 assert_eq!(update_subscription.jid, None);
290 assert_eq!(update_subscription.subscribes.len(), 1);
291 assert_eq!(
292 update_subscription.subscribes[0].node.0,
293 "urn:xmpp:mix:nodes:participants"
294 );
295 }
296
297 #[test]
298 fn leave() {
299 let elem: Element = "<leave xmlns='urn:xmpp:mix:core:1'/>".parse().unwrap();
300 Leave::try_from(elem).unwrap();
301 }
302
303 #[test]
304 fn setnick() {
305 let elem: Element = "<setnick xmlns='urn:xmpp:mix:core:1'><nick>coucou</nick></setnick>"
306 .parse()
307 .unwrap();
308 let setnick = SetNick::try_from(elem).unwrap();
309 assert_eq!(setnick.nick, "coucou");
310 }
311
312 #[test]
313 fn message_mix() {
314 let elem: Element =
315 "<mix xmlns='urn:xmpp:mix:core:1'><jid>foo@bar</jid><nick>coucou</nick></mix>"
316 .parse()
317 .unwrap();
318 let mix = Mix::try_from(elem).unwrap();
319 assert_eq!(mix.nick, "coucou");
320 assert_eq!(mix.jid, "foo@bar");
321 }
322
323 #[test]
324 fn create() {
325 let elem: Element = "<create xmlns='urn:xmpp:mix:core:1' channel='coucou'/>"
326 .parse()
327 .unwrap();
328 let create = Create::try_from(elem).unwrap();
329 assert_eq!(create.channel.unwrap().0, "coucou");
330
331 let elem: Element = "<create xmlns='urn:xmpp:mix:core:1'/>".parse().unwrap();
332 let create = Create::try_from(elem).unwrap();
333 assert_eq!(create.channel, None);
334 }
335
336 #[test]
337 fn destroy() {
338 let elem: Element = "<destroy xmlns='urn:xmpp:mix:core:1' channel='coucou'/>"
339 .parse()
340 .unwrap();
341 let destroy = Destroy::try_from(elem).unwrap();
342 assert_eq!(destroy.channel.0, "coucou");
343 }
344
345 #[test]
346 fn serialise() {
347 let elem: Element = Join::from_nick_and_nodes("coucou", &["foo", "bar"]).into();
348 let xml = String::from(&elem);
349 assert_eq!(xml, "<join xmlns='urn:xmpp:mix:core:1'><nick>coucou</nick><subscribe node=\"foo\"/><subscribe node=\"bar\"/></join>");
350
351 let elem: Element = UpdateSubscription::from_nodes(&["foo", "bar"]).into();
352 let xml = String::from(&elem);
353 assert_eq!(xml, "<update-subscription xmlns='urn:xmpp:mix:core:1'><subscribe node=\"foo\"/><subscribe node=\"bar\"/></update-subscription>");
354
355 let elem: Element = Leave.into();
356 let xml = String::from(&elem);
357 assert_eq!(xml, "<leave xmlns='urn:xmpp:mix:core:1'/>");
358
359 let elem: Element = SetNick::new("coucou").into();
360 let xml = String::from(&elem);
361 assert_eq!(
362 xml,
363 "<setnick xmlns='urn:xmpp:mix:core:1'><nick>coucou</nick></setnick>"
364 );
365
366 let elem: Element = Mix::new("coucou", "coucou@example").into();
367 let xml = String::from(&elem);
368 assert_eq!(
369 xml,
370 "<mix xmlns='urn:xmpp:mix:core:1'><nick>coucou</nick><jid>coucou@example</jid></mix>"
371 );
372
373 let elem: Element = Create::new().into();
374 let xml = String::from(&elem);
375 assert_eq!(xml, "<create xmlns='urn:xmpp:mix:core:1'/>");
376
377 let elem: Element = Create::from_channel_id("coucou").into();
378 let xml = String::from(&elem);
379 assert_eq!(
380 xml,
381 "<create xmlns='urn:xmpp:mix:core:1' channel=\"coucou\"/>"
382 );
383
384 let elem: Element = Destroy::new("coucou").into();
385 let xml = String::from(&elem);
386 assert_eq!(
387 xml,
388 "<destroy xmlns='urn:xmpp:mix:core:1' channel=\"coucou\"/>"
389 );
390 }
391}