mix.rs

  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}