// Copyright (c) 2020 Emmanuel Gil Peyrot // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // TODO: validate nicks by applying the “nickname” profile of the PRECIS OpaqueString class, as // defined in RFC 7700. use xso::{AsXml, FromXml}; use crate::iq::{IqResultPayload, IqSetPayload}; use crate::message::MessagePayload; use crate::ns; use crate::pubsub::{NodeName, PubSubPayload}; use jid::BareJid; generate_id!( /// The identifier a participant receives when joining a channel. ParticipantId ); impl ParticipantId { /// Create a new ParticipantId. pub fn new>(participant: P) -> ParticipantId { ParticipantId(participant.into()) } } generate_id!( /// A MIX channel identifier. ChannelId ); /// Represents a participant in a MIX channel, usually returned on the /// urn:xmpp:mix:nodes:participants PubSub node. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MIX_CORE, name = "participant")] pub struct Participant { /// The nick of this participant. #[xml(extract(fields(text)))] pub nick: String, /// The bare JID of this participant. #[xml(extract(fields(text)))] pub jid: BareJid, } impl PubSubPayload for Participant {} impl Participant { /// Create a new MIX participant. pub fn new, N: Into>(jid: J, nick: N) -> Participant { Participant { nick: nick.into(), jid: jid.into(), } } } /// A node to subscribe to. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MIX_CORE, name = "subscribe")] pub struct Subscribe { /// The PubSub node to subscribe to. #[xml(attribute)] pub node: NodeName, } impl Subscribe { /// Create a new Subscribe element. pub fn new>(node: N) -> Subscribe { Subscribe { node: NodeName(node.into()), } } } /// A request from a user’s server to join a MIX channel. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MIX_CORE, name = "join")] pub struct Join { /// The participant identifier returned by the MIX service on successful join. #[xml(attribute(default))] pub id: Option, /// The nick requested by the user or set by the service. #[xml(extract(fields(text)))] pub nick: String, /// Which MIX nodes to subscribe to. #[xml(child(n = ..))] pub subscribes: Vec, } impl IqSetPayload for Join {} impl IqResultPayload for Join {} impl Join { /// Create a new Join element. pub fn from_nick_and_nodes>(nick: N, nodes: &[&str]) -> Join { let subscribes = nodes.iter().cloned().map(Subscribe::new).collect(); Join { id: None, nick: nick.into(), subscribes, } } /// Sets the JID on this update-subscription. pub fn with_id>(mut self, id: I) -> Self { self.id = Some(ParticipantId(id.into())); self } } /// Update a given subscription. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MIX_CORE, name = "update-subscription")] pub struct UpdateSubscription { /// The JID of the user to be affected. // TODO: why is it not a participant id instead? #[xml(attribute(default))] pub jid: Option, /// The list of additional nodes to subscribe to. // TODO: what happens when we are already subscribed? Also, how do we unsubscribe from // just one? #[xml(child(n = ..))] pub subscribes: Vec, } impl IqSetPayload for UpdateSubscription {} impl IqResultPayload for UpdateSubscription {} impl UpdateSubscription { /// Create a new UpdateSubscription element. pub fn from_nodes(nodes: &[&str]) -> UpdateSubscription { let subscribes = nodes.iter().cloned().map(Subscribe::new).collect(); UpdateSubscription { jid: None, subscribes, } } /// Sets the JID on this update-subscription. pub fn with_jid(mut self, jid: BareJid) -> Self { self.jid = Some(jid); self } } /// Request to leave a given MIX channel. It will automatically unsubscribe the user from all /// nodes on this channel. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MIX_CORE, name = "leave")] pub struct Leave; impl IqSetPayload for Leave {} impl IqResultPayload for Leave {} /// A request to change the user’s nick. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MIX_CORE, name = "setnick")] pub struct SetNick { /// The new requested nick. #[xml(extract(fields(text)))] pub nick: String, } impl IqSetPayload for SetNick {} impl IqResultPayload for SetNick {} impl SetNick { /// Create a new SetNick element. pub fn new>(nick: N) -> SetNick { SetNick { nick: nick.into() } } } /// Message payload describing who actually sent the message, since unlike in MUC, all messages /// are sent from the channel’s JID. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MIX_CORE, name = "mix")] pub struct Mix { /// The nick of the user who said something. #[xml(extract(fields(text)))] pub nick: String, /// The JID of the user who said something. #[xml(extract(fields(text)))] pub jid: BareJid, } impl MessagePayload for Mix {} impl Mix { /// Create a new Mix element. pub fn new, J: Into>(nick: N, jid: J) -> Mix { Mix { nick: nick.into(), jid: jid.into(), } } } /// Create a new MIX channel. #[derive(FromXml, AsXml, PartialEq, Clone, Debug, Default)] #[xml(namespace = ns::MIX_CORE, name = "create")] pub struct Create { /// The requested channel identifier. #[xml(attribute(default))] pub channel: Option, } impl IqSetPayload for Create {} impl IqResultPayload for Create {} impl Create { /// Create a new ad-hoc Create element. pub fn new() -> Create { Create::default() } /// Create a new Create element with a channel identifier. pub fn from_channel_id>(channel: C) -> Create { Create { channel: Some(ChannelId(channel.into())), } } } /// Destroy a given MIX channel. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::MIX_CORE, name = "destroy")] pub struct Destroy { /// The channel identifier to be destroyed. #[xml(attribute)] pub channel: ChannelId, } // TODO: section 7.3.4, example 33, doesn’t mirror the in the iq result unlike every // other section so far. impl IqSetPayload for Destroy {} impl Destroy { /// Create a new Destroy element. pub fn new>(channel: C) -> Destroy { Destroy { channel: ChannelId(channel.into()), } } } #[cfg(test)] mod tests { use super::*; use minidom::Element; #[test] fn participant() { let elem: Element = "foo@barcoucou" .parse() .unwrap(); let participant = Participant::try_from(elem).unwrap(); assert_eq!(participant.nick, "coucou"); assert_eq!(participant.jid.as_str(), "foo@bar"); } #[test] fn join() { let elem: Element = "coucou" .parse() .unwrap(); let join = Join::try_from(elem).unwrap(); assert_eq!(join.nick, "coucou"); assert_eq!(join.id, None); assert_eq!(join.subscribes.len(), 2); assert_eq!(join.subscribes[0].node.0, "urn:xmpp:mix:nodes:messages"); assert_eq!(join.subscribes[1].node.0, "urn:xmpp:mix:nodes:info"); } #[test] fn update_subscription() { let elem: Element = "" .parse() .unwrap(); let update_subscription = UpdateSubscription::try_from(elem).unwrap(); assert_eq!(update_subscription.jid, None); assert_eq!(update_subscription.subscribes.len(), 1); assert_eq!( update_subscription.subscribes[0].node.0, "urn:xmpp:mix:nodes:participants" ); } #[test] fn leave() { let elem: Element = "".parse().unwrap(); Leave::try_from(elem).unwrap(); } #[test] fn setnick() { let elem: Element = "coucou" .parse() .unwrap(); let setnick = SetNick::try_from(elem).unwrap(); assert_eq!(setnick.nick, "coucou"); } #[test] fn message_mix() { let elem: Element = "foo@barcoucou" .parse() .unwrap(); let mix = Mix::try_from(elem).unwrap(); assert_eq!(mix.nick, "coucou"); assert_eq!(mix.jid.as_str(), "foo@bar"); } #[test] fn create() { let elem: Element = "" .parse() .unwrap(); let create = Create::try_from(elem).unwrap(); assert_eq!(create.channel.unwrap().0, "coucou"); let elem: Element = "".parse().unwrap(); let create = Create::try_from(elem).unwrap(); assert_eq!(create.channel, None); } #[test] fn destroy() { let elem: Element = "" .parse() .unwrap(); let destroy = Destroy::try_from(elem).unwrap(); assert_eq!(destroy.channel.0, "coucou"); } #[test] fn serialise() { let elem: Element = Join::from_nick_and_nodes("coucou", &["foo", "bar"]).into(); let xml = String::from(&elem); assert_eq!(xml, "coucou"); let elem: Element = UpdateSubscription::from_nodes(&["foo", "bar"]).into(); let xml = String::from(&elem); assert_eq!(xml, ""); let elem: Element = Leave.into(); let xml = String::from(&elem); assert_eq!(xml, ""); let elem: Element = SetNick::new("coucou").into(); let xml = String::from(&elem); assert_eq!( xml, "coucou" ); let elem: Element = Mix::new("coucou", "coucou@example".parse::().unwrap()).into(); let xml = String::from(&elem); assert_eq!( xml, "coucoucoucou@example" ); let elem: Element = Create::new().into(); let xml = String::from(&elem); assert_eq!(xml, ""); let elem: Element = Create::from_channel_id("coucou").into(); let xml = String::from(&elem); assert_eq!( xml, "" ); let elem: Element = Destroy::new("coucou").into(); let xml = String::from(&elem); assert_eq!( xml, "" ); } }