Updates for GStreamer 1.20, XMPP parsing improvements
* Updated GStreamer to 1.20 * Updated libnice to 0.1.18 * Vendored a minimal set of modified xmpp-parsers elements rather than the whole thing * Implemented retries of the initial Colibri connection * Updated deps
This commit is contained in:
parent
abd8bcb142
commit
3f27c90489
File diff suppressed because it is too large
Load Diff
|
@ -5,4 +5,9 @@ members = [
|
||||||
"lib-gst-meet-c",
|
"lib-gst-meet-c",
|
||||||
"nice-gst-meet",
|
"nice-gst-meet",
|
||||||
"nice-gst-meet-sys",
|
"nice-gst-meet-sys",
|
||||||
|
"jitsi-xmpp-parsers",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
jid = { git = "https://gitlab.com/xmpp-rs/xmpp-rs.git" }
|
||||||
|
minidom = { git = "https://gitlab.com/xmpp-rs/xmpp-rs.git" }
|
||||||
|
|
|
@ -10,8 +10,8 @@ authors = ["Jasper Hugo <jasper@avstack.io>"]
|
||||||
anyhow = { version = "1", default-features = false, features = ["std"] }
|
anyhow = { version = "1", default-features = false, features = ["std"] }
|
||||||
colibri = { version = "0.1", default-features = false }
|
colibri = { version = "0.1", default-features = false }
|
||||||
futures = { version = "0.3", default-features = false }
|
futures = { version = "0.3", default-features = false }
|
||||||
glib = { version = "0.14", default-features = false, features = ["log"] }
|
glib = { version = "0.15", default-features = false, features = ["log"] }
|
||||||
gstreamer = { version = "0.17", default-features = false, features = ["v1_16"] }
|
gstreamer = { version = "0.18", default-features = false, features = ["v1_20"] }
|
||||||
lib-gst-meet = { version = "0.4", path = "../lib-gst-meet", default-features = false, features = ["tracing-subscriber"] }
|
lib-gst-meet = { version = "0.4", path = "../lib-gst-meet", default-features = false, features = ["tracing-subscriber"] }
|
||||||
structopt = { version = "0.3", default-features = false }
|
structopt = { version = "0.3", default-features = false }
|
||||||
tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread", "signal", "sync", "time"] }
|
tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread", "signal", "sync", "time"] }
|
||||||
|
|
|
@ -270,7 +270,7 @@ async fn main_inner() -> Result<()> {
|
||||||
bin.set_property(
|
bin.set_property(
|
||||||
"name",
|
"name",
|
||||||
format!("participant_{}", participant.muc_jid.resource),
|
format!("participant_{}", participant.muc_jid.resource),
|
||||||
)?;
|
);
|
||||||
conference.add_bin(&bin).await?;
|
conference.add_bin(&bin).await?;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "jitsi-xmpp-parsers"
|
||||||
|
description = "Provides types for non-standard XMPP elements used by Jitsi Meet"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
readme = "../README.md"
|
||||||
|
repository = "https://github.com/avstack/gst-meet"
|
||||||
|
documentation = "https://docs.rs/jitsi-xmpp-parsers/"
|
||||||
|
authors = ["Jasper Hugo <jasper@avstack.io>"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
jid = { version = "0.9", default-features = false, features = ["minidom"] }
|
||||||
|
minidom = { version = "0.14", default-features = false }
|
||||||
|
xmpp-parsers = { git = "https://gitlab.com/xmpp-rs/xmpp-rs.git", default-features = false, features = ["disable-validation"] }
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = [ "--sort-modules-by-appearance", "-Zunstable-options" ]
|
|
@ -0,0 +1,26 @@
|
||||||
|
use xmpp_parsers::Error;
|
||||||
|
|
||||||
|
/// Codec for colon-separated bytes of uppercase hexadecimal.
|
||||||
|
pub struct ColonSeparatedHex;
|
||||||
|
|
||||||
|
impl ColonSeparatedHex {
|
||||||
|
pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
|
||||||
|
let mut bytes = vec![];
|
||||||
|
for i in 0..(1 + s.len()) / 3 {
|
||||||
|
let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16)?;
|
||||||
|
if 3 * i + 2 < s.len() {
|
||||||
|
assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
|
||||||
|
}
|
||||||
|
bytes.push(byte);
|
||||||
|
}
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(b: &[u8]) -> Option<String> {
|
||||||
|
let mut bytes = vec![];
|
||||||
|
for byte in b {
|
||||||
|
bytes.push(format!("{:02X}", byte));
|
||||||
|
}
|
||||||
|
Some(bytes.join(":"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,381 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use jid::Jid;
|
||||||
|
use xmpp_parsers::{
|
||||||
|
iq::IqSetPayload,
|
||||||
|
jingle::{ContentId, Creator, Disposition, ReasonElement, Senders, SessionId},
|
||||||
|
jingle_ibb::Transport as IbbTransport,
|
||||||
|
jingle_grouping::Group,
|
||||||
|
jingle_s5b::Transport as Socks5Transport,
|
||||||
|
ns::{JINGLE, JINGLE_GROUPING, JINGLE_IBB, JINGLE_ICE_UDP, JINGLE_RTP, JINGLE_S5B},
|
||||||
|
Element,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
jingle_ice_udp::Transport as IceUdpTransport,
|
||||||
|
jingle_rtp::Description as RtpDescription,
|
||||||
|
};
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// The action attribute.
|
||||||
|
Action, "action", {
|
||||||
|
/// Accept a content-add action received from another party.
|
||||||
|
ContentAccept => "content-accept",
|
||||||
|
|
||||||
|
/// Add one or more new content definitions to the session.
|
||||||
|
ContentAdd => "content-add",
|
||||||
|
|
||||||
|
/// Change the directionality of media sending.
|
||||||
|
ContentModify => "content-modify",
|
||||||
|
|
||||||
|
/// Reject a content-add action received from another party.
|
||||||
|
ContentReject => "content-reject",
|
||||||
|
|
||||||
|
/// Remove one or more content definitions from the session.
|
||||||
|
ContentRemove => "content-remove",
|
||||||
|
|
||||||
|
/// Exchange information about parameters for an application type.
|
||||||
|
DescriptionInfo => "description-info",
|
||||||
|
|
||||||
|
/// Exchange information about security preconditions.
|
||||||
|
SecurityInfo => "security-info",
|
||||||
|
|
||||||
|
/// Definitively accept a session negotiation.
|
||||||
|
SessionAccept => "session-accept",
|
||||||
|
|
||||||
|
/// Send session-level information, such as a ping or a ringing message.
|
||||||
|
SessionInfo => "session-info",
|
||||||
|
|
||||||
|
/// Request negotiation of a new Jingle session.
|
||||||
|
SessionInitiate => "session-initiate",
|
||||||
|
|
||||||
|
/// End an existing session.
|
||||||
|
SessionTerminate => "session-terminate",
|
||||||
|
|
||||||
|
/// Accept a transport-replace action received from another party.
|
||||||
|
TransportAccept => "transport-accept",
|
||||||
|
|
||||||
|
/// Exchange transport candidates.
|
||||||
|
TransportInfo => "transport-info",
|
||||||
|
|
||||||
|
/// Reject a transport-replace action received from another party.
|
||||||
|
TransportReject => "transport-reject",
|
||||||
|
|
||||||
|
/// Redefine a transport method or replace it with a different method.
|
||||||
|
TransportReplace => "transport-replace",
|
||||||
|
|
||||||
|
/// --- Non-standard values used by Jitsi Meet: ---
|
||||||
|
|
||||||
|
/// Add a source to existing content.
|
||||||
|
SourceAdd => "source-add",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/// The main Jingle container, to be included in an iq stanza.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Jingle {
|
||||||
|
/// The action to execute on both ends.
|
||||||
|
pub action: Action,
|
||||||
|
|
||||||
|
/// Who the initiator is.
|
||||||
|
pub initiator: Option<Jid>,
|
||||||
|
|
||||||
|
/// Who the responder is.
|
||||||
|
pub responder: Option<Jid>,
|
||||||
|
|
||||||
|
/// Unique session identifier between two entities.
|
||||||
|
pub sid: SessionId,
|
||||||
|
|
||||||
|
/// A list of contents to be negotiated in this session.
|
||||||
|
pub contents: Vec<Content>,
|
||||||
|
|
||||||
|
/// An optional reason.
|
||||||
|
pub reason: Option<ReasonElement>,
|
||||||
|
|
||||||
|
/// An optional grouping.
|
||||||
|
pub group: Option<Group>,
|
||||||
|
|
||||||
|
/// Payloads to be included.
|
||||||
|
pub other: Vec<Element>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IqSetPayload for Jingle {}
|
||||||
|
|
||||||
|
impl Jingle {
|
||||||
|
/// Create a new Jingle element.
|
||||||
|
pub fn new(action: Action, sid: SessionId) -> Jingle {
|
||||||
|
Jingle {
|
||||||
|
action,
|
||||||
|
sid,
|
||||||
|
initiator: None,
|
||||||
|
responder: None,
|
||||||
|
contents: Vec::new(),
|
||||||
|
reason: None,
|
||||||
|
group: None,
|
||||||
|
other: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the initiator’s JID.
|
||||||
|
pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
|
||||||
|
self.initiator = Some(initiator);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the responder’s JID.
|
||||||
|
pub fn with_responder(mut self, responder: Jid) -> Jingle {
|
||||||
|
self.responder = Some(responder);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a content to this Jingle container.
|
||||||
|
pub fn add_content(mut self, content: Content) -> Jingle {
|
||||||
|
self.contents.push(content);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the reason in this Jingle container.
|
||||||
|
pub fn set_reason(mut self, reason: ReasonElement) -> Jingle {
|
||||||
|
self.reason = Some(reason);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the grouping in this Jingle container.
|
||||||
|
pub fn set_group(mut self, group: Group) -> Jingle {
|
||||||
|
self.group = Some(group);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for Jingle {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(root: Element) -> Result<Jingle, Error> {
|
||||||
|
check_self!(root, "jingle", JINGLE, "Jingle");
|
||||||
|
|
||||||
|
let mut jingle = Jingle {
|
||||||
|
action: get_attr!(root, "action", Required),
|
||||||
|
initiator: get_attr!(root, "initiator", Option),
|
||||||
|
responder: get_attr!(root, "responder", Option),
|
||||||
|
sid: get_attr!(root, "sid", Required),
|
||||||
|
contents: vec![],
|
||||||
|
reason: None,
|
||||||
|
group: None,
|
||||||
|
other: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
for child in root.children().cloned() {
|
||||||
|
if child.is("content", JINGLE) {
|
||||||
|
let content = Content::try_from(child)?;
|
||||||
|
jingle.contents.push(content);
|
||||||
|
} else if child.is("reason", JINGLE) {
|
||||||
|
if jingle.reason.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Jingle must not have more than one reason.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let reason = ReasonElement::try_from(child)?;
|
||||||
|
jingle.reason = Some(reason);
|
||||||
|
} else if child.is("group", JINGLE_GROUPING) {
|
||||||
|
if jingle.group.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Jingle must not have more than one grouping.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let group = Group::try_from(child)?;
|
||||||
|
jingle.group = Some(group);
|
||||||
|
} else {
|
||||||
|
jingle.other.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(jingle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Jingle> for Element {
|
||||||
|
fn from(jingle: Jingle) -> Element {
|
||||||
|
Element::builder("jingle", JINGLE)
|
||||||
|
.attr("action", jingle.action)
|
||||||
|
.attr("initiator", jingle.initiator)
|
||||||
|
.attr("responder", jingle.responder)
|
||||||
|
.attr("sid", jingle.sid)
|
||||||
|
.append_all(jingle.contents)
|
||||||
|
.append_all(jingle.reason.map(Element::from))
|
||||||
|
.append_all(jingle.group.map(Element::from))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum wrapping all of the various supported descriptions of a Content.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Description {
|
||||||
|
/// Jingle RTP Sessions (XEP-0167) description.
|
||||||
|
Rtp(RtpDescription),
|
||||||
|
|
||||||
|
/// To be used for any description that isn’t known at compile-time.
|
||||||
|
Unknown(Element),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for Description {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<Description, Error> {
|
||||||
|
Ok(if elem.is("description", JINGLE_RTP) {
|
||||||
|
Description::Rtp(RtpDescription::try_from(elem)?)
|
||||||
|
} else {
|
||||||
|
Description::Unknown(elem)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RtpDescription> for Description {
|
||||||
|
fn from(desc: RtpDescription) -> Description {
|
||||||
|
Description::Rtp(desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Description> for Element {
|
||||||
|
fn from(desc: Description) -> Element {
|
||||||
|
match desc {
|
||||||
|
Description::Rtp(desc) => desc.into(),
|
||||||
|
Description::Unknown(elem) => elem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum wrapping all of the various supported transports of a Content.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Transport {
|
||||||
|
/// Jingle ICE-UDP Bytestreams (XEP-0176) transport.
|
||||||
|
IceUdp(IceUdpTransport),
|
||||||
|
|
||||||
|
/// Jingle In-Band Bytestreams (XEP-0261) transport.
|
||||||
|
Ibb(IbbTransport),
|
||||||
|
|
||||||
|
/// Jingle SOCKS5 Bytestreams (XEP-0260) transport.
|
||||||
|
Socks5(Socks5Transport),
|
||||||
|
|
||||||
|
/// To be used for any transport that isn’t known at compile-time.
|
||||||
|
Unknown(Element),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for Transport {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<Transport, Error> {
|
||||||
|
Ok(if elem.is("transport", JINGLE_ICE_UDP) {
|
||||||
|
Transport::IceUdp(IceUdpTransport::try_from(elem)?)
|
||||||
|
} else if elem.is("transport", JINGLE_IBB) {
|
||||||
|
Transport::Ibb(IbbTransport::try_from(elem)?)
|
||||||
|
} else if elem.is("transport", JINGLE_S5B) {
|
||||||
|
Transport::Socks5(Socks5Transport::try_from(elem)?)
|
||||||
|
} else {
|
||||||
|
Transport::Unknown(elem)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<IceUdpTransport> for Transport {
|
||||||
|
fn from(transport: IceUdpTransport) -> Transport {
|
||||||
|
Transport::IceUdp(transport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<IbbTransport> for Transport {
|
||||||
|
fn from(transport: IbbTransport) -> Transport {
|
||||||
|
Transport::Ibb(transport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Socks5Transport> for Transport {
|
||||||
|
fn from(transport: Socks5Transport) -> Transport {
|
||||||
|
Transport::Socks5(transport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Transport> for Element {
|
||||||
|
fn from(transport: Transport) -> Element {
|
||||||
|
match transport {
|
||||||
|
Transport::IceUdp(transport) => transport.into(),
|
||||||
|
Transport::Ibb(transport) => transport.into(),
|
||||||
|
Transport::Socks5(transport) => transport.into(),
|
||||||
|
Transport::Unknown(elem) => elem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Describes a session’s content, there can be multiple content in one
|
||||||
|
/// session.
|
||||||
|
Content, "content", JINGLE,
|
||||||
|
attributes: [
|
||||||
|
/// Who created this content.
|
||||||
|
creator: Option<Creator> = "creator",
|
||||||
|
|
||||||
|
/// How the content definition is to be interpreted by the recipient.
|
||||||
|
disposition: Default<Disposition> = "disposition",
|
||||||
|
|
||||||
|
/// A per-session unique identifier for this content.
|
||||||
|
name: Required<ContentId> = "name",
|
||||||
|
|
||||||
|
/// Who can send data for this content.
|
||||||
|
senders: Default<Senders> = "senders",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// What to send.
|
||||||
|
description: Option<Description> = ("description", *) => Description,
|
||||||
|
|
||||||
|
/// How to send it.
|
||||||
|
transport: Option<Transport> = ("transport", *) => Transport,
|
||||||
|
|
||||||
|
/// With which security.
|
||||||
|
security: Option<Element> = ("security", JINGLE) => Element
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Content {
|
||||||
|
/// Create a new content.
|
||||||
|
pub fn new(creator: Creator, name: ContentId) -> Content {
|
||||||
|
Content {
|
||||||
|
creator: Some(creator),
|
||||||
|
name,
|
||||||
|
disposition: Disposition::Session,
|
||||||
|
senders: Senders::Both,
|
||||||
|
description: None,
|
||||||
|
transport: None,
|
||||||
|
security: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set how the content is to be interpreted by the recipient.
|
||||||
|
pub fn with_disposition(mut self, disposition: Disposition) -> Content {
|
||||||
|
self.disposition = disposition;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Specify who can send data for this content.
|
||||||
|
pub fn with_senders(mut self, senders: Senders) -> Content {
|
||||||
|
self.senders = senders;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the description of this content.
|
||||||
|
pub fn with_description<D: Into<Description>>(mut self, description: D) -> Content {
|
||||||
|
self.description = Some(description.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the transport of this content.
|
||||||
|
pub fn with_transport<T: Into<Transport>>(mut self, transport: T) -> Content {
|
||||||
|
self.transport = Some(transport.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the security of this content.
|
||||||
|
pub fn with_security(mut self, security: Element) -> Content {
|
||||||
|
self.security = Some(security);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
use xmpp_parsers::{
|
||||||
|
hashes::{Algo, Hash},
|
||||||
|
jingle_dtls_srtp::Setup,
|
||||||
|
ns::JINGLE_DTLS,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::helpers::ColonSeparatedHex;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Fingerprint of the key used for a DTLS handshake.
|
||||||
|
Fingerprint, "fingerprint", JINGLE_DTLS,
|
||||||
|
attributes: [
|
||||||
|
/// The hash algorithm used for this fingerprint.
|
||||||
|
hash: Required<Algo> = "hash",
|
||||||
|
|
||||||
|
/// Indicates which of the end points should initiate the TCP connection establishment.
|
||||||
|
setup: Option<Setup> = "setup"
|
||||||
|
],
|
||||||
|
text: (
|
||||||
|
/// Hash value of this fingerprint.
|
||||||
|
value: ColonSeparatedHex<Vec<u8>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Fingerprint {
|
||||||
|
/// Create a new Fingerprint from a Setup and a Hash.
|
||||||
|
pub fn from_hash(setup: Setup, hash: Hash) -> Fingerprint {
|
||||||
|
Fingerprint {
|
||||||
|
hash: hash.algo,
|
||||||
|
setup: Some(setup),
|
||||||
|
value: hash.hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Fingerprint from a Setup and parsing the hash.
|
||||||
|
pub fn from_colon_separated_hex(
|
||||||
|
setup: Setup,
|
||||||
|
algo: &str,
|
||||||
|
hash: &str,
|
||||||
|
) -> Result<Fingerprint, Error> {
|
||||||
|
let algo = algo.parse()?;
|
||||||
|
let hash = Hash::from_colon_separated_hex(algo, hash)?;
|
||||||
|
Ok(Fingerprint::from_hash(setup, hash))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
use xmpp_parsers::{
|
||||||
|
jingle_ice_udp::Candidate,
|
||||||
|
ns::{JINGLE_DTLS, JINGLE_ICE_UDP},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
jingle_dtls_srtp::Fingerprint,
|
||||||
|
ns::JITSI_COLIBRI,
|
||||||
|
};
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Wrapper element for an ICE-UDP transport.
|
||||||
|
#[derive(Default)]
|
||||||
|
Transport, "transport", JINGLE_ICE_UDP,
|
||||||
|
attributes: [
|
||||||
|
/// A Password as defined in ICE-CORE.
|
||||||
|
pwd: Option<String> = "pwd",
|
||||||
|
|
||||||
|
/// A User Fragment as defined in ICE-CORE.
|
||||||
|
ufrag: Option<String> = "ufrag",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// List of candidates for this ICE-UDP session.
|
||||||
|
candidates: Vec<Candidate> = ("candidate", JINGLE_ICE_UDP) => Candidate,
|
||||||
|
|
||||||
|
/// Fingerprint of the key used for the DTLS handshake.
|
||||||
|
fingerprint: Option<Fingerprint> = ("fingerprint", JINGLE_DTLS) => Fingerprint,
|
||||||
|
|
||||||
|
/// Details of the Colibri WebSocket
|
||||||
|
web_socket: Option<WebSocket> = ("web-socket", JITSI_COLIBRI) => WebSocket
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Transport {
|
||||||
|
/// Create a new ICE-UDP transport.
|
||||||
|
pub fn new() -> Transport {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a candidate to this transport.
|
||||||
|
pub fn add_candidate(mut self, candidate: Candidate) -> Self {
|
||||||
|
self.candidates.push(candidate);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the DTLS-SRTP fingerprint of this transport.
|
||||||
|
pub fn with_fingerprint(mut self, fingerprint: Fingerprint) -> Self {
|
||||||
|
self.fingerprint = Some(fingerprint);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Colibri WebSocket details
|
||||||
|
WebSocket, "web-socket", JITSI_COLIBRI,
|
||||||
|
attributes: [
|
||||||
|
/// The WebSocket URL
|
||||||
|
url: Required<String> = "url",
|
||||||
|
]
|
||||||
|
);
|
|
@ -0,0 +1,56 @@
|
||||||
|
use xmpp_parsers::{
|
||||||
|
jingle_rtcp_fb::RtcpFb,
|
||||||
|
jingle_rtp::{PayloadType, RtcpMux},
|
||||||
|
jingle_rtp_hdrext::RtpHdrext,
|
||||||
|
ns::{JINGLE_RTP, JINGLE_RTP_HDREXT, JINGLE_SSMA},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::jingle_ssma::{Group, Source};
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Wrapper element describing an RTP session.
|
||||||
|
Description, "description", JINGLE_RTP,
|
||||||
|
attributes: [
|
||||||
|
/// Namespace of the encryption scheme used.
|
||||||
|
media: Required<String> = "media",
|
||||||
|
|
||||||
|
/// User-friendly name for the encryption scheme, should be `None` for OTR,
|
||||||
|
/// legacy OpenPGP and OX.
|
||||||
|
// XXX: is this a String or an u32?! Refer to RFC 3550.
|
||||||
|
ssrc: Option<String> = "ssrc",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// List of encodings that can be used for this RTP stream.
|
||||||
|
payload_types: Vec<PayloadType> = ("payload-type", JINGLE_RTP) => PayloadType,
|
||||||
|
|
||||||
|
/// Specifies the ability to multiplex RTP Data and Control Packets on a single port as
|
||||||
|
/// described in RFC 5761.
|
||||||
|
rtcp_mux: Option<RtcpMux> = ("rtcp-mux", JINGLE_RTP) => RtcpMux,
|
||||||
|
|
||||||
|
/// List of ssrc-group.
|
||||||
|
ssrc_groups: Vec<Group> = ("ssrc-group", JINGLE_SSMA) => Group,
|
||||||
|
|
||||||
|
/// List of ssrc.
|
||||||
|
ssrcs: Vec<Source> = ("source", JINGLE_SSMA) => Source,
|
||||||
|
|
||||||
|
/// List of header extensions.
|
||||||
|
hdrexts: Vec<RtpHdrext> = ("rtp-hdrext", JINGLE_RTP_HDREXT) => RtpHdrext
|
||||||
|
|
||||||
|
// TODO: Add support for <encryption/> and <bandwidth/>.
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Description {
|
||||||
|
/// Create a new RTP description.
|
||||||
|
pub fn new(media: String) -> Description {
|
||||||
|
Description {
|
||||||
|
media,
|
||||||
|
ssrc: None,
|
||||||
|
payload_types: Vec::new(),
|
||||||
|
rtcp_mux: None,
|
||||||
|
ssrc_groups: Vec::new(),
|
||||||
|
ssrcs: Vec::new(),
|
||||||
|
hdrexts: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
use xmpp_parsers::{jingle_ssma::{Parameter, Semantics}, ns::JINGLE_SSMA};
|
||||||
|
|
||||||
|
use crate::ns::JITSI_MEET;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Source element for the ssrc SDP attribute.
|
||||||
|
Source, "source", JINGLE_SSMA,
|
||||||
|
attributes: [
|
||||||
|
/// Maps to the ssrc-id parameter.
|
||||||
|
id: Required<u32> = "ssrc",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// List of attributes for this source.
|
||||||
|
parameters: Vec<Parameter> = ("parameter", JINGLE_SSMA) => Parameter,
|
||||||
|
|
||||||
|
/// --- Non-standard attributes used by Jitsi Meet: ---
|
||||||
|
|
||||||
|
/// ssrc-info for this source.
|
||||||
|
info: Option<SsrcInfo> = ("ssrc-info", JITSI_MEET) => SsrcInfo
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Source {
|
||||||
|
/// Create a new SSMA Source element.
|
||||||
|
pub fn new(id: u32) -> Source {
|
||||||
|
Source {
|
||||||
|
id,
|
||||||
|
parameters: Vec::new(),
|
||||||
|
info: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// ssrc-info associated with a ssrc.
|
||||||
|
SsrcInfo, "ssrc-info", JITSI_MEET,
|
||||||
|
attributes: [
|
||||||
|
/// The owner of the ssrc.
|
||||||
|
owner: Required<String> = "owner"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Element grouping multiple ssrc.
|
||||||
|
Group, "ssrc-group", JINGLE_SSMA,
|
||||||
|
attributes: [
|
||||||
|
/// The semantics of this group.
|
||||||
|
semantics: Required<Semantics> = "semantics",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The various ssrc concerned by this group.
|
||||||
|
sources: Vec<Source> = ("source", JINGLE_SSMA) => Source
|
||||||
|
]
|
||||||
|
);
|
|
@ -0,0 +1,10 @@
|
||||||
|
#[macro_use]
|
||||||
|
mod macros;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
pub mod jingle;
|
||||||
|
pub mod jingle_dtls_srtp;
|
||||||
|
pub mod jingle_ice_udp;
|
||||||
|
pub mod jingle_rtp;
|
||||||
|
pub mod jingle_ssma;
|
||||||
|
pub mod ns;
|
|
@ -0,0 +1,718 @@
|
||||||
|
// This file is copied from xmpp-parsers
|
||||||
|
|
||||||
|
// Copyright (c) 2017-2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// 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/.
|
||||||
|
|
||||||
|
macro_rules! get_attr {
|
||||||
|
($elem:ident, $attr:tt, $type:tt) => {
|
||||||
|
get_attr!($elem, $attr, $type, value, value.parse()?)
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, OptionEmpty, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some("") => None,
|
||||||
|
Some($value) => Some($func),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, Option, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some($value) => Some($func),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, Required, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some($value) => $func,
|
||||||
|
None => {
|
||||||
|
return Err(xmpp_parsers::Error::ParseError(concat!(
|
||||||
|
"Required attribute '",
|
||||||
|
$attr,
|
||||||
|
"' missing."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, RequiredNonEmpty, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some("") => {
|
||||||
|
return Err(xmpp_parsers::Error::ParseError(concat!(
|
||||||
|
"Required attribute '",
|
||||||
|
$attr,
|
||||||
|
"' must not be empty."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Some($value) => $func,
|
||||||
|
None => {
|
||||||
|
return Err(xmpp_parsers::Error::ParseError(concat!(
|
||||||
|
"Required attribute '",
|
||||||
|
$attr,
|
||||||
|
"' missing."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, Default, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some($value) => $func,
|
||||||
|
None => ::std::default::Default::default(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_attribute {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}) => (
|
||||||
|
generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+});
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}, Default = $default:ident) => (
|
||||||
|
generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+}, Default = $default);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(
|
||||||
|
$(#[$a_meta])*
|
||||||
|
$a
|
||||||
|
),+
|
||||||
|
}
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = xmpp_parsers::Error;
|
||||||
|
fn from_str(s: &str) -> Result<$elem, xmpp_parsers::Error> {
|
||||||
|
Ok(match s {
|
||||||
|
$($b => $elem::$a),+,
|
||||||
|
_ => return Err(xmpp_parsers::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for $elem {
|
||||||
|
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
write!(fmt, "{}", match self {
|
||||||
|
$($elem::$a => $b),+
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
Some(String::from(match self {
|
||||||
|
$($elem::$a => $b),+
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}, Default = $default:ident) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(
|
||||||
|
$(#[$a_meta])*
|
||||||
|
$a
|
||||||
|
),+
|
||||||
|
}
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = xmpp_parsers::Error;
|
||||||
|
fn from_str(s: &str) -> Result<$elem, xmpp_parsers::Error> {
|
||||||
|
Ok(match s {
|
||||||
|
$($b => $elem::$a),+,
|
||||||
|
_ => return Err(xmpp_parsers::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
Some(String::from(match self {
|
||||||
|
$elem::$default => return None,
|
||||||
|
$($elem::$a => $b),+
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::std::default::Default for $elem {
|
||||||
|
fn default() -> $elem {
|
||||||
|
$elem::$default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, ($(#[$meta_symbol:meta])* $symbol:ident => $value:tt)) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(#[$meta_symbol])*
|
||||||
|
$symbol,
|
||||||
|
/// Value when absent.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = xmpp_parsers::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, xmpp_parsers::Error> {
|
||||||
|
Ok(match s {
|
||||||
|
$value => $elem::$symbol,
|
||||||
|
_ => return Err(xmpp_parsers::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
$elem::$symbol => Some(String::from($value)),
|
||||||
|
$elem::None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::std::default::Default for $elem {
|
||||||
|
fn default() -> $elem {
|
||||||
|
$elem::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, bool) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
/// True value, represented by either 'true' or '1'.
|
||||||
|
True,
|
||||||
|
/// False value, represented by either 'false' or '0'.
|
||||||
|
False,
|
||||||
|
}
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = xmpp_parsers::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, xmpp_parsers::Error> {
|
||||||
|
Ok(match s {
|
||||||
|
"true" | "1" => $elem::True,
|
||||||
|
"false" | "0" => $elem::False,
|
||||||
|
_ => return Err(xmpp_parsers::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
$elem::True => Some(String::from("true")),
|
||||||
|
$elem::False => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::std::default::Default for $elem {
|
||||||
|
fn default() -> $elem {
|
||||||
|
$elem::False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $type:tt, Default = $default:expr) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct $elem(pub $type);
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = xmpp_parsers::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, xmpp_parsers::Error> {
|
||||||
|
Ok($elem($type::from_str(s)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
$elem($default) => None,
|
||||||
|
$elem(value) => Some(format!("{}", value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::std::default::Default for $elem {
|
||||||
|
fn default() -> $elem {
|
||||||
|
$elem($default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_element_enum {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => (
|
||||||
|
generate_element_enum!($(#[$meta])* $elem, $name, $ns, {$($(#[$enum_meta])* $enum => $enum_name),+});
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(
|
||||||
|
$(#[$enum_meta])*
|
||||||
|
$enum
|
||||||
|
),+
|
||||||
|
}
|
||||||
|
impl ::std::convert::TryFrom<xmpp_parsers::Element> for $elem {
|
||||||
|
type Error = xmpp_parsers::Error;
|
||||||
|
fn try_from(elem: xmpp_parsers::Element) -> Result<$elem, xmpp_parsers::Error> {
|
||||||
|
check_ns_only!(elem, $name, $ns);
|
||||||
|
Ok(match elem.name() {
|
||||||
|
$($enum_name => $elem::$enum,)+
|
||||||
|
_ => return Err(xmpp_parsers::Error::ParseError(concat!("This is not a ", $name, " element."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<$elem> for xmpp_parsers::Element {
|
||||||
|
fn from(elem: $elem) -> xmpp_parsers::Element {
|
||||||
|
xmpp_parsers::Element::builder(
|
||||||
|
match elem {
|
||||||
|
$($elem::$enum => $enum_name,)+
|
||||||
|
},
|
||||||
|
$ns,
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_attribute_enum {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => (
|
||||||
|
generate_attribute_enum!($(#[$meta])* $elem, $name, $ns, $attr, {$($(#[$enum_meta])* $enum => $enum_name),+});
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(
|
||||||
|
$(#[$enum_meta])*
|
||||||
|
$enum
|
||||||
|
),+
|
||||||
|
}
|
||||||
|
impl ::std::convert::TryFrom<xmpp_parsers::Element> for $elem {
|
||||||
|
type Error = xmpp_parsers::Error;
|
||||||
|
fn try_from(elem: xmpp_parsers::Element) -> Result<$elem, xmpp_parsers::Error> {
|
||||||
|
check_ns_only!(elem, $name, $ns);
|
||||||
|
Ok(match get_attr!(elem, $attr, Required) {
|
||||||
|
$($enum_name => $elem::$enum,)+
|
||||||
|
_ => return Err(xmpp_parsers::Error::ParseError(concat!("Invalid ", $name, " ", $attr, " value."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<$elem> for xmpp_parsers::Element {
|
||||||
|
fn from(elem: $elem) -> xmpp_parsers::Element {
|
||||||
|
xmpp_parsers::Element::builder($name, $ns)
|
||||||
|
.attr($attr, match elem {
|
||||||
|
$($elem::$enum => $enum_name,)+
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! check_self {
|
||||||
|
($elem:ident, $name:tt, $ns:ident) => {
|
||||||
|
check_self!($elem, $name, $ns, $name);
|
||||||
|
};
|
||||||
|
($elem:ident, $name:tt, $ns:ident, $pretty_name:tt) => {
|
||||||
|
if !$elem.is($name, $ns) {
|
||||||
|
return Err(xmpp_parsers::Error::ParseError(concat!(
|
||||||
|
"This is not a ",
|
||||||
|
$pretty_name,
|
||||||
|
" element."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! check_ns_only {
|
||||||
|
($elem:ident, $name:tt, $ns:ident) => {
|
||||||
|
if !$elem.has_ns($ns) {
|
||||||
|
return Err(xmpp_parsers::Error::ParseError(concat!(
|
||||||
|
"This is not a ",
|
||||||
|
$name,
|
||||||
|
" element."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_empty_element {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct $elem;
|
||||||
|
|
||||||
|
impl ::std::convert::TryFrom<xmpp_parsers::Element> for $elem {
|
||||||
|
type Error = xmpp_parsers::Error;
|
||||||
|
|
||||||
|
fn try_from(elem: xmpp_parsers::Element) -> Result<$elem, xmpp_parsers::Error> {
|
||||||
|
check_self!(elem, $name, $ns);
|
||||||
|
Ok($elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$elem> for xmpp_parsers::Element {
|
||||||
|
fn from(_: $elem) -> xmpp_parsers::Element {
|
||||||
|
xmpp_parsers::Element::builder($name, $ns)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_id {
|
||||||
|
($(#[$meta:meta])* $elem:ident) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct $elem(pub String);
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = xmpp_parsers::Error;
|
||||||
|
fn from_str(s: &str) -> Result<$elem, xmpp_parsers::Error> {
|
||||||
|
// TODO: add a way to parse that differently when needed.
|
||||||
|
Ok($elem(String::from(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_elem_id {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => (
|
||||||
|
generate_elem_id!($(#[$meta])* $elem, $name, $ns, String);
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = xmpp_parsers::Error;
|
||||||
|
fn from_str(s: &str) -> Result<$elem, xmpp_parsers::Error> {
|
||||||
|
// TODO: add a way to parse that differently when needed.
|
||||||
|
Ok($elem(String::from(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $type:ty) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct $elem(pub $type);
|
||||||
|
impl ::std::convert::TryFrom<xmpp_parsers::Element> for $elem {
|
||||||
|
type Error = xmpp_parsers::Error;
|
||||||
|
fn try_from(elem: xmpp_parsers::Element) -> Result<$elem, xmpp_parsers::Error> {
|
||||||
|
check_self!(elem, $name, $ns);
|
||||||
|
// TODO: add a way to parse that differently when needed.
|
||||||
|
Ok($elem(elem.text().parse()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<$elem> for xmpp_parsers::Element {
|
||||||
|
fn from(elem: $elem) -> xmpp_parsers::Element {
|
||||||
|
xmpp_parsers::Element::builder($name, $ns)
|
||||||
|
.append(elem.0.to_string())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! decl_attr {
|
||||||
|
(OptionEmpty, $type:ty) => (
|
||||||
|
Option<$type>
|
||||||
|
);
|
||||||
|
(Option, $type:ty) => (
|
||||||
|
Option<$type>
|
||||||
|
);
|
||||||
|
(Required, $type:ty) => (
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
(RequiredNonEmpty, $type:ty) => (
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
(Default, $type:ty) => (
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! start_decl {
|
||||||
|
(Vec, $type:ty) => (
|
||||||
|
Vec<$type>
|
||||||
|
);
|
||||||
|
(Option, $type:ty) => (
|
||||||
|
Option<$type>
|
||||||
|
);
|
||||||
|
(Required, $type:ty) => (
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
(Present, $type:ty) => (
|
||||||
|
bool
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! start_parse_elem {
|
||||||
|
($temp:ident: Vec) => {
|
||||||
|
let mut $temp = Vec::new();
|
||||||
|
};
|
||||||
|
($temp:ident: Option) => {
|
||||||
|
let mut $temp = None;
|
||||||
|
};
|
||||||
|
($temp:ident: Required) => {
|
||||||
|
let mut $temp = None;
|
||||||
|
};
|
||||||
|
($temp:ident: Present) => {
|
||||||
|
let mut $temp = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! do_parse {
|
||||||
|
($elem:ident, Element) => {
|
||||||
|
$elem.clone()
|
||||||
|
};
|
||||||
|
($elem:ident, String) => {
|
||||||
|
$elem.text()
|
||||||
|
};
|
||||||
|
($elem:ident, $constructor:ident) => {
|
||||||
|
$constructor::try_from($elem.clone())?
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! do_parse_elem {
|
||||||
|
($temp:ident: Vec = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
|
||||||
|
$temp.push(do_parse!($elem, $constructor));
|
||||||
|
};
|
||||||
|
($temp:ident: Option = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
|
||||||
|
if $temp.is_some() {
|
||||||
|
return Err(xmpp_parsers::Error::ParseError(concat!(
|
||||||
|
"Element ",
|
||||||
|
$parent_name,
|
||||||
|
" must not have more than one ",
|
||||||
|
$name,
|
||||||
|
" child."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
$temp = Some(do_parse!($elem, $constructor));
|
||||||
|
};
|
||||||
|
($temp:ident: Required = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
|
||||||
|
if $temp.is_some() {
|
||||||
|
return Err(xmpp_parsers::Error::ParseError(concat!(
|
||||||
|
"Element ",
|
||||||
|
$parent_name,
|
||||||
|
" must not have more than one ",
|
||||||
|
$name,
|
||||||
|
" child."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
$temp = Some(do_parse!($elem, $constructor));
|
||||||
|
};
|
||||||
|
($temp:ident: Present = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
|
||||||
|
if $temp {
|
||||||
|
return Err(xmpp_parsers::Error::ParseError(concat!(
|
||||||
|
"Element ",
|
||||||
|
$parent_name,
|
||||||
|
" must not have more than one ",
|
||||||
|
$name,
|
||||||
|
" child."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
$temp = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! finish_parse_elem {
|
||||||
|
($temp:ident: Vec = $name:tt, $parent_name:tt) => {
|
||||||
|
$temp
|
||||||
|
};
|
||||||
|
($temp:ident: Option = $name:tt, $parent_name:tt) => {
|
||||||
|
$temp
|
||||||
|
};
|
||||||
|
($temp:ident: Required = $name:tt, $parent_name:tt) => {
|
||||||
|
$temp.ok_or(xmpp_parsers::Error::ParseError(concat!(
|
||||||
|
"Missing child ",
|
||||||
|
$name,
|
||||||
|
" in ",
|
||||||
|
$parent_name,
|
||||||
|
" element."
|
||||||
|
)))?
|
||||||
|
};
|
||||||
|
($temp:ident: Present = $name:tt, $parent_name:tt) => {
|
||||||
|
$temp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_serialiser {
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Required, String, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append(
|
||||||
|
xmpp_parsers::Element::builder($name, $ns)
|
||||||
|
.append(::minidom::Node::Text($parent.$elem)),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Option, String, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append_all($parent.$elem.map(|elem| {
|
||||||
|
xmpp_parsers::Element::builder($name, $ns).append(::minidom::Node::Text(elem))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, *)) => {
|
||||||
|
$builder.append_all(
|
||||||
|
$parent
|
||||||
|
.$elem
|
||||||
|
.map(|elem| ::minidom::Node::Element(xmpp_parsers::Element::from(elem))),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append_all(
|
||||||
|
$parent
|
||||||
|
.$elem
|
||||||
|
.map(|elem| ::minidom::Node::Element(xmpp_parsers::Element::from(elem))),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Vec, $constructor:ident, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append_all($parent.$elem.into_iter())
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Present, $constructor:ident, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append(::minidom::Node::Element(
|
||||||
|
xmpp_parsers::Element::builder($name, $ns).build(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, $_:ident, $constructor:ident, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append(::minidom::Node::Element(xmpp_parsers::Element::from(
|
||||||
|
$parent.$elem,
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_child_test {
|
||||||
|
($child:ident, $name:tt, *) => {
|
||||||
|
$child.is($name, ::minidom::NSChoice::Any)
|
||||||
|
};
|
||||||
|
($child:ident, $name:tt, $ns:tt) => {
|
||||||
|
$child.is($name, $ns)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_element {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+,]) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+]) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*,], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>));
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+], text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>));
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*] $(, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >))*) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct $elem {
|
||||||
|
$(
|
||||||
|
$(#[$attr_meta])*
|
||||||
|
pub $attr: decl_attr!($attr_action, $attr_type),
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$(#[$child_meta])*
|
||||||
|
pub $child_ident: start_decl!($coucou, $child_type),
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$(#[$text_meta])*
|
||||||
|
pub $text_ident: $text_type,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::convert::TryFrom<xmpp_parsers::Element> for $elem {
|
||||||
|
type Error = xmpp_parsers::Error;
|
||||||
|
|
||||||
|
fn try_from(elem: xmpp_parsers::Element) -> Result<$elem, xmpp_parsers::Error> {
|
||||||
|
check_self!(elem, $name, $ns);
|
||||||
|
$(
|
||||||
|
start_parse_elem!($child_ident: $coucou);
|
||||||
|
)*
|
||||||
|
for _child in elem.children() {
|
||||||
|
$(
|
||||||
|
if generate_child_test!(_child, $child_name, $child_ns) {
|
||||||
|
do_parse_elem!($child_ident: $coucou = $child_constructor => _child, $child_name, $name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
Ok($elem {
|
||||||
|
$(
|
||||||
|
$attr: get_attr!(elem, $attr_name, $attr_action),
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$child_ident: finish_parse_elem!($child_ident: $coucou = $child_name, $name),
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$text_ident: $codec::decode(&elem.text())?,
|
||||||
|
)*
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$elem> for xmpp_parsers::Element {
|
||||||
|
fn from(elem: $elem) -> xmpp_parsers::Element {
|
||||||
|
let mut builder = xmpp_parsers::Element::builder($name, $ns);
|
||||||
|
$(
|
||||||
|
builder = builder.attr($attr_name, elem.$attr);
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
builder = generate_serialiser!(builder, elem, $child_ident, $coucou, $child_constructor, ($child_name, $child_ns));
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
builder = builder.append_all($codec::encode(&elem.$text_ident).map(::minidom::Node::Text).into_iter());
|
||||||
|
)*
|
||||||
|
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
macro_rules! assert_size (
|
||||||
|
($t:ty, $sz:expr) => (
|
||||||
|
assert_eq!(::std::mem::size_of::<$t>(), $sz);
|
||||||
|
);
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: move that to src/pubsub/mod.rs, once we figure out how to use macros from there.
|
||||||
|
macro_rules! impl_pubsub_item {
|
||||||
|
($item:ident, $ns:ident) => {
|
||||||
|
impl ::std::convert::TryFrom<xmpp_parsers::Element> for $item {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: xmpp_parsers::Element) -> Result<$item, Error> {
|
||||||
|
check_self!(elem, "item", $ns);
|
||||||
|
let mut payloads = elem.children().cloned().collect::<Vec<_>>();
|
||||||
|
let payload = payloads.pop();
|
||||||
|
if !payloads.is_empty() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"More than a single payload in item element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok($item(xmpp_parsers::pubsub::Item {
|
||||||
|
id: get_attr!(elem, "id", Option),
|
||||||
|
publisher: get_attr!(elem, "publisher", Option),
|
||||||
|
payload,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$item> for xmpp_parsers::Element {
|
||||||
|
fn from(item: $item) -> xmpp_parsers::Element {
|
||||||
|
xmpp_parsers::Element::builder("item", $ns)
|
||||||
|
.attr("id", item.0.id)
|
||||||
|
.attr("publisher", item.0.publisher)
|
||||||
|
.append_all(item.0.payload)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::ops::Deref for $item {
|
||||||
|
type Target = xmpp_parsers::pubsub::Item;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::ops::DerefMut for $item {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
/// Jitsi Meet general namespace
|
||||||
|
pub const JITSI_MEET: &str = "http://jitsi.org/jitmeet";
|
||||||
|
|
||||||
|
/// Jitsi Meet Colibri namespace
|
||||||
|
pub const JITSI_COLIBRI: &str = "http://jitsi.org/protocol/colibri";
|
|
@ -8,8 +8,8 @@ authors = ["Jasper Hugo <jasper@avstack.io>"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1", default-features = false }
|
anyhow = { version = "1", default-features = false }
|
||||||
glib = { version = "0.14", default-features = false }
|
glib = { version = "0.15", default-features = false }
|
||||||
gstreamer = { version = "0.17", default-features = false }
|
gstreamer = { version = "0.18", default-features = false }
|
||||||
lib-gst-meet = { version = "0.4", path = "../lib-gst-meet", default-features = false, features = ["tracing-subscriber"] }
|
lib-gst-meet = { version = "0.4", path = "../lib-gst-meet", default-features = false, features = ["tracing-subscriber"] }
|
||||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] }
|
tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] }
|
||||||
tracing = { version = "0.1", default-features = false }
|
tracing = { version = "0.1", default-features = false }
|
||||||
|
|
|
@ -13,27 +13,29 @@ authors = ["Jasper Hugo <jasper@avstack.io>"]
|
||||||
anyhow = { version = "1", default-features = false, features = ["std"] }
|
anyhow = { version = "1", default-features = false, features = ["std"] }
|
||||||
async-stream = { version = "0.3", default-features = false }
|
async-stream = { version = "0.3", default-features = false }
|
||||||
async-trait = { version = "0.1", default-features = false }
|
async-trait = { version = "0.1", default-features = false }
|
||||||
|
base64 = { version = "0.13", default-features = false }
|
||||||
bytes = { version = "1", default-features = false, features = ["std"] }
|
bytes = { version = "1", default-features = false, features = ["std"] }
|
||||||
colibri = { version = "0.1", default-features = false }
|
colibri = { version = "0.1", default-features = false }
|
||||||
futures = { version = "0.3", default-features = false }
|
futures = { version = "0.3", default-features = false }
|
||||||
glib = { version = "0.14", default-features = false }
|
glib = { version = "0.15", default-features = false }
|
||||||
gstreamer = { version = "0.17", default-features = false, features = ["v1_20"] }
|
gstreamer = { version = "0.18", default-features = false, features = ["v1_20"] }
|
||||||
gstreamer-rtp = { version = "0.17", default-features = false, features = ["v1_20"] }
|
gstreamer-rtp = { version = "0.18", default-features = false, features = ["v1_20"] }
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
hex = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
itertools = { version = "0.10", default-features = false, features = ["use_std"] }
|
itertools = { version = "0.10", default-features = false, features = ["use_std"] }
|
||||||
|
jitsi-xmpp-parsers = { version = "0.1", path = "../jitsi-xmpp-parsers", default-features = false }
|
||||||
libc = { version = "0.2", default-features = false }
|
libc = { version = "0.2", default-features = false }
|
||||||
maplit = { version = "1", default-features = false }
|
maplit = { version = "1", default-features = false }
|
||||||
nice-gst-meet = { version = "0.1", path = "../nice-gst-meet", default-features = false, features = ["v0_1_16"] }
|
nice-gst-meet = { version = "0.1", path = "../nice-gst-meet", default-features = false, features = ["v0_1_18"] }
|
||||||
once_cell = { version = "1", default-features = false, features = ["std"] }
|
once_cell = { version = "1", default-features = false, features = ["std"] }
|
||||||
pem = { version = "1", default-features = false }
|
pem = { version = "1", default-features = false }
|
||||||
rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] }
|
rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] }
|
||||||
rcgen = { version = "0.8", default-features = false }
|
rcgen = { version = "0.9", default-features = false }
|
||||||
ring = { version = "0.16", default-features = false }
|
ring = { version = "0.16", default-features = false }
|
||||||
serde = { version = "1", default-features = false, features = ["derive"] }
|
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||||
serde_json = { version = "1", default-features = false, features = ["std"] }
|
serde_json = { version = "1", default-features = false, features = ["std"] }
|
||||||
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros", "sync", "time"] }
|
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros", "sync", "time"] }
|
||||||
tokio-stream = { version = "0.1", default-features = false, features = ["time"] }
|
tokio-stream = { version = "0.1", default-features = false, features = ["time"] }
|
||||||
tokio-tungstenite = { version = "0.16", default-features = false, features = ["connect"] }
|
tokio-tungstenite = { version = "0.17", default-features = false, features = ["connect"] }
|
||||||
tracing = { version = "0.1", default-features = false, features = ["attributes", "std"] }
|
tracing = { version = "0.1", default-features = false, features = ["attributes", "std"] }
|
||||||
tracing-subscriber = { version = "0.3", optional = true, default-features = false, features = [
|
tracing-subscriber = { version = "0.3", optional = true, default-features = false, features = [
|
||||||
"fmt",
|
"fmt",
|
||||||
|
@ -43,7 +45,7 @@ tracing-subscriber = { version = "0.3", optional = true, default-features = fals
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
] }
|
] }
|
||||||
uuid = { version = "0.8", default-features = false, features = ["v4"] }
|
uuid = { version = "0.8", default-features = false, features = ["v4"] }
|
||||||
xmpp-parsers = { path = "../xmpp-parsers", package = "xmpp-parsers-gst-meet", version = "0.18", default-features = false }
|
xmpp-parsers = { git = "https://gitlab.com/xmpp-rs/xmpp-rs.git", default-features = false, features = ["disable-validation"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tls-native"]
|
default = ["tls-native"]
|
||||||
|
|
|
@ -1,25 +1,63 @@
|
||||||
use std::sync::Arc;
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use colibri::ColibriMessage;
|
use colibri::ColibriMessage;
|
||||||
use futures::{
|
use futures::{
|
||||||
sink::SinkExt,
|
sink::SinkExt,
|
||||||
stream::{StreamExt, TryStreamExt},
|
stream::{StreamExt, TryStreamExt},
|
||||||
};
|
};
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use rand::{RngCore, thread_rng};
|
||||||
|
use tokio::{
|
||||||
|
sync::{mpsc, Mutex},
|
||||||
|
time::sleep,
|
||||||
|
};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tokio_tungstenite::tungstenite::{http::Request, Message};
|
use tokio_tungstenite::tungstenite::{
|
||||||
|
http::{Request, Uri},
|
||||||
|
Message,
|
||||||
|
};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
const MAX_CONNECT_RETRIES: u8 = 3;
|
||||||
|
const CONNECT_RETRY_SLEEP: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
pub(crate) struct ColibriChannel {
|
pub(crate) struct ColibriChannel {
|
||||||
send_tx: mpsc::Sender<ColibriMessage>,
|
send_tx: mpsc::Sender<ColibriMessage>,
|
||||||
recv_tx: Arc<Mutex<Vec<mpsc::Sender<ColibriMessage>>>>,
|
recv_tx: Arc<Mutex<Vec<mpsc::Sender<ColibriMessage>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColibriChannel {
|
impl ColibriChannel {
|
||||||
pub(crate) async fn new(colibri_url: &str) -> Result<Self> {
|
pub(crate) async fn new(uri: &str) -> Result<Self> {
|
||||||
let request = Request::get(colibri_url).body(())?;
|
let uri: Uri = uri.parse()?;
|
||||||
let (colibri_websocket, _response) = tokio_tungstenite::connect_async(request).await?;
|
let host = uri.host().context("invalid WebSocket URL: missing host")?;
|
||||||
|
|
||||||
|
let mut retries = 0;
|
||||||
|
let colibri_websocket = loop {
|
||||||
|
let mut key = [0u8; 16];
|
||||||
|
thread_rng().fill_bytes(&mut key);
|
||||||
|
let request = Request::get(&uri)
|
||||||
|
.header("sec-websocket-key", base64::encode(&key))
|
||||||
|
.header("sec-websocket-version", "13")
|
||||||
|
.header("host", host)
|
||||||
|
// TODO: the server should probably not enforce this since non-browser clients are now possible
|
||||||
|
.header("origin", format!("https://{}", host))
|
||||||
|
.header("connection", "Upgrade")
|
||||||
|
.header("upgrade", "websocket")
|
||||||
|
.body(())?;
|
||||||
|
match tokio_tungstenite::connect_async(request).await {
|
||||||
|
Ok((websocket, _)) => break websocket,
|
||||||
|
Err(e) => {
|
||||||
|
if retries < MAX_CONNECT_RETRIES {
|
||||||
|
warn!("Failed to connect Colibri WebSocket, will retry: {:?}", e);
|
||||||
|
sleep(CONNECT_RETRY_SLEEP).await;
|
||||||
|
retries += 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return Err(e).context("Failed to connect Colibri WebSocket");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
info!("Connected Colibri WebSocket");
|
info!("Connected Colibri WebSocket");
|
||||||
|
|
||||||
|
@ -37,7 +75,7 @@ impl ColibriChannel {
|
||||||
let mut txs = recv_tx.lock().await;
|
let mut txs = recv_tx.lock().await;
|
||||||
let txs_clone = txs.clone();
|
let txs_clone = txs.clone();
|
||||||
for (i, tx) in txs_clone.iter().enumerate().rev() {
|
for (i, tx) in txs_clone.iter().enumerate().rev() {
|
||||||
if let Err(_) = tx.send(colibri_msg.clone()).await {
|
if tx.send(colibri_msg.clone()).await.is_err() {
|
||||||
debug!("colibri subscriber closed, removing");
|
debug!("colibri subscriber closed, removing");
|
||||||
txs.remove(i);
|
txs.remove(i);
|
||||||
}
|
}
|
||||||
|
@ -53,12 +91,12 @@ impl ColibriChannel {
|
||||||
"received unexpected {} byte binary frame on colibri websocket",
|
"received unexpected {} byte binary frame on colibri websocket",
|
||||||
data.len()
|
data.len()
|
||||||
),
|
),
|
||||||
Message::Ping(_) | Message::Pong(_) => {}, // handled automatically by tungstenite
|
|
||||||
Message::Close(_) => {
|
Message::Close(_) => {
|
||||||
debug!("received close frame on colibri websocket");
|
debug!("received close frame on colibri websocket");
|
||||||
// TODO reconnect
|
// TODO reconnect
|
||||||
break;
|
break;
|
||||||
},
|
},
|
||||||
|
Message::Frame(_) | Message::Ping(_) | Message::Pong(_) => {}, // handled automatically by tungstenite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
|
|
|
@ -5,6 +5,7 @@ use async_trait::async_trait;
|
||||||
use colibri::ColibriMessage;
|
use colibri::ColibriMessage;
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use gstreamer::prelude::{ElementExt, ElementExtManual, GstBinExt};
|
use gstreamer::prelude::{ElementExt, ElementExtManual, GstBinExt};
|
||||||
|
use jitsi_xmpp_parsers::jingle::{Action, Jingle};
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
@ -19,7 +20,6 @@ use xmpp_parsers::{
|
||||||
ecaps2::{self, ECaps2},
|
ecaps2::{self, ECaps2},
|
||||||
hashes::{Algo, Hash},
|
hashes::{Algo, Hash},
|
||||||
iq::{Iq, IqType},
|
iq::{Iq, IqType},
|
||||||
jingle::{Action, Jingle},
|
|
||||||
message::{Message, MessageType},
|
message::{Message, MessageType},
|
||||||
muc::{Muc, MucUser, user::Status as MucStatus},
|
muc::{Muc, MucUser, user::Status as MucStatus},
|
||||||
nick::Nick,
|
nick::Nick,
|
||||||
|
@ -391,8 +391,7 @@ impl JitsiConference {
|
||||||
if let Err(e) = f(self.clone(), participant.clone()).await {
|
if let Err(e) = f(self.clone(), participant.clone()).await {
|
||||||
warn!("on_participant failed: {:?}", e);
|
warn!("on_participant failed: {:?}", e);
|
||||||
}
|
}
|
||||||
else {
|
else if let Ok(pipeline) = self.pipeline().await {
|
||||||
if let Ok(pipeline) = self.pipeline().await {
|
|
||||||
gstreamer::debug_bin_to_dot_file(
|
gstreamer::debug_bin_to_dot_file(
|
||||||
&pipeline,
|
&pipeline,
|
||||||
gstreamer::DebugGraphDetails::ALL,
|
gstreamer::DebugGraphDetails::ALL,
|
||||||
|
@ -401,7 +400,6 @@ impl JitsiConference {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,8 +423,7 @@ impl JitsiConference {
|
||||||
if let Err(e) = f(self.clone(), participant.clone()).await {
|
if let Err(e) = f(self.clone(), participant.clone()).await {
|
||||||
warn!("on_participant failed: {:?}", e);
|
warn!("on_participant failed: {:?}", e);
|
||||||
}
|
}
|
||||||
else {
|
else if let Ok(pipeline) = self.pipeline().await {
|
||||||
if let Ok(pipeline) = self.pipeline().await {
|
|
||||||
gstreamer::debug_bin_to_dot_file(
|
gstreamer::debug_bin_to_dot_file(
|
||||||
&pipeline,
|
&pipeline,
|
||||||
gstreamer::DebugGraphDetails::ALL,
|
gstreamer::DebugGraphDetails::ALL,
|
||||||
|
@ -435,7 +432,6 @@ impl JitsiConference {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", skip(f))]
|
#[tracing::instrument(level = "trace", skip(f))]
|
||||||
pub async fn on_participant_left(
|
pub async fn on_participant_left(
|
||||||
|
@ -542,8 +538,8 @@ impl StanzaFilter for JitsiConference {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
IqType::Set(element) => {
|
IqType::Set(element) => match Jingle::try_from(element) {
|
||||||
if let Ok(jingle) = Jingle::try_from(element) {
|
Ok(jingle) => {
|
||||||
if let Some(Jid::Full(from_jid)) = iq.from {
|
if let Some(Jid::Full(from_jid)) = iq.from {
|
||||||
if jingle.action == Action::SessionInitiate {
|
if jingle.action == Action::SessionInitiate {
|
||||||
if from_jid.resource == "focus" {
|
if from_jid.resource == "focus" {
|
||||||
|
@ -580,10 +576,8 @@ impl StanzaFilter for JitsiConference {
|
||||||
else {
|
else {
|
||||||
debug!("Received Jingle IQ from invalid JID: {:?}", iq.from);
|
debug!("Received Jingle IQ from invalid JID: {:?}", iq.from);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
else {
|
Err(e) => debug!("IQ did not successfully parse as Jingle: {:?}", e),
|
||||||
debug!("Received non-Jingle IQ");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
IqType::Result(_) => {
|
IqType::Result(_) => {
|
||||||
if let Some(jingle_session) = self.jingle_session.lock().await.as_mut() {
|
if let Some(jingle_session) = self.jingle_session.lock().await.as_mut() {
|
||||||
|
@ -696,8 +690,7 @@ impl StanzaFilter for JitsiConference {
|
||||||
if let Err(e) = f(self.clone(), participant.clone()).await {
|
if let Err(e) = f(self.clone(), participant.clone()).await {
|
||||||
warn!("on_participant failed: {:?}", e);
|
warn!("on_participant failed: {:?}", e);
|
||||||
}
|
}
|
||||||
else {
|
else if let Some(jingle_session) = self.jingle_session.lock().await.as_ref() {
|
||||||
if let Some(jingle_session) = self.jingle_session.lock().await.as_ref() {
|
|
||||||
gstreamer::debug_bin_to_dot_file(
|
gstreamer::debug_bin_to_dot_file(
|
||||||
&jingle_session.pipeline(),
|
&jingle_session.pipeline(),
|
||||||
gstreamer::DebugGraphDetails::ALL,
|
gstreamer::DebugGraphDetails::ALL,
|
||||||
|
@ -712,7 +705,6 @@ impl StanzaFilter for JitsiConference {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -5,6 +5,13 @@ use futures::stream::StreamExt;
|
||||||
use glib::{ObjectExt, ToValue};
|
use glib::{ObjectExt, ToValue};
|
||||||
use gstreamer::prelude::{ElementExt, GObjectExtManualGst, GstBinExt, PadExt};
|
use gstreamer::prelude::{ElementExt, GObjectExtManualGst, GstBinExt, PadExt};
|
||||||
use gstreamer_rtp::{prelude::RTPHeaderExtensionExt, RTPHeaderExtension};
|
use gstreamer_rtp::{prelude::RTPHeaderExtensionExt, RTPHeaderExtension};
|
||||||
|
use jitsi_xmpp_parsers::{
|
||||||
|
jingle::{Action, Content, Description, Jingle, Transport},
|
||||||
|
jingle_dtls_srtp::Fingerprint,
|
||||||
|
jingle_ice_udp::Transport as IceUdpTransport,
|
||||||
|
jingle_rtp::Description as RtpDescription,
|
||||||
|
jingle_ssma,
|
||||||
|
};
|
||||||
use nice_gst_meet as nice;
|
use nice_gst_meet as nice;
|
||||||
use pem::Pem;
|
use pem::Pem;
|
||||||
use rand::random;
|
use rand::random;
|
||||||
|
@ -16,14 +23,14 @@ use uuid::Uuid;
|
||||||
use xmpp_parsers::{
|
use xmpp_parsers::{
|
||||||
hashes::Algo,
|
hashes::Algo,
|
||||||
iq::Iq,
|
iq::Iq,
|
||||||
jingle::{Action, Content, Creator, Description, Jingle, Senders, Transport},
|
jingle::{Creator, Senders},
|
||||||
jingle_dtls_srtp::{Fingerprint, Setup},
|
jingle_dtls_srtp::Setup,
|
||||||
jingle_grouping::{self, Semantics},
|
jingle_grouping::{self, Content as GroupContent},
|
||||||
jingle_ice_udp::{self, Transport as IceUdpTransport},
|
jingle_ice_udp,
|
||||||
jingle_rtcp_fb::RtcpFb,
|
jingle_rtcp_fb::RtcpFb,
|
||||||
jingle_rtp::{self, Description as RtpDescription, PayloadType, RtcpMux},
|
jingle_rtp::{self, PayloadType, RtcpMux},
|
||||||
jingle_rtp_hdrext::RtpHdrext,
|
jingle_rtp_hdrext::RtpHdrext,
|
||||||
jingle_ssma::{self, Parameter},
|
jingle_ssma::{Parameter, Semantics},
|
||||||
Jid,
|
Jid,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -117,9 +124,9 @@ impl Codec {
|
||||||
|
|
||||||
struct ParsedRtpDescription {
|
struct ParsedRtpDescription {
|
||||||
codecs: Vec<Codec>,
|
codecs: Vec<Codec>,
|
||||||
audio_hdrext_ssrc_audio_level: Option<u8>,
|
audio_hdrext_ssrc_audio_level: Option<u16>,
|
||||||
audio_hdrext_transport_cc: Option<u8>,
|
audio_hdrext_transport_cc: Option<u16>,
|
||||||
video_hdrext_transport_cc: Option<u8>,
|
video_hdrext_transport_cc: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct JingleSession {
|
pub(crate) struct JingleSession {
|
||||||
|
@ -156,7 +163,7 @@ impl JingleSession {
|
||||||
pub(crate) fn pause_all_sinks(&self) {
|
pub(crate) fn pause_all_sinks(&self) {
|
||||||
if let Some(rtpbin) = self.pipeline.by_name("rtpbin") {
|
if let Some(rtpbin) = self.pipeline.by_name("rtpbin") {
|
||||||
rtpbin.foreach_src_pad(|_, pad| {
|
rtpbin.foreach_src_pad(|_, pad| {
|
||||||
let pad_name: String = pad.property("name").unwrap().get().unwrap();
|
let pad_name: String = pad.property("name");
|
||||||
if pad_name.starts_with("recv_rtp_src_0_") {
|
if pad_name.starts_with("recv_rtp_src_0_") {
|
||||||
if let Some(peer_pad) = pad.peer() {
|
if let Some(peer_pad) = pad.peer() {
|
||||||
if let Some(element) = peer_pad.parent_element() {
|
if let Some(element) = peer_pad.parent_element() {
|
||||||
|
@ -185,9 +192,7 @@ impl JingleSession {
|
||||||
if description.media == "audio" {
|
if description.media == "audio" {
|
||||||
for pt in description.payload_types.iter() {
|
for pt in description.payload_types.iter() {
|
||||||
// We don’t support any static codec, so name MUST be set.
|
// We don’t support any static codec, so name MUST be set.
|
||||||
if let Some(name) = &pt.name {
|
if pt.name.as_deref() == Some("opus") {
|
||||||
match name.as_str() {
|
|
||||||
"opus" => {
|
|
||||||
opus = Some(Codec {
|
opus = Some(Codec {
|
||||||
name: CodecName::Opus,
|
name: CodecName::Opus,
|
||||||
pt: pt.id,
|
pt: pt.id,
|
||||||
|
@ -195,18 +200,13 @@ impl JingleSession {
|
||||||
rtcp_fbs: pt.rtcp_fbs.clone(),
|
rtcp_fbs: pt.rtcp_fbs.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for hdrext in description.hdrexts.iter() {
|
for hdrext in description.hdrexts.iter() {
|
||||||
// TODO: .parse::<u8>() won’t be needed after updating xmpp-parsers, it is now a u16 as
|
|
||||||
// defined in the XEP and related RFC.
|
|
||||||
if hdrext.uri == RTP_HDREXT_SSRC_AUDIO_LEVEL {
|
if hdrext.uri == RTP_HDREXT_SSRC_AUDIO_LEVEL {
|
||||||
audio_hdrext_ssrc_audio_level = Some(hdrext.id.parse::<u8>()?);
|
audio_hdrext_ssrc_audio_level = Some(hdrext.id);
|
||||||
}
|
}
|
||||||
else if hdrext.uri == RTP_HDREXT_TRANSPORT_CC {
|
else if hdrext.uri == RTP_HDREXT_TRANSPORT_CC {
|
||||||
audio_hdrext_transport_cc = Some(hdrext.id.parse::<u8>()?);
|
audio_hdrext_transport_cc = Some(hdrext.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ impl JingleSession {
|
||||||
// TODO: .parse::<u8>() won’t be needed after updating xmpp-parsers, it is now a u16 as
|
// TODO: .parse::<u8>() won’t be needed after updating xmpp-parsers, it is now a u16 as
|
||||||
// defined in the XEP and related RFC.
|
// defined in the XEP and related RFC.
|
||||||
if hdrext.uri == RTP_HDREXT_TRANSPORT_CC {
|
if hdrext.uri == RTP_HDREXT_TRANSPORT_CC {
|
||||||
video_hdrext_transport_cc = Some(hdrext.id.parse::<u8>()?);
|
video_hdrext_transport_cc = Some(hdrext.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -294,9 +294,9 @@ impl JingleSession {
|
||||||
|
|
||||||
debug!("adding ssrc to remote_ssrc_map: {:?}", ssrc);
|
debug!("adding ssrc to remote_ssrc_map: {:?}", ssrc);
|
||||||
remote_ssrc_map.insert(
|
remote_ssrc_map.insert(
|
||||||
ssrc.id.parse()?,
|
ssrc.id,
|
||||||
Source {
|
Source {
|
||||||
ssrc: ssrc.id.parse()?,
|
ssrc: ssrc.id,
|
||||||
participant_id: if owner == "jvb" {
|
participant_id: if owner == "jvb" {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -532,34 +532,34 @@ impl JingleSession {
|
||||||
|
|
||||||
let rtpbin = gstreamer::ElementFactory::make("rtpbin", Some("rtpbin"))?;
|
let rtpbin = gstreamer::ElementFactory::make("rtpbin", Some("rtpbin"))?;
|
||||||
rtpbin.set_property_from_str("rtp-profile", "savpf");
|
rtpbin.set_property_from_str("rtp-profile", "savpf");
|
||||||
rtpbin.set_property("autoremove", true)?;
|
rtpbin.set_property("autoremove", true);
|
||||||
pipeline.add(&rtpbin)?;
|
pipeline.add(&rtpbin)?;
|
||||||
|
|
||||||
let nicesrc = gstreamer::ElementFactory::make("nicesrc", None)?;
|
let nicesrc = gstreamer::ElementFactory::make("nicesrc", None)?;
|
||||||
nicesrc.set_property("stream", ice_stream_id)?;
|
nicesrc.set_property("stream", ice_stream_id);
|
||||||
nicesrc.set_property("component", ice_component_id)?;
|
nicesrc.set_property("component", ice_component_id);
|
||||||
nicesrc.set_property("agent", &ice_agent)?;
|
nicesrc.set_property("agent", &ice_agent);
|
||||||
pipeline.add(&nicesrc)?;
|
pipeline.add(&nicesrc)?;
|
||||||
|
|
||||||
let nicesink = gstreamer::ElementFactory::make("nicesink", None)?;
|
let nicesink = gstreamer::ElementFactory::make("nicesink", None)?;
|
||||||
nicesink.set_property("stream", ice_stream_id)?;
|
nicesink.set_property("stream", ice_stream_id);
|
||||||
nicesink.set_property("component", ice_component_id)?;
|
nicesink.set_property("component", ice_component_id);
|
||||||
nicesink.set_property("agent", &ice_agent)?;
|
nicesink.set_property("agent", &ice_agent);
|
||||||
pipeline.add(&nicesink)?;
|
pipeline.add(&nicesink)?;
|
||||||
|
|
||||||
let dtls_srtp_connection_id = "gst-meet";
|
let dtls_srtp_connection_id = "gst-meet";
|
||||||
|
|
||||||
let dtlssrtpenc = gstreamer::ElementFactory::make("dtlssrtpenc", None)?;
|
let dtlssrtpenc = gstreamer::ElementFactory::make("dtlssrtpenc", None)?;
|
||||||
dtlssrtpenc.set_property("connection-id", dtls_srtp_connection_id)?;
|
dtlssrtpenc.set_property("connection-id", dtls_srtp_connection_id);
|
||||||
dtlssrtpenc.set_property("is-client", true)?;
|
dtlssrtpenc.set_property("is-client", true);
|
||||||
pipeline.add(&dtlssrtpenc)?;
|
pipeline.add(&dtlssrtpenc)?;
|
||||||
|
|
||||||
let dtlssrtpdec = gstreamer::ElementFactory::make("dtlssrtpdec", None)?;
|
let dtlssrtpdec = gstreamer::ElementFactory::make("dtlssrtpdec", None)?;
|
||||||
dtlssrtpdec.set_property("connection-id", dtls_srtp_connection_id)?;
|
dtlssrtpdec.set_property("connection-id", dtls_srtp_connection_id);
|
||||||
dtlssrtpdec.set_property(
|
dtlssrtpdec.set_property(
|
||||||
"pem",
|
"pem",
|
||||||
format!("{}\n{}", dtls_cert_pem, dtls_private_key_pem),
|
format!("{}\n{}", dtls_cert_pem, dtls_private_key_pem),
|
||||||
)?;
|
);
|
||||||
pipeline.add(&dtlssrtpdec)?;
|
pipeline.add(&dtlssrtpdec)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -619,7 +619,7 @@ impl JingleSession {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})?;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let handle = Handle::current();
|
let handle = Handle::current();
|
||||||
|
@ -652,7 +652,7 @@ impl JingleSession {
|
||||||
debug!("jitterbuffer is for remote source: {:?}", source);
|
debug!("jitterbuffer is for remote source: {:?}", source);
|
||||||
if source.media_type == MediaType::Video && source.participant_id.is_some() {
|
if source.media_type == MediaType::Video && source.participant_id.is_some() {
|
||||||
debug!("enabling RTX for ssrc {}", ssrc);
|
debug!("enabling RTX for ssrc {}", ssrc);
|
||||||
rtpjitterbuffer.set_property("do-retransmission", true)?;
|
rtpjitterbuffer.set_property("do-retransmission", true);
|
||||||
}
|
}
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
};
|
};
|
||||||
|
@ -660,17 +660,11 @@ impl JingleSession {
|
||||||
warn!("new-jitterbuffer: {:?}", e);
|
warn!("new-jitterbuffer: {:?}", e);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
})?;
|
});
|
||||||
|
|
||||||
let pts: Vec<(String, u32)> = codecs.iter()
|
let pts: Vec<(String, u32)> = codecs.iter()
|
||||||
.filter(|codec| codec.is_video())
|
.filter(|codec| codec.is_video())
|
||||||
.flat_map(|codec| {
|
.flat_map(|codec| codec.rtx_pt.map(|rtx_pt| (codec.pt.to_string(), rtx_pt as u32)))
|
||||||
if let Some(rtx_pt) = codec.rtx_pt {
|
|
||||||
Some((codec.pt.to_string(), rtx_pt as u32))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
{
|
{
|
||||||
let pts = pts.clone();
|
let pts = pts.clone();
|
||||||
|
@ -686,8 +680,8 @@ impl JingleSession {
|
||||||
ssrc_map = ssrc_map.field(&video_ssrc.to_string(), &(video_rtx_ssrc as u32));
|
ssrc_map = ssrc_map.field(&video_ssrc.to_string(), &(video_rtx_ssrc as u32));
|
||||||
let bin = gstreamer::Bin::new(None);
|
let bin = gstreamer::Bin::new(None);
|
||||||
let rtx_sender = gstreamer::ElementFactory::make("rtprtxsend", None)?;
|
let rtx_sender = gstreamer::ElementFactory::make("rtprtxsend", None)?;
|
||||||
rtx_sender.set_property("payload-type-map", pt_map.build())?;
|
rtx_sender.set_property("payload-type-map", pt_map.build());
|
||||||
rtx_sender.set_property("ssrc-map", ssrc_map.build())?;
|
rtx_sender.set_property("ssrc-map", ssrc_map.build());
|
||||||
bin.add(&rtx_sender)?;
|
bin.add(&rtx_sender)?;
|
||||||
bin.add_pad(&gstreamer::GhostPad::with_target(
|
bin.add_pad(&gstreamer::GhostPad::with_target(
|
||||||
Some(&format!("src_{}", session)),
|
Some(&format!("src_{}", session)),
|
||||||
|
@ -710,7 +704,7 @@ impl JingleSession {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})?;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
rtpbin.connect("request-aux-receiver", false, move |values| {
|
rtpbin.connect("request-aux-receiver", false, move |values| {
|
||||||
|
@ -723,7 +717,7 @@ impl JingleSession {
|
||||||
}
|
}
|
||||||
let bin = gstreamer::Bin::new(None);
|
let bin = gstreamer::Bin::new(None);
|
||||||
let rtx_receiver = gstreamer::ElementFactory::make("rtprtxreceive", None)?;
|
let rtx_receiver = gstreamer::ElementFactory::make("rtprtxreceive", None)?;
|
||||||
rtx_receiver.set_property("payload-type-map", pt_map.build())?;
|
rtx_receiver.set_property("payload-type-map", pt_map.build());
|
||||||
bin.add(&rtx_receiver)?;
|
bin.add(&rtx_receiver)?;
|
||||||
bin.add_pad(&gstreamer::GhostPad::with_target(
|
bin.add_pad(&gstreamer::GhostPad::with_target(
|
||||||
Some(&format!("src_{}", session)),
|
Some(&format!("src_{}", session)),
|
||||||
|
@ -746,7 +740,7 @@ impl JingleSession {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})?;
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
let handle = Handle::current();
|
let handle = Handle::current();
|
||||||
|
@ -759,7 +753,7 @@ impl JingleSession {
|
||||||
let f = || {
|
let f = || {
|
||||||
debug!("rtpbin pad-added {:?}", values);
|
debug!("rtpbin pad-added {:?}", values);
|
||||||
let pad: gstreamer::Pad = values[1].get()?;
|
let pad: gstreamer::Pad = values[1].get()?;
|
||||||
let pad_name: String = pad.property("name")?.get()?;
|
let pad_name: String = pad.property("name");
|
||||||
if pad_name.starts_with("recv_rtp_src_0_") {
|
if pad_name.starts_with("recv_rtp_src_0_") {
|
||||||
let mut parts = pad_name.split('_').skip(4);
|
let mut parts = pad_name.split('_').skip(4);
|
||||||
let ssrc: u32 = parts.next().context("malformed pad name")?.parse()?;
|
let ssrc: u32 = parts.next().context("malformed pad name")?.parse()?;
|
||||||
|
@ -798,9 +792,7 @@ impl JingleSession {
|
||||||
.filter(|codec| codec.is_video())
|
.filter(|codec| codec.is_video())
|
||||||
.find(|codec| codec.is(pt));
|
.find(|codec| codec.is(pt));
|
||||||
if let Some(codec) = codec {
|
if let Some(codec) = codec {
|
||||||
let element = gstreamer::ElementFactory::make(codec.make_depay_name(), None)?;
|
gstreamer::ElementFactory::make(codec.make_depay_name(), None)?
|
||||||
element.set_property("request-keyframe", true)?;
|
|
||||||
element
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
bail!("received video with unsupported PT {}", pt);
|
bail!("received video with unsupported PT {}", pt);
|
||||||
|
@ -808,7 +800,7 @@ impl JingleSession {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
source_element.set_property("auto-header-extension", false)?;
|
source_element.set_property("auto-header-extension", false);
|
||||||
source_element.connect("request-extension", false, move |values| {
|
source_element.connect("request-extension", false, move |values| {
|
||||||
let f = || {
|
let f = || {
|
||||||
let ext_id: u32 = values[1].get()?;
|
let ext_id: u32 = values[1].get()?;
|
||||||
|
@ -817,14 +809,14 @@ impl JingleSession {
|
||||||
let hdrext = RTPHeaderExtension::create_from_uri(&ext_uri)
|
let hdrext = RTPHeaderExtension::create_from_uri(&ext_uri)
|
||||||
.context("failed to create hdrext")?;
|
.context("failed to create hdrext")?;
|
||||||
hdrext.set_id(ext_id);
|
hdrext.set_id(ext_id);
|
||||||
if ext_uri == RTP_HDREXT_SSRC_AUDIO_LEVEL {
|
// if ext_uri == RTP_HDREXT_SSRC_AUDIO_LEVEL {
|
||||||
}
|
// }
|
||||||
else if ext_uri == RTP_HDREXT_TRANSPORT_CC {
|
// else if ext_uri == RTP_HDREXT_TRANSPORT_CC {
|
||||||
// hdrext.set_property("n-streams", 2u32)?;
|
// // hdrext.set_property("n-streams", 2u32);
|
||||||
}
|
// }
|
||||||
else {
|
// else {
|
||||||
bail!("unknown rtp hdrext: {}", ext_uri);
|
// bail!("unknown rtp hdrext: {}", ext_uri);
|
||||||
};
|
// };
|
||||||
Ok::<_, anyhow::Error>(hdrext)
|
Ok::<_, anyhow::Error>(hdrext)
|
||||||
};
|
};
|
||||||
match f() {
|
match f() {
|
||||||
|
@ -834,7 +826,8 @@ impl JingleSession {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})?;
|
});
|
||||||
|
|
||||||
pipeline
|
pipeline
|
||||||
.add(&source_element)
|
.add(&source_element)
|
||||||
.context("failed to add depayloader to pipeline")?;
|
.context("failed to add depayloader to pipeline")?;
|
||||||
|
@ -901,21 +894,21 @@ impl JingleSession {
|
||||||
error!("handling pad-added: {:?}", e);
|
error!("handling pad-added: {:?}", e);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
})?;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let opus = codecs.iter().find(|codec| codec.name == CodecName::Opus);
|
let opus = codecs.iter().find(|codec| codec.name == CodecName::Opus);
|
||||||
let audio_sink_element = if let Some(opus) = opus {
|
let audio_sink_element = if let Some(opus) = opus {
|
||||||
let audio_sink_element = gstreamer::ElementFactory::make(opus.make_pay_name(), None)?;
|
let audio_sink_element = gstreamer::ElementFactory::make(opus.make_pay_name(), None)?;
|
||||||
audio_sink_element.set_property("pt", opus.pt as u32)?;
|
audio_sink_element.set_property("pt", opus.pt as u32);
|
||||||
audio_sink_element
|
audio_sink_element
|
||||||
} else {
|
} else {
|
||||||
bail!("no opus payload type in jingle session-initiate");
|
bail!("no opus payload type in jingle session-initiate");
|
||||||
};
|
};
|
||||||
audio_sink_element.set_property("min-ptime", 10i64 * 1000 * 1000)?;
|
audio_sink_element.set_property("min-ptime", 10i64 * 1000 * 1000);
|
||||||
audio_sink_element.set_property("ssrc", audio_ssrc)?;
|
audio_sink_element.set_property("ssrc", audio_ssrc);
|
||||||
if audio_sink_element.has_property("auto-header-extension", None) {
|
if audio_sink_element.has_property("auto-header-extension", None) {
|
||||||
audio_sink_element.set_property("auto-header-extension", false)?;
|
audio_sink_element.set_property("auto-header-extension", false);
|
||||||
audio_sink_element.connect("request-extension", false, move |values| {
|
audio_sink_element.connect("request-extension", false, move |values| {
|
||||||
let f = || {
|
let f = || {
|
||||||
let ext_id: u32 = values[1].get()?;
|
let ext_id: u32 = values[1].get()?;
|
||||||
|
@ -927,14 +920,14 @@ impl JingleSession {
|
||||||
let hdrext =
|
let hdrext =
|
||||||
RTPHeaderExtension::create_from_uri(&ext_uri).context("failed to create hdrext")?;
|
RTPHeaderExtension::create_from_uri(&ext_uri).context("failed to create hdrext")?;
|
||||||
hdrext.set_id(ext_id);
|
hdrext.set_id(ext_id);
|
||||||
if ext_uri == RTP_HDREXT_SSRC_AUDIO_LEVEL {
|
// if ext_uri == RTP_HDREXT_SSRC_AUDIO_LEVEL {
|
||||||
}
|
// }
|
||||||
else if ext_uri == RTP_HDREXT_TRANSPORT_CC {
|
// else if ext_uri == RTP_HDREXT_TRANSPORT_CC {
|
||||||
// hdrext.set_property("n-streams", 2u32)?;
|
// // hdrext.set_property("n-streams", 2u32);
|
||||||
}
|
// }
|
||||||
else {
|
// else {
|
||||||
bail!("unknown rtp hdrext: {}", ext_uri);
|
// bail!("unknown rtp hdrext: {}", ext_uri);
|
||||||
}
|
// }
|
||||||
Ok::<_, anyhow::Error>(hdrext)
|
Ok::<_, anyhow::Error>(hdrext)
|
||||||
};
|
};
|
||||||
match f() {
|
match f() {
|
||||||
|
@ -944,7 +937,7 @@ impl JingleSession {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})?;
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
debug!("audio payloader: no rtp header extension support");
|
debug!("audio payloader: no rtp header extension support");
|
||||||
|
@ -955,7 +948,7 @@ impl JingleSession {
|
||||||
let codec = codecs.iter().find(|codec| codec.is_codec(codec_name));
|
let codec = codecs.iter().find(|codec| codec.is_codec(codec_name));
|
||||||
let video_sink_element = if let Some(codec) = codec {
|
let video_sink_element = if let Some(codec) = codec {
|
||||||
let element = gstreamer::ElementFactory::make(codec.make_pay_name(), None)?;
|
let element = gstreamer::ElementFactory::make(codec.make_pay_name(), None)?;
|
||||||
element.set_property("pt", codec.pt as u32)?;
|
element.set_property("pt", codec.pt as u32);
|
||||||
if codec.name == CodecName::H264 {
|
if codec.name == CodecName::H264 {
|
||||||
element.set_property_from_str("aggregate-mode", "zero-latency");
|
element.set_property_from_str("aggregate-mode", "zero-latency");
|
||||||
}
|
}
|
||||||
|
@ -967,9 +960,9 @@ impl JingleSession {
|
||||||
else {
|
else {
|
||||||
bail!("unsupported video codec: {}", codec_name);
|
bail!("unsupported video codec: {}", codec_name);
|
||||||
};
|
};
|
||||||
video_sink_element.set_property("ssrc", video_ssrc)?;
|
video_sink_element.set_property("ssrc", video_ssrc);
|
||||||
if video_sink_element.has_property("auto-header-extension", None) {
|
if video_sink_element.has_property("auto-header-extension", None) {
|
||||||
video_sink_element.set_property("auto-header-extension", false)?;
|
video_sink_element.set_property("auto-header-extension", false);
|
||||||
video_sink_element.connect("request-extension", false, move |values| {
|
video_sink_element.connect("request-extension", false, move |values| {
|
||||||
let f = || {
|
let f = || {
|
||||||
let ext_id: u32 = values[1].get()?;
|
let ext_id: u32 = values[1].get()?;
|
||||||
|
@ -982,7 +975,7 @@ impl JingleSession {
|
||||||
RTPHeaderExtension::create_from_uri(&ext_uri).context("failed to create hdrext")?;
|
RTPHeaderExtension::create_from_uri(&ext_uri).context("failed to create hdrext")?;
|
||||||
hdrext.set_id(ext_id);
|
hdrext.set_id(ext_id);
|
||||||
if ext_uri == RTP_HDREXT_TRANSPORT_CC {
|
if ext_uri == RTP_HDREXT_TRANSPORT_CC {
|
||||||
// hdrext.set_property("n-streams", 2u32)?;
|
// hdrext.set_property("n-streams", 2u32);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
bail!("unknown rtp hdrext: {}", ext_uri);
|
bail!("unknown rtp hdrext: {}", ext_uri);
|
||||||
|
@ -996,7 +989,7 @@ impl JingleSession {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})?;
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
debug!("video payloader: no rtp header extension support");
|
debug!("video payloader: no rtp header extension support");
|
||||||
|
@ -1134,12 +1127,12 @@ impl JingleSession {
|
||||||
});
|
});
|
||||||
|
|
||||||
description.ssrcs = if initiate_content.name.0 == "audio" {
|
description.ssrcs = if initiate_content.name.0 == "audio" {
|
||||||
vec![jingle_ssma::Source::new(audio_ssrc.to_string())]
|
vec![jingle_ssma::Source::new(audio_ssrc)]
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
vec![
|
vec![
|
||||||
jingle_ssma::Source::new(video_ssrc.to_string()),
|
jingle_ssma::Source::new(video_ssrc),
|
||||||
jingle_ssma::Source::new(video_rtx_ssrc.to_string()),
|
jingle_ssma::Source::new(video_rtx_ssrc),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1159,10 +1152,10 @@ impl JingleSession {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
vec![jingle_ssma::Group {
|
vec![jingle_ssma::Group {
|
||||||
semantics: "FID".to_owned(),
|
semantics: Semantics::Fid,
|
||||||
sources: vec![
|
sources: vec![
|
||||||
jingle_ssma::Source::new(video_ssrc.to_string()),
|
jingle_ssma::Source::new(video_ssrc),
|
||||||
jingle_ssma::Source::new(video_rtx_ssrc.to_string()),
|
jingle_ssma::Source::new(video_rtx_ssrc),
|
||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
@ -1170,13 +1163,13 @@ impl JingleSession {
|
||||||
if initiate_content.name.0 == "audio" {
|
if initiate_content.name.0 == "audio" {
|
||||||
if let Some(hdrext) = audio_hdrext_ssrc_audio_level {
|
if let Some(hdrext) = audio_hdrext_ssrc_audio_level {
|
||||||
description.hdrexts.push(RtpHdrext::new(
|
description.hdrexts.push(RtpHdrext::new(
|
||||||
hdrext.to_string(),
|
hdrext,
|
||||||
RTP_HDREXT_SSRC_AUDIO_LEVEL.to_owned(),
|
RTP_HDREXT_SSRC_AUDIO_LEVEL.to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(hdrext) = audio_hdrext_transport_cc {
|
if let Some(hdrext) = audio_hdrext_transport_cc {
|
||||||
description.hdrexts.push(RtpHdrext::new(
|
description.hdrexts.push(RtpHdrext::new(
|
||||||
hdrext.to_string(),
|
hdrext,
|
||||||
RTP_HDREXT_TRANSPORT_CC.to_owned(),
|
RTP_HDREXT_TRANSPORT_CC.to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1184,7 +1177,7 @@ impl JingleSession {
|
||||||
else if initiate_content.name.0 == "video" {
|
else if initiate_content.name.0 == "video" {
|
||||||
if let Some(hdrext) = video_hdrext_transport_cc {
|
if let Some(hdrext) = video_hdrext_transport_cc {
|
||||||
description.hdrexts.push(RtpHdrext::new(
|
description.hdrexts.push(RtpHdrext::new(
|
||||||
hdrext.to_string(),
|
hdrext,
|
||||||
RTP_HDREXT_TRANSPORT_CC.to_owned(),
|
RTP_HDREXT_TRANSPORT_CC.to_owned(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1194,7 +1187,7 @@ impl JingleSession {
|
||||||
hash: Algo::Sha_256,
|
hash: Algo::Sha_256,
|
||||||
setup: Some(Setup::Active),
|
setup: Some(Setup::Active),
|
||||||
value: fingerprint.clone(),
|
value: fingerprint.clone(),
|
||||||
required: Some(true.to_string()),
|
// required: Some(true.to_string()),
|
||||||
});
|
});
|
||||||
transport.ufrag = Some(ice_local_ufrag.clone());
|
transport.ufrag = Some(ice_local_ufrag.clone());
|
||||||
transport.pwd = Some(ice_local_pwd.clone());
|
transport.pwd = Some(ice_local_pwd.clone());
|
||||||
|
@ -1233,10 +1226,10 @@ impl JingleSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
jingle_accept = jingle_accept.set_group(jingle_grouping::Group {
|
jingle_accept = jingle_accept.set_group(jingle_grouping::Group {
|
||||||
semantics: Semantics::Bundle,
|
semantics: jingle_grouping::Semantics::Bundle,
|
||||||
contents: vec![
|
contents: vec![
|
||||||
jingle_grouping::Content::new("video"),
|
GroupContent::new("video"),
|
||||||
jingle_grouping::Content::new("audio"),
|
GroupContent::new("audio"),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1272,9 +1265,9 @@ impl JingleSession {
|
||||||
|
|
||||||
debug!("adding ssrc to remote_ssrc_map: {:?}", ssrc);
|
debug!("adding ssrc to remote_ssrc_map: {:?}", ssrc);
|
||||||
self.remote_ssrc_map.insert(
|
self.remote_ssrc_map.insert(
|
||||||
ssrc.id.parse()?,
|
ssrc.id,
|
||||||
Source {
|
Source {
|
||||||
ssrc: ssrc.id.parse()?,
|
ssrc: ssrc.id,
|
||||||
participant_id: if owner == "jvb" {
|
participant_id: if owner == "jvb" {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ use futures::{
|
||||||
sink::{Sink, SinkExt},
|
sink::{Sink, SinkExt},
|
||||||
stream::{Stream, StreamExt, TryStreamExt},
|
stream::{Stream, StreamExt, TryStreamExt},
|
||||||
};
|
};
|
||||||
|
use rand::{RngCore, thread_rng};
|
||||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tokio_tungstenite::tungstenite::{
|
use tokio_tungstenite::tungstenite::{
|
||||||
|
@ -76,8 +77,15 @@ impl Connection {
|
||||||
let xmpp_domain: BareJid = xmpp_domain.parse().context("invalid XMPP domain")?;
|
let xmpp_domain: BareJid = xmpp_domain.parse().context("invalid XMPP domain")?;
|
||||||
|
|
||||||
info!("Connecting XMPP WebSocket to {}", websocket_url);
|
info!("Connecting XMPP WebSocket to {}", websocket_url);
|
||||||
let request = Request::get(websocket_url)
|
let mut key = [0u8; 16];
|
||||||
.header("Sec-Websocket-Protocol", "xmpp")
|
thread_rng().fill_bytes(&mut key);
|
||||||
|
let request = Request::get(&websocket_url)
|
||||||
|
.header("sec-websocket-protocol", "xmpp")
|
||||||
|
.header("sec-websocket-key", base64::encode(&key))
|
||||||
|
.header("sec-websocket-version", "13")
|
||||||
|
.header("host", websocket_url.host().context("invalid WebSocket URL: missing host")?)
|
||||||
|
.header("connection", "Upgrade")
|
||||||
|
.header("upgrade", "websocket")
|
||||||
.body(())
|
.body(())
|
||||||
.context("failed to build WebSocket request")?;
|
.context("failed to build WebSocket request")?;
|
||||||
let (websocket, _response) = tokio_tungstenite::connect_async(request)
|
let (websocket, _response) = tokio_tungstenite::connect_async(request)
|
||||||
|
|
|
@ -50,11 +50,11 @@ name = "nice_sys"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libc = { version = "0.2", default-features = false }
|
libc = { version = "0.2", default-features = false }
|
||||||
glib = { version = "0.14", default-features = false }
|
glib = { version = "0.15", default-features = false }
|
||||||
gio = { version = "0.14", default-features = false }
|
gio = { version = "0.15", default-features = false }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
system-deps = { version = "3", default-features = false }
|
system-deps = { version = "6", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
shell-words = { version = "1", default-features = false }
|
shell-words = { version = "1", default-features = false }
|
||||||
|
|
|
@ -8,10 +8,10 @@ authors = ["Jasper Hugo <jasper@avstack.io>"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = { version = "1", default-features = false, optional = true }
|
bitflags = { version = "1", default-features = false, optional = true }
|
||||||
glib = { version = "0.14", default-features = false }
|
glib = { version = "0.15", default-features = false }
|
||||||
libc = { version = "0.2", default-features = false }
|
libc = { version = "0.2", default-features = false }
|
||||||
nice-gst-meet-sys = { version = "0.1", path = "../nice-gst-meet-sys", default-features = false }
|
nice-gst-meet-sys = { version = "0.1", path = "../nice-gst-meet-sys", default-features = false }
|
||||||
nix = { version = "0.22", default-features = false }
|
nix = { version = "0.23", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
v0_1_4 = ["nice-gst-meet-sys/v0_1_4"]
|
v0_1_4 = ["nice-gst-meet-sys/v0_1_4"]
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
boxed::Box as Box_,
|
boxed::Box as Box_,
|
||||||
fmt,
|
fmt,
|
||||||
mem::{self, transmute},
|
mem::transmute,
|
||||||
ptr, slice,
|
ptr, slice,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ extern "C" fn attach_recv_cb(
|
||||||
user_data: gpointer,
|
user_data: gpointer,
|
||||||
) {
|
) {
|
||||||
if !user_data.is_null() {
|
if !user_data.is_null() {
|
||||||
let closure: &mut Box<dyn FnMut(Agent, u32, u32, &str)> = unsafe { mem::transmute(user_data) };
|
let closure: &mut Box<dyn FnMut(Agent, u32, u32, &str)> = unsafe { &mut *(user_data as *mut _) };
|
||||||
let slice = unsafe { slice::from_raw_parts(data, len as usize) };
|
let slice = unsafe { slice::from_raw_parts(data, len as usize) };
|
||||||
let bytes: Vec<_> = slice.iter().map(|b| *b as u8).collect();
|
let bytes: Vec<_> = slice.iter().map(|b| *b as u8).collect();
|
||||||
if let Ok(s) = std::str::from_utf8(&bytes) {
|
if let Ok(s) = std::str::from_utf8(&bytes) {
|
||||||
|
@ -419,6 +419,7 @@ impl Agent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[doc(alias = "nice_agent_set_relay_info")]
|
#[doc(alias = "nice_agent_set_relay_info")]
|
||||||
pub fn set_relay_info(
|
pub fn set_relay_info(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -47,7 +47,7 @@ impl<'a> ToGlibPtr<'a, *mut ffi::NiceCandidate> for Candidate {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn to_glib_none(&'a self) -> Stash<'a, *mut ffi::NiceCandidate, Self> {
|
fn to_glib_none(&'a self) -> Stash<'a, *mut ffi::NiceCandidate, Self> {
|
||||||
Stash(&*self.0 as *const _ as *mut _, self)
|
Stash(&*self.inner as *const _ as *mut _, self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,26 +58,26 @@ impl Candidate {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn type_(&self) -> CandidateType {
|
pub fn type_(&self) -> CandidateType {
|
||||||
unsafe { CandidateType::from_glib(self.0.type_) }
|
unsafe { CandidateType::from_glib(self.inner.type_) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "v0_1_18", feature = "dox"))]
|
#[cfg(any(feature = "v0_1_18", feature = "dox"))]
|
||||||
#[cfg_attr(feature = "dox", doc(cfg(feature = "v0_1_18")))]
|
#[cfg_attr(feature = "dox", doc(cfg(feature = "v0_1_18")))]
|
||||||
pub fn transport(&self) -> CandidateTransport {
|
pub fn transport(&self) -> CandidateTransport {
|
||||||
unsafe { CandidateTransport::from_glib(self.0.transport) }
|
unsafe { CandidateTransport::from_glib(self.inner.transport) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "v0_1_18", feature = "dox"))]
|
#[cfg(any(feature = "v0_1_18", feature = "dox"))]
|
||||||
#[cfg_attr(feature = "dox", doc(cfg(feature = "v0_1_18")))]
|
#[cfg_attr(feature = "dox", doc(cfg(feature = "v0_1_18")))]
|
||||||
pub fn set_transport(&mut self, transport: CandidateTransport) {
|
pub fn set_transport(&mut self, transport: CandidateTransport) {
|
||||||
self.0.transport = transport.into_glib();
|
self.inner.transport = transport.into_glib();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addr(&self) -> SocketAddr {
|
pub fn addr(&self) -> SocketAddr {
|
||||||
unsafe {
|
unsafe {
|
||||||
match AddressFamily::from_i32(self.0.addr.s.addr.sa_family as i32).unwrap() {
|
match AddressFamily::from_i32(self.inner.addr.s.addr.sa_family as i32).unwrap() {
|
||||||
AddressFamily::Inet => InetAddr::V4(self.0.addr.s.ip4).to_std(),
|
AddressFamily::Inet => InetAddr::V4(self.inner.addr.s.ip4).to_std(),
|
||||||
AddressFamily::Inet6 => InetAddr::V6(self.0.addr.s.ip6).to_std(),
|
AddressFamily::Inet6 => InetAddr::V6(self.inner.addr.s.ip6).to_std(),
|
||||||
other => panic!("unsupported address family: {:?}", other),
|
other => panic!("unsupported address family: {:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,21 +87,21 @@ impl Candidate {
|
||||||
match InetAddr::from_std(&addr) {
|
match InetAddr::from_std(&addr) {
|
||||||
InetAddr::V4(ip4) => unsafe {
|
InetAddr::V4(ip4) => unsafe {
|
||||||
ffi::nice_address_set_ipv4(
|
ffi::nice_address_set_ipv4(
|
||||||
&mut self.0.addr as *mut _,
|
&mut self.inner.addr as *mut _,
|
||||||
u32::from_be(ip4.sin_addr.s_addr),
|
u32::from_be(ip4.sin_addr.s_addr),
|
||||||
);
|
);
|
||||||
ffi::nice_address_set_port(
|
ffi::nice_address_set_port(
|
||||||
&mut self.0.addr as *mut _,
|
&mut self.inner.addr as *mut _,
|
||||||
u16::from_be(ip4.sin_port) as u32,
|
u16::from_be(ip4.sin_port) as u32,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
InetAddr::V6(ip6) => unsafe {
|
InetAddr::V6(ip6) => unsafe {
|
||||||
ffi::nice_address_set_ipv6(
|
ffi::nice_address_set_ipv6(
|
||||||
&mut self.0.addr as *mut _,
|
&mut self.inner.addr as *mut _,
|
||||||
&ip6.sin6_addr.s6_addr as *const _,
|
&ip6.sin6_addr.s6_addr as *const _,
|
||||||
);
|
);
|
||||||
ffi::nice_address_set_port(
|
ffi::nice_address_set_port(
|
||||||
&mut self.0.addr as *mut _,
|
&mut self.inner.addr as *mut _,
|
||||||
u16::from_be(ip6.sin6_port) as u32,
|
u16::from_be(ip6.sin6_port) as u32,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -109,31 +109,31 @@ impl Candidate {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn priority(&self) -> u32 {
|
pub fn priority(&self) -> u32 {
|
||||||
self.0.priority
|
self.inner.priority
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_priority(&mut self, priority: u32) {
|
pub fn set_priority(&mut self, priority: u32) {
|
||||||
self.0.priority = priority;
|
self.inner.priority = priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stream_id(&self) -> u32 {
|
pub fn stream_id(&self) -> u32 {
|
||||||
self.0.stream_id
|
self.inner.stream_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_stream_id(&mut self, stream_id: u32) {
|
pub fn set_stream_id(&mut self, stream_id: u32) {
|
||||||
self.0.stream_id = stream_id;
|
self.inner.stream_id = stream_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn component_id(&self) -> u32 {
|
pub fn component_id(&self) -> u32 {
|
||||||
self.0.component_id
|
self.inner.component_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_component_id(&mut self, component_id: u32) {
|
pub fn set_component_id(&mut self, component_id: u32) {
|
||||||
self.0.component_id = component_id;
|
self.inner.component_id = component_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn foundation(&self) -> Result<&str, std::str::Utf8Error> {
|
pub fn foundation(&self) -> Result<&str, std::str::Utf8Error> {
|
||||||
unsafe { CStr::from_ptr(&self.0.foundation as *const c_char).to_str() }
|
unsafe { CStr::from_ptr(&self.inner.foundation as *const c_char).to_str() }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_foundation(&mut self, foundation: &str) {
|
pub fn set_foundation(&mut self, foundation: &str) {
|
||||||
|
@ -144,33 +144,33 @@ impl Candidate {
|
||||||
.map(|c| *c as c_char)
|
.map(|c| *c as c_char)
|
||||||
.collect();
|
.collect();
|
||||||
bytes.resize(33, 0);
|
bytes.resize(33, 0);
|
||||||
self.0.foundation.copy_from_slice(&bytes);
|
self.inner.foundation.copy_from_slice(&bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn username(&self) -> Result<&str, std::str::Utf8Error> {
|
pub fn username(&self) -> Result<&str, std::str::Utf8Error> {
|
||||||
if self.0.username.is_null() {
|
if self.inner.username.is_null() {
|
||||||
Ok("")
|
Ok("")
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
unsafe { CStr::from_ptr(self.0.username).to_str() }
|
unsafe { CStr::from_ptr(self.inner.username).to_str() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_username(&mut self, username: &str) {
|
pub fn set_username(&mut self, username: &str) {
|
||||||
self.0.username = username.to_owned().to_glib_full();
|
self.inner.username = username.to_owned().to_glib_full();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn password(&self) -> Result<&str, std::str::Utf8Error> {
|
pub fn password(&self) -> Result<&str, std::str::Utf8Error> {
|
||||||
if self.0.password.is_null() {
|
if self.inner.password.is_null() {
|
||||||
Ok("")
|
Ok("")
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
unsafe { CStr::from_ptr(self.0.password).to_str() }
|
unsafe { CStr::from_ptr(self.inner.password).to_str() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_password(&mut self, password: &str) {
|
pub fn set_password(&mut self, password: &str) {
|
||||||
self.0.password = password.to_owned().to_glib_full();
|
self.inner.password = password.to_owned().to_glib_full();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "v0_1_15", feature = "dox"))]
|
#[cfg(any(feature = "v0_1_15", feature = "dox"))]
|
||||||
|
|
73
shell.nix
73
shell.nix
|
@ -1,71 +1,14 @@
|
||||||
with import <nixpkgs> {};
|
with import <nixpkgs> {};
|
||||||
let
|
let
|
||||||
meson-patched = meson.overridePythonAttrs(old: rec {
|
libnice-patched = libnice.overrideAttrs(old: rec {
|
||||||
version = "0.59.4";
|
|
||||||
src = pythonPackages.fetchPypi {
|
|
||||||
inherit version;
|
|
||||||
pname = old.pname;
|
|
||||||
sha256 = "a77988cc50554f73ede075bc9bf77a2d7ecb6ff892f2a0180d4940920eaaec84";
|
|
||||||
};
|
|
||||||
patches = builtins.filter (patch: baseNameOf patch != "gir-fallback-path.patch") old.patches;
|
|
||||||
});
|
|
||||||
gstreamer = (gst_all_1.gstreamer.override {
|
|
||||||
meson = meson-patched;
|
|
||||||
}).overrideAttrs(old: rec {
|
|
||||||
version = "1.19.3";
|
|
||||||
src = fetchurl {
|
|
||||||
url = "https://gstreamer.freedesktop.org/src/${old.pname}/${old.pname}-${version}.tar.xz";
|
|
||||||
sha256 = "906d7d4bf92f941586c0cbce717d9cad6aac36994e16fa6f2f153e07e3221bca";
|
|
||||||
};
|
|
||||||
patches = [];
|
|
||||||
mesonFlags = old.mesonFlags ++ ["-Dorc=disabled"];
|
|
||||||
});
|
|
||||||
gst-plugins-base = (gst_all_1.gst-plugins-base.override {
|
|
||||||
meson = meson-patched;
|
|
||||||
gstreamer = gstreamer;
|
|
||||||
}).overrideAttrs(old: rec {
|
|
||||||
version = "1.19.3";
|
|
||||||
src = fetchurl {
|
|
||||||
url = "https://gstreamer.freedesktop.org/src/${old.pname}/${old.pname}-${version}.tar.xz";
|
|
||||||
sha256 = "e277f198623a26c1b0a1e19734656392e9368bebf3677cd94262a1316a960827";
|
|
||||||
};
|
|
||||||
patches = [];
|
|
||||||
mesonFlags = old.mesonFlags ++ ["-Dorc=disabled"];
|
|
||||||
});
|
|
||||||
gst-plugins-good = (gst_all_1.gst-plugins-good.override {
|
|
||||||
meson = meson-patched;
|
|
||||||
gst-plugins-base = gst-plugins-base;
|
|
||||||
}).overrideAttrs(old: rec {
|
|
||||||
version = "1.19.3";
|
|
||||||
src = fetchurl {
|
|
||||||
url = "https://gstreamer.freedesktop.org/src/${old.pname}/${old.pname}-${version}.tar.xz";
|
|
||||||
sha256 = "79ea32a77fa47e6596530e38113bf97c113fd95658087d9a91ffb8af47d11d07";
|
|
||||||
};
|
|
||||||
patches = [];
|
|
||||||
mesonFlags = old.mesonFlags ++ ["-Dorc=disabled"];
|
|
||||||
});
|
|
||||||
gst-plugins-bad = (gst_all_1.gst-plugins-bad.override {
|
|
||||||
meson = meson-patched;
|
|
||||||
gst-plugins-base = gst-plugins-base;
|
|
||||||
}).overrideAttrs(old: rec {
|
|
||||||
version = "1.19.3";
|
|
||||||
src = fetchurl {
|
|
||||||
url = "https://gstreamer.freedesktop.org/src/${old.pname}/${old.pname}-${version}.tar.xz";
|
|
||||||
sha256 = "50193a23b13713ccb32ee5d1852faeeaed29b91f8398285acdfd522fa3e16835";
|
|
||||||
};
|
|
||||||
patches = [];
|
|
||||||
mesonFlags = old.mesonFlags ++ ["-Dorc=disabled" "-Dgs=disabled" "-Disac=disabled" "-Dldac=disabled" "-Donnx=disabled" "-Dopenaptx=disabled" "-Dqroverlay=disabled" "-Dtests=disabled" "-Dfaad=disabled" "-Dmpeg2enc=disabled" "-Dmplex=disabled" "-Dresindvd=disabled" "-Dx265=disabled"];
|
|
||||||
});
|
|
||||||
libnice-patched = (libnice.override {
|
|
||||||
meson = meson-patched;
|
|
||||||
}).overrideAttrs(old: rec {
|
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
gstreamer
|
gst_all_1.gstreamer
|
||||||
gst-plugins-base
|
gst_all_1.gst-plugins-base
|
||||||
openssl
|
openssl
|
||||||
];
|
];
|
||||||
outputs = [ "bin" "out" "dev" ];
|
outputs = [ "bin" "out" "dev" ];
|
||||||
mesonFlags = old.mesonFlags ++ ["-Dgupnp=disabled" "-Dgtk_doc=disabled"];
|
mesonFlags = old.mesonFlags ++ ["-Dgupnp=disabled" "-Dgtk_doc=disabled"];
|
||||||
|
meta.platforms = lib.platforms.unix;
|
||||||
});
|
});
|
||||||
in
|
in
|
||||||
mkShell {
|
mkShell {
|
||||||
|
@ -76,10 +19,10 @@ mkShell {
|
||||||
openssl
|
openssl
|
||||||
glib
|
glib
|
||||||
glib-networking
|
glib-networking
|
||||||
gstreamer
|
gst_all_1.gstreamer
|
||||||
gst-plugins-base
|
gst_all_1.gst-plugins-base
|
||||||
gst-plugins-good
|
gst_all_1.gst-plugins-good
|
||||||
gst-plugins-bad
|
gst_all_1.gst-plugins-bad
|
||||||
libnice-patched
|
libnice-patched
|
||||||
] ++ (if stdenv.isDarwin then [
|
] ++ (if stdenv.isDarwin then [
|
||||||
darwin.apple_sdk.frameworks.AppKit
|
darwin.apple_sdk.frameworks.AppKit
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "xmpp-parsers-gst-meet"
|
|
||||||
version = "0.18.2"
|
|
||||||
authors = [
|
|
||||||
"Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>",
|
|
||||||
"Maxime “pep” Buquet <pep@bouah.net>",
|
|
||||||
]
|
|
||||||
description = "Collection of parsers and serialisers for XMPP extensions"
|
|
||||||
homepage = "https://gitlab.com/xmpp-rs/xmpp-rs"
|
|
||||||
repository = "https://gitlab.com/xmpp-rs/xmpp-rs"
|
|
||||||
keywords = ["xmpp", "jabber", "xml"]
|
|
||||||
categories = ["parsing", "network-programming"]
|
|
||||||
license = "MPL-2.0"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
minidom = { package = "minidom-gst-meet", version = "0.13" }
|
|
||||||
jid = { package = "jid-gst-meet", version = "0.9", features = ["minidom"] }
|
|
||||||
base64 = "0.13"
|
|
||||||
digest = "0.9"
|
|
||||||
sha-1 = "0.9"
|
|
||||||
sha2 = "0.9"
|
|
||||||
sha3 = "0.9"
|
|
||||||
blake2 = "0.9"
|
|
||||||
chrono = { version = "0.4.5", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
# Build xmpp-parsers to make components instead of clients.
|
|
||||||
component = []
|
|
||||||
# Disable validation of unknown attributes.
|
|
||||||
disable-validation = []
|
|
||||||
serde = ["jid/serde"]
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
rustdoc-args = [ "--sort-modules-by-appearance", "-Zunstable-options" ]
|
|
|
@ -1,365 +0,0 @@
|
||||||
Version 0.18.0:
|
|
||||||
2021-01-13 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Bugfixes:
|
|
||||||
- Bump minidom to 0.13, as 0.12.1 got yanked.
|
|
||||||
|
|
||||||
Version 0.18.0:
|
|
||||||
2021-01-13 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- Jingle Raw UDP Transport Method (XEP-0177).
|
|
||||||
- Jingle RTP Header Extensions Negotiation (XEP-0294).
|
|
||||||
- Jingle Grouping Framework (XEP-0338).
|
|
||||||
- Mediated Information eXchange (MIX) (XEP-0369).
|
|
||||||
* Improvements:
|
|
||||||
- Everything is now PartialEq!
|
|
||||||
- Add "serde" feature to enable "jid/serde".
|
|
||||||
- Implement more of XEP-0060.
|
|
||||||
- Bump XEP-0167 to version 1.2.0, adding rtcp-mux.
|
|
||||||
- Bump XEP-0176 to version 1.1, fixing interoperability with other
|
|
||||||
clients.
|
|
||||||
- Bump XEP-0402 to version 1.1.1, bumping its namespace and adding
|
|
||||||
support for extension data.
|
|
||||||
- Bump all dependencies to their latest version.
|
|
||||||
- Some more helper constructors.
|
|
||||||
- Make public some stuff that should have been public from the very
|
|
||||||
beginning.
|
|
||||||
* Bugfixes:
|
|
||||||
- Jingle::set_reason() does what it says now (copy/paste error).
|
|
||||||
- Bookmarks’ names are now optional like they should.
|
|
||||||
|
|
||||||
Version 0.17.0:
|
|
||||||
2020-02-15 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>, Maxime “pep” Buquet <pep@bouah.net>, Paul Fariello <paul@fariello.eu>
|
|
||||||
* Improvements:
|
|
||||||
- Add serialization tests where possible
|
|
||||||
- Use minidom's NSChoice API for Jingle parser
|
|
||||||
- Remove NamespaceAwareCompare. Move to minidom
|
|
||||||
* Breaking changes:
|
|
||||||
- Prevent generate_serializer macro from adding another layer of Node.
|
|
||||||
Fixes some serializers.
|
|
||||||
- ecaps2: Use the Error type instead of ()
|
|
||||||
|
|
||||||
Version 0.16.0:
|
|
||||||
2019-10-15 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- Client Certificate Management for SASL EXTERNAL (XEP-0257)
|
|
||||||
- JID Prep (XEP-0328)
|
|
||||||
- Client State Indication (XEP-0352)
|
|
||||||
- OpenPGP for XMPP (XEP-0373)
|
|
||||||
- Bookmarks 2 (This Time it's Serious) (XEP-0402)
|
|
||||||
- Anonymous unique occupant identifiers for MUCs (XEP-0421)
|
|
||||||
- Source-Specific Media Attributes in Jingle (XEP-0339)
|
|
||||||
- Jingle RTP Feedback Negotiation (XEP-0293)
|
|
||||||
* Breaking changes:
|
|
||||||
- Presence constructors now take Into<Jid> and assume Some.
|
|
||||||
* Improvements:
|
|
||||||
- CI: refactor, add caching
|
|
||||||
- Update jid-rs to 0.8
|
|
||||||
|
|
||||||
Version 0.15.0:
|
|
||||||
2019-09-06 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- XHTML-IM (XEP-0071)
|
|
||||||
- User Tune (XEP-0118)
|
|
||||||
- Bits of Binary (XEP-0231)
|
|
||||||
- Message Carbons (XEP-0280)
|
|
||||||
* Breaking changes:
|
|
||||||
- Stop reexporting TryFrom and TryInto, they are available in
|
|
||||||
std::convert nowadays.
|
|
||||||
- Bind has been split into BindQuery and BindResponse.
|
|
||||||
* Improvements:
|
|
||||||
- New DOAP file for a machine-readable description of the features.
|
|
||||||
- Add various parser and formatter helpers on Hash.
|
|
||||||
|
|
||||||
Version 0.14.0:
|
|
||||||
2019-07-13 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>, Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- Entity Time (XEP-0202).
|
|
||||||
* Improvements:
|
|
||||||
- Microblog NS (XEP-0227).
|
|
||||||
- Update jid-rs dependency with jid split change (Jid, FullJid,
|
|
||||||
BareJid) and reexport them.
|
|
||||||
- Fix rustdoc options in Cargo.toml for docs.rs
|
|
||||||
* Breaking changes:
|
|
||||||
- Presence's show attribute is now Option<Show> and Show::None is no
|
|
||||||
more.
|
|
||||||
|
|
||||||
Version 0.13.1:
|
|
||||||
2019-04-12 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Bugfixes:
|
|
||||||
- Fix invalid serialisation of priority in presence.
|
|
||||||
- Bump image size to u16 from u8, as per XEP-0084 version 1.1.2.
|
|
||||||
* Improvements:
|
|
||||||
- Drop try_from dependency, as std::convert::TryFrom got
|
|
||||||
stabilised.
|
|
||||||
|
|
||||||
Version 0.13.0:
|
|
||||||
2019-03-20 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- User Avatar (XEP-0084).
|
|
||||||
- Contact Addresses for XMPP Services (XEP-0157).
|
|
||||||
- Jingle RTP Sessions (XEP-0167).
|
|
||||||
- Jingle ICE-UDP Transport Method (XEP-0176).
|
|
||||||
- Use of DTLS-SRTP in Jingle Sessions (XEP-0320).
|
|
||||||
* Breaking changes:
|
|
||||||
- Make 'id' required on iq, as per RFC6120 §8.1.3.
|
|
||||||
- Refactor PubSub to have more type-safety.
|
|
||||||
- Treat FORM_TYPE as a special case in data forms, to avoid
|
|
||||||
duplicating it into a field.
|
|
||||||
- Add forgotten i18n to Jingle text element.
|
|
||||||
* Improvements:
|
|
||||||
- Add various helpers for hash representations.
|
|
||||||
- Add helpers constructors for multiple extensions (disco, caps,
|
|
||||||
pubsub, stanza_error).
|
|
||||||
- Use Into<String> in more constructors.
|
|
||||||
- Internal change on attribute declaration in macros.
|
|
||||||
- Reexport missing try_from::TryInto.
|
|
||||||
|
|
||||||
Version 0.12.2:
|
|
||||||
2019-01-16 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Improvements:
|
|
||||||
- Reexport missing util::error::Error and try_from::TryFrom.
|
|
||||||
|
|
||||||
Version 0.12.1:
|
|
||||||
2019-01-16 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Improvements:
|
|
||||||
- Reexport missing JidParseError from the jid crate.
|
|
||||||
|
|
||||||
Version 0.12.0:
|
|
||||||
2019-01-16 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Breaking changes:
|
|
||||||
- Update dependencies.
|
|
||||||
- Switch to git, upstream is now available at
|
|
||||||
https://gitlab.com/xmpp-rs/xmpp-parsers
|
|
||||||
- Switch to Edition 2018, this removes support for rustc
|
|
||||||
versions older than 1.31.
|
|
||||||
- Implement support for XEP-0030 2.5rc3, relaxing the ordering
|
|
||||||
of children in disco#info.
|
|
||||||
* Improvements:
|
|
||||||
- Test for struct size, to keep them known and avoid bloat.
|
|
||||||
- Add various constructors to make the API easier to use.
|
|
||||||
- Reexport Jid from the jid crate, to avoid any weird issue on
|
|
||||||
using different incompatible versions of the same crate.
|
|
||||||
- Add forgotten 'ask' attribute on roster item (thanks O01eg!).
|
|
||||||
- Use cargo-fmt on the codebase, to lower the barrier of entry.
|
|
||||||
- Add a disable-validation feature, disabling many checks
|
|
||||||
xmpp-parsers is doing. This should be used for software
|
|
||||||
which want to let invalid XMPP pass through instead of being
|
|
||||||
rejected as invalid (thanks Astro-!).
|
|
||||||
|
|
||||||
Version 0.11.1:
|
|
||||||
2018-09-20 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Improvements:
|
|
||||||
- Document all of the modules.
|
|
||||||
|
|
||||||
Version 0.11.0:
|
|
||||||
2018-08-03 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Breaking changes:
|
|
||||||
- Split Software Version (XEP-0092) into a query and response
|
|
||||||
elements.
|
|
||||||
- Split RSM (XEP-0059) into a query and response elements.
|
|
||||||
- Fix type safety and spec issues in RSM and MAM (XEP-0313).
|
|
||||||
- Remove item@node and EmptyItems from PubSub events
|
|
||||||
(XEP-0060).
|
|
||||||
* Improvements:
|
|
||||||
- Document many additional modules.
|
|
||||||
- Add the <failure/> SASL nonza, as well as the SCRAM-SHA-256
|
|
||||||
and the two -PLUS mechanisms.
|
|
||||||
|
|
||||||
Version 0.10.0:
|
|
||||||
2018-07-31 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- Added <stream:stream>, SASL and bind (RFC6120) parsers.
|
|
||||||
- Added a WebSocket <open/> (RFC7395) implementation.
|
|
||||||
- Added a Jabber Component <handshake/> (XEP-0114).
|
|
||||||
- Added support for User Nickname (XEP-0172).
|
|
||||||
- Added support for Stream Management (XEP-0198).
|
|
||||||
- Added support for Bookmarks (XEP-0048).
|
|
||||||
- Publish-Subscribe (XEP-0060) now supports requests in
|
|
||||||
addition to events.
|
|
||||||
* Breaking changes:
|
|
||||||
- Switch from std::error to failure to report better errors.
|
|
||||||
- Bump to minidom 0.9.1, and reexport minidom::Element.
|
|
||||||
* Improvements:
|
|
||||||
- Add getters for the best body and subject in message, to make
|
|
||||||
it easier to determine which one the user wants based on
|
|
||||||
their language preferences.
|
|
||||||
- Add constructors and setters for most Jingle elements, to
|
|
||||||
ease their creation.
|
|
||||||
- Add constructors for hash, MUC item, iq and more.
|
|
||||||
- Use more macros to simplify and factorise the code.
|
|
||||||
- Use traits to define iq payloads.
|
|
||||||
- Document more modules.
|
|
||||||
|
|
||||||
Version 0.9.0:
|
|
||||||
2017-10-31 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- Blocking Command (XEP-0191) has been added.
|
|
||||||
- Date and Time Profiles (XEP-0082) has been added, replacing
|
|
||||||
ad-hoc use of chrono in various places.
|
|
||||||
- User Mood (XEP-0107) has been added.
|
|
||||||
* Breaking changes:
|
|
||||||
- Fix subscription="none" not being the default.
|
|
||||||
- Add more type safety to pubsub#event.
|
|
||||||
- Reuse Jingle’s ContentId type in JingleFT.
|
|
||||||
- Import the disposition attribute values in Jingle.
|
|
||||||
* Improvements:
|
|
||||||
- Refactor a good part of the code using macros.
|
|
||||||
- Simplify the parsing code wherever it makes sense.
|
|
||||||
- Check for children ordering in disco#info result.
|
|
||||||
- Finish implementation of <received/>, <checksum/> and
|
|
||||||
<range/> in JingleFT.
|
|
||||||
- Correctly serialise <ping/>, and test it.
|
|
||||||
|
|
||||||
Version 0.8.0:
|
|
||||||
2017-08-27 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- iq:version (XEP-0092) has been added.
|
|
||||||
- Finally implement extension serialisation in disco.
|
|
||||||
* Breaking changes:
|
|
||||||
- Wrap even more elements into their own type, in jingle,
|
|
||||||
jingle_ft, roster, message.
|
|
||||||
- Split loose enums into multiple structs where it makes sense,
|
|
||||||
such as for IBB, StanzaId, Receipts.
|
|
||||||
- Split disco query and answer elements into their own struct,
|
|
||||||
to enforce more guarantees on both.
|
|
||||||
* Improvements:
|
|
||||||
- Use Vec::into_iter() more to avoid references and clones.
|
|
||||||
- Make data_forms propagate a media_element error.
|
|
||||||
- Document more of disco, roster, chatstates.
|
|
||||||
- Use the minidom feature of jid, for IntoAttributeValue.
|
|
||||||
- Add a component feature, changing the default namespace to
|
|
||||||
jabber:component:accept.
|
|
||||||
- Add support for indicating ranged transfers in jingle_ft.
|
|
||||||
|
|
||||||
Version 0.7.1:
|
|
||||||
2017-07-24 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Hotfixes:
|
|
||||||
- Stub out blake2 support, since the blake2 crate broke its API
|
|
||||||
between their 0.6.0 and 0.6.1 releases…
|
|
||||||
|
|
||||||
Version 0.7.0:
|
|
||||||
2017-07-23 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- Jingle Message Initialisation (XEP-0353) was added.
|
|
||||||
- The disco#items query (XEP-0030) is now supported, in
|
|
||||||
addition to the existing disco#info one.
|
|
||||||
* Breaking changes:
|
|
||||||
- Replaced many type aliases with proper wrapping structs.
|
|
||||||
- Split Disco into a query and a result part, since they have
|
|
||||||
very different constraints.
|
|
||||||
- Split IqPayload in three to avoid parsing queries as results
|
|
||||||
for example.
|
|
||||||
* Improvements:
|
|
||||||
- Use TryFrom from the try_from crate, thus removing the
|
|
||||||
dependency on nightly!
|
|
||||||
- Always implement From instead of Into, the latter is
|
|
||||||
generated anyway.
|
|
||||||
- Add helpers to construct your Presence stanza.
|
|
||||||
|
|
||||||
Version 0.6.0:
|
|
||||||
2017-06-27 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- In-Band Registration (XEP-0077) was added.
|
|
||||||
- Multi-User Chat (XEP-0045) got expanded a lot, thanks pep.!
|
|
||||||
* Breaking changes:
|
|
||||||
- Added wrappers for Strings used as identifiers, to add type
|
|
||||||
safety.
|
|
||||||
- Use chrono’s DateTime for JingleFT’s date element.
|
|
||||||
- Use Jid for JingleS5B’s jid attribute.
|
|
||||||
* Improvements:
|
|
||||||
- Use more macros for common tasks.
|
|
||||||
- Add a constructor for Message and Presence.
|
|
||||||
- Implement std::fmt::Display and std::error::Error on our
|
|
||||||
error type.
|
|
||||||
- Fix DataForms serialisation.
|
|
||||||
- Fix roster group serialisation.
|
|
||||||
- Update libraries, notably chrono whose version 0.3.1 got
|
|
||||||
yanked.
|
|
||||||
|
|
||||||
Version 0.5.0:
|
|
||||||
2017-06-11 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- Implementation of the roster management protocol defined in
|
|
||||||
RFC 6121 §2.
|
|
||||||
- Implementation of PubSub events (except collections).
|
|
||||||
- Early implementation of MUC.
|
|
||||||
* Breaking changes:
|
|
||||||
- Rename presence enums to make them easier to use.
|
|
||||||
* Improvements:
|
|
||||||
- Make hashes comparable and hashable.
|
|
||||||
- Make data forms embeddable easily into minidom
|
|
||||||
Element::builder.
|
|
||||||
|
|
||||||
Version 0.4.0:
|
|
||||||
2017-05-28 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Incompatible changes:
|
|
||||||
- Receipts now make the id optional, as per the specification.
|
|
||||||
- Hashes now expose their raw binary value, instead of staying
|
|
||||||
base64-encoded.
|
|
||||||
- Parse dates (XEP-0082) in delayed delivery (XEP-0203) and
|
|
||||||
last user interaction (XEP-0319), using the chrono crate.
|
|
||||||
* Improvements:
|
|
||||||
- Removal of most of the remaining clones, the only ones left
|
|
||||||
are due to minidom not exposing a draining iterator over the
|
|
||||||
children.
|
|
||||||
- Finish to parse all of the attributes using get_attr!().
|
|
||||||
- More attribute checks.
|
|
||||||
- Split more parsers into one parser per element.
|
|
||||||
- Rely on minidom 0.4.3 to serialise more standard types
|
|
||||||
automatically.
|
|
||||||
- Implement forgotten serialisation for data forms (XEP-0004).
|
|
||||||
- Implement legacy capabilities (XEP-0115) for compatibility
|
|
||||||
with older software.
|
|
||||||
|
|
||||||
Version 0.3.0:
|
|
||||||
2017-05-23 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Big changes:
|
|
||||||
- All parsers and serialisers now consume their argument, this
|
|
||||||
makes the API way more efficient, but you will have to clone
|
|
||||||
before passing your structs in it if you want to keep them.
|
|
||||||
- Payloads of stanzas are not parsed automatically anymore, to
|
|
||||||
let applications which want to forward them as-is do so more
|
|
||||||
easily. Parsing now always succeeds on unknown payloads, it
|
|
||||||
just puts them into an Unknown value containing the existing
|
|
||||||
minidom Element.
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- Last User Interaction in Presence, XEP-0319.
|
|
||||||
* Improved parsers/serialisers:
|
|
||||||
- Message now supports subject, bodies and threads as per
|
|
||||||
RFC 6121 §5.2.
|
|
||||||
- Replace most attribute reads with a nice macro.
|
|
||||||
- Use enums for more enum-like things, for example Algo in
|
|
||||||
Hash, or FieldType in DataForm.
|
|
||||||
- Wire up stanza-id and origin-id to MessagePayload.
|
|
||||||
- Wire up MAM elements to message and iq payloads.
|
|
||||||
- Changes in the RSM API.
|
|
||||||
- Add support for more data forms elements, but still not the
|
|
||||||
complete set.
|
|
||||||
- Thanks to minidom 0.3.1, check for explicitly disallowed
|
|
||||||
extra attributes in some elements.
|
|
||||||
* Crate updates:
|
|
||||||
- minidom 0.4.1
|
|
||||||
|
|
||||||
Version 0.2.0:
|
|
||||||
2017-05-06 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* New parsers/serialisers:
|
|
||||||
- Stanza error, as per RFC 6120 §8.3.
|
|
||||||
- Jingle SOCKS5 Transport, XEP-0260.
|
|
||||||
* Incompatible changes:
|
|
||||||
- Parsers and serialisers now all implement TryFrom<Element>
|
|
||||||
and Into<Element>, instead of the old parse_* and serialise_*
|
|
||||||
functions.
|
|
||||||
- Presence has got an overhaul, it now hosts show, statuses and
|
|
||||||
priority in its struct. The status module has also been
|
|
||||||
dropped.
|
|
||||||
- Message now supports multiple bodies, each in a different
|
|
||||||
language. The body module has also been dropped.
|
|
||||||
- Iq now gets a proper StanzaError when the type is error.
|
|
||||||
- Fix bogus Jingle payload, which was requiring both
|
|
||||||
description and transport.
|
|
||||||
* Crate updates:
|
|
||||||
- minidom 0.3.0
|
|
||||||
|
|
||||||
Version 0.1.0:
|
|
||||||
2017-04-29 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
* Implement many extensions.
|
|
|
@ -1,373 +0,0 @@
|
||||||
Mozilla Public License Version 2.0
|
|
||||||
==================================
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
--------------
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
means each individual or legal entity that creates, contributes to
|
|
||||||
the creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
means the combination of the Contributions of others (if any) used
|
|
||||||
by a Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
means Source Code Form to which the initial Contributor has attached
|
|
||||||
the notice in Exhibit A, the Executable Form of such Source Code
|
|
||||||
Form, and Modifications of such Source Code Form, in each case
|
|
||||||
including portions thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
(a) that the initial Contributor has attached the notice described
|
|
||||||
in Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
(b) that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the
|
|
||||||
terms of a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
means a work that combines Covered Software with other material, in
|
|
||||||
a separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
means having the right to grant, to the maximum extent possible,
|
|
||||||
whether at the time of the initial grant or subsequently, any and
|
|
||||||
all of the rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
(a) any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered
|
|
||||||
Software; or
|
|
||||||
|
|
||||||
(b) any new file in Source Code Form that contains any Covered
|
|
||||||
Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the
|
|
||||||
License, by the making, using, selling, offering for sale, having
|
|
||||||
made, import, or transfer of either its Contributions or its
|
|
||||||
Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU
|
|
||||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
|
||||||
Public License, Version 3.0, or any later versions of those
|
|
||||||
licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that
|
|
||||||
controls, is controlled by, or is under common control with You. For
|
|
||||||
purposes of this definition, "control" means (a) the power, direct
|
|
||||||
or indirect, to cause the direction or management of such entity,
|
|
||||||
whether by contract or otherwise, or (b) ownership of more than
|
|
||||||
fifty percent (50%) of the outstanding shares or beneficial
|
|
||||||
ownership of such entity.
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
(a) under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
|
||||||
for sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
(a) for any code that a Contributor has removed from Covered Software;
|
|
||||||
or
|
|
||||||
|
|
||||||
(b) for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights
|
|
||||||
to grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
|
||||||
in Section 2.1.
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
(a) such Covered Software must also be made available in Source Code
|
|
||||||
Form, as described in Section 3.1, and You must inform recipients of
|
|
||||||
the Executable Form how they can obtain a copy of such Source Code
|
|
||||||
Form by reasonable means in a timely manner, at a charge no more
|
|
||||||
than the cost of distribution to the recipient; and
|
|
||||||
|
|
||||||
(b) You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter
|
|
||||||
the recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty,
|
|
||||||
or limitations of liability) contained within the Source Code Form of
|
|
||||||
the Covered Software, except that You may alter any license notices to
|
|
||||||
the extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this
|
|
||||||
License with respect to some or all of the Covered Software due to
|
|
||||||
statute, judicial order, or regulation then You must: (a) comply with
|
|
||||||
the terms of this License to the maximum extent possible; and (b)
|
|
||||||
describe the limitations and the code they affect. Such description must
|
|
||||||
be placed in a text file included with all distributions of the Covered
|
|
||||||
Software under this License. Except to the extent prohibited by statute
|
|
||||||
or regulation, such description must be sufficiently detailed for a
|
|
||||||
recipient of ordinary skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
--------------
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically
|
|
||||||
if You fail to comply with any of its terms. However, if You become
|
|
||||||
compliant, then the rights granted under this License from a particular
|
|
||||||
Contributor are reinstated (a) provisionally, unless and until such
|
|
||||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
|
||||||
ongoing basis, if such Contributor fails to notify You of the
|
|
||||||
non-compliance by some reasonable means prior to 60 days after You have
|
|
||||||
come back into compliance. Moreover, Your grants from a particular
|
|
||||||
Contributor are reinstated on an ongoing basis if such Contributor
|
|
||||||
notifies You of the non-compliance by some reasonable means, this is the
|
|
||||||
first time You have received notice of non-compliance with this License
|
|
||||||
from such Contributor, and You become compliant prior to 30 days after
|
|
||||||
Your receipt of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
|
||||||
end user license agreements (excluding distributors and resellers) which
|
|
||||||
have been validly granted by You or Your distributors under this License
|
|
||||||
prior to termination shall survive termination.
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 6. Disclaimer of Warranty *
|
|
||||||
* ------------------------- *
|
|
||||||
* *
|
|
||||||
* Covered Software is provided under this License on an "as is" *
|
|
||||||
* basis, without warranty of any kind, either expressed, implied, or *
|
|
||||||
* statutory, including, without limitation, warranties that the *
|
|
||||||
* Covered Software is free of defects, merchantable, fit for a *
|
|
||||||
* particular purpose or non-infringing. The entire risk as to the *
|
|
||||||
* quality and performance of the Covered Software is with You. *
|
|
||||||
* Should any Covered Software prove defective in any respect, You *
|
|
||||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
|
||||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
|
||||||
* essential part of this License. No use of any Covered Software is *
|
|
||||||
* authorized under this License except under this disclaimer. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 7. Limitation of Liability *
|
|
||||||
* -------------------------- *
|
|
||||||
* *
|
|
||||||
* Under no circumstances and under no legal theory, whether tort *
|
|
||||||
* (including negligence), contract, or otherwise, shall any *
|
|
||||||
* Contributor, or anyone who distributes Covered Software as *
|
|
||||||
* permitted above, be liable to You for any direct, indirect, *
|
|
||||||
* special, incidental, or consequential damages of any character *
|
|
||||||
* including, without limitation, damages for lost profits, loss of *
|
|
||||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
|
||||||
* and all other commercial damages or losses, even if such party *
|
|
||||||
* shall have been informed of the possibility of such damages. This *
|
|
||||||
* limitation of liability shall not apply to liability for death or *
|
|
||||||
* personal injury resulting from such party's negligence to the *
|
|
||||||
* extent applicable law prohibits such limitation. Some *
|
|
||||||
* jurisdictions do not allow the exclusion or limitation of *
|
|
||||||
* incidental or consequential damages, so this exclusion and *
|
|
||||||
* limitation may not apply to You. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the
|
|
||||||
courts of a jurisdiction where the defendant maintains its principal
|
|
||||||
place of business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions.
|
|
||||||
Nothing in this Section shall prevent a party's ability to bring
|
|
||||||
cross-claims or counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
----------------
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides
|
|
||||||
that the language of a contract shall be construed against the drafter
|
|
||||||
shall not be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses
|
|
||||||
|
|
||||||
If You choose to distribute Source Code Form that is Incompatible With
|
|
||||||
Secondary Licenses under the terms of this version of the License, the
|
|
||||||
notice described in Exhibit B of this License must be attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
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/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
|
||||||
file in a relevant directory) where a recipient would be likely to look
|
|
||||||
for such a notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
---------------------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
||||||
defined by the Mozilla Public License, v. 2.0.
|
|
|
@ -1,684 +0,0 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
<?xml-stylesheet href="../style.xsl" type="text/xsl"?>
|
|
||||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
||||||
<Project xmlns="http://usefulinc.com/ns/doap#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#">
|
|
||||||
<name>xmpp-parsers</name>
|
|
||||||
|
|
||||||
<created>2017-04-18</created>
|
|
||||||
|
|
||||||
<shortdesc xml:lang="en">Collection of parsers and serialisers for XMPP extensions</shortdesc>
|
|
||||||
<shortdesc xml:lang="fr">Collection de parseurs et de sérialiseurs pour extensions XMPP</shortdesc>
|
|
||||||
|
|
||||||
<description xml:lang="en">TODO</description>
|
|
||||||
<description xml:lang="fr">TODO</description>
|
|
||||||
|
|
||||||
<homepage rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers"/>
|
|
||||||
<!-- TODO: https://github.com/ewilderj/doap/issues/51 -->
|
|
||||||
<!--<doc rdf:resource="https://docs.rs/xmpp-parsers/"/>-->
|
|
||||||
<download-page rdf:resource="https://crates.io/crates/xmpp-parsers"/>
|
|
||||||
<bug-database rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers/issues"/>
|
|
||||||
<!-- See https://github.com/ewilderj/doap/issues/53 -->
|
|
||||||
<developer-forum rdf:resource="xmpp:chat@xmpp.rs?join"/>
|
|
||||||
<support-forum rdf:resource="xmpp:chat@xmpp.rs?join"/>
|
|
||||||
|
|
||||||
<license rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers/raw/master/LICENSE"/>
|
|
||||||
|
|
||||||
<!-- TODO: https://github.com/ewilderj/doap/issues/40 -->
|
|
||||||
<!--<logo rdf:resource="https://poez.io/img/logo.png"/>-->
|
|
||||||
|
|
||||||
<programming-language>Rust</programming-language>
|
|
||||||
|
|
||||||
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-library"/>
|
|
||||||
|
|
||||||
<maintainer>
|
|
||||||
<foaf:Person>
|
|
||||||
<foaf:name>Link Mauve</foaf:name>
|
|
||||||
<foaf:homepage rdf:resource="https://linkmauve.fr/"/>
|
|
||||||
<foaf:mbox_sha1sum>aaa4dac2b31c1be4ee8f8e2ab986d34fb261974f</foaf:mbox_sha1sum>
|
|
||||||
</foaf:Person>
|
|
||||||
</maintainer>
|
|
||||||
<maintainer>
|
|
||||||
<foaf:Person>
|
|
||||||
<foaf:name>pep.</foaf:name>
|
|
||||||
<foaf:homepage rdf:resource="https://bouah.net/"/>
|
|
||||||
<foaf:mbox_sha1sum>99bcf9784288e323b0d2dea9c9ac7a2ede98395a</foaf:mbox_sha1sum>
|
|
||||||
</foaf:Person>
|
|
||||||
</maintainer>
|
|
||||||
|
|
||||||
<repository>
|
|
||||||
<GitRepository>
|
|
||||||
<browse rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers"/>
|
|
||||||
<location rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers.git"/>
|
|
||||||
</GitRepository>
|
|
||||||
</repository>
|
|
||||||
|
|
||||||
<implements rdf:resource="https://xmpp.org/rfcs/rfc6120.html"/>
|
|
||||||
<implements rdf:resource="https://xmpp.org/rfcs/rfc6121.html"/>
|
|
||||||
<implements rdf:resource="https://xmpp.org/rfcs/rfc7395.html"/>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/>
|
|
||||||
<xmpp:status>partial</xmpp:status>
|
|
||||||
<xmpp:version>2.9</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>2.5rc3</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.32.0</xmpp:version>
|
|
||||||
<xmpp:since>0.5.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0047.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>2.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0048.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1</xmpp:version>
|
|
||||||
<xmpp:since>0.10.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0059.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html"/>
|
|
||||||
<xmpp:status>partial</xmpp:status>
|
|
||||||
<xmpp:version>1.15.8</xmpp:version>
|
|
||||||
<xmpp:since>0.5.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0068.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.2</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
<xmpp:note>there is no specific module for this, the feature is all in the XEP-0004 module</xmpp:note>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0071.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.5.4</xmpp:version>
|
|
||||||
<xmpp:since>0.15.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0077.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>2.4</xmpp:version>
|
|
||||||
<xmpp:since>0.6.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0082.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1</xmpp:version>
|
|
||||||
<xmpp:since>0.9.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1.2</xmpp:version>
|
|
||||||
<xmpp:since>0.13.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>2.1</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0092.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1</xmpp:version>
|
|
||||||
<xmpp:since>0.8.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0107.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.2.1</xmpp:version>
|
|
||||||
<xmpp:since>0.9.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0114.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.6</xmpp:version>
|
|
||||||
<xmpp:since>0.10.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.5.1</xmpp:version>
|
|
||||||
<xmpp:since>0.4.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0118.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.2</xmpp:version>
|
|
||||||
<xmpp:since>0.15.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0157.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0.1</xmpp:version>
|
|
||||||
<xmpp:since>0.13.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0166.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1.2</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0167.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.2.0</xmpp:version>
|
|
||||||
<xmpp:since>0.13.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0172.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1</xmpp:version>
|
|
||||||
<xmpp:since>0.10.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0176.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1</xmpp:version>
|
|
||||||
<xmpp:since>0.13.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0177.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1</xmpp:version>
|
|
||||||
<xmpp:since>NEXT</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.4.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.3</xmpp:version>
|
|
||||||
<xmpp:since>0.9.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.6</xmpp:version>
|
|
||||||
<xmpp:since>0.10.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0199.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>2.0.1</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0202.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>2.0</xmpp:version>
|
|
||||||
<xmpp:since>0.14.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0203.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>2.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0221.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0224.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0231.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0</xmpp:version>
|
|
||||||
<xmpp:since>0.15.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0234.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.19.1</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0257.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.3</xmpp:version>
|
|
||||||
<xmpp:since>0.16.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0260.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0.3</xmpp:version>
|
|
||||||
<xmpp:since>0.2.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0261.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0277.html"/>
|
|
||||||
<xmpp:status>partial</xmpp:status>
|
|
||||||
<xmpp:version>0.6.3</xmpp:version>
|
|
||||||
<xmpp:since>0.14.0</xmpp:since>
|
|
||||||
<xmpp:note>only the namespace is included for now</xmpp:note>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.13.0</xmpp:version>
|
|
||||||
<xmpp:since>0.15.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0293.html"/>
|
|
||||||
<xmpp:status>partial</xmpp:status>
|
|
||||||
<xmpp:version>1.0.1</xmpp:version>
|
|
||||||
<xmpp:since>0.16.0</xmpp:since>
|
|
||||||
<xmpp:note>Only supported in payload-type, and only for rtcp-fb.</xmpp:note>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0294.html"/>
|
|
||||||
<xmpp:status>partial</xmpp:status>
|
|
||||||
<xmpp:version>1.0</xmpp:version>
|
|
||||||
<xmpp:since>NEXT</xmpp:since>
|
|
||||||
<xmpp:note>Parameters aren’t yet implemented.</xmpp:note>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0300.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.6.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.7.5</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0319.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0.2</xmpp:version>
|
|
||||||
<xmpp:since>0.3.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0320.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.3.1</xmpp:version>
|
|
||||||
<xmpp:since>0.13.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0328.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.1</xmpp:version>
|
|
||||||
<xmpp:since>0.16.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0338.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.0.0</xmpp:version>
|
|
||||||
<xmpp:since>NEXT</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0339.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.3</xmpp:version>
|
|
||||||
<xmpp:since>0.16.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0352.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.3.0</xmpp:version>
|
|
||||||
<xmpp:since>0.16.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0353.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.3</xmpp:version>
|
|
||||||
<xmpp:since>0.7.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.6.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0369.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.14.3</xmpp:version>
|
|
||||||
<xmpp:since>NEXT</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0373.html"/>
|
|
||||||
<xmpp:status>partial</xmpp:status>
|
|
||||||
<xmpp:version>0.4.0</xmpp:version>
|
|
||||||
<xmpp:since>0.16.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.2.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0390.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.3.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0402.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>1.1.1</xmpp:version>
|
|
||||||
<xmpp:since>0.16.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0421.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.1.0</xmpp:version>
|
|
||||||
<xmpp:since>0.16.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
<implements>
|
|
||||||
<xmpp:SupportedXep>
|
|
||||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0441.html"/>
|
|
||||||
<xmpp:status>complete</xmpp:status>
|
|
||||||
<xmpp:version>0.2.0</xmpp:version>
|
|
||||||
<xmpp:since>0.1.0</xmpp:since>
|
|
||||||
</xmpp:SupportedXep>
|
|
||||||
</implements>
|
|
||||||
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.15.0</revision>
|
|
||||||
<created>2019-09-06</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.15.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.14.0</revision>
|
|
||||||
<created>2019-07-13</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.14.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.13.1</revision>
|
|
||||||
<created>2019-04-12</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.13.1/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.13.0</revision>
|
|
||||||
<created>2019-03-20</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.13.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.12.2</revision>
|
|
||||||
<created>2019-01-16</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.12.2/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.12.1</revision>
|
|
||||||
<created>2019-01-16</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.12.1/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.12.0</revision>
|
|
||||||
<created>2019-01-16</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.12.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.11.1</revision>
|
|
||||||
<created>2018-09-20</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.11.1/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.11.0</revision>
|
|
||||||
<created>2018-08-02</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.11.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.10.0</revision>
|
|
||||||
<created>2018-07-31</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.10.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.9.0</revision>
|
|
||||||
<created>2017-12-27</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.9.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.8.0</revision>
|
|
||||||
<created>2017-11-30</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.8.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.7.1</revision>
|
|
||||||
<created>2017-11-30</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.7.1/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.7.0</revision>
|
|
||||||
<created>2017-11-30</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.7.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.6.0</revision>
|
|
||||||
<created>2017-11-30</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.6.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.5.0</revision>
|
|
||||||
<created>2017-11-30</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.5.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.4.0</revision>
|
|
||||||
<created>2017-11-30</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.4.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.3.0</revision>
|
|
||||||
<created>2017-11-30</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.3.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.2.0</revision>
|
|
||||||
<created>2017-11-30</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.2.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
<release>
|
|
||||||
<Version>
|
|
||||||
<revision>0.1.0</revision>
|
|
||||||
<created>2017-11-30</created>
|
|
||||||
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.1.0/download"/>
|
|
||||||
</Version>
|
|
||||||
</release>
|
|
||||||
</Project>
|
|
||||||
</rdf:RDF>
|
|
|
@ -1,67 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::env;
|
|
||||||
use std::io::{self, Read};
|
|
||||||
use xmpp_parsers::{
|
|
||||||
caps::{compute_disco as compute_disco_caps, hash_caps, Caps},
|
|
||||||
disco::DiscoInfoResult,
|
|
||||||
ecaps2::{compute_disco as compute_disco_ecaps2, hash_ecaps2, ECaps2},
|
|
||||||
hashes::Algo,
|
|
||||||
Element, Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn get_caps(disco: &DiscoInfoResult, node: String) -> Result<Caps, String> {
|
|
||||||
let data = compute_disco_caps(&disco);
|
|
||||||
let hash = hash_caps(&data, Algo::Sha_1)?;
|
|
||||||
Ok(Caps::new(node, hash))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_ecaps2(disco: &DiscoInfoResult) -> Result<ECaps2, Error> {
|
|
||||||
let data = compute_disco_ecaps2(&disco)?;
|
|
||||||
let hash_sha256 = hash_ecaps2(&data, Algo::Sha_256)?;
|
|
||||||
let hash_sha3_256 = hash_ecaps2(&data, Algo::Sha3_256)?;
|
|
||||||
let hash_blake2b_256 = hash_ecaps2(&data, Algo::Blake2b_256)?;
|
|
||||||
Ok(ECaps2::new(vec![
|
|
||||||
hash_sha256,
|
|
||||||
hash_sha3_256,
|
|
||||||
hash_blake2b_256,
|
|
||||||
]))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let args: Vec<_> = env::args().collect();
|
|
||||||
if args.len() != 2 {
|
|
||||||
println!("Usage: {} <node>", args[0]);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
let node = args[1].clone();
|
|
||||||
|
|
||||||
eprintln!("Reading a disco#info payload from stdin...");
|
|
||||||
|
|
||||||
// Read from stdin.
|
|
||||||
let stdin = io::stdin();
|
|
||||||
let mut data = String::new();
|
|
||||||
let mut handle = stdin.lock();
|
|
||||||
handle.read_to_string(&mut data)?;
|
|
||||||
|
|
||||||
// Parse the payload into a DiscoInfoResult.
|
|
||||||
let elem: Element = data.parse()?;
|
|
||||||
let disco = DiscoInfoResult::try_from(elem)?;
|
|
||||||
|
|
||||||
// Compute both kinds of caps.
|
|
||||||
let caps = get_caps(&disco, node)?;
|
|
||||||
let ecaps2 = get_ecaps2(&disco)?;
|
|
||||||
|
|
||||||
// Print them.
|
|
||||||
let caps_elem = Element::from(caps);
|
|
||||||
let ecaps2_elem = Element::from(ecaps2);
|
|
||||||
println!("{}", String::from(&caps_elem));
|
|
||||||
println!("{}", String::from(&ecaps2_elem));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Requests the attention of the recipient.
|
|
||||||
Attention,
|
|
||||||
"attention",
|
|
||||||
ATTENTION
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Attention {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Attention, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
|
|
||||||
Attention::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'><coucou/></attention>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Attention::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in attention element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_attribute() {
|
|
||||||
let elem: Element = "<attention xmlns='urn:xmpp:attention:0' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Attention::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in attention element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
|
|
||||||
let attention = Attention;
|
|
||||||
let elem2: Element = attention.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::hashes::Sha1HexAttribute;
|
|
||||||
use crate::pubsub::PubSubPayload;
|
|
||||||
use crate::util::helpers::WhitespaceAwareBase64;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Communicates information about an avatar.
|
|
||||||
Metadata, "metadata", AVATAR_METADATA,
|
|
||||||
children: [
|
|
||||||
/// List of information elements describing this avatar.
|
|
||||||
infos: Vec<Info> = ("info", AVATAR_METADATA) => Info
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PubSubPayload for Metadata {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Communicates avatar metadata.
|
|
||||||
Info, "info", AVATAR_METADATA,
|
|
||||||
attributes: [
|
|
||||||
/// The size of the image data in bytes.
|
|
||||||
bytes: Required<u16> = "bytes",
|
|
||||||
|
|
||||||
/// The width of the image in pixels.
|
|
||||||
width: Option<u16> = "width",
|
|
||||||
|
|
||||||
/// The height of the image in pixels.
|
|
||||||
height: Option<u16> = "height",
|
|
||||||
|
|
||||||
/// The SHA-1 hash of the image data for the specified content-type.
|
|
||||||
id: Required<Sha1HexAttribute> = "id",
|
|
||||||
|
|
||||||
/// The IANA-registered content type of the image data.
|
|
||||||
type_: Required<String> = "type",
|
|
||||||
|
|
||||||
/// The http: or https: URL at which the image data file is hosted.
|
|
||||||
url: Option<String> = "url",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The actual avatar data.
|
|
||||||
Data, "data", AVATAR_DATA,
|
|
||||||
text: (
|
|
||||||
/// Vector of bytes representing the avatar’s image.
|
|
||||||
data: WhitespaceAwareBase64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PubSubPayload for Data {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::hashes::Algo;
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Metadata, 12);
|
|
||||||
assert_size!(Info, 64);
|
|
||||||
assert_size!(Data, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Metadata, 24);
|
|
||||||
assert_size!(Info, 120);
|
|
||||||
assert_size!(Data, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<metadata xmlns='urn:xmpp:avatar:metadata'>
|
|
||||||
<info bytes='12345' width='64' height='64'
|
|
||||||
id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'
|
|
||||||
type='image/png'/>
|
|
||||||
</metadata>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let metadata = Metadata::try_from(elem).unwrap();
|
|
||||||
assert_eq!(metadata.infos.len(), 1);
|
|
||||||
let info = &metadata.infos[0];
|
|
||||||
assert_eq!(info.bytes, 12345);
|
|
||||||
assert_eq!(info.width, Some(64));
|
|
||||||
assert_eq!(info.height, Some(64));
|
|
||||||
assert_eq!(info.id.algo, Algo::Sha_1);
|
|
||||||
assert_eq!(info.type_, "image/png");
|
|
||||||
assert_eq!(info.url, None);
|
|
||||||
assert_eq!(
|
|
||||||
info.id.hash,
|
|
||||||
[
|
|
||||||
17, 31, 75, 60, 80, 215, 176, 223, 114, 157, 41, 155, 198, 248, 233, 239, 144, 102,
|
|
||||||
151, 31
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = "<data xmlns='urn:xmpp:avatar:data'>AAAA</data>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let data = Data::try_from(elem).unwrap();
|
|
||||||
assert_eq!(data.data, b"\0\0\0");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element = "<data xmlns='urn:xmpp:avatar:data' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Data::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in data element.")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,203 +0,0 @@
|
||||||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::iq::{IqResultPayload, IqSetPayload};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::{FullJid, Jid};
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// The request for resource binding, which is the process by which a client
|
|
||||||
/// can obtain a full JID and start exchanging on the XMPP network.
|
|
||||||
///
|
|
||||||
/// See https://xmpp.org/rfcs/rfc6120.html#bind
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct BindQuery {
|
|
||||||
/// Requests this resource, the server may associate another one though.
|
|
||||||
///
|
|
||||||
/// If this is None, we request no particular resource, and a random one
|
|
||||||
/// will be affected by the server.
|
|
||||||
resource: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BindQuery {
|
|
||||||
/// Creates a resource binding request.
|
|
||||||
pub fn new(resource: Option<String>) -> BindQuery {
|
|
||||||
BindQuery { resource }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IqSetPayload for BindQuery {}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for BindQuery {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<BindQuery, Error> {
|
|
||||||
check_self!(elem, "bind", BIND);
|
|
||||||
check_no_attributes!(elem, "bind");
|
|
||||||
|
|
||||||
let mut resource = None;
|
|
||||||
for child in elem.children() {
|
|
||||||
if resource.is_some() {
|
|
||||||
return Err(Error::ParseError("Bind can only have one child."));
|
|
||||||
}
|
|
||||||
if child.is("resource", ns::BIND) {
|
|
||||||
check_no_attributes!(child, "resource");
|
|
||||||
check_no_children!(child, "resource");
|
|
||||||
resource = Some(child.text());
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown element in bind request."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(BindQuery { resource })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BindQuery> for Element {
|
|
||||||
fn from(bind: BindQuery) -> Element {
|
|
||||||
Element::builder("bind", ns::BIND)
|
|
||||||
.append_all(
|
|
||||||
bind.resource
|
|
||||||
.map(|resource| Element::builder("resource", ns::BIND).append(resource)),
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The response for resource binding, containing the client’s full JID.
|
|
||||||
///
|
|
||||||
/// See https://xmpp.org/rfcs/rfc6120.html#bind
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct BindResponse {
|
|
||||||
/// The full JID returned by the server for this client.
|
|
||||||
jid: FullJid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IqResultPayload for BindResponse {}
|
|
||||||
|
|
||||||
impl From<BindResponse> for FullJid {
|
|
||||||
fn from(bind: BindResponse) -> FullJid {
|
|
||||||
bind.jid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BindResponse> for Jid {
|
|
||||||
fn from(bind: BindResponse) -> Jid {
|
|
||||||
Jid::Full(bind.jid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for BindResponse {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<BindResponse, Error> {
|
|
||||||
check_self!(elem, "bind", BIND);
|
|
||||||
check_no_attributes!(elem, "bind");
|
|
||||||
|
|
||||||
let mut jid = None;
|
|
||||||
for child in elem.children() {
|
|
||||||
if jid.is_some() {
|
|
||||||
return Err(Error::ParseError("Bind can only have one child."));
|
|
||||||
}
|
|
||||||
if child.is("jid", ns::BIND) {
|
|
||||||
check_no_attributes!(child, "jid");
|
|
||||||
check_no_children!(child, "jid");
|
|
||||||
jid = Some(FullJid::from_str(&child.text())?);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown element in bind response."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(BindResponse {
|
|
||||||
jid: match jid {
|
|
||||||
None => {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Bind response must contain a jid element.",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Some(jid) => jid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<BindResponse> for Element {
|
|
||||||
fn from(bind: BindResponse) -> Element {
|
|
||||||
Element::builder("bind", ns::BIND)
|
|
||||||
.append(Element::builder("jid", ns::BIND).append(bind.jid))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(BindQuery, 12);
|
|
||||||
assert_size!(BindResponse, 36);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(BindQuery, 24);
|
|
||||||
assert_size!(BindResponse, 72);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let bind = BindQuery::try_from(elem).unwrap();
|
|
||||||
assert_eq!(bind.resource, None);
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>Hello™</resource></bind>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let bind = BindQuery::try_from(elem).unwrap();
|
|
||||||
// FIXME: “™” should be resourceprep’d into “TM” here…
|
|
||||||
//assert_eq!(bind.resource.unwrap(), "HelloTM");
|
|
||||||
assert_eq!(bind.resource.unwrap(), "Hello™");
|
|
||||||
|
|
||||||
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>coucou@linkmauve.fr/HelloTM</jid></bind>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let bind = BindResponse::try_from(elem).unwrap();
|
|
||||||
assert_eq!(bind.jid, FullJid::new("coucou", "linkmauve.fr", "HelloTM"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_resource() {
|
|
||||||
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource attr='coucou'>resource</resource></bind>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = BindQuery::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in resource element.");
|
|
||||||
|
|
||||||
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource><hello-world/>resource</resource></bind>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = BindQuery::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in resource element.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,221 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// The element requesting the blocklist, the result iq will contain a
|
|
||||||
/// [BlocklistResult].
|
|
||||||
BlocklistRequest,
|
|
||||||
"blocklist",
|
|
||||||
BLOCKING
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqGetPayload for BlocklistRequest {}
|
|
||||||
|
|
||||||
macro_rules! generate_blocking_element {
|
|
||||||
($(#[$meta:meta])* $elem:ident, $name:tt) => (
|
|
||||||
$(#[$meta])*
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct $elem {
|
|
||||||
/// List of JIDs affected by this command.
|
|
||||||
pub items: Vec<Jid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for $elem {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<$elem, Error> {
|
|
||||||
check_self!(elem, $name, BLOCKING);
|
|
||||||
check_no_attributes!(elem, $name);
|
|
||||||
let mut items = vec!();
|
|
||||||
for child in elem.children() {
|
|
||||||
check_self!(child, "item", BLOCKING);
|
|
||||||
check_no_unknown_attributes!(child, "item", ["jid"]);
|
|
||||||
check_no_children!(child, "item");
|
|
||||||
items.push(get_attr!(child, "jid", Required));
|
|
||||||
}
|
|
||||||
Ok($elem { items })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<$elem> for Element {
|
|
||||||
fn from(elem: $elem) -> Element {
|
|
||||||
Element::builder($name, ns::BLOCKING)
|
|
||||||
.append_all(elem.items.into_iter().map(|jid| {
|
|
||||||
Element::builder("item", ns::BLOCKING)
|
|
||||||
.attr("jid", jid)
|
|
||||||
}))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_blocking_element!(
|
|
||||||
/// The element containing the current blocklist, as a reply from
|
|
||||||
/// [BlocklistRequest].
|
|
||||||
BlocklistResult,
|
|
||||||
"blocklist"
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqResultPayload for BlocklistResult {}
|
|
||||||
|
|
||||||
// TODO: Prevent zero elements from being allowed.
|
|
||||||
generate_blocking_element!(
|
|
||||||
/// A query to block one or more JIDs.
|
|
||||||
Block,
|
|
||||||
"block"
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Block {}
|
|
||||||
|
|
||||||
generate_blocking_element!(
|
|
||||||
/// A query to unblock one or more JIDs, or all of them.
|
|
||||||
///
|
|
||||||
/// Warning: not putting any JID there means clearing out the blocklist.
|
|
||||||
Unblock,
|
|
||||||
"unblock"
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Unblock {}
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// The application-specific error condition when a message is blocked.
|
|
||||||
Blocked,
|
|
||||||
"blocked",
|
|
||||||
BLOCKING_ERRORS
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use jid::BareJid;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(BlocklistRequest, 0);
|
|
||||||
assert_size!(BlocklistResult, 12);
|
|
||||||
assert_size!(Block, 12);
|
|
||||||
assert_size!(Unblock, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(BlocklistRequest, 0);
|
|
||||||
assert_size!(BlocklistResult, 24);
|
|
||||||
assert_size!(Block, 24);
|
|
||||||
assert_size!(Unblock, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'/>".parse().unwrap();
|
|
||||||
let request_elem = elem.clone();
|
|
||||||
BlocklistRequest::try_from(request_elem).unwrap();
|
|
||||||
|
|
||||||
let result_elem = elem.clone();
|
|
||||||
let result = BlocklistResult::try_from(result_elem).unwrap();
|
|
||||||
assert!(result.items.is_empty());
|
|
||||||
|
|
||||||
let elem: Element = "<block xmlns='urn:xmpp:blocking'/>".parse().unwrap();
|
|
||||||
let block = Block::try_from(elem).unwrap();
|
|
||||||
assert!(block.items.is_empty());
|
|
||||||
|
|
||||||
let elem: Element = "<unblock xmlns='urn:xmpp:blocking'/>".parse().unwrap();
|
|
||||||
let unblock = Unblock::try_from(elem).unwrap();
|
|
||||||
assert!(unblock.items.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_items() {
|
|
||||||
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></blocklist>".parse().unwrap();
|
|
||||||
let two_items = vec![
|
|
||||||
Jid::Bare(BareJid {
|
|
||||||
node: Some(String::from("coucou")),
|
|
||||||
domain: String::from("coucou"),
|
|
||||||
}),
|
|
||||||
Jid::Bare(BareJid {
|
|
||||||
node: None,
|
|
||||||
domain: String::from("domain"),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
let result_elem = elem.clone();
|
|
||||||
let result = BlocklistResult::try_from(result_elem).unwrap();
|
|
||||||
assert_eq!(result.items, two_items);
|
|
||||||
|
|
||||||
let elem: Element = "<block xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></block>".parse().unwrap();
|
|
||||||
let block = Block::try_from(elem).unwrap();
|
|
||||||
assert_eq!(block.items, two_items);
|
|
||||||
|
|
||||||
let elem: Element = "<unblock xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></unblock>".parse().unwrap();
|
|
||||||
let unblock = Unblock::try_from(elem).unwrap();
|
|
||||||
assert_eq!(unblock.items, two_items);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let request_elem = elem.clone();
|
|
||||||
let error = BlocklistRequest::try_from(request_elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in blocklist element.");
|
|
||||||
|
|
||||||
let result_elem = elem.clone();
|
|
||||||
let error = BlocklistResult::try_from(result_elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in blocklist element.");
|
|
||||||
|
|
||||||
let elem: Element = "<block xmlns='urn:xmpp:blocking' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Block::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in block element.");
|
|
||||||
|
|
||||||
let elem: Element = "<unblock xmlns='urn:xmpp:blocking' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Unblock::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in unblock element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_non_empty_blocklist_request() {
|
|
||||||
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></blocklist>".parse().unwrap();
|
|
||||||
let error = BlocklistRequest::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in blocklist element.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,182 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::hashes::{Algo, Hash};
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::util::helpers::Base64;
|
|
||||||
use minidom::IntoAttributeValue;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// A Content-ID, as defined in RFC2111.
|
|
||||||
///
|
|
||||||
/// The text value SHOULD be of the form algo+hash@bob.xmpp.org, this struct
|
|
||||||
/// enforces that format.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct ContentId {
|
|
||||||
hash: Hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for ContentId {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Error> {
|
|
||||||
let temp: Vec<_> = s.splitn(2, '@').collect();
|
|
||||||
let temp: Vec<_> = match temp[..] {
|
|
||||||
[lhs, rhs] => {
|
|
||||||
if rhs != "bob.xmpp.org" {
|
|
||||||
return Err(Error::ParseError("Wrong domain for cid URI."));
|
|
||||||
}
|
|
||||||
lhs.splitn(2, '+').collect()
|
|
||||||
}
|
|
||||||
_ => return Err(Error::ParseError("Missing @ in cid URI.")),
|
|
||||||
};
|
|
||||||
let (algo, hex) = match temp[..] {
|
|
||||||
[lhs, rhs] => {
|
|
||||||
let algo = match lhs {
|
|
||||||
"sha1" => Algo::Sha_1,
|
|
||||||
"sha256" => Algo::Sha_256,
|
|
||||||
_ => unimplemented!(),
|
|
||||||
};
|
|
||||||
(algo, rhs)
|
|
||||||
}
|
|
||||||
_ => return Err(Error::ParseError("Missing + in cid URI.")),
|
|
||||||
};
|
|
||||||
let hash = Hash::from_hex(algo, hex)?;
|
|
||||||
Ok(ContentId { hash })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoAttributeValue for ContentId {
|
|
||||||
fn into_attribute_value(self) -> Option<String> {
|
|
||||||
let algo = match self.hash.algo {
|
|
||||||
Algo::Sha_1 => "sha1",
|
|
||||||
Algo::Sha_256 => "sha256",
|
|
||||||
_ => unimplemented!(),
|
|
||||||
};
|
|
||||||
Some(format!("{}+{}@bob.xmpp.org", algo, self.hash.to_hex()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request for an uncached cid file.
|
|
||||||
Data, "data", BOB,
|
|
||||||
attributes: [
|
|
||||||
/// The cid in question.
|
|
||||||
cid: Required<ContentId> = "cid",
|
|
||||||
|
|
||||||
/// How long to cache it (in seconds).
|
|
||||||
max_age: Option<usize> = "max-age",
|
|
||||||
|
|
||||||
/// The MIME type of the data being transmitted.
|
|
||||||
///
|
|
||||||
/// See the [IANA MIME Media Types Registry][1] for a list of
|
|
||||||
/// registered types, but unregistered or yet-to-be-registered are
|
|
||||||
/// accepted too.
|
|
||||||
///
|
|
||||||
/// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
|
||||||
type_: Option<String> = "type"
|
|
||||||
],
|
|
||||||
text: (
|
|
||||||
/// The actual data.
|
|
||||||
data: Base64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ContentId, 28);
|
|
||||||
assert_size!(Data, 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ContentId, 56);
|
|
||||||
assert_size!(Data, 120);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let cid: ContentId = "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(cid.hash.algo, Algo::Sha_1);
|
|
||||||
assert_eq!(
|
|
||||||
cid.hash.hash,
|
|
||||||
b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
cid.into_attribute_value().unwrap(),
|
|
||||||
"sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org"
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = "<data xmlns='urn:xmpp:bob' cid='sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org'/>".parse().unwrap();
|
|
||||||
let data = Data::try_from(elem).unwrap();
|
|
||||||
assert_eq!(data.cid.hash.algo, Algo::Sha_1);
|
|
||||||
assert_eq!(
|
|
||||||
data.cid.hash.hash,
|
|
||||||
b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42"
|
|
||||||
);
|
|
||||||
assert!(data.max_age.is_none());
|
|
||||||
assert!(data.type_.is_none());
|
|
||||||
assert!(data.data.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_cid() {
|
|
||||||
let error = "Hello world!".parse::<ContentId>().unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Missing @ in cid URI.");
|
|
||||||
|
|
||||||
let error = "Hello world@bob.xmpp.org".parse::<ContentId>().unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Missing + in cid URI.");
|
|
||||||
|
|
||||||
let error = "sha1+1234@coucou.linkmauve.fr"
|
|
||||||
.parse::<ContentId>()
|
|
||||||
.unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Wrong domain for cid URI.");
|
|
||||||
|
|
||||||
let error = "sha1+invalid@bob.xmpp.org"
|
|
||||||
.parse::<ContentId>()
|
|
||||||
.unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseIntError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "invalid digit found in string");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unknown_child() {
|
|
||||||
let elem: Element = "<data xmlns='urn:xmpp:bob'><coucou/></data>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Data::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in data element.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use jid::BareJid;
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Whether a conference bookmark should be joined automatically.
|
|
||||||
Autojoin,
|
|
||||||
"autojoin",
|
|
||||||
bool
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A conference bookmark.
|
|
||||||
Conference, "conference", BOOKMARKS,
|
|
||||||
attributes: [
|
|
||||||
/// Whether a conference bookmark should be joined automatically.
|
|
||||||
autojoin: Default<Autojoin> = "autojoin",
|
|
||||||
|
|
||||||
/// The JID of the conference.
|
|
||||||
jid: Required<BareJid> = "jid",
|
|
||||||
|
|
||||||
/// A user-defined name for this conference.
|
|
||||||
name: Option<String> = "name",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The nick the user will use to join this conference.
|
|
||||||
nick: Option<String> = ("nick", BOOKMARKS) => String,
|
|
||||||
|
|
||||||
/// The password required to join this conference.
|
|
||||||
password: Option<String> = ("password", BOOKMARKS) => String
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// An URL bookmark.
|
|
||||||
Url, "url", BOOKMARKS,
|
|
||||||
attributes: [
|
|
||||||
/// A user-defined name for this URL.
|
|
||||||
name: Option<String> = "name",
|
|
||||||
|
|
||||||
/// The URL of this bookmark.
|
|
||||||
url: Required<String> = "url",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Container element for multiple bookmarks.
|
|
||||||
#[derive(Default)]
|
|
||||||
Storage, "storage", BOOKMARKS,
|
|
||||||
children: [
|
|
||||||
/// Conferences the user has expressed an interest in.
|
|
||||||
conferences: Vec<Conference> = ("conference", BOOKMARKS) => Conference,
|
|
||||||
|
|
||||||
/// URLs the user is interested in.
|
|
||||||
urls: Vec<Url> = ("url", BOOKMARKS) => Url
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Storage {
|
|
||||||
/// Create an empty bookmarks storage.
|
|
||||||
pub fn new() -> Storage {
|
|
||||||
Storage::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Conference, 64);
|
|
||||||
assert_size!(Url, 24);
|
|
||||||
assert_size!(Storage, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Conference, 128);
|
|
||||||
assert_size!(Url, 48);
|
|
||||||
assert_size!(Storage, 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty() {
|
|
||||||
let elem: Element = "<storage xmlns='storage:bookmarks'/>".parse().unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let storage = Storage::try_from(elem).unwrap();
|
|
||||||
assert_eq!(storage.conferences.len(), 0);
|
|
||||||
assert_eq!(storage.urls.len(), 0);
|
|
||||||
|
|
||||||
let elem2 = Element::from(Storage::new());
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn complete() {
|
|
||||||
let elem: Element = "<storage xmlns='storage:bookmarks'><url name='Example' url='https://example.org/'/><conference autojoin='true' jid='test-muc@muc.localhost' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></storage>".parse().unwrap();
|
|
||||||
let storage = Storage::try_from(elem).unwrap();
|
|
||||||
assert_eq!(storage.urls.len(), 1);
|
|
||||||
assert_eq!(storage.urls[0].clone().name.unwrap(), "Example");
|
|
||||||
assert_eq!(storage.urls[0].url, "https://example.org/");
|
|
||||||
assert_eq!(storage.conferences.len(), 1);
|
|
||||||
assert_eq!(storage.conferences[0].autojoin, Autojoin::True);
|
|
||||||
assert_eq!(
|
|
||||||
storage.conferences[0].jid,
|
|
||||||
BareJid::new("test-muc", "muc.localhost")
|
|
||||||
);
|
|
||||||
assert_eq!(storage.conferences[0].clone().name.unwrap(), "Test MUC");
|
|
||||||
assert_eq!(storage.conferences[0].clone().nick.unwrap(), "Coucou");
|
|
||||||
assert_eq!(storage.conferences[0].clone().password.unwrap(), "secret");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,201 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Whether a conference bookmark should be joined automatically.
|
|
||||||
Autojoin,
|
|
||||||
"autojoin",
|
|
||||||
bool
|
|
||||||
);
|
|
||||||
|
|
||||||
/// A conference bookmark.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Conference {
|
|
||||||
/// Whether a conference bookmark should be joined automatically.
|
|
||||||
pub autojoin: Autojoin,
|
|
||||||
|
|
||||||
/// A user-defined name for this conference.
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
/// The nick the user will use to join this conference.
|
|
||||||
pub nick: Option<String>,
|
|
||||||
|
|
||||||
/// The password required to join this conference.
|
|
||||||
pub password: Option<String>,
|
|
||||||
|
|
||||||
/// Extensions elements.
|
|
||||||
pub extensions: Option<Vec<Element>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Conference {
|
|
||||||
/// Create a new conference.
|
|
||||||
pub fn new() -> Conference {
|
|
||||||
Conference {
|
|
||||||
autojoin: Autojoin::False,
|
|
||||||
name: None,
|
|
||||||
nick: None,
|
|
||||||
password: None,
|
|
||||||
extensions: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Conference {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(root: Element) -> Result<Conference, Error> {
|
|
||||||
check_self!(root, "conference", BOOKMARKS2, "Conference");
|
|
||||||
check_no_unknown_attributes!(root, "Conference", ["autojoin", "name"]);
|
|
||||||
|
|
||||||
let mut conference = Conference {
|
|
||||||
autojoin: get_attr!(root, "autojoin", Default),
|
|
||||||
name: get_attr!(root, "name", Option),
|
|
||||||
nick: None,
|
|
||||||
password: None,
|
|
||||||
extensions: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
for child in root.children().cloned() {
|
|
||||||
if child.is("extensions", ns::BOOKMARKS2) {
|
|
||||||
if conference.extensions.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Conference must not have more than one extensions element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
conference.extensions = Some(child.children().cloned().collect());
|
|
||||||
} else if child.is("nick", ns::BOOKMARKS2) {
|
|
||||||
if conference.nick.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Conference must not have more than one nick.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
check_no_children!(child, "nick");
|
|
||||||
check_no_attributes!(child, "nick");
|
|
||||||
conference.nick = Some(child.text());
|
|
||||||
} else if child.is("password", ns::BOOKMARKS2) {
|
|
||||||
if conference.password.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Conference must not have more than one password.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
check_no_children!(child, "password");
|
|
||||||
check_no_attributes!(child, "password");
|
|
||||||
conference.password = Some(child.text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(conference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Conference> for Element {
|
|
||||||
fn from(conference: Conference) -> Element {
|
|
||||||
Element::builder("conference", ns::BOOKMARKS2)
|
|
||||||
.attr("autojoin", conference.autojoin)
|
|
||||||
.attr("name", conference.name)
|
|
||||||
.append_all(
|
|
||||||
conference
|
|
||||||
.nick
|
|
||||||
.map(|nick| Element::builder("nick", ns::BOOKMARKS2).append(nick)),
|
|
||||||
)
|
|
||||||
.append_all(
|
|
||||||
conference
|
|
||||||
.password
|
|
||||||
.map(|password| Element::builder("password", ns::BOOKMARKS2).append(password)),
|
|
||||||
)
|
|
||||||
.append_all(match conference.extensions {
|
|
||||||
Some(extensions) => extensions,
|
|
||||||
None => vec![],
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::pubsub::pubsub::Item as PubSubItem;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Conference, 52);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Conference, 104);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple() {
|
|
||||||
let elem: Element = "<conference xmlns='urn:xmpp:bookmarks:1'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let conference = Conference::try_from(elem).unwrap();
|
|
||||||
assert_eq!(conference.autojoin, Autojoin::False);
|
|
||||||
assert_eq!(conference.name, None);
|
|
||||||
assert_eq!(conference.nick, None);
|
|
||||||
assert_eq!(conference.password, None);
|
|
||||||
|
|
||||||
let elem2 = Element::from(Conference::new());
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn complete() {
|
|
||||||
let elem: Element = "<conference xmlns='urn:xmpp:bookmarks:1' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password><extensions><test xmlns='urn:xmpp:unknown' /></extensions></conference>".parse().unwrap();
|
|
||||||
let conference = Conference::try_from(elem).unwrap();
|
|
||||||
assert_eq!(conference.autojoin, Autojoin::True);
|
|
||||||
assert_eq!(conference.name, Some(String::from("Test MUC")));
|
|
||||||
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
|
|
||||||
assert_eq!(conference.clone().password.unwrap(), "secret");
|
|
||||||
assert_eq!(conference.clone().extensions.unwrap().len(), 1);
|
|
||||||
assert!(conference.clone().extensions.unwrap()[0].is("test", "urn:xmpp:unknown"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn wrapped() {
|
|
||||||
let elem: Element = "<item xmlns='http://jabber.org/protocol/pubsub' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:1' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></item>".parse().unwrap();
|
|
||||||
let item = PubSubItem::try_from(elem).unwrap();
|
|
||||||
let payload = item.payload.clone().unwrap();
|
|
||||||
println!("FOO: payload: {:?}", payload);
|
|
||||||
// let conference = Conference::try_from(payload).unwrap();
|
|
||||||
let conference = Conference::try_from(payload);
|
|
||||||
println!("FOO: conference: {:?}", conference);
|
|
||||||
/*
|
|
||||||
assert_eq!(conference.autojoin, Autojoin::True);
|
|
||||||
assert_eq!(conference.name, Some(String::from("Test MUC")));
|
|
||||||
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
|
|
||||||
assert_eq!(conference.clone().password.unwrap(), "secret");
|
|
||||||
|
|
||||||
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='urn:xmpp:bookmarks:1'><item xmlns='http://jabber.org/protocol/pubsub#event' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:1' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></item></items></event>".parse().unwrap();
|
|
||||||
let mut items = match PubSubEvent::try_from(elem) {
|
|
||||||
Ok(PubSubEvent::PublishedItems { node, items }) => {
|
|
||||||
assert_eq!(&node.0, ns::BOOKMARKS2);
|
|
||||||
items
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(items.len(), 1);
|
|
||||||
let item = items.pop().unwrap();
|
|
||||||
let payload = item.payload.clone().unwrap();
|
|
||||||
let conference = Conference::try_from(payload).unwrap();
|
|
||||||
assert_eq!(conference.autojoin, Autojoin::True);
|
|
||||||
assert_eq!(conference.name, Some(String::from("Test MUC")));
|
|
||||||
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
|
|
||||||
assert_eq!(conference.clone().password.unwrap(), "secret");
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,342 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::data_forms::DataForm;
|
|
||||||
use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity};
|
|
||||||
use crate::hashes::{Algo, Hash};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::presence::PresencePayload;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use blake2::VarBlake2b;
|
|
||||||
use digest::{Digest, Update, VariableOutput};
|
|
||||||
use sha1::Sha1;
|
|
||||||
use sha2::{Sha256, Sha512};
|
|
||||||
use sha3::{Sha3_256, Sha3_512};
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Represents a capability hash for a given client.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Caps {
|
|
||||||
/// Deprecated list of additional feature bundles.
|
|
||||||
pub ext: Option<String>,
|
|
||||||
|
|
||||||
/// A URI identifying an XMPP application.
|
|
||||||
pub node: String,
|
|
||||||
|
|
||||||
/// The hash of that application’s
|
|
||||||
/// [disco#info](../disco/struct.DiscoInfoResult.html).
|
|
||||||
///
|
|
||||||
/// Warning: This protocol is insecure, you may want to switch to
|
|
||||||
/// [ecaps2](../ecaps2/index.html) instead, see [this
|
|
||||||
/// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html).
|
|
||||||
pub hash: Hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PresencePayload for Caps {}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Caps {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Caps, Error> {
|
|
||||||
check_self!(elem, "c", CAPS, "caps");
|
|
||||||
check_no_children!(elem, "caps");
|
|
||||||
check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]);
|
|
||||||
let ver: String = get_attr!(elem, "ver", Required);
|
|
||||||
let hash = Hash {
|
|
||||||
algo: get_attr!(elem, "hash", Required),
|
|
||||||
hash: base64::decode(&ver)?,
|
|
||||||
};
|
|
||||||
Ok(Caps {
|
|
||||||
ext: get_attr!(elem, "ext", Option),
|
|
||||||
node: get_attr!(elem, "node", Required),
|
|
||||||
hash,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Caps> for Element {
|
|
||||||
fn from(caps: Caps) -> Element {
|
|
||||||
Element::builder("c", ns::CAPS)
|
|
||||||
.attr("ext", caps.ext)
|
|
||||||
.attr("hash", caps.hash.algo)
|
|
||||||
.attr("node", caps.node)
|
|
||||||
.attr("ver", base64::encode(&caps.hash.hash))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Caps {
|
|
||||||
/// Create a Caps element from its node and hash.
|
|
||||||
pub fn new<N: Into<String>>(node: N, hash: Hash) -> Caps {
|
|
||||||
Caps {
|
|
||||||
ext: None,
|
|
||||||
node: node.into(),
|
|
||||||
hash,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_item(field: &str) -> Vec<u8> {
|
|
||||||
let mut bytes = field.as_bytes().to_vec();
|
|
||||||
bytes.push(b'<');
|
|
||||||
bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_items<T, F: Fn(&T) -> Vec<u8>>(things: &[T], encode: F) -> Vec<u8> {
|
|
||||||
let mut string: Vec<u8> = vec![];
|
|
||||||
let mut accumulator: Vec<Vec<u8>> = vec![];
|
|
||||||
for thing in things {
|
|
||||||
let bytes = encode(thing);
|
|
||||||
accumulator.push(bytes);
|
|
||||||
}
|
|
||||||
// This works using the expected i;octet collation.
|
|
||||||
accumulator.sort();
|
|
||||||
for mut bytes in accumulator {
|
|
||||||
string.append(&mut bytes);
|
|
||||||
}
|
|
||||||
string
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_features(features: &[Feature]) -> Vec<u8> {
|
|
||||||
compute_items(features, |feature| compute_item(&feature.var))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_identities(identities: &[Identity]) -> Vec<u8> {
|
|
||||||
compute_items(identities, |identity| {
|
|
||||||
let lang = identity.lang.clone().unwrap_or_default();
|
|
||||||
let name = identity.name.clone().unwrap_or_default();
|
|
||||||
let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name);
|
|
||||||
let bytes = string.as_bytes();
|
|
||||||
let mut vec = Vec::with_capacity(bytes.len());
|
|
||||||
vec.extend_from_slice(bytes);
|
|
||||||
vec.push(b'<');
|
|
||||||
vec
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_extensions(extensions: &[DataForm]) -> Vec<u8> {
|
|
||||||
compute_items(extensions, |extension| {
|
|
||||||
let mut bytes = vec![];
|
|
||||||
// TODO: maybe handle the error case?
|
|
||||||
if let Some(ref form_type) = extension.form_type {
|
|
||||||
bytes.extend_from_slice(form_type.as_bytes());
|
|
||||||
}
|
|
||||||
bytes.push(b'<');
|
|
||||||
for field in extension.fields.clone() {
|
|
||||||
if field.var == "FORM_TYPE" {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bytes.append(&mut compute_item(&field.var));
|
|
||||||
bytes.append(&mut compute_items(&field.values, |value| {
|
|
||||||
compute_item(value)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
bytes
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies the caps algorithm on the provided disco#info result, to generate
|
|
||||||
/// the hash input.
|
|
||||||
///
|
|
||||||
/// Warning: This protocol is insecure, you may want to switch to
|
|
||||||
/// [ecaps2](../ecaps2/index.html) instead, see [this
|
|
||||||
/// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html).
|
|
||||||
pub fn compute_disco(disco: &DiscoInfoResult) -> Vec<u8> {
|
|
||||||
let identities_string = compute_identities(&disco.identities);
|
|
||||||
let features_string = compute_features(&disco.features);
|
|
||||||
let extensions_string = compute_extensions(&disco.extensions);
|
|
||||||
|
|
||||||
let mut final_string = vec![];
|
|
||||||
final_string.extend(identities_string);
|
|
||||||
final_string.extend(features_string);
|
|
||||||
final_string.extend(extensions_string);
|
|
||||||
final_string
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_hash_vec(hash: &[u8]) -> Vec<u8> {
|
|
||||||
hash.to_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hashes the result of [compute_disco()] with one of the supported [hash
|
|
||||||
/// algorithms](../hashes/enum.Algo.html).
|
|
||||||
pub fn hash_caps(data: &[u8], algo: Algo) -> Result<Hash, String> {
|
|
||||||
Ok(Hash {
|
|
||||||
hash: match algo {
|
|
||||||
Algo::Sha_1 => {
|
|
||||||
let hash = Sha1::digest(data);
|
|
||||||
get_hash_vec(hash.as_slice())
|
|
||||||
}
|
|
||||||
Algo::Sha_256 => {
|
|
||||||
let hash = Sha256::digest(data);
|
|
||||||
get_hash_vec(hash.as_slice())
|
|
||||||
}
|
|
||||||
Algo::Sha_512 => {
|
|
||||||
let hash = Sha512::digest(data);
|
|
||||||
get_hash_vec(hash.as_slice())
|
|
||||||
}
|
|
||||||
Algo::Sha3_256 => {
|
|
||||||
let hash = Sha3_256::digest(data);
|
|
||||||
get_hash_vec(hash.as_slice())
|
|
||||||
}
|
|
||||||
Algo::Sha3_512 => {
|
|
||||||
let hash = Sha3_512::digest(data);
|
|
||||||
get_hash_vec(hash.as_slice())
|
|
||||||
}
|
|
||||||
Algo::Blake2b_256 => {
|
|
||||||
let mut hasher = VarBlake2b::new(32).unwrap();
|
|
||||||
hasher.update(data);
|
|
||||||
let mut vec = Vec::with_capacity(32);
|
|
||||||
hasher.finalize_variable(|slice| vec.extend_from_slice(slice));
|
|
||||||
vec
|
|
||||||
}
|
|
||||||
Algo::Blake2b_512 => {
|
|
||||||
let mut hasher = VarBlake2b::new(64).unwrap();
|
|
||||||
hasher.update(data);
|
|
||||||
let mut vec = Vec::with_capacity(64);
|
|
||||||
hasher.finalize_variable(|slice| vec.extend_from_slice(slice));
|
|
||||||
vec
|
|
||||||
}
|
|
||||||
Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)),
|
|
||||||
},
|
|
||||||
algo,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to create the query for the disco#info corresponding to a
|
|
||||||
/// caps hash.
|
|
||||||
pub fn query_caps(caps: Caps) -> DiscoInfoQuery {
|
|
||||||
DiscoInfoQuery {
|
|
||||||
node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::caps;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Caps, 52);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Caps, 104);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse() {
|
|
||||||
let elem: Element = "<c xmlns='http://jabber.org/protocol/caps' hash='sha-256' node='coucou' ver='K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4='/>".parse().unwrap();
|
|
||||||
let caps = Caps::try_from(elem).unwrap();
|
|
||||||
assert_eq!(caps.node, String::from("coucou"));
|
|
||||||
assert_eq!(caps.hash.algo, Algo::Sha_256);
|
|
||||||
assert_eq!(
|
|
||||||
caps.hash.hash,
|
|
||||||
base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<c xmlns='http://jabber.org/protocol/caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash></c>".parse().unwrap();
|
|
||||||
let error = Caps::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in caps element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
|
||||||
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
let caps = caps::compute_disco(&disco);
|
|
||||||
assert_eq!(caps.len(), 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_xep_5_2() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns='http://jabber.org/protocol/disco#info'
|
|
||||||
node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
|
|
||||||
<identity category='client' name='Exodus 0.9.1' type='pc'/>
|
|
||||||
<feature var='http://jabber.org/protocol/caps'/>
|
|
||||||
<feature var='http://jabber.org/protocol/disco#info'/>
|
|
||||||
<feature var='http://jabber.org/protocol/disco#items'/>
|
|
||||||
<feature var='http://jabber.org/protocol/muc'/>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let data = b"client/pc//Exodus 0.9.1<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<";
|
|
||||||
let mut expected = Vec::with_capacity(data.len());
|
|
||||||
expected.extend_from_slice(data);
|
|
||||||
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
let caps = caps::compute_disco(&disco);
|
|
||||||
assert_eq!(caps, expected);
|
|
||||||
|
|
||||||
let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
sha_1.hash,
|
|
||||||
base64::decode("QgayPKawpkPSDYmwT/WM94uAlu0=").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_xep_5_3() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns='http://jabber.org/protocol/disco#info'
|
|
||||||
node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
|
|
||||||
<identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
|
|
||||||
<identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
|
|
||||||
<feature var='http://jabber.org/protocol/caps'/>
|
|
||||||
<feature var='http://jabber.org/protocol/disco#info'/>
|
|
||||||
<feature var='http://jabber.org/protocol/disco#items'/>
|
|
||||||
<feature var='http://jabber.org/protocol/muc'/>
|
|
||||||
<x xmlns='jabber:x:data' type='result'>
|
|
||||||
<field var='FORM_TYPE' type='hidden'>
|
|
||||||
<value>urn:xmpp:dataforms:softwareinfo</value>
|
|
||||||
</field>
|
|
||||||
<field var='ip_version'>
|
|
||||||
<value>ipv4</value>
|
|
||||||
<value>ipv6</value>
|
|
||||||
</field>
|
|
||||||
<field var='os'>
|
|
||||||
<value>Mac</value>
|
|
||||||
</field>
|
|
||||||
<field var='os_version'>
|
|
||||||
<value>10.5.1</value>
|
|
||||||
</field>
|
|
||||||
<field var='software'>
|
|
||||||
<value>Psi</value>
|
|
||||||
</field>
|
|
||||||
<field var='software_version'>
|
|
||||||
<value>0.11</value>
|
|
||||||
</field>
|
|
||||||
</x>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let expected = b"client/pc/el/\xce\xa8 0.11<client/pc/en/Psi 0.11<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<urn:xmpp:dataforms:softwareinfo<ip_version<ipv4<ipv6<os<Mac<os_version<10.5.1<software<Psi<software_version<0.11<".to_vec();
|
|
||||||
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
let caps = caps::compute_disco(&disco);
|
|
||||||
assert_eq!(caps, expected);
|
|
||||||
|
|
||||||
let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
sha_1.hash,
|
|
||||||
base64::decode("q07IKJEyjvHSyhy//CH0CxmKi8w=").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,165 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::forwarding::Forwarded;
|
|
||||||
use crate::iq::IqSetPayload;
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Enable carbons for this session.
|
|
||||||
Enable,
|
|
||||||
"enable",
|
|
||||||
CARBONS
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Enable {}
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Disable a previously-enabled carbons.
|
|
||||||
Disable,
|
|
||||||
"disable",
|
|
||||||
CARBONS
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Disable {}
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Request the enclosing message to not be copied to other carbons-enabled
|
|
||||||
/// resources of the user.
|
|
||||||
Private,
|
|
||||||
"private",
|
|
||||||
CARBONS
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Private {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Wrapper for a message received on another resource.
|
|
||||||
Received, "received", CARBONS,
|
|
||||||
|
|
||||||
children: [
|
|
||||||
/// Wrapper for the enclosed message.
|
|
||||||
forwarded: Required<Forwarded> = ("forwarded", FORWARD) => Forwarded
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Received {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Wrapper for a message sent from another resource.
|
|
||||||
Sent, "sent", CARBONS,
|
|
||||||
|
|
||||||
children: [
|
|
||||||
/// Wrapper for the enclosed message.
|
|
||||||
forwarded: Required<Forwarded> = ("forwarded", FORWARD) => Forwarded
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Sent {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Enable, 0);
|
|
||||||
assert_size!(Disable, 0);
|
|
||||||
assert_size!(Private, 0);
|
|
||||||
assert_size!(Received, 212);
|
|
||||||
assert_size!(Sent, 212);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Enable, 0);
|
|
||||||
assert_size!(Disable, 0);
|
|
||||||
assert_size!(Private, 0);
|
|
||||||
assert_size!(Received, 408);
|
|
||||||
assert_size!(Sent, 408);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_elements() {
|
|
||||||
let elem: Element = "<enable xmlns='urn:xmpp:carbons:2'/>".parse().unwrap();
|
|
||||||
Enable::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<disable xmlns='urn:xmpp:carbons:2'/>".parse().unwrap();
|
|
||||||
Disable::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<private xmlns='urn:xmpp:carbons:2'/>".parse().unwrap();
|
|
||||||
Private::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn forwarded_elements() {
|
|
||||||
let elem: Element = "<received xmlns='urn:xmpp:carbons:2'>
|
|
||||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
|
||||||
<message xmlns='jabber:client'
|
|
||||||
to='juliet@capulet.example/balcony'
|
|
||||||
from='romeo@montague.example/home'/>
|
|
||||||
</forwarded>
|
|
||||||
</received>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let received = Received::try_from(elem).unwrap();
|
|
||||||
assert!(received.forwarded.stanza.is_some());
|
|
||||||
|
|
||||||
let elem: Element = "<sent xmlns='urn:xmpp:carbons:2'>
|
|
||||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
|
||||||
<message xmlns='jabber:client'
|
|
||||||
to='juliet@capulet.example/balcony'
|
|
||||||
from='romeo@montague.example/home'/>
|
|
||||||
</forwarded>
|
|
||||||
</sent>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let sent = Sent::try_from(elem).unwrap();
|
|
||||||
assert!(sent.forwarded.stanza.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_received() {
|
|
||||||
let reference: Element = "<received xmlns='urn:xmpp:carbons:2'><forwarded xmlns='urn:xmpp:forward:0'><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded></received>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let forwarded = Forwarded::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let received = Received {
|
|
||||||
forwarded: forwarded,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = received.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_sent() {
|
|
||||||
let reference: Element = "<sent xmlns='urn:xmpp:carbons:2'><forwarded xmlns='urn:xmpp:forward:0'><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded></sent>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let forwarded = Forwarded::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let sent = Sent {
|
|
||||||
forwarded: forwarded,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = sent.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,297 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
|
||||||
use crate::util::helpers::Base64;
|
|
||||||
|
|
||||||
generate_elem_id!(
|
|
||||||
/// The name of a certificate.
|
|
||||||
Name,
|
|
||||||
"name",
|
|
||||||
SASL_CERT
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// An X.509 certificate.
|
|
||||||
Cert, "x509cert", SASL_CERT,
|
|
||||||
text: (
|
|
||||||
/// The BER X.509 data.
|
|
||||||
data: Base64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// For the client to upload an X.509 certificate.
|
|
||||||
Append, "append", SASL_CERT,
|
|
||||||
children: [
|
|
||||||
/// The name of this certificate.
|
|
||||||
name: Required<Name> = ("name", SASL_CERT) => Name,
|
|
||||||
|
|
||||||
/// The X.509 certificate to set.
|
|
||||||
cert: Required<Cert> = ("x509cert", SASL_CERT) => Cert,
|
|
||||||
|
|
||||||
/// This client is forbidden from managing certificates.
|
|
||||||
no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Append {}
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Client requests the current list of X.509 certificates.
|
|
||||||
ListCertsQuery,
|
|
||||||
"items",
|
|
||||||
SASL_CERT
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqGetPayload for ListCertsQuery {}
|
|
||||||
|
|
||||||
generate_elem_id!(
|
|
||||||
/// One resource currently using a certificate.
|
|
||||||
Resource,
|
|
||||||
"resource",
|
|
||||||
SASL_CERT
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A list of resources currently using this certificate.
|
|
||||||
Users, "users", SASL_CERT,
|
|
||||||
children: [
|
|
||||||
/// Resources currently using this certificate.
|
|
||||||
resources: Vec<Resource> = ("resource", SASL_CERT) => Resource
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// An X.509 certificate being set for this user.
|
|
||||||
Item, "item", SASL_CERT,
|
|
||||||
children: [
|
|
||||||
/// The name of this certificate.
|
|
||||||
name: Required<Name> = ("name", SASL_CERT) => Name,
|
|
||||||
|
|
||||||
/// The X.509 certificate to set.
|
|
||||||
cert: Required<Cert> = ("x509cert", SASL_CERT) => Cert,
|
|
||||||
|
|
||||||
/// This client is forbidden from managing certificates.
|
|
||||||
no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool,
|
|
||||||
|
|
||||||
/// List of resources currently using this certificate.
|
|
||||||
users: Option<Users> = ("users", SASL_CERT) => Users
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Server answers with the current list of X.509 certificates.
|
|
||||||
ListCertsResponse, "items", SASL_CERT,
|
|
||||||
children: [
|
|
||||||
/// List of certificates.
|
|
||||||
items: Vec<Item> = ("item", SASL_CERT) => Item
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqResultPayload for ListCertsResponse {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Client disables an X.509 certificate.
|
|
||||||
Disable, "disable", SASL_CERT,
|
|
||||||
children: [
|
|
||||||
/// Name of the certificate to disable.
|
|
||||||
name: Required<Name> = ("name", SASL_CERT) => Name
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Disable {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Client revokes an X.509 certificate.
|
|
||||||
Revoke, "revoke", SASL_CERT,
|
|
||||||
children: [
|
|
||||||
/// Name of the certificate to revoke.
|
|
||||||
name: Required<Name> = ("name", SASL_CERT) => Name
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Revoke {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Append, 28);
|
|
||||||
assert_size!(Disable, 12);
|
|
||||||
assert_size!(Revoke, 12);
|
|
||||||
assert_size!(ListCertsQuery, 0);
|
|
||||||
assert_size!(ListCertsResponse, 12);
|
|
||||||
assert_size!(Item, 40);
|
|
||||||
assert_size!(Resource, 12);
|
|
||||||
assert_size!(Users, 12);
|
|
||||||
assert_size!(Cert, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Append, 56);
|
|
||||||
assert_size!(Disable, 24);
|
|
||||||
assert_size!(Revoke, 24);
|
|
||||||
assert_size!(ListCertsQuery, 0);
|
|
||||||
assert_size!(ListCertsResponse, 24);
|
|
||||||
assert_size!(Item, 80);
|
|
||||||
assert_size!(Resource, 24);
|
|
||||||
assert_size!(Users, 24);
|
|
||||||
assert_size!(Cert, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple() {
|
|
||||||
let elem: Element = "<append xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name><x509cert>AAAA</x509cert></append>".parse().unwrap();
|
|
||||||
let append = Append::try_from(elem).unwrap();
|
|
||||||
assert_eq!(append.name.0, "Mobile Client");
|
|
||||||
assert_eq!(append.cert.data, b"\0\0\0");
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<disable xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></disable>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let disable = Disable::try_from(elem).unwrap();
|
|
||||||
assert_eq!(disable.name.0, "Mobile Client");
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<revoke xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></revoke>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let revoke = Revoke::try_from(elem).unwrap();
|
|
||||||
assert_eq!(revoke.name.0, "Mobile Client");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<items xmlns='urn:xmpp:saslcert:1'>
|
|
||||||
<item>
|
|
||||||
<name>Mobile Client</name>
|
|
||||||
<x509cert>AAAA</x509cert>
|
|
||||||
<users>
|
|
||||||
<resource>Phone</resource>
|
|
||||||
</users>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<name>Laptop</name>
|
|
||||||
<x509cert>BBBB</x509cert>
|
|
||||||
</item>
|
|
||||||
</items>"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let mut list = ListCertsResponse::try_from(elem).unwrap();
|
|
||||||
assert_eq!(list.items.len(), 2);
|
|
||||||
|
|
||||||
let item = list.items.pop().unwrap();
|
|
||||||
assert_eq!(item.name.0, "Laptop");
|
|
||||||
assert_eq!(item.cert.data, [4, 16, 65]);
|
|
||||||
assert!(item.users.is_none());
|
|
||||||
|
|
||||||
let item = list.items.pop().unwrap();
|
|
||||||
assert_eq!(item.name.0, "Mobile Client");
|
|
||||||
assert_eq!(item.cert.data, b"\0\0\0");
|
|
||||||
assert_eq!(item.users.unwrap().resources.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let append = Append {
|
|
||||||
name: Name::from_str("Mobile Client").unwrap(),
|
|
||||||
cert: Cert {
|
|
||||||
data: b"\0\0\0".to_vec(),
|
|
||||||
},
|
|
||||||
no_cert_management: false,
|
|
||||||
};
|
|
||||||
let elem: Element = append.into();
|
|
||||||
assert!(elem.is("append", ns::SASL_CERT));
|
|
||||||
|
|
||||||
let disable = Disable {
|
|
||||||
name: Name::from_str("Mobile Client").unwrap(),
|
|
||||||
};
|
|
||||||
let elem: Element = disable.into();
|
|
||||||
assert!(elem.is("disable", ns::SASL_CERT));
|
|
||||||
let elem = elem.children().cloned().collect::<Vec<_>>().pop().unwrap();
|
|
||||||
assert!(elem.is("name", ns::SASL_CERT));
|
|
||||||
assert_eq!(elem.text(), "Mobile Client");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_item() {
|
|
||||||
let reference: Element = "<item xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name><x509cert>AAAA</x509cert></item>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let item = Item {
|
|
||||||
name: Name::from_str("Mobile Client").unwrap(),
|
|
||||||
cert: Cert {
|
|
||||||
data: b"\0\0\0".to_vec(),
|
|
||||||
},
|
|
||||||
no_cert_management: false,
|
|
||||||
users: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = item.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_append() {
|
|
||||||
let reference: Element = "<append xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name><x509cert>AAAA</x509cert></append>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let append = Append {
|
|
||||||
name: Name::from_str("Mobile Client").unwrap(),
|
|
||||||
cert: Cert {
|
|
||||||
data: b"\0\0\0".to_vec(),
|
|
||||||
},
|
|
||||||
no_cert_management: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = append.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_disable() {
|
|
||||||
let reference: Element =
|
|
||||||
"<disable xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></disable>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let disable = Disable {
|
|
||||||
name: Name::from_str("Mobile Client").unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = disable.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_revoke() {
|
|
||||||
let reference: Element =
|
|
||||||
"<revoke xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></revoke>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let revoke = Revoke {
|
|
||||||
name: Name::from_str("Mobile Client").unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = revoke.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
|
|
||||||
generate_element_enum!(
|
|
||||||
/// Enum representing chatstate elements part of the
|
|
||||||
/// `http://jabber.org/protocol/chatstates` namespace.
|
|
||||||
ChatState, "chatstate", CHATSTATES, {
|
|
||||||
/// `<active xmlns='http://jabber.org/protocol/chatstates'/>`
|
|
||||||
Active => "active",
|
|
||||||
|
|
||||||
/// `<composing xmlns='http://jabber.org/protocol/chatstates'/>`
|
|
||||||
Composing => "composing",
|
|
||||||
|
|
||||||
/// `<gone xmlns='http://jabber.org/protocol/chatstates'/>`
|
|
||||||
Gone => "gone",
|
|
||||||
|
|
||||||
/// `<inactive xmlns='http://jabber.org/protocol/chatstates'/>`
|
|
||||||
Inactive => "inactive",
|
|
||||||
|
|
||||||
/// `<paused xmlns='http://jabber.org/protocol/chatstates'/>`
|
|
||||||
Paused => "paused",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for ChatState {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ChatState, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<active xmlns='http://jabber.org/protocol/chatstates'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
ChatState::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element = "<coucou xmlns='http://jabber.org/protocol/chatstates'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = ChatState::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "This is not a chatstate element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<gone xmlns='http://jabber.org/protocol/chatstates'><coucou/></gone>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = ChatState::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in chatstate element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_attribute() {
|
|
||||||
let elem: Element = "<inactive xmlns='http://jabber.org/protocol/chatstates' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = ChatState::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in chatstate element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let chatstate = ChatState::Active;
|
|
||||||
let elem: Element = chatstate.into();
|
|
||||||
assert!(elem.is("active", ns::CHATSTATES));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::util::helpers::PlainText;
|
|
||||||
use digest::Digest;
|
|
||||||
use sha1::Sha1;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The main authentication mechanism for components.
|
|
||||||
#[derive(Default)]
|
|
||||||
Handshake, "handshake", COMPONENT,
|
|
||||||
text: (
|
|
||||||
/// If Some, contains the hex-encoded SHA-1 of the concatenation of the
|
|
||||||
/// stream id and the password, and is used to authenticate against the
|
|
||||||
/// server.
|
|
||||||
///
|
|
||||||
/// If None, it is the successful reply from the server, the stream is now
|
|
||||||
/// fully established and both sides can now exchange stanzas.
|
|
||||||
data: PlainText<Option<String>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Handshake {
|
|
||||||
/// Creates a successful reply from a server.
|
|
||||||
pub fn new() -> Handshake {
|
|
||||||
Handshake::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an authentication request from the component.
|
|
||||||
pub fn from_password_and_stream_id(password: &str, stream_id: &str) -> Handshake {
|
|
||||||
let input = String::from(stream_id) + password;
|
|
||||||
let hash = Sha1::digest(input.as_bytes());
|
|
||||||
let content = format!("{:x}", hash);
|
|
||||||
Handshake {
|
|
||||||
data: Some(content),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Handshake, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Handshake, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<handshake xmlns='jabber:component:accept'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let handshake = Handshake::try_from(elem).unwrap();
|
|
||||||
assert_eq!(handshake.data, None);
|
|
||||||
|
|
||||||
let elem: Element = "<handshake xmlns='jabber:component:accept'>Coucou</handshake>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let handshake = Handshake::try_from(elem).unwrap();
|
|
||||||
assert_eq!(handshake.data, Some(String::from("Coucou")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_constructors() {
|
|
||||||
let handshake = Handshake::new();
|
|
||||||
assert_eq!(handshake.data, None);
|
|
||||||
|
|
||||||
let handshake = Handshake::from_password_and_stream_id("123456", "sid");
|
|
||||||
assert_eq!(
|
|
||||||
handshake.data,
|
|
||||||
Some(String::from("9accec263ab84a43c6037ccf7cd48cb1d3f6df8e"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Stream:feature sent by the server to advertise it supports CSI.
|
|
||||||
Feature,
|
|
||||||
"csi",
|
|
||||||
CSI
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Client indicates it is inactive.
|
|
||||||
Inactive,
|
|
||||||
"inactive",
|
|
||||||
CSI
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Client indicates it is active again.
|
|
||||||
Active,
|
|
||||||
"active",
|
|
||||||
CSI
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Feature, 0);
|
|
||||||
assert_size!(Inactive, 0);
|
|
||||||
assert_size!(Active, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parsing() {
|
|
||||||
let elem: Element = "<csi xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
|
|
||||||
Feature::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<inactive xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
|
|
||||||
Inactive::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<active xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
|
|
||||||
Active::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialising() {
|
|
||||||
let elem: Element = Feature.into();
|
|
||||||
assert!(elem.is("csi", ns::CSI));
|
|
||||||
|
|
||||||
let elem: Element = Inactive.into();
|
|
||||||
assert!(elem.is("inactive", ns::CSI));
|
|
||||||
|
|
||||||
let elem: Element = Active.into();
|
|
||||||
assert!(elem.is("active", ns::CSI));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,385 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::media_element::MediaElement;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Represents one of the possible values for a list- field.
|
|
||||||
Option_, "option", DATA_FORMS,
|
|
||||||
attributes: [
|
|
||||||
/// The optional label to be displayed to the user for this option.
|
|
||||||
label: Option<String> = "label"
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The value returned to the server when selecting this option.
|
|
||||||
value: Required<String> = ("value", DATA_FORMS) => String
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The type of a [field](struct.Field.html) element.
|
|
||||||
FieldType, "type", {
|
|
||||||
/// This field can only take the values "0" or "false" for a false
|
|
||||||
/// value, and "1" or "true" for a true value.
|
|
||||||
Boolean => "boolean",
|
|
||||||
|
|
||||||
/// This field describes data, it must not be sent back to the
|
|
||||||
/// requester.
|
|
||||||
Fixed => "fixed",
|
|
||||||
|
|
||||||
/// This field is hidden, it should not be displayed to the user but
|
|
||||||
/// should be sent back to the requester.
|
|
||||||
Hidden => "hidden",
|
|
||||||
|
|
||||||
/// This field accepts one or more [JIDs](../../jid/struct.Jid.html).
|
|
||||||
/// A client may want to let the user autocomplete them based on their
|
|
||||||
/// contacts list for instance.
|
|
||||||
JidMulti => "jid-multi",
|
|
||||||
|
|
||||||
/// This field accepts one [JID](../../jid/struct.Jid.html). A client
|
|
||||||
/// may want to let the user autocomplete it based on their contacts
|
|
||||||
/// list for instance.
|
|
||||||
JidSingle => "jid-single",
|
|
||||||
|
|
||||||
/// This field accepts one or more values from the list provided as
|
|
||||||
/// [options](struct.Option_.html).
|
|
||||||
ListMulti => "list-multi",
|
|
||||||
|
|
||||||
/// This field accepts one value from the list provided as
|
|
||||||
/// [options](struct.Option_.html).
|
|
||||||
ListSingle => "list-single",
|
|
||||||
|
|
||||||
/// This field accepts one or more free form text lines.
|
|
||||||
TextMulti => "text-multi",
|
|
||||||
|
|
||||||
/// This field accepts one free form password, a client should hide it
|
|
||||||
/// in its user interface.
|
|
||||||
TextPrivate => "text-private",
|
|
||||||
|
|
||||||
/// This field accepts one free form text line.
|
|
||||||
TextSingle => "text-single",
|
|
||||||
}, Default = TextSingle
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Represents a field in a [data form](struct.DataForm.html).
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Field {
|
|
||||||
/// The unique identifier for this field, in the form.
|
|
||||||
pub var: String,
|
|
||||||
|
|
||||||
/// The type of this field.
|
|
||||||
pub type_: FieldType,
|
|
||||||
|
|
||||||
/// The label to be possibly displayed to the user for this field.
|
|
||||||
pub label: Option<String>,
|
|
||||||
|
|
||||||
/// The form will be rejected if this field isn’t present.
|
|
||||||
pub required: bool,
|
|
||||||
|
|
||||||
/// A list of allowed values.
|
|
||||||
pub options: Vec<Option_>,
|
|
||||||
|
|
||||||
/// The values provided for this field.
|
|
||||||
pub values: Vec<String>,
|
|
||||||
|
|
||||||
/// A list of media related to this field.
|
|
||||||
pub media: Vec<MediaElement>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Field {
|
|
||||||
fn is_list(&self) -> bool {
|
|
||||||
self.type_ == FieldType::ListSingle || self.type_ == FieldType::ListMulti
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Field {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Field, Error> {
|
|
||||||
check_self!(elem, "field", DATA_FORMS);
|
|
||||||
check_no_unknown_attributes!(elem, "field", ["label", "type", "var"]);
|
|
||||||
let mut field = Field {
|
|
||||||
var: get_attr!(elem, "var", Required),
|
|
||||||
type_: get_attr!(elem, "type", Default),
|
|
||||||
label: get_attr!(elem, "label", Option),
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![],
|
|
||||||
media: vec![],
|
|
||||||
};
|
|
||||||
for element in elem.children() {
|
|
||||||
if element.is("value", ns::DATA_FORMS) {
|
|
||||||
check_no_children!(element, "value");
|
|
||||||
check_no_attributes!(element, "value");
|
|
||||||
field.values.push(element.text());
|
|
||||||
} else if element.is("required", ns::DATA_FORMS) {
|
|
||||||
if field.required {
|
|
||||||
return Err(Error::ParseError("More than one required element."));
|
|
||||||
}
|
|
||||||
check_no_children!(element, "required");
|
|
||||||
check_no_attributes!(element, "required");
|
|
||||||
field.required = true;
|
|
||||||
} else if element.is("option", ns::DATA_FORMS) {
|
|
||||||
if !field.is_list() {
|
|
||||||
return Err(Error::ParseError("Option element found in non-list field."));
|
|
||||||
}
|
|
||||||
let option = Option_::try_from(element.clone())?;
|
|
||||||
field.options.push(option);
|
|
||||||
} else if element.is("media", ns::MEDIA_ELEMENT) {
|
|
||||||
let media_element = MediaElement::try_from(element.clone())?;
|
|
||||||
field.media.push(media_element);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Field child isn’t a value, option or media element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Field> for Element {
|
|
||||||
fn from(field: Field) -> Element {
|
|
||||||
Element::builder("field", ns::DATA_FORMS)
|
|
||||||
.attr("var", field.var)
|
|
||||||
.attr("type", field.type_)
|
|
||||||
.attr("label", field.label)
|
|
||||||
.append_all(if field.required {
|
|
||||||
Some(Element::builder("required", ns::DATA_FORMS))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.append_all(field.options.iter().cloned().map(Element::from))
|
|
||||||
.append_all(
|
|
||||||
field
|
|
||||||
.values
|
|
||||||
.into_iter()
|
|
||||||
.map(|value| Element::builder("value", ns::DATA_FORMS).append(value)),
|
|
||||||
)
|
|
||||||
.append_all(field.media.iter().cloned().map(Element::from))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Represents the type of a [data form](struct.DataForm.html).
|
|
||||||
DataFormType, "type", {
|
|
||||||
/// This is a cancel request for a prior type="form" data form.
|
|
||||||
Cancel => "cancel",
|
|
||||||
|
|
||||||
/// This is a request for the recipient to fill this form and send it
|
|
||||||
/// back as type="submit".
|
|
||||||
Form => "form",
|
|
||||||
|
|
||||||
/// This is a result form, which contains what the requester asked for.
|
|
||||||
Result_ => "result",
|
|
||||||
|
|
||||||
/// This is a complete response to a form received before.
|
|
||||||
Submit => "submit",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/// This is a form to be sent to another entity for filling.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct DataForm {
|
|
||||||
/// The type of this form, telling the other party which action to execute.
|
|
||||||
pub type_: DataFormType,
|
|
||||||
|
|
||||||
/// An easy accessor for the FORM_TYPE of this form, see
|
|
||||||
/// [XEP-0068](https://xmpp.org/extensions/xep-0068.html) for more
|
|
||||||
/// information.
|
|
||||||
pub form_type: Option<String>,
|
|
||||||
|
|
||||||
/// The title of this form.
|
|
||||||
pub title: Option<String>,
|
|
||||||
|
|
||||||
/// The instructions given with this form.
|
|
||||||
pub instructions: Option<String>,
|
|
||||||
|
|
||||||
/// A list of fields comprising this form.
|
|
||||||
pub fields: Vec<Field>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for DataForm {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<DataForm, Error> {
|
|
||||||
check_self!(elem, "x", DATA_FORMS);
|
|
||||||
check_no_unknown_attributes!(elem, "x", ["type"]);
|
|
||||||
let type_ = get_attr!(elem, "type", Required);
|
|
||||||
let mut form = DataForm {
|
|
||||||
type_,
|
|
||||||
form_type: None,
|
|
||||||
title: None,
|
|
||||||
instructions: None,
|
|
||||||
fields: vec![],
|
|
||||||
};
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("title", ns::DATA_FORMS) {
|
|
||||||
if form.title.is_some() {
|
|
||||||
return Err(Error::ParseError("More than one title in form element."));
|
|
||||||
}
|
|
||||||
check_no_children!(child, "title");
|
|
||||||
check_no_attributes!(child, "title");
|
|
||||||
form.title = Some(child.text());
|
|
||||||
} else if child.is("instructions", ns::DATA_FORMS) {
|
|
||||||
if form.instructions.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"More than one instructions in form element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
check_no_children!(child, "instructions");
|
|
||||||
check_no_attributes!(child, "instructions");
|
|
||||||
form.instructions = Some(child.text());
|
|
||||||
} else if child.is("field", ns::DATA_FORMS) {
|
|
||||||
let field = Field::try_from(child.clone())?;
|
|
||||||
if field.var == "FORM_TYPE" {
|
|
||||||
let mut field = field;
|
|
||||||
if form.form_type.is_some() {
|
|
||||||
return Err(Error::ParseError("More than one FORM_TYPE in a data form."));
|
|
||||||
}
|
|
||||||
if field.type_ != FieldType::Hidden {
|
|
||||||
return Err(Error::ParseError("Invalid field type for FORM_TYPE."));
|
|
||||||
}
|
|
||||||
if field.values.len() != 1 {
|
|
||||||
return Err(Error::ParseError("Wrong number of values in FORM_TYPE."));
|
|
||||||
}
|
|
||||||
form.form_type = field.values.pop();
|
|
||||||
} else {
|
|
||||||
form.fields.push(field);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in data form element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(form)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DataForm> for Element {
|
|
||||||
fn from(form: DataForm) -> Element {
|
|
||||||
Element::builder("x", ns::DATA_FORMS)
|
|
||||||
.attr("type", form.type_)
|
|
||||||
.append_all(
|
|
||||||
form.title
|
|
||||||
.map(|title| Element::builder("title", ns::DATA_FORMS).append(title)),
|
|
||||||
)
|
|
||||||
.append_all(
|
|
||||||
form.instructions
|
|
||||||
.map(|text| Element::builder("instructions", ns::DATA_FORMS).append(text)),
|
|
||||||
)
|
|
||||||
.append_all(form.form_type.map(|form_type| {
|
|
||||||
Element::builder("field", ns::DATA_FORMS)
|
|
||||||
.attr("var", "FORM_TYPE")
|
|
||||||
.attr("type", "hidden")
|
|
||||||
.append(Element::builder("value", ns::DATA_FORMS).append(form_type))
|
|
||||||
}))
|
|
||||||
.append_all(form.fields.iter().cloned().map(Element::from))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Option_, 24);
|
|
||||||
assert_size!(FieldType, 1);
|
|
||||||
assert_size!(Field, 64);
|
|
||||||
assert_size!(DataFormType, 1);
|
|
||||||
assert_size!(DataForm, 52);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Option_, 48);
|
|
||||||
assert_size!(FieldType, 1);
|
|
||||||
assert_size!(Field, 128);
|
|
||||||
assert_size!(DataFormType, 1);
|
|
||||||
assert_size!(DataForm, 104);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<x xmlns='jabber:x:data' type='result'/>".parse().unwrap();
|
|
||||||
let form = DataForm::try_from(elem).unwrap();
|
|
||||||
assert_eq!(form.type_, DataFormType::Result_);
|
|
||||||
assert!(form.form_type.is_none());
|
|
||||||
assert!(form.fields.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element = "<x xmlns='jabber:x:data'/>".parse().unwrap();
|
|
||||||
let error = DataForm::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'type' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<x xmlns='jabber:x:data' type='coucou'/>".parse().unwrap();
|
|
||||||
let error = DataForm::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'type' attribute.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_wrong_child() {
|
|
||||||
let elem: Element = "<x xmlns='jabber:x:data' type='cancel'><coucou/></x>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = DataForm::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in data form element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn option() {
|
|
||||||
let elem: Element =
|
|
||||||
"<option xmlns='jabber:x:data' label='Coucou !'><value>coucou</value></option>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let option = Option_::try_from(elem).unwrap();
|
|
||||||
assert_eq!(&option.label.unwrap(), "Coucou !");
|
|
||||||
assert_eq!(&option.value, "coucou");
|
|
||||||
|
|
||||||
let elem: Element = "<option xmlns='jabber:x:data' label='Coucou !'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Option_::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Missing child value in option element.");
|
|
||||||
|
|
||||||
let elem: Element = "<option xmlns='jabber:x:data' label='Coucou !'><value>coucou</value><value>error</value></option>".parse().unwrap();
|
|
||||||
let error = Option_::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
message,
|
|
||||||
"Element option must not have more than one value child."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use chrono::{DateTime as ChronoDateTime, FixedOffset};
|
|
||||||
use minidom::{IntoAttributeValue, Node};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// Implements the DateTime profile of XEP-0082, which represents a
|
|
||||||
/// non-recurring moment in time, with an accuracy of seconds or fraction of
|
|
||||||
/// seconds, and includes a timezone.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct DateTime(pub ChronoDateTime<FixedOffset>);
|
|
||||||
|
|
||||||
impl DateTime {
|
|
||||||
/// Retrieves the associated timezone.
|
|
||||||
pub fn timezone(&self) -> FixedOffset {
|
|
||||||
self.0.timezone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a new `DateTime` with a different timezone.
|
|
||||||
pub fn with_timezone(&self, tz: FixedOffset) -> DateTime {
|
|
||||||
DateTime(self.0.with_timezone(&tz))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formats this `DateTime` with the specified format string.
|
|
||||||
pub fn format(&self, fmt: &str) -> String {
|
|
||||||
format!("{}", self.0.format(fmt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for DateTime {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<DateTime, Error> {
|
|
||||||
Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoAttributeValue for DateTime {
|
|
||||||
fn into_attribute_value(self) -> Option<String> {
|
|
||||||
Some(self.0.to_rfc3339())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<Node> for DateTime {
|
|
||||||
fn into(self) -> Node {
|
|
||||||
Node::Text(self.0.to_rfc3339())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use chrono::{Datelike, Timelike};
|
|
||||||
|
|
||||||
// DateTime’s size doesn’t depend on the architecture.
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(DateTime, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
|
|
||||||
assert_eq!(date.0.year(), 2002);
|
|
||||||
assert_eq!(date.0.month(), 9);
|
|
||||||
assert_eq!(date.0.day(), 10);
|
|
||||||
assert_eq!(date.0.hour(), 23);
|
|
||||||
assert_eq!(date.0.minute(), 08);
|
|
||||||
assert_eq!(date.0.second(), 25);
|
|
||||||
assert_eq!(date.0.nanosecond(), 0);
|
|
||||||
assert_eq!(date.0.timezone(), FixedOffset::east(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_date() {
|
|
||||||
// There is no thirteenth month.
|
|
||||||
let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input is out of range");
|
|
||||||
|
|
||||||
// Timezone ≥24:00 aren’t allowed.
|
|
||||||
let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input is out of range");
|
|
||||||
|
|
||||||
// Timezone without the : separator aren’t allowed.
|
|
||||||
let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input contains invalid characters");
|
|
||||||
|
|
||||||
// No seconds, error message could be improved.
|
|
||||||
let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input contains invalid characters");
|
|
||||||
|
|
||||||
// TODO: maybe we’ll want to support this one, as per XEP-0082 §4.
|
|
||||||
let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input contains invalid characters");
|
|
||||||
|
|
||||||
// No timezone.
|
|
||||||
let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "premature end of input");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let date =
|
|
||||||
DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
|
|
||||||
let attr = date.into_attribute_value();
|
|
||||||
assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::date::DateTime;
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
use crate::presence::PresencePayload;
|
|
||||||
use crate::util::helpers::PlainText;
|
|
||||||
use jid::Jid;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Notes when and by whom a message got stored for later delivery.
|
|
||||||
Delay, "delay", DELAY,
|
|
||||||
attributes: [
|
|
||||||
/// The entity which delayed this message.
|
|
||||||
from: Option<Jid> = "from",
|
|
||||||
|
|
||||||
/// The time at which this message got stored.
|
|
||||||
stamp: Required<DateTime> = "stamp"
|
|
||||||
],
|
|
||||||
text: (
|
|
||||||
/// The optional reason this message got delayed.
|
|
||||||
data: PlainText<Option<String>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Delay {}
|
|
||||||
impl PresencePayload for Delay {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::BareJid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Delay, 68);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Delay, 120);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element =
|
|
||||||
"<delay xmlns='urn:xmpp:delay' from='capulet.com' stamp='2002-09-10T23:08:25Z'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let delay = Delay::try_from(elem).unwrap();
|
|
||||||
assert_eq!(delay.from.unwrap(), BareJid::domain("capulet.com"));
|
|
||||||
assert_eq!(
|
|
||||||
delay.stamp,
|
|
||||||
DateTime::from_str("2002-09-10T23:08:25Z").unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(delay.data, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unknown() {
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Delay::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "This is not a delay element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<delay xmlns='urn:xmpp:delay'><coucou/></delay>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Delay::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in delay element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "<delay xmlns='urn:xmpp:delay' stamp='2002-09-10T23:08:25+00:00'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let delay = Delay {
|
|
||||||
from: None,
|
|
||||||
stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(),
|
|
||||||
data: None,
|
|
||||||
};
|
|
||||||
let elem2 = delay.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise_data() {
|
|
||||||
let elem: Element = "<delay xmlns='urn:xmpp:delay' from='juliet@example.org' stamp='2002-09-10T23:08:25+00:00'>Reason</delay>".parse().unwrap();
|
|
||||||
let delay = Delay {
|
|
||||||
from: Some(Jid::Bare(BareJid::new("juliet", "example.org"))),
|
|
||||||
stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(),
|
|
||||||
data: Some(String::from("Reason")),
|
|
||||||
};
|
|
||||||
let elem2 = delay.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,452 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::data_forms::{DataForm, DataFormType};
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
|
|
||||||
///
|
|
||||||
/// It should only be used in an `<iq type='get'/>`, as it can only represent
|
|
||||||
/// the request, and not a result.
|
|
||||||
DiscoInfoQuery, "query", DISCO_INFO,
|
|
||||||
attributes: [
|
|
||||||
/// Node on which we are doing the discovery.
|
|
||||||
node: Option<String> = "node",
|
|
||||||
]);
|
|
||||||
|
|
||||||
impl IqGetPayload for DiscoInfoQuery {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
#[derive(Eq, Hash)]
|
|
||||||
/// Structure representing a `<feature xmlns='http://jabber.org/protocol/disco#info'/>` element.
|
|
||||||
Feature, "feature", DISCO_INFO,
|
|
||||||
attributes: [
|
|
||||||
/// Namespace of the feature we want to represent.
|
|
||||||
var: Required<String> = "var",
|
|
||||||
]);
|
|
||||||
|
|
||||||
impl Feature {
|
|
||||||
/// Create a new `<feature/>` with the according `@var`.
|
|
||||||
pub fn new<S: Into<String>>(var: S) -> Feature {
|
|
||||||
Feature { var: var.into() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Structure representing an `<identity xmlns='http://jabber.org/protocol/disco#info'/>` element.
|
|
||||||
Identity, "identity", DISCO_INFO,
|
|
||||||
attributes: [
|
|
||||||
/// Category of this identity.
|
|
||||||
// TODO: use an enum here.
|
|
||||||
category: RequiredNonEmpty<String> = "category",
|
|
||||||
|
|
||||||
/// Type of this identity.
|
|
||||||
// TODO: use an enum here.
|
|
||||||
type_: RequiredNonEmpty<String> = "type",
|
|
||||||
|
|
||||||
/// Lang of the name of this identity.
|
|
||||||
lang: Option<String> = "xml:lang",
|
|
||||||
|
|
||||||
/// Name of this identity.
|
|
||||||
name: Option<String> = "name",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Identity {
|
|
||||||
/// Create a new `<identity/>`.
|
|
||||||
pub fn new<C, T, L, N>(category: C, type_: T, lang: L, name: N) -> Identity
|
|
||||||
where
|
|
||||||
C: Into<String>,
|
|
||||||
T: Into<String>,
|
|
||||||
L: Into<String>,
|
|
||||||
N: Into<String>,
|
|
||||||
{
|
|
||||||
Identity {
|
|
||||||
category: category.into(),
|
|
||||||
type_: type_.into(),
|
|
||||||
lang: Some(lang.into()),
|
|
||||||
name: Some(name.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new `<identity/>` without a name.
|
|
||||||
pub fn new_anonymous<C, T, L, N>(category: C, type_: T) -> Identity
|
|
||||||
where
|
|
||||||
C: Into<String>,
|
|
||||||
T: Into<String>,
|
|
||||||
{
|
|
||||||
Identity {
|
|
||||||
category: category.into(),
|
|
||||||
type_: type_.into(),
|
|
||||||
lang: None,
|
|
||||||
name: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
|
|
||||||
///
|
|
||||||
/// It should only be used in an `<iq type='result'/>`, as it can only
|
|
||||||
/// represent the result, and not a request.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DiscoInfoResult {
|
|
||||||
/// Node on which we have done this discovery.
|
|
||||||
pub node: Option<String>,
|
|
||||||
|
|
||||||
/// List of identities exposed by this entity.
|
|
||||||
pub identities: Vec<Identity>,
|
|
||||||
|
|
||||||
/// List of features supported by this entity.
|
|
||||||
pub features: Vec<Feature>,
|
|
||||||
|
|
||||||
/// List of extensions reported by this entity.
|
|
||||||
pub extensions: Vec<DataForm>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IqResultPayload for DiscoInfoResult {}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for DiscoInfoResult {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<DiscoInfoResult, Error> {
|
|
||||||
check_self!(elem, "query", DISCO_INFO, "disco#info result");
|
|
||||||
check_no_unknown_attributes!(elem, "disco#info result", ["node"]);
|
|
||||||
|
|
||||||
let mut result = DiscoInfoResult {
|
|
||||||
node: get_attr!(elem, "node", Option),
|
|
||||||
identities: vec![],
|
|
||||||
features: vec![],
|
|
||||||
extensions: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("identity", ns::DISCO_INFO) {
|
|
||||||
let identity = Identity::try_from(child.clone())?;
|
|
||||||
result.identities.push(identity);
|
|
||||||
} else if child.is("feature", ns::DISCO_INFO) {
|
|
||||||
let feature = Feature::try_from(child.clone())?;
|
|
||||||
result.features.push(feature);
|
|
||||||
} else if child.is("x", ns::DATA_FORMS) {
|
|
||||||
let data_form = DataForm::try_from(child.clone())?;
|
|
||||||
if data_form.type_ != DataFormType::Result_ {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Data form must have a 'result' type in disco#info.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if data_form.form_type.is_none() {
|
|
||||||
return Err(Error::ParseError("Data form found without a FORM_TYPE."));
|
|
||||||
}
|
|
||||||
result.extensions.push(data_form);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown element in disco#info."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.identities.is_empty() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"There must be at least one identity in disco#info.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if result.features.is_empty() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"There must be at least one feature in disco#info.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if !result.features.contains(&Feature {
|
|
||||||
var: ns::DISCO_INFO.to_owned(),
|
|
||||||
}) {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"disco#info feature not present in disco#info.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DiscoInfoResult> for Element {
|
|
||||||
fn from(disco: DiscoInfoResult) -> Element {
|
|
||||||
Element::builder("query", ns::DISCO_INFO)
|
|
||||||
.attr("node", disco.node)
|
|
||||||
.append_all(disco.identities.into_iter())
|
|
||||||
.append_all(disco.features.into_iter())
|
|
||||||
.append_all(disco.extensions.iter().cloned().map(Element::from))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#items'/>` element.
|
|
||||||
///
|
|
||||||
/// It should only be used in an `<iq type='get'/>`, as it can only represent
|
|
||||||
/// the request, and not a result.
|
|
||||||
DiscoItemsQuery, "query", DISCO_ITEMS,
|
|
||||||
attributes: [
|
|
||||||
/// Node on which we are doing the discovery.
|
|
||||||
node: Option<String> = "node",
|
|
||||||
]);
|
|
||||||
|
|
||||||
impl IqGetPayload for DiscoItemsQuery {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Structure representing an `<item xmlns='http://jabber.org/protocol/disco#items'/>` element.
|
|
||||||
Item, "item", DISCO_ITEMS,
|
|
||||||
attributes: [
|
|
||||||
/// JID of the entity pointed by this item.
|
|
||||||
jid: Required<Jid> = "jid",
|
|
||||||
/// Node of the entity pointed by this item.
|
|
||||||
node: Option<String> = "node",
|
|
||||||
/// Name of the entity pointed by this item.
|
|
||||||
name: Option<String> = "name",
|
|
||||||
]);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Structure representing a `<query
|
|
||||||
/// xmlns='http://jabber.org/protocol/disco#items'/>` element.
|
|
||||||
///
|
|
||||||
/// It should only be used in an `<iq type='result'/>`, as it can only
|
|
||||||
/// represent the result, and not a request.
|
|
||||||
DiscoItemsResult, "query", DISCO_ITEMS,
|
|
||||||
attributes: [
|
|
||||||
/// Node on which we have done this discovery.
|
|
||||||
node: Option<String> = "node"
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// List of items pointed by this entity.
|
|
||||||
items: Vec<Item> = ("item", DISCO_ITEMS) => Item
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqResultPayload for DiscoItemsResult {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use jid::BareJid;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Identity, 48);
|
|
||||||
assert_size!(Feature, 12);
|
|
||||||
assert_size!(DiscoInfoQuery, 12);
|
|
||||||
assert_size!(DiscoInfoResult, 48);
|
|
||||||
|
|
||||||
assert_size!(Item, 64);
|
|
||||||
assert_size!(DiscoItemsQuery, 12);
|
|
||||||
assert_size!(DiscoItemsResult, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Identity, 96);
|
|
||||||
assert_size!(Feature, 24);
|
|
||||||
assert_size!(DiscoInfoQuery, 24);
|
|
||||||
assert_size!(DiscoInfoResult, 96);
|
|
||||||
|
|
||||||
assert_size!(Item, 128);
|
|
||||||
assert_size!(DiscoItemsQuery, 24);
|
|
||||||
assert_size!(DiscoItemsResult, 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
|
||||||
let query = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
assert!(query.node.is_none());
|
|
||||||
assert_eq!(query.identities.len(), 1);
|
|
||||||
assert_eq!(query.features.len(), 1);
|
|
||||||
assert!(query.extensions.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_identity_after_feature() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><feature var='http://jabber.org/protocol/disco#info'/><identity category='client' type='pc'/></query>".parse().unwrap();
|
|
||||||
let query = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
assert_eq!(query.identities.len(), 1);
|
|
||||||
assert_eq!(query.features.len(), 1);
|
|
||||||
assert!(query.extensions.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_feature_after_dataform() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>coucou</value></field></x><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
|
||||||
let query = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
assert_eq!(query.identities.len(), 1);
|
|
||||||
assert_eq!(query.features.len(), 1);
|
|
||||||
assert_eq!(query.extensions.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extension() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>example</value></field></x></query>".parse().unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let query = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
assert!(query.node.is_none());
|
|
||||||
assert_eq!(query.identities.len(), 1);
|
|
||||||
assert_eq!(query.features.len(), 1);
|
|
||||||
assert_eq!(query.extensions.len(), 1);
|
|
||||||
assert_eq!(query.extensions[0].form_type, Some(String::from("example")));
|
|
||||||
|
|
||||||
let elem2 = query.into();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element =
|
|
||||||
"<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown element in disco#info.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_identity() {
|
|
||||||
let elem: Element =
|
|
||||||
"<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'category' missing.");
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<query xmlns='http://jabber.org/protocol/disco#info'><identity category=''/></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'category' must not be empty.");
|
|
||||||
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
|
|
||||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'type' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou' type=''/></query>".parse().unwrap();
|
|
||||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'type' must not be empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_feature() {
|
|
||||||
let elem: Element =
|
|
||||||
"<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'var' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_result() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
message,
|
|
||||||
"There must be at least one identity in disco#info."
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
|
|
||||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "There must be at least one feature in disco#info.");
|
|
||||||
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#items'/></query>".parse().unwrap();
|
|
||||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "disco#info feature not present in disco#info.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_items() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let query = DiscoItemsQuery::try_from(elem).unwrap();
|
|
||||||
assert!(query.node.is_none());
|
|
||||||
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let query = DiscoItemsQuery::try_from(elem).unwrap();
|
|
||||||
assert_eq!(query.node, Some(String::from("coucou")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_items_result() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let query = DiscoItemsResult::try_from(elem).unwrap();
|
|
||||||
assert!(query.node.is_none());
|
|
||||||
assert!(query.items.is_empty());
|
|
||||||
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let query = DiscoItemsResult::try_from(elem).unwrap();
|
|
||||||
assert_eq!(query.node, Some(String::from("coucou")));
|
|
||||||
assert!(query.items.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_answers_items_result() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'><item jid='component'/><item jid='component2' node='test' name='A component'/></query>".parse().unwrap();
|
|
||||||
let query = DiscoItemsResult::try_from(elem).unwrap();
|
|
||||||
let elem2 = Element::from(query);
|
|
||||||
let query = DiscoItemsResult::try_from(elem2).unwrap();
|
|
||||||
assert_eq!(query.items.len(), 2);
|
|
||||||
assert_eq!(query.items[0].jid, BareJid::domain("component"));
|
|
||||||
assert_eq!(query.items[0].node, None);
|
|
||||||
assert_eq!(query.items[0].name, None);
|
|
||||||
assert_eq!(query.items[1].jid, BareJid::domain("component2"));
|
|
||||||
assert_eq!(query.items[1].node, Some(String::from("test")));
|
|
||||||
assert_eq!(query.items[1].name, Some(String::from("A component")));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,481 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::data_forms::DataForm;
|
|
||||||
use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity};
|
|
||||||
use crate::hashes::{Algo, Hash};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::presence::PresencePayload;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use blake2::VarBlake2b;
|
|
||||||
use digest::{Digest, Update, VariableOutput};
|
|
||||||
use sha2::{Sha256, Sha512};
|
|
||||||
use sha3::{Sha3_256, Sha3_512};
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Represents a set of capability hashes, all of them must correspond to
|
|
||||||
/// the same input [disco#info](../disco/struct.DiscoInfoResult.html),
|
|
||||||
/// using different [algorithms](../hashes/enum.Algo.html).
|
|
||||||
ECaps2, "c", ECAPS2,
|
|
||||||
children: [
|
|
||||||
/// Hashes of the [disco#info](../disco/struct.DiscoInfoResult.html).
|
|
||||||
hashes: Vec<Hash> = ("hash", HASHES) => Hash
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PresencePayload for ECaps2 {}
|
|
||||||
|
|
||||||
impl ECaps2 {
|
|
||||||
/// Create an ECaps2 element from a list of hashes.
|
|
||||||
pub fn new(hashes: Vec<Hash>) -> ECaps2 {
|
|
||||||
ECaps2 { hashes }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_item(field: &str) -> Vec<u8> {
|
|
||||||
let mut bytes = field.as_bytes().to_vec();
|
|
||||||
bytes.push(0x1f);
|
|
||||||
bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_items<T, F: Fn(&T) -> Vec<u8>>(things: &[T], separator: u8, encode: F) -> Vec<u8> {
|
|
||||||
let mut string: Vec<u8> = vec![];
|
|
||||||
let mut accumulator: Vec<Vec<u8>> = vec![];
|
|
||||||
for thing in things {
|
|
||||||
let bytes = encode(thing);
|
|
||||||
accumulator.push(bytes);
|
|
||||||
}
|
|
||||||
// This works using the expected i;octet collation.
|
|
||||||
accumulator.sort();
|
|
||||||
for mut bytes in accumulator {
|
|
||||||
string.append(&mut bytes);
|
|
||||||
}
|
|
||||||
string.push(separator);
|
|
||||||
string
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_features(features: &[Feature]) -> Vec<u8> {
|
|
||||||
compute_items(features, 0x1c, |feature| compute_item(&feature.var))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_identities(identities: &[Identity]) -> Vec<u8> {
|
|
||||||
compute_items(identities, 0x1c, |identity| {
|
|
||||||
let mut bytes = compute_item(&identity.category);
|
|
||||||
bytes.append(&mut compute_item(&identity.type_));
|
|
||||||
bytes.append(&mut compute_item(
|
|
||||||
&identity.lang.clone().unwrap_or_default(),
|
|
||||||
));
|
|
||||||
bytes.append(&mut compute_item(
|
|
||||||
&identity.name.clone().unwrap_or_default(),
|
|
||||||
));
|
|
||||||
bytes.push(0x1e);
|
|
||||||
bytes
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_extensions(extensions: &[DataForm]) -> Result<Vec<u8>, Error> {
|
|
||||||
for extension in extensions {
|
|
||||||
if extension.form_type.is_none() {
|
|
||||||
return Err(Error::ParseError("Missing FORM_TYPE in extension."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(compute_items(extensions, 0x1c, |extension| {
|
|
||||||
let mut bytes = compute_item("FORM_TYPE");
|
|
||||||
bytes.append(&mut compute_item(
|
|
||||||
if let Some(ref form_type) = extension.form_type {
|
|
||||||
form_type
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
bytes.push(0x1e);
|
|
||||||
bytes.append(&mut compute_items(&extension.fields, 0x1d, |field| {
|
|
||||||
let mut bytes = compute_item(&field.var);
|
|
||||||
bytes.append(&mut compute_items(&field.values, 0x1e, |value| {
|
|
||||||
compute_item(value)
|
|
||||||
}));
|
|
||||||
bytes
|
|
||||||
}));
|
|
||||||
bytes
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Applies the [algorithm from
|
|
||||||
/// XEP-0390](https://xmpp.org/extensions/xep-0390.html#algorithm-input) on a
|
|
||||||
/// [disco#info query element](../disco/struct.DiscoInfoResult.html).
|
|
||||||
pub fn compute_disco(disco: &DiscoInfoResult) -> Result<Vec<u8>, Error> {
|
|
||||||
let features_string = compute_features(&disco.features);
|
|
||||||
let identities_string = compute_identities(&disco.identities);
|
|
||||||
let extensions_string = compute_extensions(&disco.extensions)?;
|
|
||||||
|
|
||||||
let mut final_string = vec![];
|
|
||||||
final_string.extend(features_string);
|
|
||||||
final_string.extend(identities_string);
|
|
||||||
final_string.extend(extensions_string);
|
|
||||||
Ok(final_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_hash_vec(hash: &[u8]) -> Vec<u8> {
|
|
||||||
let mut vec = Vec::with_capacity(hash.len());
|
|
||||||
vec.extend_from_slice(hash);
|
|
||||||
vec
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hashes the result of [compute_disco()] with one of the supported [hash
|
|
||||||
/// algorithms](../hashes/enum.Algo.html).
|
|
||||||
pub fn hash_ecaps2(data: &[u8], algo: Algo) -> Result<Hash, Error> {
|
|
||||||
Ok(Hash {
|
|
||||||
hash: match algo {
|
|
||||||
Algo::Sha_256 => {
|
|
||||||
let hash = Sha256::digest(data);
|
|
||||||
get_hash_vec(hash.as_slice())
|
|
||||||
}
|
|
||||||
Algo::Sha_512 => {
|
|
||||||
let hash = Sha512::digest(data);
|
|
||||||
get_hash_vec(hash.as_slice())
|
|
||||||
}
|
|
||||||
Algo::Sha3_256 => {
|
|
||||||
let hash = Sha3_256::digest(data);
|
|
||||||
get_hash_vec(hash.as_slice())
|
|
||||||
}
|
|
||||||
Algo::Sha3_512 => {
|
|
||||||
let hash = Sha3_512::digest(data);
|
|
||||||
get_hash_vec(hash.as_slice())
|
|
||||||
}
|
|
||||||
Algo::Blake2b_256 => {
|
|
||||||
let mut hasher = VarBlake2b::new(32).unwrap();
|
|
||||||
hasher.update(data);
|
|
||||||
let mut vec = Vec::with_capacity(32);
|
|
||||||
hasher.finalize_variable(|slice| vec.extend_from_slice(slice));
|
|
||||||
vec
|
|
||||||
}
|
|
||||||
Algo::Blake2b_512 => {
|
|
||||||
let mut hasher = VarBlake2b::new(64).unwrap();
|
|
||||||
hasher.update(data);
|
|
||||||
let mut vec = Vec::with_capacity(64);
|
|
||||||
hasher.finalize_variable(|slice| vec.extend_from_slice(slice));
|
|
||||||
vec
|
|
||||||
}
|
|
||||||
Algo::Sha_1 => return Err(Error::ParseError("Disabled algorithm sha-1: unsafe.")),
|
|
||||||
Algo::Unknown(_algo) => return Err(Error::ParseError("Unknown algorithm in ecaps2.")),
|
|
||||||
},
|
|
||||||
algo,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to create the query for the disco#info corresponding to an
|
|
||||||
/// ecaps2 hash.
|
|
||||||
pub fn query_ecaps2(hash: Hash) -> DiscoInfoQuery {
|
|
||||||
DiscoInfoQuery {
|
|
||||||
node: Some(format!(
|
|
||||||
"{}#{}.{}",
|
|
||||||
ns::ECAPS2,
|
|
||||||
String::from(hash.algo),
|
|
||||||
base64::encode(&hash.hash)
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ECaps2, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ECaps2, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse() {
|
|
||||||
let elem: Element = "<c xmlns='urn:xmpp:caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash><hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=</hash></c>".parse().unwrap();
|
|
||||||
let ecaps2 = ECaps2::try_from(elem).unwrap();
|
|
||||||
assert_eq!(ecaps2.hashes.len(), 2);
|
|
||||||
assert_eq!(ecaps2.hashes[0].algo, Algo::Sha_256);
|
|
||||||
assert_eq!(
|
|
||||||
ecaps2.hashes[0].hash,
|
|
||||||
base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(ecaps2.hashes[1].algo, Algo::Sha3_256);
|
|
||||||
assert_eq!(
|
|
||||||
ecaps2.hashes[1].hash,
|
|
||||||
base64::decode("+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<c xmlns='urn:xmpp:caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash><hash xmlns='urn:xmpp:hashes:1' algo='sha3-256'>+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=</hash></c>".parse().unwrap();
|
|
||||||
let error = ECaps2::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in c element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
|
||||||
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
let ecaps2 = compute_disco(&disco).unwrap();
|
|
||||||
assert_eq!(ecaps2.len(), 54);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_xep_ex1() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns="http://jabber.org/protocol/disco#info">
|
|
||||||
<identity category="client" name="BombusMod" type="mobile"/>
|
|
||||||
<feature var="http://jabber.org/protocol/si"/>
|
|
||||||
<feature var="http://jabber.org/protocol/bytestreams"/>
|
|
||||||
<feature var="http://jabber.org/protocol/chatstates"/>
|
|
||||||
<feature var="http://jabber.org/protocol/disco#info"/>
|
|
||||||
<feature var="http://jabber.org/protocol/disco#items"/>
|
|
||||||
<feature var="urn:xmpp:ping"/>
|
|
||||||
<feature var="jabber:iq:time"/>
|
|
||||||
<feature var="jabber:iq:privacy"/>
|
|
||||||
<feature var="jabber:iq:version"/>
|
|
||||||
<feature var="http://jabber.org/protocol/rosterx"/>
|
|
||||||
<feature var="urn:xmpp:time"/>
|
|
||||||
<feature var="jabber:x:oob"/>
|
|
||||||
<feature var="http://jabber.org/protocol/ibb"/>
|
|
||||||
<feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
|
|
||||||
<feature var="urn:xmpp:receipts"/>
|
|
||||||
<feature var="jabber:iq:roster"/>
|
|
||||||
<feature var="jabber:iq:last"/>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let expected = vec![
|
|
||||||
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
|
||||||
114, 111, 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109,
|
|
||||||
115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103,
|
|
||||||
47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116,
|
|
||||||
101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
|
||||||
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105,
|
|
||||||
110, 102, 111, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111,
|
|
||||||
114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35,
|
|
||||||
105, 116, 101, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114,
|
|
||||||
46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104,
|
|
||||||
116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114,
|
|
||||||
111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, 114, 120, 31, 104, 116, 116,
|
|
||||||
112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116,
|
|
||||||
111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98,
|
|
||||||
101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105,
|
|
||||||
47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, 101, 45, 116, 114, 97, 110,
|
|
||||||
115, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116,
|
|
||||||
31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31,
|
|
||||||
106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97,
|
|
||||||
98, 98, 101, 114, 58, 105, 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114,
|
|
||||||
58, 105, 113, 58, 118, 101, 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58,
|
|
||||||
120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110,
|
|
||||||
103, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116,
|
|
||||||
115, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99,
|
|
||||||
108, 105, 101, 110, 116, 31, 109, 111, 98, 105, 108, 101, 31, 31, 66, 111, 109, 98,
|
|
||||||
117, 115, 77, 111, 100, 31, 30, 28, 28,
|
|
||||||
];
|
|
||||||
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
let ecaps2 = compute_disco(&disco).unwrap();
|
|
||||||
assert_eq!(ecaps2.len(), 0x1d9);
|
|
||||||
assert_eq!(ecaps2, expected);
|
|
||||||
|
|
||||||
let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
sha_256.hash,
|
|
||||||
base64::decode("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=").unwrap()
|
|
||||||
);
|
|
||||||
let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
sha3_256.hash,
|
|
||||||
base64::decode("79mdYAfU9rEdTOcWDO7UEAt6E56SUzk/g6TnqUeuD9Q=").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_xep_ex2() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns="http://jabber.org/protocol/disco#info">
|
|
||||||
<identity category="client" name="Tkabber" type="pc" xml:lang="en"/>
|
|
||||||
<identity category="client" name="Ткаббер" type="pc" xml:lang="ru"/>
|
|
||||||
<feature var="games:board"/>
|
|
||||||
<feature var="http://jabber.org/protocol/activity"/>
|
|
||||||
<feature var="http://jabber.org/protocol/activity+notify"/>
|
|
||||||
<feature var="http://jabber.org/protocol/bytestreams"/>
|
|
||||||
<feature var="http://jabber.org/protocol/chatstates"/>
|
|
||||||
<feature var="http://jabber.org/protocol/commands"/>
|
|
||||||
<feature var="http://jabber.org/protocol/disco#info"/>
|
|
||||||
<feature var="http://jabber.org/protocol/disco#items"/>
|
|
||||||
<feature var="http://jabber.org/protocol/evil"/>
|
|
||||||
<feature var="http://jabber.org/protocol/feature-neg"/>
|
|
||||||
<feature var="http://jabber.org/protocol/geoloc"/>
|
|
||||||
<feature var="http://jabber.org/protocol/geoloc+notify"/>
|
|
||||||
<feature var="http://jabber.org/protocol/ibb"/>
|
|
||||||
<feature var="http://jabber.org/protocol/iqibb"/>
|
|
||||||
<feature var="http://jabber.org/protocol/mood"/>
|
|
||||||
<feature var="http://jabber.org/protocol/mood+notify"/>
|
|
||||||
<feature var="http://jabber.org/protocol/rosterx"/>
|
|
||||||
<feature var="http://jabber.org/protocol/si"/>
|
|
||||||
<feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
|
|
||||||
<feature var="http://jabber.org/protocol/tune"/>
|
|
||||||
<feature var="http://www.facebook.com/xmpp/messages"/>
|
|
||||||
<feature var="http://www.xmpp.org/extensions/xep-0084.html#ns-metadata+notify"/>
|
|
||||||
<feature var="jabber:iq:avatar"/>
|
|
||||||
<feature var="jabber:iq:browse"/>
|
|
||||||
<feature var="jabber:iq:dtcp"/>
|
|
||||||
<feature var="jabber:iq:filexfer"/>
|
|
||||||
<feature var="jabber:iq:ibb"/>
|
|
||||||
<feature var="jabber:iq:inband"/>
|
|
||||||
<feature var="jabber:iq:jidlink"/>
|
|
||||||
<feature var="jabber:iq:last"/>
|
|
||||||
<feature var="jabber:iq:oob"/>
|
|
||||||
<feature var="jabber:iq:privacy"/>
|
|
||||||
<feature var="jabber:iq:roster"/>
|
|
||||||
<feature var="jabber:iq:time"/>
|
|
||||||
<feature var="jabber:iq:version"/>
|
|
||||||
<feature var="jabber:x:data"/>
|
|
||||||
<feature var="jabber:x:event"/>
|
|
||||||
<feature var="jabber:x:oob"/>
|
|
||||||
<feature var="urn:xmpp:avatar:metadata+notify"/>
|
|
||||||
<feature var="urn:xmpp:ping"/>
|
|
||||||
<feature var="urn:xmpp:receipts"/>
|
|
||||||
<feature var="urn:xmpp:time"/>
|
|
||||||
<x xmlns="jabber:x:data" type="result">
|
|
||||||
<field type="hidden" var="FORM_TYPE">
|
|
||||||
<value>urn:xmpp:dataforms:softwareinfo</value>
|
|
||||||
</field>
|
|
||||||
<field var="software">
|
|
||||||
<value>Tkabber</value>
|
|
||||||
</field>
|
|
||||||
<field var="software_version">
|
|
||||||
<value>0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2)</value>
|
|
||||||
</field>
|
|
||||||
<field var="os">
|
|
||||||
<value>Windows</value>
|
|
||||||
</field>
|
|
||||||
<field var="os_version">
|
|
||||||
<value>XP</value>
|
|
||||||
</field>
|
|
||||||
</x>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let expected = vec![
|
|
||||||
103, 97, 109, 101, 115, 58, 98, 111, 97, 114, 100, 31, 104, 116, 116, 112, 58, 47, 47,
|
|
||||||
106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
|
|
||||||
108, 47, 97, 99, 116, 105, 118, 105, 116, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106,
|
|
||||||
97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47,
|
|
||||||
97, 99, 116, 105, 118, 105, 116, 121, 43, 110, 111, 116, 105, 102, 121, 31, 104, 116,
|
|
||||||
116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111,
|
|
||||||
116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, 115, 31,
|
|
||||||
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
|
||||||
114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, 101, 115,
|
|
||||||
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
|
|
||||||
112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 111, 109, 109, 97, 110, 100, 115, 31,
|
|
||||||
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
|
||||||
114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 110, 102, 111,
|
|
||||||
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
|
|
||||||
112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 116, 101,
|
|
||||||
109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
|
||||||
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 101, 118, 105, 108, 31, 104, 116,
|
|
||||||
116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111,
|
|
||||||
116, 111, 99, 111, 108, 47, 102, 101, 97, 116, 117, 114, 101, 45, 110, 101, 103, 31,
|
|
||||||
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
|
||||||
114, 111, 116, 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 31, 104, 116, 116,
|
|
||||||
112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116,
|
|
||||||
111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 43, 110, 111, 116, 105, 102, 121,
|
|
||||||
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
|
|
||||||
112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47,
|
|
||||||
47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
|
|
||||||
108, 47, 105, 113, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98,
|
|
||||||
101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111,
|
|
||||||
111, 100, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
|
||||||
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, 111, 100, 43, 110, 111,
|
|
||||||
116, 105, 102, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46,
|
|
||||||
111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101,
|
|
||||||
114, 120, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
|
||||||
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112,
|
|
||||||
58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111,
|
|
||||||
99, 111, 108, 47, 115, 105, 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108,
|
|
||||||
101, 45, 116, 114, 97, 110, 115, 102, 101, 114, 31, 104, 116, 116, 112, 58, 47, 47,
|
|
||||||
106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
|
|
||||||
108, 47, 116, 117, 110, 101, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46,
|
|
||||||
102, 97, 99, 101, 98, 111, 111, 107, 46, 99, 111, 109, 47, 120, 109, 112, 112, 47, 109,
|
|
||||||
101, 115, 115, 97, 103, 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119,
|
|
||||||
46, 120, 109, 112, 112, 46, 111, 114, 103, 47, 101, 120, 116, 101, 110, 115, 105, 111,
|
|
||||||
110, 115, 47, 120, 101, 112, 45, 48, 48, 56, 52, 46, 104, 116, 109, 108, 35, 110, 115,
|
|
||||||
45, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, 105, 102, 121, 31, 106, 97,
|
|
||||||
98, 98, 101, 114, 58, 105, 113, 58, 97, 118, 97, 116, 97, 114, 31, 106, 97, 98, 98,
|
|
||||||
101, 114, 58, 105, 113, 58, 98, 114, 111, 119, 115, 101, 31, 106, 97, 98, 98, 101, 114,
|
|
||||||
58, 105, 113, 58, 100, 116, 99, 112, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58,
|
|
||||||
102, 105, 108, 101, 120, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113,
|
|
||||||
58, 105, 98, 98, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 105, 110, 98, 97,
|
|
||||||
110, 100, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 106, 105, 100, 108, 105,
|
|
||||||
110, 107, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, 31, 106,
|
|
||||||
97, 98, 98, 101, 114, 58, 105, 113, 58, 111, 111, 98, 31, 106, 97, 98, 98, 101, 114,
|
|
||||||
58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, 106, 97, 98, 98, 101, 114, 58,
|
|
||||||
105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105,
|
|
||||||
113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 118, 101,
|
|
||||||
114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 100, 97, 116, 97,
|
|
||||||
31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 101, 118, 101, 110, 116, 31, 106, 97, 98,
|
|
||||||
98, 101, 114, 58, 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58,
|
|
||||||
97, 118, 97, 116, 97, 114, 58, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116,
|
|
||||||
105, 102, 121, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, 103, 31,
|
|
||||||
117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, 115, 31,
|
|
||||||
117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, 108, 105,
|
|
||||||
101, 110, 116, 31, 112, 99, 31, 101, 110, 31, 84, 107, 97, 98, 98, 101, 114, 31, 30,
|
|
||||||
99, 108, 105, 101, 110, 116, 31, 112, 99, 31, 114, 117, 31, 208, 162, 208, 186, 208,
|
|
||||||
176, 208, 177, 208, 177, 208, 181, 209, 128, 31, 30, 28, 70, 79, 82, 77, 95, 84, 89,
|
|
||||||
80, 69, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 100, 97, 116, 97, 102, 111, 114,
|
|
||||||
109, 115, 58, 115, 111, 102, 116, 119, 97, 114, 101, 105, 110, 102, 111, 31, 30, 111,
|
|
||||||
115, 31, 87, 105, 110, 100, 111, 119, 115, 31, 30, 111, 115, 95, 118, 101, 114, 115,
|
|
||||||
105, 111, 110, 31, 88, 80, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 31, 84, 107,
|
|
||||||
97, 98, 98, 101, 114, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 95, 118, 101, 114,
|
|
||||||
115, 105, 111, 110, 31, 48, 46, 49, 49, 46, 49, 45, 115, 118, 110, 45, 50, 48, 49, 49,
|
|
||||||
49, 50, 49, 54, 45, 109, 111, 100, 32, 40, 84, 99, 108, 47, 84, 107, 32, 56, 46, 54,
|
|
||||||
98, 50, 41, 31, 30, 29, 28,
|
|
||||||
];
|
|
||||||
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
|
||||||
let ecaps2 = compute_disco(&disco).unwrap();
|
|
||||||
assert_eq!(ecaps2.len(), 0x543);
|
|
||||||
assert_eq!(ecaps2, expected);
|
|
||||||
|
|
||||||
let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
sha_256.hash,
|
|
||||||
base64::decode("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=").unwrap()
|
|
||||||
);
|
|
||||||
let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
sha3_256.hash,
|
|
||||||
base64::decode("XpUJzLAc93258sMECZ3FJpebkzuyNXDzRNwQog8eycg=").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_blake2b_512() {
|
|
||||||
let hash = hash_ecaps2("abc".as_bytes(), Algo::Blake2b_512).unwrap();
|
|
||||||
let known_hash: Vec<u8> = vec![
|
|
||||||
0xBA, 0x80, 0xA5, 0x3F, 0x98, 0x1C, 0x4D, 0x0D, 0x6A, 0x27, 0x97, 0xB6, 0x9F, 0x12,
|
|
||||||
0xF6, 0xE9, 0x4C, 0x21, 0x2F, 0x14, 0x68, 0x5A, 0xC4, 0xB7, 0x4B, 0x12, 0xBB, 0x6F,
|
|
||||||
0xDB, 0xFF, 0xA2, 0xD1, 0x7D, 0x87, 0xC5, 0x39, 0x2A, 0xAB, 0x79, 0x2D, 0xC2, 0x52,
|
|
||||||
0xD5, 0xDE, 0x45, 0x33, 0xCC, 0x95, 0x18, 0xD3, 0x8A, 0xA8, 0xDB, 0xF1, 0x92, 0x5A,
|
|
||||||
0xB9, 0x23, 0x86, 0xED, 0xD4, 0x00, 0x99, 0x23,
|
|
||||||
];
|
|
||||||
assert_eq!(hash.hash, known_hash);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Structure representing an `<encryption xmlns='urn:xmpp:eme:0'/>` element.
|
|
||||||
ExplicitMessageEncryption, "encryption", EME,
|
|
||||||
attributes: [
|
|
||||||
/// Namespace of the encryption scheme used.
|
|
||||||
namespace: Required<String> = "namespace",
|
|
||||||
|
|
||||||
/// User-friendly name for the encryption scheme, should be `None` for OTR,
|
|
||||||
/// legacy OpenPGP and OX.
|
|
||||||
name: Option<String> = "name",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for ExplicitMessageEncryption {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ExplicitMessageEncryption, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ExplicitMessageEncryption, 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='urn:xmpp:otr:0'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let encryption = ExplicitMessageEncryption::try_from(elem).unwrap();
|
|
||||||
assert_eq!(encryption.namespace, "urn:xmpp:otr:0");
|
|
||||||
assert_eq!(encryption.name, None);
|
|
||||||
|
|
||||||
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='some.unknown.mechanism' name='SuperMechanism'/>".parse().unwrap();
|
|
||||||
let encryption = ExplicitMessageEncryption::try_from(elem).unwrap();
|
|
||||||
assert_eq!(encryption.namespace, "some.unknown.mechanism");
|
|
||||||
assert_eq!(encryption.name, Some(String::from("SuperMechanism")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unknown() {
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = ExplicitMessageEncryption::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "This is not a encryption element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0'><coucou/></encryption>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = ExplicitMessageEncryption::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in encryption element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let eme = ExplicitMessageEncryption {
|
|
||||||
namespace: String::from("coucou"),
|
|
||||||
name: None,
|
|
||||||
};
|
|
||||||
let elem2 = eme.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::delay::Delay;
|
|
||||||
use crate::message::Message;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Contains a forwarded stanza, either standalone or part of another
|
|
||||||
/// extension (such as carbons).
|
|
||||||
Forwarded, "forwarded", FORWARD,
|
|
||||||
children: [
|
|
||||||
/// When the stanza originally got sent.
|
|
||||||
delay: Option<Delay> = ("delay", DELAY) => Delay,
|
|
||||||
|
|
||||||
// XXX: really? Option?
|
|
||||||
/// The stanza being forwarded.
|
|
||||||
stanza: Option<Message> = ("message", DEFAULT_NS) => Message
|
|
||||||
|
|
||||||
// TODO: also handle the two other stanza possibilities.
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Forwarded, 212);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Forwarded, 408);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'/>".parse().unwrap();
|
|
||||||
Forwarded::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'><coucou/></forwarded>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Forwarded::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in forwarded element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'/>".parse().unwrap();
|
|
||||||
let forwarded = Forwarded {
|
|
||||||
delay: None,
|
|
||||||
stanza: None,
|
|
||||||
};
|
|
||||||
let elem2 = forwarded.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_with_delay_and_stanza() {
|
|
||||||
let reference: Element = "<forwarded xmlns='urn:xmpp:forward:0'><delay xmlns='urn:xmpp:delay' from='capulet.com' stamp='2002-09-10T23:08:25+00:00'/><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let message = Message::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<delay xmlns='urn:xmpp:delay' from='capulet.com' stamp='2002-09-10T23:08:25Z'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let delay = Delay::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let forwarded = Forwarded {
|
|
||||||
delay: Some(delay),
|
|
||||||
stanza: Some(message),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = forwarded.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,274 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::util::helpers::Base64;
|
|
||||||
use minidom::IntoAttributeValue;
|
|
||||||
use std::num::ParseIntError;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// List of the algorithms we support, or Unknown.
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum Algo {
|
|
||||||
/// The Secure Hash Algorithm 1, with known vulnerabilities, do not use it.
|
|
||||||
///
|
|
||||||
/// See https://tools.ietf.org/html/rfc3174
|
|
||||||
Sha_1,
|
|
||||||
|
|
||||||
/// The Secure Hash Algorithm 2, in its 256-bit version.
|
|
||||||
///
|
|
||||||
/// See https://tools.ietf.org/html/rfc6234
|
|
||||||
Sha_256,
|
|
||||||
|
|
||||||
/// The Secure Hash Algorithm 2, in its 512-bit version.
|
|
||||||
///
|
|
||||||
/// See https://tools.ietf.org/html/rfc6234
|
|
||||||
Sha_512,
|
|
||||||
|
|
||||||
/// The Secure Hash Algorithm 3, based on Keccak, in its 256-bit version.
|
|
||||||
///
|
|
||||||
/// See https://keccak.team/files/Keccak-submission-3.pdf
|
|
||||||
Sha3_256,
|
|
||||||
|
|
||||||
/// The Secure Hash Algorithm 3, based on Keccak, in its 512-bit version.
|
|
||||||
///
|
|
||||||
/// See https://keccak.team/files/Keccak-submission-3.pdf
|
|
||||||
Sha3_512,
|
|
||||||
|
|
||||||
/// The BLAKE2 hash algorithm, for a 256-bit output.
|
|
||||||
///
|
|
||||||
/// See https://tools.ietf.org/html/rfc7693
|
|
||||||
Blake2b_256,
|
|
||||||
|
|
||||||
/// The BLAKE2 hash algorithm, for a 512-bit output.
|
|
||||||
///
|
|
||||||
/// See https://tools.ietf.org/html/rfc7693
|
|
||||||
Blake2b_512,
|
|
||||||
|
|
||||||
/// An unknown hash not in this list, you can probably reject it.
|
|
||||||
Unknown(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Algo {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Algo, Error> {
|
|
||||||
Ok(match s {
|
|
||||||
"" => return Err(Error::ParseError("'algo' argument can’t be empty.")),
|
|
||||||
|
|
||||||
"sha-1" => Algo::Sha_1,
|
|
||||||
"sha-256" => Algo::Sha_256,
|
|
||||||
"sha-512" => Algo::Sha_512,
|
|
||||||
"sha3-256" => Algo::Sha3_256,
|
|
||||||
"sha3-512" => Algo::Sha3_512,
|
|
||||||
"blake2b-256" => Algo::Blake2b_256,
|
|
||||||
"blake2b-512" => Algo::Blake2b_512,
|
|
||||||
value => Algo::Unknown(value.to_owned()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Algo> for String {
|
|
||||||
fn from(algo: Algo) -> String {
|
|
||||||
String::from(match algo {
|
|
||||||
Algo::Sha_1 => "sha-1",
|
|
||||||
Algo::Sha_256 => "sha-256",
|
|
||||||
Algo::Sha_512 => "sha-512",
|
|
||||||
Algo::Sha3_256 => "sha3-256",
|
|
||||||
Algo::Sha3_512 => "sha3-512",
|
|
||||||
Algo::Blake2b_256 => "blake2b-256",
|
|
||||||
Algo::Blake2b_512 => "blake2b-512",
|
|
||||||
Algo::Unknown(text) => return text,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoAttributeValue for Algo {
|
|
||||||
fn into_attribute_value(self) -> Option<String> {
|
|
||||||
Some(String::from(self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// This element represents a hash of some data, defined by the hash
|
|
||||||
/// algorithm used and the computed value.
|
|
||||||
Hash, "hash", HASHES,
|
|
||||||
attributes: [
|
|
||||||
/// The algorithm used to create this hash.
|
|
||||||
algo: Required<Algo> = "algo"
|
|
||||||
],
|
|
||||||
text: (
|
|
||||||
/// The hash value, as a vector of bytes.
|
|
||||||
hash: Base64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Hash {
|
|
||||||
/// Creates a [Hash] element with the given algo and data.
|
|
||||||
pub fn new(algo: Algo, hash: Vec<u8>) -> Hash {
|
|
||||||
Hash { algo, hash }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Like [new](#method.new) but takes base64-encoded data before decoding
|
|
||||||
/// it.
|
|
||||||
pub fn from_base64(algo: Algo, hash: &str) -> Result<Hash, Error> {
|
|
||||||
Ok(Hash::new(algo, base64::decode(hash)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Like [new](#method.new) but takes hex-encoded data before decoding it.
|
|
||||||
pub fn from_hex(algo: Algo, hex: &str) -> Result<Hash, ParseIntError> {
|
|
||||||
let mut bytes = vec![];
|
|
||||||
for i in 0..hex.len() / 2 {
|
|
||||||
let byte = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16)?;
|
|
||||||
bytes.push(byte);
|
|
||||||
}
|
|
||||||
Ok(Hash::new(algo, bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Like [new](#method.new) but takes hex-encoded data before decoding it.
|
|
||||||
pub fn from_colon_separated_hex(algo: Algo, hex: &str) -> Result<Hash, ParseIntError> {
|
|
||||||
let mut bytes = vec![];
|
|
||||||
for i in 0..(1 + hex.len()) / 3 {
|
|
||||||
let byte = u8::from_str_radix(&hex[3 * i..3 * i + 2], 16)?;
|
|
||||||
if 3 * i + 2 < hex.len() {
|
|
||||||
assert_eq!(&hex[3 * i + 2..3 * i + 3], ":");
|
|
||||||
}
|
|
||||||
bytes.push(byte);
|
|
||||||
}
|
|
||||||
Ok(Hash::new(algo, bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formats this hash into base64.
|
|
||||||
pub fn to_base64(&self) -> String {
|
|
||||||
base64::encode(&self.hash[..])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formats this hash into hexadecimal.
|
|
||||||
pub fn to_hex(&self) -> String {
|
|
||||||
self.hash
|
|
||||||
.iter()
|
|
||||||
.map(|byte| format!("{:02x}", byte))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formats this hash into colon-separated hexadecimal.
|
|
||||||
pub fn to_colon_separated_hex(&self) -> String {
|
|
||||||
self.hash
|
|
||||||
.iter()
|
|
||||||
.map(|byte| format!("{:02x}", byte))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(":")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper for parsing and serialising a SHA-1 attribute.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Sha1HexAttribute(Hash);
|
|
||||||
|
|
||||||
impl FromStr for Sha1HexAttribute {
|
|
||||||
type Err = ParseIntError;
|
|
||||||
|
|
||||||
fn from_str(hex: &str) -> Result<Self, Self::Err> {
|
|
||||||
let hash = Hash::from_hex(Algo::Sha_1, hex)?;
|
|
||||||
Ok(Sha1HexAttribute(hash))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoAttributeValue for Sha1HexAttribute {
|
|
||||||
fn into_attribute_value(self) -> Option<String> {
|
|
||||||
Some(self.to_hex())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Sha1HexAttribute {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Sha1HexAttribute {
|
|
||||||
type Target = Hash;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Algo, 16);
|
|
||||||
assert_size!(Hash, 28);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Algo, 32);
|
|
||||||
assert_size!(Hash, 56);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=</hash>".parse().unwrap();
|
|
||||||
let hash = Hash::try_from(elem).unwrap();
|
|
||||||
assert_eq!(hash.algo, Algo::Sha_256);
|
|
||||||
assert_eq!(
|
|
||||||
hash.hash,
|
|
||||||
base64::decode("2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn value_serialisation() {
|
|
||||||
let elem: Element = "<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=</hash>".parse().unwrap();
|
|
||||||
let hash = Hash::try_from(elem).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
hash.to_base64(),
|
|
||||||
"2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU="
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
hash.to_hex(),
|
|
||||||
"d976ab9b04e53710c0324bf29a5a17dd2e7e55bca536b26dfe5e50c8f6be6285"
|
|
||||||
);
|
|
||||||
assert_eq!(hash.to_colon_separated_hex(), "d9:76:ab:9b:04:e5:37:10:c0:32:4b:f2:9a:5a:17:dd:2e:7e:55:bc:a5:36:b2:6d:fe:5e:50:c8:f6:be:62:85");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unknown() {
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Hash::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "This is not a hash element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<hash xmlns='urn:xmpp:hashes:2'><coucou/></hash>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Hash::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in hash element.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,171 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::iq::IqSetPayload;
|
|
||||||
use crate::util::helpers::Base64;
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// An identifier matching a stream.
|
|
||||||
StreamId
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Which stanza type to use to exchange data.
|
|
||||||
Stanza, "stanza", {
|
|
||||||
/// `<iq/>` gives a feedback on whether the chunk has been received or not,
|
|
||||||
/// which is useful in the case the recipient might not receive them in a
|
|
||||||
/// timely manner, or to do your own throttling based on the results.
|
|
||||||
Iq => "iq",
|
|
||||||
|
|
||||||
/// `<message/>` can be faster, since it doesn’t require any feedback, but in
|
|
||||||
/// practice it will be throttled by the servers on the way.
|
|
||||||
Message => "message",
|
|
||||||
}, Default = Iq);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Starts an In-Band Bytestream session with the given parameters.
|
|
||||||
Open, "open", IBB,
|
|
||||||
attributes: [
|
|
||||||
/// Maximum size in bytes for each chunk.
|
|
||||||
block_size: Required<u16> = "block-size",
|
|
||||||
|
|
||||||
/// The identifier to be used to create a stream.
|
|
||||||
sid: Required<StreamId> = "sid",
|
|
||||||
|
|
||||||
/// Which stanza type to use to exchange data.
|
|
||||||
stanza: Default<Stanza> = "stanza",
|
|
||||||
]);
|
|
||||||
|
|
||||||
impl IqSetPayload for Open {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Exchange a chunk of data in an open stream.
|
|
||||||
Data, "data", IBB,
|
|
||||||
attributes: [
|
|
||||||
/// Sequence number of this chunk, must wraparound after 65535.
|
|
||||||
seq: Required<u16> = "seq",
|
|
||||||
|
|
||||||
/// The identifier of the stream on which data is being exchanged.
|
|
||||||
sid: Required<StreamId> = "sid"
|
|
||||||
],
|
|
||||||
text: (
|
|
||||||
/// Vector of bytes to be exchanged.
|
|
||||||
data: Base64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Data {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Close an open stream.
|
|
||||||
Close, "close", IBB,
|
|
||||||
attributes: [
|
|
||||||
/// The identifier of the stream to be closed.
|
|
||||||
sid: Required<StreamId> = "sid",
|
|
||||||
]);
|
|
||||||
|
|
||||||
impl IqSetPayload for Close {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(StreamId, 12);
|
|
||||||
assert_size!(Stanza, 1);
|
|
||||||
assert_size!(Open, 16);
|
|
||||||
assert_size!(Data, 28);
|
|
||||||
assert_size!(Close, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(StreamId, 24);
|
|
||||||
assert_size!(Stanza, 1);
|
|
||||||
assert_size!(Open, 32);
|
|
||||||
assert_size!(Data, 56);
|
|
||||||
assert_size!(Close, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let sid = StreamId(String::from("coucou"));
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<open xmlns='http://jabber.org/protocol/ibb' block-size='3' sid='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let open = Open::try_from(elem).unwrap();
|
|
||||||
assert_eq!(open.block_size, 3);
|
|
||||||
assert_eq!(open.sid, sid);
|
|
||||||
assert_eq!(open.stanza, Stanza::Iq);
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<data xmlns='http://jabber.org/protocol/ibb' seq='0' sid='coucou'>AAAA</data>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let data = Data::try_from(elem).unwrap();
|
|
||||||
assert_eq!(data.seq, 0);
|
|
||||||
assert_eq!(data.sid, sid);
|
|
||||||
assert_eq!(data.data, vec!(0, 0, 0));
|
|
||||||
|
|
||||||
let elem: Element = "<close xmlns='http://jabber.org/protocol/ibb' sid='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let close = Close::try_from(elem).unwrap();
|
|
||||||
assert_eq!(close.sid, sid);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Open::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'block-size' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='-5'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Open::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseIntError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "invalid digit found in string");
|
|
||||||
|
|
||||||
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='128'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Open::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'sid' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_stanza() {
|
|
||||||
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='128' sid='coucou' stanza='fdsq'/>".parse().unwrap();
|
|
||||||
let error = Open::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'stanza' attribute.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,195 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::data_forms::DataForm;
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Query for registering against a service.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Query {
|
|
||||||
/// Deprecated fixed list of possible fields to fill before the user can
|
|
||||||
/// register.
|
|
||||||
pub fields: HashMap<String, String>,
|
|
||||||
|
|
||||||
/// Whether this account is already registered.
|
|
||||||
pub registered: bool,
|
|
||||||
|
|
||||||
/// Whether to remove this account.
|
|
||||||
pub remove: bool,
|
|
||||||
|
|
||||||
/// A data form the user must fill before being allowed to register.
|
|
||||||
pub form: Option<DataForm>,
|
|
||||||
// Not yet implemented.
|
|
||||||
//pub oob: Option<Oob>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IqGetPayload for Query {}
|
|
||||||
impl IqSetPayload for Query {}
|
|
||||||
impl IqResultPayload for Query {}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Query {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Query, Error> {
|
|
||||||
check_self!(elem, "query", REGISTER, "IBR query");
|
|
||||||
let mut query = Query {
|
|
||||||
registered: false,
|
|
||||||
fields: HashMap::new(),
|
|
||||||
remove: false,
|
|
||||||
form: None,
|
|
||||||
};
|
|
||||||
for child in elem.children() {
|
|
||||||
let namespace = child.ns();
|
|
||||||
if namespace == ns::REGISTER {
|
|
||||||
let name = child.name();
|
|
||||||
let fields = vec![
|
|
||||||
"address",
|
|
||||||
"city",
|
|
||||||
"date",
|
|
||||||
"email",
|
|
||||||
"first",
|
|
||||||
"instructions",
|
|
||||||
"key",
|
|
||||||
"last",
|
|
||||||
"misc",
|
|
||||||
"name",
|
|
||||||
"nick",
|
|
||||||
"password",
|
|
||||||
"phone",
|
|
||||||
"state",
|
|
||||||
"text",
|
|
||||||
"url",
|
|
||||||
"username",
|
|
||||||
"zip",
|
|
||||||
];
|
|
||||||
if fields.binary_search(&name).is_ok() {
|
|
||||||
query.fields.insert(name.to_owned(), child.text());
|
|
||||||
} else if name == "registered" {
|
|
||||||
query.registered = true;
|
|
||||||
} else if name == "remove" {
|
|
||||||
query.remove = true;
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Wrong field in ibr element."));
|
|
||||||
}
|
|
||||||
} else if child.is("x", ns::DATA_FORMS) {
|
|
||||||
query.form = Some(DataForm::try_from(child.clone())?);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in ibr element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Query> for Element {
|
|
||||||
fn from(query: Query) -> Element {
|
|
||||||
Element::builder("query", ns::REGISTER)
|
|
||||||
.append_all(if query.registered {
|
|
||||||
Some(Element::builder("registered", ns::REGISTER))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.append_all(
|
|
||||||
query
|
|
||||||
.fields
|
|
||||||
.into_iter()
|
|
||||||
.map(|(name, value)| Element::builder(name, ns::REGISTER).append(value)),
|
|
||||||
)
|
|
||||||
.append_all(if query.remove {
|
|
||||||
Some(Element::builder("remove", ns::REGISTER))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.append_all(query.form.map(Element::from))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Query, 88);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Query, 160);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:register'/>".parse().unwrap();
|
|
||||||
Query::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ex2() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns='jabber:iq:register'>
|
|
||||||
<instructions>
|
|
||||||
Choose a username and password for use with this service.
|
|
||||||
Please also provide your email address.
|
|
||||||
</instructions>
|
|
||||||
<username/>
|
|
||||||
<password/>
|
|
||||||
<email/>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let query = Query::try_from(elem).unwrap();
|
|
||||||
assert_eq!(query.registered, false);
|
|
||||||
assert_eq!(query.fields["instructions"], "\n Choose a username and password for use with this service.\n Please also provide your email address.\n ");
|
|
||||||
assert_eq!(query.fields["username"], "");
|
|
||||||
assert_eq!(query.fields["password"], "");
|
|
||||||
assert_eq!(query.fields["email"], "");
|
|
||||||
assert_eq!(query.fields.contains_key("name"), false);
|
|
||||||
|
|
||||||
// FIXME: HashMap doesn’t keep the order right.
|
|
||||||
//let elem2 = query.into();
|
|
||||||
//assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ex9() {
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:register'><instructions>Use the enclosed form to register. If your Jabber client does not support Data Forms, visit http://www.shakespeare.lit/contests.php</instructions><x xmlns='jabber:x:data' type='form'><title>Contest Registration</title><instructions>Please provide the following information to sign up for our special contests!</instructions><field type='hidden' var='FORM_TYPE'><value>jabber:iq:register</value></field><field label='Given Name' var='first'><required/></field><field label='Family Name' var='last'><required/></field><field label='Email Address' var='email'><required/></field><field type='list-single' label='Gender' var='x-gender'><option label='Male'><value>M</value></option><option label='Female'><value>F</value></option></field></x></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let query = Query::try_from(elem).unwrap();
|
|
||||||
assert_eq!(query.registered, false);
|
|
||||||
assert!(!query.fields["instructions"].is_empty());
|
|
||||||
let form = query.form.clone().unwrap();
|
|
||||||
assert!(!form.instructions.unwrap().is_empty());
|
|
||||||
let elem2 = query.into();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ex10() {
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:register'><x xmlns='jabber:x:data' type='submit'><field type='hidden' var='FORM_TYPE'><value>jabber:iq:register</value></field><field label='Given Name' var='first'><value>Juliet</value></field><field label='Family Name' var='last'><value>Capulet</value></field><field label='Email Address' var='email'><value>juliet@capulet.com</value></field><field type='list-single' label='Gender' var='x-gender'><value>F</value></field></x></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let query = Query::try_from(elem).unwrap();
|
|
||||||
assert_eq!(query.registered, false);
|
|
||||||
for _ in &query.fields {
|
|
||||||
panic!();
|
|
||||||
}
|
|
||||||
let elem2 = query.into();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,146 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::date::DateTime;
|
|
||||||
use crate::presence::PresencePayload;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Represents the last time the user interacted with their system.
|
|
||||||
Idle, "idle", IDLE,
|
|
||||||
attributes: [
|
|
||||||
/// The time at which the user stopped interacting.
|
|
||||||
since: Required<DateTime> = "since",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PresencePayload for Idle {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Idle, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-21T20:19:55+01:00'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Idle::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1'><coucou/></idle>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Idle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in idle element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_id() {
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1'/>".parse().unwrap();
|
|
||||||
let error = Idle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'since' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_date() {
|
|
||||||
// There is no thirteenth month.
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-13-01T12:23:34Z'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Idle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input is out of range");
|
|
||||||
|
|
||||||
// Timezone ≥24:00 aren’t allowed.
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11:02+25:00'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Idle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input is out of range");
|
|
||||||
|
|
||||||
// Timezone without the : separator aren’t allowed.
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11:02+0100'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Idle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input contains invalid characters");
|
|
||||||
|
|
||||||
// No seconds, error message could be improved.
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11+01:00'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Idle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input contains invalid characters");
|
|
||||||
|
|
||||||
// TODO: maybe we’ll want to support this one, as per XEP-0082 §4.
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='20170527T12:11:02+01:00'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Idle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "input contains invalid characters");
|
|
||||||
|
|
||||||
// No timezone.
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11:02'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Idle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ChronoParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "premature end of input");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-21T20:19:55+01:00'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let idle = Idle {
|
|
||||||
since: DateTime::from_str("2017-05-21T20:19:55+01:00").unwrap(),
|
|
||||||
};
|
|
||||||
let elem2 = idle.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,461 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::ns;
|
|
||||||
use crate::stanza_error::StanzaError;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use minidom::IntoAttributeValue;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Should be implemented on every known payload of an `<iq type='get'/>`.
|
|
||||||
pub trait IqGetPayload: TryFrom<Element> + Into<Element> {}
|
|
||||||
|
|
||||||
/// Should be implemented on every known payload of an `<iq type='set'/>`.
|
|
||||||
pub trait IqSetPayload: TryFrom<Element> + Into<Element> {}
|
|
||||||
|
|
||||||
/// Should be implemented on every known payload of an `<iq type='result'/>`.
|
|
||||||
pub trait IqResultPayload: TryFrom<Element> + Into<Element> {}
|
|
||||||
|
|
||||||
/// Represents one of the four possible iq types.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum IqType {
|
|
||||||
/// This is a request for accessing some data.
|
|
||||||
Get(Element),
|
|
||||||
|
|
||||||
/// This is a request for modifying some data.
|
|
||||||
Set(Element),
|
|
||||||
|
|
||||||
/// This is a result containing some data.
|
|
||||||
Result(Option<Element>),
|
|
||||||
|
|
||||||
/// A get or set request failed.
|
|
||||||
Error(StanzaError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoAttributeValue for &'a IqType {
|
|
||||||
fn into_attribute_value(self) -> Option<String> {
|
|
||||||
Some(
|
|
||||||
match *self {
|
|
||||||
IqType::Get(_) => "get",
|
|
||||||
IqType::Set(_) => "set",
|
|
||||||
IqType::Result(_) => "result",
|
|
||||||
IqType::Error(_) => "error",
|
|
||||||
}
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main structure representing the `<iq/>` stanza.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Iq {
|
|
||||||
/// The JID emitting this stanza.
|
|
||||||
pub from: Option<Jid>,
|
|
||||||
|
|
||||||
/// The recipient of this stanza.
|
|
||||||
pub to: Option<Jid>,
|
|
||||||
|
|
||||||
/// The @id attribute of this stanza, which is required in order to match a
|
|
||||||
/// request with its result/error.
|
|
||||||
pub id: String,
|
|
||||||
|
|
||||||
/// The payload content of this stanza.
|
|
||||||
pub payload: IqType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Iq {
|
|
||||||
/// Creates an `<iq/>` stanza containing a get request.
|
|
||||||
pub fn from_get<S: Into<String>>(id: S, payload: impl IqGetPayload) -> Iq {
|
|
||||||
Iq {
|
|
||||||
from: None,
|
|
||||||
to: None,
|
|
||||||
id: id.into(),
|
|
||||||
payload: IqType::Get(payload.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an `<iq/>` stanza containing a set request.
|
|
||||||
pub fn from_set<S: Into<String>>(id: S, payload: impl IqSetPayload) -> Iq {
|
|
||||||
Iq {
|
|
||||||
from: None,
|
|
||||||
to: None,
|
|
||||||
id: id.into(),
|
|
||||||
payload: IqType::Set(payload.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an empty `<iq type="result"/>` stanza.
|
|
||||||
pub fn empty_result<S: Into<String>>(to: Jid, id: S) -> Iq {
|
|
||||||
Iq {
|
|
||||||
from: None,
|
|
||||||
to: Some(to),
|
|
||||||
id: id.into(),
|
|
||||||
payload: IqType::Result(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an `<iq/>` stanza containing a result.
|
|
||||||
pub fn from_result<S: Into<String>>(id: S, payload: Option<impl IqResultPayload>) -> Iq {
|
|
||||||
Iq {
|
|
||||||
from: None,
|
|
||||||
to: None,
|
|
||||||
id: id.into(),
|
|
||||||
payload: IqType::Result(payload.map(Into::into)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an `<iq/>` stanza containing an error.
|
|
||||||
pub fn from_error<S: Into<String>>(id: S, payload: StanzaError) -> Iq {
|
|
||||||
Iq {
|
|
||||||
from: None,
|
|
||||||
to: None,
|
|
||||||
id: id.into(),
|
|
||||||
payload: IqType::Error(payload),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the recipient of this stanza.
|
|
||||||
pub fn with_to(mut self, to: Jid) -> Iq {
|
|
||||||
self.to = Some(to);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the emitter of this stanza.
|
|
||||||
pub fn with_from(mut self, from: Jid) -> Iq {
|
|
||||||
self.from = Some(from);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the id of this stanza, in order to later match its response.
|
|
||||||
pub fn with_id(mut self, id: String) -> Iq {
|
|
||||||
self.id = id;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Iq {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(root: Element) -> Result<Iq, Error> {
|
|
||||||
check_self!(root, "iq", DEFAULT_NS);
|
|
||||||
let from = get_attr!(root, "from", Option);
|
|
||||||
let to = get_attr!(root, "to", Option);
|
|
||||||
let id = get_attr!(root, "id", Required);
|
|
||||||
let type_: String = get_attr!(root, "type", Required);
|
|
||||||
|
|
||||||
let mut payload = None;
|
|
||||||
let mut error_payload = None;
|
|
||||||
for elem in root.children() {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
|
||||||
}
|
|
||||||
if type_ == "error" {
|
|
||||||
if elem.is("error", ns::DEFAULT_NS) {
|
|
||||||
if error_payload.is_some() {
|
|
||||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
|
||||||
}
|
|
||||||
error_payload = Some(StanzaError::try_from(elem.clone())?);
|
|
||||||
} else if root.children().count() != 2 {
|
|
||||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
payload = Some(elem.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let type_ = if type_ == "get" {
|
|
||||||
if let Some(payload) = payload {
|
|
||||||
IqType::Get(payload)
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
|
||||||
}
|
|
||||||
} else if type_ == "set" {
|
|
||||||
if let Some(payload) = payload {
|
|
||||||
IqType::Set(payload)
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
|
||||||
}
|
|
||||||
} else if type_ == "result" {
|
|
||||||
if let Some(payload) = payload {
|
|
||||||
IqType::Result(Some(payload))
|
|
||||||
} else {
|
|
||||||
IqType::Result(None)
|
|
||||||
}
|
|
||||||
} else if type_ == "error" {
|
|
||||||
if let Some(payload) = error_payload {
|
|
||||||
IqType::Error(payload)
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown iq type."));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Iq {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
id,
|
|
||||||
payload: type_,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Iq> for Element {
|
|
||||||
fn from(iq: Iq) -> Element {
|
|
||||||
let mut stanza = Element::builder("iq", ns::DEFAULT_NS)
|
|
||||||
.attr("from", iq.from)
|
|
||||||
.attr("to", iq.to)
|
|
||||||
.attr("id", iq.id)
|
|
||||||
.attr("type", &iq.payload)
|
|
||||||
.build();
|
|
||||||
let elem = match iq.payload {
|
|
||||||
IqType::Get(elem) | IqType::Set(elem) | IqType::Result(Some(elem)) => elem,
|
|
||||||
IqType::Error(error) => error.into(),
|
|
||||||
IqType::Result(None) => return stanza,
|
|
||||||
};
|
|
||||||
stanza.append_child(elem);
|
|
||||||
stanza
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::disco::DiscoInfoQuery;
|
|
||||||
use crate::stanza_error::{DefinedCondition, ErrorType};
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(IqType, 136);
|
|
||||||
assert_size!(Iq, 228);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(IqType, 272);
|
|
||||||
assert_size!(Iq, 456);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_require_type() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client'/>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept'/>".parse().unwrap();
|
|
||||||
let error = Iq::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'id' missing.");
|
|
||||||
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client' id='coucou'/>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Iq::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'type' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client' type='get' id='foo'>
|
|
||||||
<foo xmlns='bar'/>
|
|
||||||
</iq>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='get' id='foo'>
|
|
||||||
<foo xmlns='bar'/>
|
|
||||||
</iq>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let iq = Iq::try_from(elem).unwrap();
|
|
||||||
let query: Element = "<foo xmlns='bar'/>".parse().unwrap();
|
|
||||||
assert_eq!(iq.from, None);
|
|
||||||
assert_eq!(iq.to, None);
|
|
||||||
assert_eq!(&iq.id, "foo");
|
|
||||||
assert!(match iq.payload {
|
|
||||||
IqType::Get(element) => element == query,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_set() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client' type='set' id='vcard'>
|
|
||||||
<vCard xmlns='vcard-temp'/>
|
|
||||||
</iq>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='set' id='vcard'>
|
|
||||||
<vCard xmlns='vcard-temp'/>
|
|
||||||
</iq>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let iq = Iq::try_from(elem).unwrap();
|
|
||||||
let vcard: Element = "<vCard xmlns='vcard-temp'/>".parse().unwrap();
|
|
||||||
assert_eq!(iq.from, None);
|
|
||||||
assert_eq!(iq.to, None);
|
|
||||||
assert_eq!(&iq.id, "vcard");
|
|
||||||
assert!(match iq.payload {
|
|
||||||
IqType::Set(element) => element == vcard,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_result_empty() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let iq = Iq::try_from(elem).unwrap();
|
|
||||||
assert_eq!(iq.from, None);
|
|
||||||
assert_eq!(iq.to, None);
|
|
||||||
assert_eq!(&iq.id, "res");
|
|
||||||
assert!(match iq.payload {
|
|
||||||
IqType::Result(None) => true,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_result() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'>
|
|
||||||
<query xmlns='http://jabber.org/protocol/disco#items'/>
|
|
||||||
</iq>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'>
|
|
||||||
<query xmlns='http://jabber.org/protocol/disco#items'/>
|
|
||||||
</iq>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let iq = Iq::try_from(elem).unwrap();
|
|
||||||
let query: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(iq.from, None);
|
|
||||||
assert_eq!(iq.to, None);
|
|
||||||
assert_eq!(&iq.id, "res");
|
|
||||||
assert!(match iq.payload {
|
|
||||||
IqType::Result(Some(element)) => element == query,
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_error() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client' type='error' id='err1'>
|
|
||||||
<ping xmlns='urn:xmpp:ping'/>
|
|
||||||
<error type='cancel'>
|
|
||||||
<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
|
||||||
</error>
|
|
||||||
</iq>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='error' id='err1'>
|
|
||||||
<ping xmlns='urn:xmpp:ping'/>
|
|
||||||
<error type='cancel'>
|
|
||||||
<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
|
||||||
</error>
|
|
||||||
</iq>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let iq = Iq::try_from(elem).unwrap();
|
|
||||||
assert_eq!(iq.from, None);
|
|
||||||
assert_eq!(iq.to, None);
|
|
||||||
assert_eq!(iq.id, "err1");
|
|
||||||
match iq.payload {
|
|
||||||
IqType::Error(error) => {
|
|
||||||
assert_eq!(error.type_, ErrorType::Cancel);
|
|
||||||
assert_eq!(error.by, None);
|
|
||||||
assert_eq!(
|
|
||||||
error.defined_condition,
|
|
||||||
DefinedCondition::ServiceUnavailable
|
|
||||||
);
|
|
||||||
assert_eq!(error.texts.len(), 0);
|
|
||||||
assert_eq!(error.other, None);
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_children_invalid() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client' type='error' id='error'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='error' id='error'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Iq::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Wrong number of children in iq element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let iq2 = Iq {
|
|
||||||
from: None,
|
|
||||||
to: None,
|
|
||||||
id: String::from("res"),
|
|
||||||
payload: IqType::Result(None),
|
|
||||||
};
|
|
||||||
let elem2 = iq2.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_disco() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:client' type='get' id='disco'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='get' id='disco'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>".parse().unwrap();
|
|
||||||
let iq = Iq::try_from(elem).unwrap();
|
|
||||||
let disco_info = match iq.payload {
|
|
||||||
IqType::Get(payload) => DiscoInfoQuery::try_from(payload).unwrap(),
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert!(disco_info.node.is_none());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload};
|
|
||||||
use crate::util::helpers::{JidCodec, Text};
|
|
||||||
use jid::Jid;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request from a client to stringprep/PRECIS a string into a JID.
|
|
||||||
JidPrepQuery, "jid", JID_PREP,
|
|
||||||
text: (
|
|
||||||
/// The potential JID.
|
|
||||||
data: Text<String>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqGetPayload for JidPrepQuery {}
|
|
||||||
|
|
||||||
impl JidPrepQuery {
|
|
||||||
/// Create a new JID Prep query.
|
|
||||||
pub fn new<J: Into<String>>(jid: J) -> JidPrepQuery {
|
|
||||||
JidPrepQuery { data: jid.into() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Response from the server with the stringprep’d/PRECIS’d JID.
|
|
||||||
JidPrepResponse, "jid", JID_PREP,
|
|
||||||
text: (
|
|
||||||
/// The JID.
|
|
||||||
jid: JidCodec<Jid>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqResultPayload for JidPrepResponse {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::FullJid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(JidPrepQuery, 12);
|
|
||||||
assert_size!(JidPrepResponse, 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(JidPrepQuery, 24);
|
|
||||||
assert_size!(JidPrepResponse, 80);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple() {
|
|
||||||
let elem: Element = "<jid xmlns='urn:xmpp:jidprep:0'>ROMeo@montague.lit/orchard</jid>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let query = JidPrepQuery::try_from(elem).unwrap();
|
|
||||||
assert_eq!(query.data, "ROMeo@montague.lit/orchard");
|
|
||||||
|
|
||||||
let elem: Element = "<jid xmlns='urn:xmpp:jidprep:0'>romeo@montague.lit/orchard</jid>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let response = JidPrepResponse::try_from(elem).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
response.jid,
|
|
||||||
FullJid::new("romeo", "montague.lit", "orchard")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,911 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::iq::IqSetPayload;
|
|
||||||
use crate::jingle_grouping::Group;
|
|
||||||
use crate::jingle_ibb::Transport as IbbTransport;
|
|
||||||
use crate::jingle_ice_udp::Transport as IceUdpTransport;
|
|
||||||
use crate::jingle_rtp::Description as RtpDescription;
|
|
||||||
use crate::jingle_s5b::Transport as Socks5Transport;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The action attribute.
|
|
||||||
Action, "action", {
|
|
||||||
/// Accept a content-add action received from another party.
|
|
||||||
ContentAccept => "content-accept",
|
|
||||||
|
|
||||||
/// Add one or more new content definitions to the session.
|
|
||||||
ContentAdd => "content-add",
|
|
||||||
|
|
||||||
/// Change the directionality of media sending.
|
|
||||||
ContentModify => "content-modify",
|
|
||||||
|
|
||||||
/// Reject a content-add action received from another party.
|
|
||||||
ContentReject => "content-reject",
|
|
||||||
|
|
||||||
/// Remove one or more content definitions from the session.
|
|
||||||
ContentRemove => "content-remove",
|
|
||||||
|
|
||||||
/// Exchange information about parameters for an application type.
|
|
||||||
DescriptionInfo => "description-info",
|
|
||||||
|
|
||||||
/// Exchange information about security preconditions.
|
|
||||||
SecurityInfo => "security-info",
|
|
||||||
|
|
||||||
/// Definitively accept a session negotiation.
|
|
||||||
SessionAccept => "session-accept",
|
|
||||||
|
|
||||||
/// Send session-level information, such as a ping or a ringing message.
|
|
||||||
SessionInfo => "session-info",
|
|
||||||
|
|
||||||
/// Request negotiation of a new Jingle session.
|
|
||||||
SessionInitiate => "session-initiate",
|
|
||||||
|
|
||||||
/// End an existing session.
|
|
||||||
SessionTerminate => "session-terminate",
|
|
||||||
|
|
||||||
/// Accept a transport-replace action received from another party.
|
|
||||||
TransportAccept => "transport-accept",
|
|
||||||
|
|
||||||
/// Exchange transport candidates.
|
|
||||||
TransportInfo => "transport-info",
|
|
||||||
|
|
||||||
/// Reject a transport-replace action received from another party.
|
|
||||||
TransportReject => "transport-reject",
|
|
||||||
|
|
||||||
/// Redefine a transport method or replace it with a different method.
|
|
||||||
TransportReplace => "transport-replace",
|
|
||||||
|
|
||||||
/// --- Non-standard messages used by Jitsi Meet:
|
|
||||||
|
|
||||||
/// Add a source to existing content.
|
|
||||||
SourceAdd => "source-add",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Which party originally generated the content type.
|
|
||||||
Creator, "creator", {
|
|
||||||
/// This content was created by the initiator of this session.
|
|
||||||
Initiator => "initiator",
|
|
||||||
|
|
||||||
/// This content was created by the responder of this session.
|
|
||||||
Responder => "responder",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Which parties in the session will be generating content.
|
|
||||||
Senders, "senders", {
|
|
||||||
/// Both parties can send for this content.
|
|
||||||
Both => "both",
|
|
||||||
|
|
||||||
/// Only the initiator can send for this content.
|
|
||||||
Initiator => "initiator",
|
|
||||||
|
|
||||||
/// No one can send for this content.
|
|
||||||
None => "none",
|
|
||||||
|
|
||||||
/// Only the responder can send for this content.
|
|
||||||
Responder => "responder",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// How the content definition is to be interpreted by the recipient. The
|
|
||||||
/// meaning of this attribute matches the "Content-Disposition" header as
|
|
||||||
/// defined in RFC 2183 and applied to SIP by RFC 3261.
|
|
||||||
///
|
|
||||||
/// Possible values are defined here:
|
|
||||||
/// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
|
|
||||||
Disposition, "disposition", {
|
|
||||||
/// Displayed automatically.
|
|
||||||
Inline => "inline",
|
|
||||||
|
|
||||||
/// User controlled display.
|
|
||||||
Attachment => "attachment",
|
|
||||||
|
|
||||||
/// Process as form response.
|
|
||||||
FormData => "form-data",
|
|
||||||
|
|
||||||
/// Tunneled content to be processed silently.
|
|
||||||
Signal => "signal",
|
|
||||||
|
|
||||||
/// The body is a custom ring tone to alert the user.
|
|
||||||
Alert => "alert",
|
|
||||||
|
|
||||||
/// The body is displayed as an icon to the user.
|
|
||||||
Icon => "icon",
|
|
||||||
|
|
||||||
/// The body should be displayed to the user.
|
|
||||||
Render => "render",
|
|
||||||
|
|
||||||
/// The body contains a list of URIs that indicates the recipients of
|
|
||||||
/// the request.
|
|
||||||
RecipientListHistory => "recipient-list-history",
|
|
||||||
|
|
||||||
/// The body describes a communications session, for example, an
|
|
||||||
/// RFC2327 SDP body.
|
|
||||||
Session => "session",
|
|
||||||
|
|
||||||
/// Authenticated Identity Body.
|
|
||||||
Aib => "aib",
|
|
||||||
|
|
||||||
/// The body describes an early communications session, for example,
|
|
||||||
/// and [RFC2327] SDP body.
|
|
||||||
EarlySession => "early-session",
|
|
||||||
|
|
||||||
/// The body includes a list of URIs to which URI-list services are to
|
|
||||||
/// be applied.
|
|
||||||
RecipientList => "recipient-list",
|
|
||||||
|
|
||||||
/// The payload of the message carrying this Content-Disposition header
|
|
||||||
/// field value is an Instant Message Disposition Notification as
|
|
||||||
/// requested in the corresponding Instant Message.
|
|
||||||
Notification => "notification",
|
|
||||||
|
|
||||||
/// The body needs to be handled according to a reference to the body
|
|
||||||
/// that is located in the same SIP message as the body.
|
|
||||||
ByReference => "by-reference",
|
|
||||||
|
|
||||||
/// The body contains information associated with an Info Package.
|
|
||||||
InfoPackage => "info-package",
|
|
||||||
|
|
||||||
/// The body describes either metadata about the RS or the reason for
|
|
||||||
/// the metadata snapshot request as determined by the MIME value
|
|
||||||
/// indicated in the Content-Type.
|
|
||||||
RecordingSession => "recording-session",
|
|
||||||
}, Default = Session
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// An unique identifier in a session, referencing a
|
|
||||||
/// [struct.Content.html](Content element).
|
|
||||||
ContentId
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Enum wrapping all of the various supported descriptions of a Content.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum Description {
|
|
||||||
/// Jingle RTP Sessions (XEP-0167) description.
|
|
||||||
Rtp(RtpDescription),
|
|
||||||
|
|
||||||
/// To be used for any description that isn’t known at compile-time.
|
|
||||||
Unknown(Element),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Description {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Description, Error> {
|
|
||||||
Ok(if elem.is("description", ns::JINGLE_RTP) {
|
|
||||||
Description::Rtp(RtpDescription::try_from(elem)?)
|
|
||||||
} else {
|
|
||||||
Description::Unknown(elem)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RtpDescription> for Description {
|
|
||||||
fn from(desc: RtpDescription) -> Description {
|
|
||||||
Description::Rtp(desc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Description> for Element {
|
|
||||||
fn from(desc: Description) -> Element {
|
|
||||||
match desc {
|
|
||||||
Description::Rtp(desc) => desc.into(),
|
|
||||||
Description::Unknown(elem) => elem,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enum wrapping all of the various supported transports of a Content.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum Transport {
|
|
||||||
/// Jingle ICE-UDP Bytestreams (XEP-0176) transport.
|
|
||||||
IceUdp(IceUdpTransport),
|
|
||||||
|
|
||||||
/// Jingle In-Band Bytestreams (XEP-0261) transport.
|
|
||||||
Ibb(IbbTransport),
|
|
||||||
|
|
||||||
/// Jingle SOCKS5 Bytestreams (XEP-0260) transport.
|
|
||||||
Socks5(Socks5Transport),
|
|
||||||
|
|
||||||
/// To be used for any transport that isn’t known at compile-time.
|
|
||||||
Unknown(Element),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Transport {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Transport, Error> {
|
|
||||||
Ok(if elem.is("transport", ns::JINGLE_ICE_UDP) {
|
|
||||||
Transport::IceUdp(IceUdpTransport::try_from(elem)?)
|
|
||||||
} else if elem.is("transport", ns::JINGLE_IBB) {
|
|
||||||
Transport::Ibb(IbbTransport::try_from(elem)?)
|
|
||||||
} else if elem.is("transport", ns::JINGLE_S5B) {
|
|
||||||
Transport::Socks5(Socks5Transport::try_from(elem)?)
|
|
||||||
} else {
|
|
||||||
Transport::Unknown(elem)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<IceUdpTransport> for Transport {
|
|
||||||
fn from(transport: IceUdpTransport) -> Transport {
|
|
||||||
Transport::IceUdp(transport)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<IbbTransport> for Transport {
|
|
||||||
fn from(transport: IbbTransport) -> Transport {
|
|
||||||
Transport::Ibb(transport)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Socks5Transport> for Transport {
|
|
||||||
fn from(transport: Socks5Transport) -> Transport {
|
|
||||||
Transport::Socks5(transport)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Transport> for Element {
|
|
||||||
fn from(transport: Transport) -> Element {
|
|
||||||
match transport {
|
|
||||||
Transport::IceUdp(transport) => transport.into(),
|
|
||||||
Transport::Ibb(transport) => transport.into(),
|
|
||||||
Transport::Socks5(transport) => transport.into(),
|
|
||||||
Transport::Unknown(elem) => elem,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Describes a session’s content, there can be multiple content in one
|
|
||||||
/// session.
|
|
||||||
Content, "content", JINGLE,
|
|
||||||
attributes: [
|
|
||||||
/// Who created this content.
|
|
||||||
creator: Option<Creator> = "creator",
|
|
||||||
|
|
||||||
/// How the content definition is to be interpreted by the recipient.
|
|
||||||
disposition: Default<Disposition> = "disposition",
|
|
||||||
|
|
||||||
/// A per-session unique identifier for this content.
|
|
||||||
name: Required<ContentId> = "name",
|
|
||||||
|
|
||||||
/// Who can send data for this content.
|
|
||||||
senders: Option<Senders> = "senders",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// What to send.
|
|
||||||
description: Option<Description> = ("description", *) => Description,
|
|
||||||
|
|
||||||
/// How to send it.
|
|
||||||
transport: Option<Transport> = ("transport", *) => Transport,
|
|
||||||
|
|
||||||
/// With which security.
|
|
||||||
security: Option<Element> = ("security", JINGLE) => Element
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Content {
|
|
||||||
/// Create a new content.
|
|
||||||
pub fn new(creator: Creator, name: ContentId) -> Content {
|
|
||||||
Content {
|
|
||||||
creator: Some(creator),
|
|
||||||
name,
|
|
||||||
disposition: Disposition::Session,
|
|
||||||
senders: Some(Senders::Both),
|
|
||||||
description: None,
|
|
||||||
transport: None,
|
|
||||||
security: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set how the content is to be interpreted by the recipient.
|
|
||||||
pub fn with_disposition(mut self, disposition: Disposition) -> Content {
|
|
||||||
self.disposition = disposition;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Specify who can send data for this content.
|
|
||||||
pub fn with_senders(mut self, senders: Senders) -> Content {
|
|
||||||
self.senders = Some(senders);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the description of this content.
|
|
||||||
pub fn with_description<D: Into<Description>>(mut self, description: D) -> Content {
|
|
||||||
self.description = Some(description.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the transport of this content.
|
|
||||||
pub fn with_transport<T: Into<Transport>>(mut self, transport: T) -> Content {
|
|
||||||
self.transport = Some(transport.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the security of this content.
|
|
||||||
pub fn with_security(mut self, security: Element) -> Content {
|
|
||||||
self.security = Some(security);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lists the possible reasons to be included in a Jingle iq.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum Reason {
|
|
||||||
/// The party prefers to use an existing session with the peer rather than
|
|
||||||
/// initiate a new session; the Jingle session ID of the alternative
|
|
||||||
/// session SHOULD be provided as the XML character data of the <sid/>
|
|
||||||
/// child.
|
|
||||||
AlternativeSession, //(String),
|
|
||||||
|
|
||||||
/// The party is busy and cannot accept a session.
|
|
||||||
Busy,
|
|
||||||
|
|
||||||
/// The initiator wishes to formally cancel the session initiation request.
|
|
||||||
Cancel,
|
|
||||||
|
|
||||||
/// The action is related to connectivity problems.
|
|
||||||
ConnectivityError,
|
|
||||||
|
|
||||||
/// The party wishes to formally decline the session.
|
|
||||||
Decline,
|
|
||||||
|
|
||||||
/// The session length has exceeded a pre-defined time limit (e.g., a
|
|
||||||
/// meeting hosted at a conference service).
|
|
||||||
Expired,
|
|
||||||
|
|
||||||
/// The party has been unable to initialize processing related to the
|
|
||||||
/// application type.
|
|
||||||
FailedApplication,
|
|
||||||
|
|
||||||
/// The party has been unable to establish connectivity for the transport
|
|
||||||
/// method.
|
|
||||||
FailedTransport,
|
|
||||||
|
|
||||||
/// The action is related to a non-specific application error.
|
|
||||||
GeneralError,
|
|
||||||
|
|
||||||
/// The entity is going offline or is no longer available.
|
|
||||||
Gone,
|
|
||||||
|
|
||||||
/// The party supports the offered application type but does not support
|
|
||||||
/// the offered or negotiated parameters.
|
|
||||||
IncompatibleParameters,
|
|
||||||
|
|
||||||
/// The action is related to media processing problems.
|
|
||||||
MediaError,
|
|
||||||
|
|
||||||
/// The action is related to a violation of local security policies.
|
|
||||||
SecurityError,
|
|
||||||
|
|
||||||
/// The action is generated during the normal course of state management
|
|
||||||
/// and does not reflect any error.
|
|
||||||
Success,
|
|
||||||
|
|
||||||
/// A request has not been answered so the sender is timing out the
|
|
||||||
/// request.
|
|
||||||
Timeout,
|
|
||||||
|
|
||||||
/// The party supports none of the offered application types.
|
|
||||||
UnsupportedApplications,
|
|
||||||
|
|
||||||
/// The party supports none of the offered transport methods.
|
|
||||||
UnsupportedTransports,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Reason {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Reason, Error> {
|
|
||||||
Ok(match s {
|
|
||||||
"alternative-session" => Reason::AlternativeSession,
|
|
||||||
"busy" => Reason::Busy,
|
|
||||||
"cancel" => Reason::Cancel,
|
|
||||||
"connectivity-error" => Reason::ConnectivityError,
|
|
||||||
"decline" => Reason::Decline,
|
|
||||||
"expired" => Reason::Expired,
|
|
||||||
"failed-application" => Reason::FailedApplication,
|
|
||||||
"failed-transport" => Reason::FailedTransport,
|
|
||||||
"general-error" => Reason::GeneralError,
|
|
||||||
"gone" => Reason::Gone,
|
|
||||||
"incompatible-parameters" => Reason::IncompatibleParameters,
|
|
||||||
"media-error" => Reason::MediaError,
|
|
||||||
"security-error" => Reason::SecurityError,
|
|
||||||
"success" => Reason::Success,
|
|
||||||
"timeout" => Reason::Timeout,
|
|
||||||
"unsupported-applications" => Reason::UnsupportedApplications,
|
|
||||||
"unsupported-transports" => Reason::UnsupportedTransports,
|
|
||||||
|
|
||||||
_ => return Err(Error::ParseError("Unknown reason.")),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Reason> for Element {
|
|
||||||
fn from(reason: Reason) -> Element {
|
|
||||||
Element::builder(
|
|
||||||
match reason {
|
|
||||||
Reason::AlternativeSession => "alternative-session",
|
|
||||||
Reason::Busy => "busy",
|
|
||||||
Reason::Cancel => "cancel",
|
|
||||||
Reason::ConnectivityError => "connectivity-error",
|
|
||||||
Reason::Decline => "decline",
|
|
||||||
Reason::Expired => "expired",
|
|
||||||
Reason::FailedApplication => "failed-application",
|
|
||||||
Reason::FailedTransport => "failed-transport",
|
|
||||||
Reason::GeneralError => "general-error",
|
|
||||||
Reason::Gone => "gone",
|
|
||||||
Reason::IncompatibleParameters => "incompatible-parameters",
|
|
||||||
Reason::MediaError => "media-error",
|
|
||||||
Reason::SecurityError => "security-error",
|
|
||||||
Reason::Success => "success",
|
|
||||||
Reason::Timeout => "timeout",
|
|
||||||
Reason::UnsupportedApplications => "unsupported-applications",
|
|
||||||
Reason::UnsupportedTransports => "unsupported-transports",
|
|
||||||
},
|
|
||||||
ns::JINGLE,
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Lang = String;
|
|
||||||
|
|
||||||
/// Informs the recipient of something.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct ReasonElement {
|
|
||||||
/// The list of possible reasons to be included in a Jingle iq.
|
|
||||||
pub reason: Reason,
|
|
||||||
|
|
||||||
/// A human-readable description of this reason.
|
|
||||||
pub texts: BTreeMap<Lang, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ReasonElement {
|
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(fmt, "{}", Element::from(self.reason.clone()).name())?;
|
|
||||||
if let Some(text) = self.texts.get("en") {
|
|
||||||
write!(fmt, ": {}", text)?;
|
|
||||||
} else if let Some(text) = self.texts.get("") {
|
|
||||||
write!(fmt, ": {}", text)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for ReasonElement {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<ReasonElement, Error> {
|
|
||||||
check_self!(elem, "reason", JINGLE);
|
|
||||||
check_no_attributes!(elem, "reason");
|
|
||||||
let mut reason = None;
|
|
||||||
let mut texts = BTreeMap::new();
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("text", ns::JINGLE) {
|
|
||||||
check_no_children!(child, "text");
|
|
||||||
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
|
|
||||||
let lang = get_attr!(elem, "xml:lang", Default);
|
|
||||||
if texts.insert(lang, child.text()).is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Text element present twice for the same xml:lang.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if child.has_ns(ns::JINGLE) {
|
|
||||||
if reason.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Reason must not have more than one reason.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
check_no_children!(child, "reason");
|
|
||||||
check_no_attributes!(child, "reason");
|
|
||||||
reason = Some(child.name().parse()?);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Reason contains a foreign element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?;
|
|
||||||
Ok(ReasonElement { reason, texts })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ReasonElement> for Element {
|
|
||||||
fn from(reason: ReasonElement) -> Element {
|
|
||||||
Element::builder("reason", ns::JINGLE)
|
|
||||||
.append(Element::from(reason.reason))
|
|
||||||
.append_all(reason.texts.into_iter().map(|(lang, text)| {
|
|
||||||
Element::builder("text", ns::JINGLE)
|
|
||||||
.attr("xml:lang", lang)
|
|
||||||
.append(text)
|
|
||||||
}))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// Unique identifier for a session between two JIDs.
|
|
||||||
SessionId
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The main Jingle container, to be included in an iq stanza.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Jingle {
|
|
||||||
/// The action to execute on both ends.
|
|
||||||
pub action: Action,
|
|
||||||
|
|
||||||
/// Who the initiator is.
|
|
||||||
pub initiator: Option<Jid>,
|
|
||||||
|
|
||||||
/// Who the responder is.
|
|
||||||
pub responder: Option<Jid>,
|
|
||||||
|
|
||||||
/// Unique session identifier between two entities.
|
|
||||||
pub sid: SessionId,
|
|
||||||
|
|
||||||
/// A list of contents to be negotiated in this session.
|
|
||||||
pub contents: Vec<Content>,
|
|
||||||
|
|
||||||
/// An optional reason.
|
|
||||||
pub reason: Option<ReasonElement>,
|
|
||||||
|
|
||||||
/// An optional grouping.
|
|
||||||
pub group: Option<Group>,
|
|
||||||
|
|
||||||
/// Payloads to be included.
|
|
||||||
pub other: Vec<Element>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IqSetPayload for Jingle {}
|
|
||||||
|
|
||||||
impl Jingle {
|
|
||||||
/// Create a new Jingle element.
|
|
||||||
pub fn new(action: Action, sid: SessionId) -> Jingle {
|
|
||||||
Jingle {
|
|
||||||
action,
|
|
||||||
sid,
|
|
||||||
initiator: None,
|
|
||||||
responder: None,
|
|
||||||
contents: Vec::new(),
|
|
||||||
reason: None,
|
|
||||||
group: None,
|
|
||||||
other: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the initiator’s JID.
|
|
||||||
pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
|
|
||||||
self.initiator = Some(initiator);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the responder’s JID.
|
|
||||||
pub fn with_responder(mut self, responder: Jid) -> Jingle {
|
|
||||||
self.responder = Some(responder);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a content to this Jingle container.
|
|
||||||
pub fn add_content(mut self, content: Content) -> Jingle {
|
|
||||||
self.contents.push(content);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the reason in this Jingle container.
|
|
||||||
pub fn set_reason(mut self, reason: ReasonElement) -> Jingle {
|
|
||||||
self.reason = Some(reason);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the grouping in this Jingle container.
|
|
||||||
pub fn set_group(mut self, group: Group) -> Jingle {
|
|
||||||
self.group = Some(group);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Jingle {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(root: Element) -> Result<Jingle, Error> {
|
|
||||||
check_self!(root, "jingle", JINGLE, "Jingle");
|
|
||||||
check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]);
|
|
||||||
|
|
||||||
let mut jingle = Jingle {
|
|
||||||
action: get_attr!(root, "action", Required),
|
|
||||||
initiator: get_attr!(root, "initiator", Option),
|
|
||||||
responder: get_attr!(root, "responder", Option),
|
|
||||||
sid: get_attr!(root, "sid", Required),
|
|
||||||
contents: vec![],
|
|
||||||
reason: None,
|
|
||||||
group: None,
|
|
||||||
other: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
for child in root.children().cloned() {
|
|
||||||
if child.is("content", ns::JINGLE) {
|
|
||||||
let content = Content::try_from(child)?;
|
|
||||||
jingle.contents.push(content);
|
|
||||||
} else if child.is("reason", ns::JINGLE) {
|
|
||||||
if jingle.reason.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Jingle must not have more than one reason.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let reason = ReasonElement::try_from(child)?;
|
|
||||||
jingle.reason = Some(reason);
|
|
||||||
} else if child.is("group", ns::JINGLE_GROUPING) {
|
|
||||||
if jingle.group.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Jingle must not have more than one grouping.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let group = Group::try_from(child)?;
|
|
||||||
jingle.group = Some(group);
|
|
||||||
} else {
|
|
||||||
jingle.other.push(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(jingle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Jingle> for Element {
|
|
||||||
fn from(jingle: Jingle) -> Element {
|
|
||||||
Element::builder("jingle", ns::JINGLE)
|
|
||||||
.attr("action", jingle.action)
|
|
||||||
.attr("initiator", jingle.initiator)
|
|
||||||
.attr("responder", jingle.responder)
|
|
||||||
.attr("sid", jingle.sid)
|
|
||||||
.append_all(jingle.contents)
|
|
||||||
.append_all(jingle.reason.map(Element::from))
|
|
||||||
.append_all(jingle.group.map(Element::from))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Action, 1);
|
|
||||||
assert_size!(Creator, 1);
|
|
||||||
assert_size!(Senders, 1);
|
|
||||||
assert_size!(Disposition, 1);
|
|
||||||
assert_size!(ContentId, 12);
|
|
||||||
assert_size!(Content, 252);
|
|
||||||
assert_size!(Reason, 1);
|
|
||||||
assert_size!(ReasonElement, 16);
|
|
||||||
assert_size!(SessionId, 12);
|
|
||||||
assert_size!(Jingle, 152);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Action, 1);
|
|
||||||
assert_size!(Creator, 1);
|
|
||||||
assert_size!(Senders, 1);
|
|
||||||
assert_size!(Disposition, 1);
|
|
||||||
assert_size!(ContentId, 24);
|
|
||||||
assert_size!(Content, 504);
|
|
||||||
assert_size!(Reason, 1);
|
|
||||||
assert_size!(ReasonElement, 32);
|
|
||||||
assert_size!(SessionId, 24);
|
|
||||||
assert_size!(Jingle, 304);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element =
|
|
||||||
"<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let jingle = Jingle::try_from(elem).unwrap();
|
|
||||||
assert_eq!(jingle.action, Action::SessionInitiate);
|
|
||||||
assert_eq!(jingle.sid, SessionId(String::from("coucou")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_jingle() {
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'action' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'sid' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'action' attribute.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_content() {
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou'><description/><transport xmlns='urn:xmpp:jingle:transports:stub:0'/></content></jingle>".parse().unwrap();
|
|
||||||
let jingle = Jingle::try_from(elem).unwrap();
|
|
||||||
assert_eq!(jingle.contents[0].creator, Creator::Initiator);
|
|
||||||
assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
|
|
||||||
assert_eq!(jingle.contents[0].senders, Senders::Both);
|
|
||||||
assert_eq!(jingle.contents[0].disposition, Disposition::Session);
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='both'><description/><transport xmlns='urn:xmpp:jingle:transports:stub:0'/></content></jingle>".parse().unwrap();
|
|
||||||
let jingle = Jingle::try_from(elem).unwrap();
|
|
||||||
assert_eq!(jingle.contents[0].senders, Senders::Both);
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' disposition='early-session'><description/><transport xmlns='urn:xmpp:jingle:transports:stub:0'/></content></jingle>".parse().unwrap();
|
|
||||||
let jingle = Jingle::try_from(elem).unwrap();
|
|
||||||
assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_content() {
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'creator' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'name' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'creator' attribute.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'senders' attribute.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'senders' attribute.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_reason() {
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
|
|
||||||
let jingle = Jingle::try_from(elem).unwrap();
|
|
||||||
let reason = jingle.reason.unwrap();
|
|
||||||
assert_eq!(reason.reason, Reason::Success);
|
|
||||||
assert_eq!(reason.texts, BTreeMap::new());
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
|
|
||||||
let jingle = Jingle::try_from(elem).unwrap();
|
|
||||||
let reason = jingle.reason.unwrap();
|
|
||||||
assert_eq!(reason.reason, Reason::Success);
|
|
||||||
assert_eq!(reason.texts.get(""), Some(&String::from("coucou")));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_reason() {
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Reason doesn’t contain a valid reason.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown reason.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a xmlns='http://www.w3.org/1999/xhtml'/></reason></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Reason contains a foreign element.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Jingle must not have more than one reason.");
|
|
||||||
|
|
||||||
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
|
|
||||||
let error = Jingle::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Text element present twice for the same xml:lang.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_jingle() {
|
|
||||||
let reference: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='a73sjjvkla37jfea'><content xmlns='urn:xmpp:jingle:1' creator='initiator' name='this-is-a-stub'><description xmlns='urn:xmpp:jingle:apps:stub:0'/><transport xmlns='urn:xmpp:jingle:transports:stub:0'/></content></jingle>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let jingle = Jingle {
|
|
||||||
action: Action::SessionInitiate,
|
|
||||||
initiator: None,
|
|
||||||
responder: None,
|
|
||||||
sid: SessionId(String::from("a73sjjvkla37jfea")),
|
|
||||||
contents: vec![Content {
|
|
||||||
creator: Creator::Initiator,
|
|
||||||
disposition: Disposition::default(),
|
|
||||||
name: ContentId(String::from("this-is-a-stub")),
|
|
||||||
senders: Senders::default(),
|
|
||||||
description: Some(Description::Unknown(
|
|
||||||
Element::builder("description", "urn:xmpp:jingle:apps:stub:0").build(),
|
|
||||||
)),
|
|
||||||
transport: Some(Transport::Unknown(
|
|
||||||
Element::builder("transport", "urn:xmpp:jingle:transports:stub:0").build(),
|
|
||||||
)),
|
|
||||||
security: None,
|
|
||||||
}],
|
|
||||||
reason: None,
|
|
||||||
group: None,
|
|
||||||
other: vec![],
|
|
||||||
};
|
|
||||||
let serialized: Element = jingle.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::hashes::{Algo, Hash};
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::util::helpers::ColonSeparatedHex;
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Indicates which of the end points should initiate the TCP connection establishment.
|
|
||||||
Setup, "setup", {
|
|
||||||
/// The endpoint will initiate an outgoing connection.
|
|
||||||
Active => "active",
|
|
||||||
|
|
||||||
/// The endpoint will accept an incoming connection.
|
|
||||||
Passive => "passive",
|
|
||||||
|
|
||||||
/// The endpoint is willing to accept an incoming connection or to initiate an outgoing
|
|
||||||
/// connection.
|
|
||||||
Actpass => "actpass",
|
|
||||||
|
|
||||||
/*
|
|
||||||
/// The endpoint does not want the connection to be established for the time being.
|
|
||||||
///
|
|
||||||
/// Note that this value isn’t used, as per the XEP.
|
|
||||||
Holdconn => "holdconn",
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: use a hashes::Hash instead of two different fields here.
|
|
||||||
generate_element!(
|
|
||||||
/// Fingerprint of the key used for a DTLS handshake.
|
|
||||||
Fingerprint, "fingerprint", JINGLE_DTLS,
|
|
||||||
attributes: [
|
|
||||||
/// The hash algorithm used for this fingerprint.
|
|
||||||
hash: Required<Algo> = "hash",
|
|
||||||
|
|
||||||
/// Indicates which of the end points should initiate the TCP connection establishment.
|
|
||||||
setup: Option<Setup> = "setup",
|
|
||||||
|
|
||||||
/// Indicates whether DTLS is mandatory
|
|
||||||
required: Option<String> = "required"
|
|
||||||
],
|
|
||||||
text: (
|
|
||||||
/// Hash value of this fingerprint.
|
|
||||||
value: ColonSeparatedHex<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Fingerprint {
|
|
||||||
/// Create a new Fingerprint from a Setup and a Hash.
|
|
||||||
pub fn from_hash(setup: Setup, hash: Hash) -> Fingerprint {
|
|
||||||
Fingerprint {
|
|
||||||
hash: hash.algo,
|
|
||||||
setup: Some(setup),
|
|
||||||
value: hash.hash,
|
|
||||||
required: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new Fingerprint from a Setup and parsing the hash.
|
|
||||||
pub fn from_colon_separated_hex(
|
|
||||||
setup: Setup,
|
|
||||||
algo: &str,
|
|
||||||
hash: &str,
|
|
||||||
) -> Result<Fingerprint, Error> {
|
|
||||||
let algo = algo.parse()?;
|
|
||||||
let hash = Hash::from_colon_separated_hex(algo, hash)?;
|
|
||||||
Ok(Fingerprint::from_hash(setup, hash))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Setup, 1);
|
|
||||||
assert_size!(Fingerprint, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Setup, 1);
|
|
||||||
assert_size!(Fingerprint, 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ex1() {
|
|
||||||
let elem: Element = "<fingerprint xmlns='urn:xmpp:jingle:apps:dtls:0' hash='sha-256' setup='actpass'>02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2</fingerprint>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let fingerprint = Fingerprint::try_from(elem).unwrap();
|
|
||||||
assert_eq!(fingerprint.setup, Some(Setup::Actpass));
|
|
||||||
assert_eq!(fingerprint.hash, Algo::Sha_256);
|
|
||||||
assert_eq!(
|
|
||||||
fingerprint.value,
|
|
||||||
[
|
|
||||||
2, 26, 204, 84, 39, 171, 235, 156, 83, 63, 62, 75, 101, 46, 125, 70, 63, 84, 66,
|
|
||||||
205, 84, 241, 122, 3, 162, 125, 249, 176, 127, 70, 25, 178
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,620 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::date::DateTime;
|
|
||||||
use crate::hashes::Hash;
|
|
||||||
use crate::jingle::{ContentId, Creator};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use minidom::{Element, Node};
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Represents a range in a file.
|
|
||||||
#[derive(Default)]
|
|
||||||
Range, "range", JINGLE_FT,
|
|
||||||
attributes: [
|
|
||||||
/// The offset in bytes from the beginning of the file.
|
|
||||||
offset: Default<u64> = "offset",
|
|
||||||
|
|
||||||
/// The length in bytes of the range, or None to be the entire
|
|
||||||
/// remaining of the file.
|
|
||||||
length: Option<u64> = "length"
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// List of hashes for this range.
|
|
||||||
hashes: Vec<Hash> = ("hash", HASHES) => Hash
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Range {
|
|
||||||
/// Creates a new range.
|
|
||||||
pub fn new() -> Range {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Lang = String;
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// Wrapper for a file description.
|
|
||||||
Desc
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Represents a file to be transferred.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct File {
|
|
||||||
/// The date of last modification of this file.
|
|
||||||
pub date: Option<DateTime>,
|
|
||||||
|
|
||||||
/// The MIME type of this file.
|
|
||||||
pub media_type: Option<String>,
|
|
||||||
|
|
||||||
/// The name of this file.
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
/// The description of this file, possibly localised.
|
|
||||||
pub descs: BTreeMap<Lang, Desc>,
|
|
||||||
|
|
||||||
/// The size of this file, in bytes.
|
|
||||||
pub size: Option<u64>,
|
|
||||||
|
|
||||||
/// Used to request only a part of this file.
|
|
||||||
pub range: Option<Range>,
|
|
||||||
|
|
||||||
/// A list of hashes matching this entire file.
|
|
||||||
pub hashes: Vec<Hash>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl File {
|
|
||||||
/// Creates a new file descriptor.
|
|
||||||
pub fn new() -> File {
|
|
||||||
File::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the date of last modification on this file.
|
|
||||||
pub fn with_date(mut self, date: DateTime) -> File {
|
|
||||||
self.date = Some(date);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the date of last modification on this file from an ISO-8601
|
|
||||||
/// string.
|
|
||||||
pub fn with_date_str(mut self, date: &str) -> Result<File, Error> {
|
|
||||||
self.date = Some(DateTime::from_str(date)?);
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the MIME type of this file.
|
|
||||||
pub fn with_media_type(mut self, media_type: String) -> File {
|
|
||||||
self.media_type = Some(media_type);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the name of this file.
|
|
||||||
pub fn with_name(mut self, name: String) -> File {
|
|
||||||
self.name = Some(name);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a description for this file.
|
|
||||||
pub fn add_desc(mut self, lang: &str, desc: Desc) -> File {
|
|
||||||
self.descs.insert(Lang::from(lang), desc);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the file size of this file, in bytes.
|
|
||||||
pub fn with_size(mut self, size: u64) -> File {
|
|
||||||
self.size = Some(size);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request only a range of this file.
|
|
||||||
pub fn with_range(mut self, range: Range) -> File {
|
|
||||||
self.range = Some(range);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a hash on this file.
|
|
||||||
pub fn add_hash(mut self, hash: Hash) -> File {
|
|
||||||
self.hashes.push(hash);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for File {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<File, Error> {
|
|
||||||
check_self!(elem, "file", JINGLE_FT);
|
|
||||||
check_no_attributes!(elem, "file");
|
|
||||||
|
|
||||||
let mut file = File {
|
|
||||||
date: None,
|
|
||||||
media_type: None,
|
|
||||||
name: None,
|
|
||||||
descs: BTreeMap::new(),
|
|
||||||
size: None,
|
|
||||||
range: None,
|
|
||||||
hashes: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("date", ns::JINGLE_FT) {
|
|
||||||
if file.date.is_some() {
|
|
||||||
return Err(Error::ParseError("File must not have more than one date."));
|
|
||||||
}
|
|
||||||
file.date = Some(child.text().parse()?);
|
|
||||||
} else if child.is("media-type", ns::JINGLE_FT) {
|
|
||||||
if file.media_type.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"File must not have more than one media-type.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
file.media_type = Some(child.text());
|
|
||||||
} else if child.is("name", ns::JINGLE_FT) {
|
|
||||||
if file.name.is_some() {
|
|
||||||
return Err(Error::ParseError("File must not have more than one name."));
|
|
||||||
}
|
|
||||||
file.name = Some(child.text());
|
|
||||||
} else if child.is("desc", ns::JINGLE_FT) {
|
|
||||||
let lang = get_attr!(child, "xml:lang", Default);
|
|
||||||
let desc = Desc(child.text());
|
|
||||||
if file.descs.insert(lang, desc).is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Desc element present twice for the same xml:lang.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if child.is("size", ns::JINGLE_FT) {
|
|
||||||
if file.size.is_some() {
|
|
||||||
return Err(Error::ParseError("File must not have more than one size."));
|
|
||||||
}
|
|
||||||
file.size = Some(child.text().parse()?);
|
|
||||||
} else if child.is("range", ns::JINGLE_FT) {
|
|
||||||
if file.range.is_some() {
|
|
||||||
return Err(Error::ParseError("File must not have more than one range."));
|
|
||||||
}
|
|
||||||
file.range = Some(Range::try_from(child.clone())?);
|
|
||||||
} else if child.is("hash", ns::HASHES) {
|
|
||||||
file.hashes.push(Hash::try_from(child.clone())?);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown element in JingleFT file."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<File> for Element {
|
|
||||||
fn from(file: File) -> Element {
|
|
||||||
Element::builder("file", ns::JINGLE_FT)
|
|
||||||
.append_all(
|
|
||||||
file.date
|
|
||||||
.map(|date| Element::builder("date", ns::JINGLE_FT).append(date)),
|
|
||||||
)
|
|
||||||
.append_all(
|
|
||||||
file.media_type.map(|media_type| {
|
|
||||||
Element::builder("media-type", ns::JINGLE_FT).append(media_type)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.append_all(
|
|
||||||
file.name
|
|
||||||
.map(|name| Element::builder("name", ns::JINGLE_FT).append(name)),
|
|
||||||
)
|
|
||||||
.append_all(file.descs.into_iter().map(|(lang, desc)| {
|
|
||||||
Element::builder("desc", ns::JINGLE_FT)
|
|
||||||
.attr("xml:lang", lang)
|
|
||||||
.append(desc.0)
|
|
||||||
}))
|
|
||||||
.append_all(
|
|
||||||
file.size.map(|size| {
|
|
||||||
Element::builder("size", ns::JINGLE_FT).append(format!("{}", size))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.append_all(file.range)
|
|
||||||
.append_all(file.hashes)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A wrapper element for a file.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Description {
|
|
||||||
/// The actual file descriptor.
|
|
||||||
pub file: File,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Description {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Description, Error> {
|
|
||||||
check_self!(elem, "description", JINGLE_FT, "JingleFT description");
|
|
||||||
check_no_attributes!(elem, "JingleFT description");
|
|
||||||
let mut file = None;
|
|
||||||
for child in elem.children() {
|
|
||||||
if file.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"JingleFT description element must have exactly one child.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
file = Some(File::try_from(child.clone())?);
|
|
||||||
}
|
|
||||||
if file.is_none() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"JingleFT description element must have exactly one child.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(Description {
|
|
||||||
file: file.unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Description> for Element {
|
|
||||||
fn from(description: Description) -> Element {
|
|
||||||
Element::builder("description", ns::JINGLE_FT)
|
|
||||||
.append(Node::Element(description.file.into()))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A checksum for checking that the file has been transferred correctly.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Checksum {
|
|
||||||
/// The identifier of the file transfer content.
|
|
||||||
pub name: ContentId,
|
|
||||||
|
|
||||||
/// The creator of this file transfer.
|
|
||||||
pub creator: Creator,
|
|
||||||
|
|
||||||
/// The file being checksummed.
|
|
||||||
pub file: File,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Checksum {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Checksum, Error> {
|
|
||||||
check_self!(elem, "checksum", JINGLE_FT);
|
|
||||||
check_no_unknown_attributes!(elem, "checksum", ["name", "creator"]);
|
|
||||||
let mut file = None;
|
|
||||||
for child in elem.children() {
|
|
||||||
if file.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"JingleFT checksum element must have exactly one child.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
file = Some(File::try_from(child.clone())?);
|
|
||||||
}
|
|
||||||
if file.is_none() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"JingleFT checksum element must have exactly one child.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(Checksum {
|
|
||||||
name: get_attr!(elem, "name", Required),
|
|
||||||
creator: get_attr!(elem, "creator", Required),
|
|
||||||
file: file.unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Checksum> for Element {
|
|
||||||
fn from(checksum: Checksum) -> Element {
|
|
||||||
Element::builder("checksum", ns::JINGLE_FT)
|
|
||||||
.attr("name", checksum.name)
|
|
||||||
.attr("creator", checksum.creator)
|
|
||||||
.append(Node::Element(checksum.file.into()))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A notice that the file transfer has been completed.
|
|
||||||
Received, "received", JINGLE_FT,
|
|
||||||
attributes: [
|
|
||||||
/// The content identifier of this Jingle session.
|
|
||||||
name: Required<ContentId> = "name",
|
|
||||||
|
|
||||||
/// The creator of this file transfer.
|
|
||||||
creator: Required<Creator> = "creator",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::hashes::Algo;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Range, 40);
|
|
||||||
assert_size!(File, 128);
|
|
||||||
assert_size!(Description, 128);
|
|
||||||
assert_size!(Checksum, 144);
|
|
||||||
assert_size!(Received, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Range, 48);
|
|
||||||
assert_size!(File, 184);
|
|
||||||
assert_size!(Description, 184);
|
|
||||||
assert_size!(Checksum, 216);
|
|
||||||
assert_size!(Received, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_description() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
|
||||||
<file>
|
|
||||||
<media-type>text/plain</media-type>
|
|
||||||
<name>test.txt</name>
|
|
||||||
<date>2015-07-26T21:46:00+01:00</date>
|
|
||||||
<size>6144</size>
|
|
||||||
<hash xmlns='urn:xmpp:hashes:2'
|
|
||||||
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
|
||||||
</file>
|
|
||||||
</description>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let desc = Description::try_from(elem).unwrap();
|
|
||||||
assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
|
|
||||||
assert_eq!(desc.file.name, Some(String::from("test.txt")));
|
|
||||||
assert_eq!(desc.file.descs, BTreeMap::new());
|
|
||||||
assert_eq!(
|
|
||||||
desc.file.date,
|
|
||||||
DateTime::from_str("2015-07-26T21:46:00+01:00").ok()
|
|
||||||
);
|
|
||||||
assert_eq!(desc.file.size, Some(6144u64));
|
|
||||||
assert_eq!(desc.file.range, None);
|
|
||||||
assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
|
|
||||||
assert_eq!(
|
|
||||||
desc.file.hashes[0].hash,
|
|
||||||
base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_request() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
|
||||||
<file>
|
|
||||||
<hash xmlns='urn:xmpp:hashes:2'
|
|
||||||
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
|
||||||
</file>
|
|
||||||
</description>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let desc = Description::try_from(elem).unwrap();
|
|
||||||
assert_eq!(desc.file.media_type, None);
|
|
||||||
assert_eq!(desc.file.name, None);
|
|
||||||
assert_eq!(desc.file.descs, BTreeMap::new());
|
|
||||||
assert_eq!(desc.file.date, None);
|
|
||||||
assert_eq!(desc.file.size, None);
|
|
||||||
assert_eq!(desc.file.range, None);
|
|
||||||
assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
|
|
||||||
assert_eq!(
|
|
||||||
desc.file.hashes[0].hash,
|
|
||||||
base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_descs() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
|
||||||
<file>
|
|
||||||
<media-type>text/plain</media-type>
|
|
||||||
<desc xml:lang='fr'>Fichier secret !</desc>
|
|
||||||
<desc xml:lang='en'>Secret file!</desc>
|
|
||||||
<hash xmlns='urn:xmpp:hashes:2'
|
|
||||||
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
|
||||||
</file>
|
|
||||||
</description>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let desc = Description::try_from(elem).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
desc.file.descs.keys().cloned().collect::<Vec<_>>(),
|
|
||||||
["en", "fr"]
|
|
||||||
);
|
|
||||||
assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!")));
|
|
||||||
assert_eq!(
|
|
||||||
desc.file.descs["fr"],
|
|
||||||
Desc(String::from("Fichier secret !"))
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = r#"
|
|
||||||
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
|
||||||
<file>
|
|
||||||
<media-type>text/plain</media-type>
|
|
||||||
<desc xml:lang='fr'>Fichier secret !</desc>
|
|
||||||
<desc xml:lang='fr'>Secret file!</desc>
|
|
||||||
<hash xmlns='urn:xmpp:hashes:2'
|
|
||||||
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
|
||||||
</file>
|
|
||||||
</description>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Description::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Desc element present twice for the same xml:lang.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_received() {
|
|
||||||
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'/>".parse().unwrap();
|
|
||||||
let received = Received::try_from(elem).unwrap();
|
|
||||||
assert_eq!(received.name, ContentId(String::from("coucou")));
|
|
||||||
assert_eq!(received.creator, Creator::Initiator);
|
|
||||||
let elem2 = Element::from(received.clone());
|
|
||||||
let received2 = Received::try_from(elem2).unwrap();
|
|
||||||
assert_eq!(received2.name, ContentId(String::from("coucou")));
|
|
||||||
assert_eq!(received2.creator, Creator::Initiator);
|
|
||||||
|
|
||||||
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></received>".parse().unwrap();
|
|
||||||
let error = Received::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in received element.");
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Received::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'name' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'/>".parse().unwrap();
|
|
||||||
let error = Received::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'creator' attribute.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_received() {
|
|
||||||
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''/>".parse().unwrap();
|
|
||||||
let error = Received::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in received element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_checksum() {
|
|
||||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
|
||||||
let hash = vec![
|
|
||||||
195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138,
|
|
||||||
63, 143,
|
|
||||||
];
|
|
||||||
let checksum = Checksum::try_from(elem).unwrap();
|
|
||||||
assert_eq!(checksum.name, ContentId(String::from("coucou")));
|
|
||||||
assert_eq!(checksum.creator, Creator::Initiator);
|
|
||||||
assert_eq!(
|
|
||||||
checksum.file.hashes,
|
|
||||||
vec!(Hash {
|
|
||||||
algo: Algo::Sha_1,
|
|
||||||
hash: hash.clone()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
let elem2 = Element::from(checksum);
|
|
||||||
let checksum2 = Checksum::try_from(elem2).unwrap();
|
|
||||||
assert_eq!(checksum2.name, ContentId(String::from("coucou")));
|
|
||||||
assert_eq!(checksum2.creator, Creator::Initiator);
|
|
||||||
assert_eq!(
|
|
||||||
checksum2.file.hashes,
|
|
||||||
vec!(Hash {
|
|
||||||
algo: Algo::Sha_1,
|
|
||||||
hash: hash.clone()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></checksum>".parse().unwrap();
|
|
||||||
let error = Checksum::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "This is not a file element.");
|
|
||||||
|
|
||||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
|
||||||
let error = Checksum::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'name' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
|
||||||
let error = Checksum::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'creator' attribute.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_checksum() {
|
|
||||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
|
||||||
let error = Checksum::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in checksum element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_range() {
|
|
||||||
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let range = Range::try_from(elem).unwrap();
|
|
||||||
assert_eq!(range.offset, 0);
|
|
||||||
assert_eq!(range.length, None);
|
|
||||||
assert_eq!(range.hashes, vec!());
|
|
||||||
|
|
||||||
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' offset='2048' length='1024'><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>kHp5RSzW/h7Gm1etSf90Mr5PC/k=</hash></range>".parse().unwrap();
|
|
||||||
let hashes = vec![Hash {
|
|
||||||
algo: Algo::Sha_1,
|
|
||||||
hash: vec![
|
|
||||||
144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79,
|
|
||||||
11, 249,
|
|
||||||
],
|
|
||||||
}];
|
|
||||||
let range = Range::try_from(elem).unwrap();
|
|
||||||
assert_eq!(range.offset, 2048);
|
|
||||||
assert_eq!(range.length, Some(1024));
|
|
||||||
assert_eq!(range.hashes, hashes);
|
|
||||||
let elem2 = Element::from(range);
|
|
||||||
let range2 = Range::try_from(elem2).unwrap();
|
|
||||||
assert_eq!(range2.offset, 2048);
|
|
||||||
assert_eq!(range2.length, Some(1024));
|
|
||||||
assert_eq!(range2.hashes, hashes);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_range() {
|
|
||||||
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Range::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in range element.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
// Copyright (c) 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::jingle::ContentId;
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The semantics of the grouping.
|
|
||||||
Semantics, "semantics", {
|
|
||||||
/// Lip synchronsation.
|
|
||||||
Ls => "LS",
|
|
||||||
|
|
||||||
/// Bundle.
|
|
||||||
Bundle => "BUNDLE",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Describes a content that should be grouped with other ones.
|
|
||||||
Content, "content", JINGLE_GROUPING,
|
|
||||||
attributes: [
|
|
||||||
/// The name of the matching [`Content`](crate::jingle::Content).
|
|
||||||
name: Required<ContentId> = "name",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Content {
|
|
||||||
/// Creates a new <content/> element.
|
|
||||||
pub fn new(name: &str) -> Content {
|
|
||||||
Content {
|
|
||||||
name: ContentId(name.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A semantic group of contents.
|
|
||||||
Group, "group", JINGLE_GROUPING,
|
|
||||||
attributes: [
|
|
||||||
/// Semantics of the grouping.
|
|
||||||
semantics: Required<Semantics> = "semantics",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// List of contents that should be grouped with each other.
|
|
||||||
contents: Vec<Content> = ("content", JINGLE_GROUPING) => Content
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Semantics, 1);
|
|
||||||
assert_size!(Content, 12);
|
|
||||||
assert_size!(Group, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Semantics, 1);
|
|
||||||
assert_size!(Content, 24);
|
|
||||||
assert_size!(Group, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_group() {
|
|
||||||
let elem: Element = "
|
|
||||||
<group xmlns='urn:xmpp:jingle:apps:grouping:0' semantics='BUNDLE'>
|
|
||||||
<content name='voice'/>
|
|
||||||
<content name='webcam'/>
|
|
||||||
</group>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let group = Group::try_from(elem).unwrap();
|
|
||||||
assert_eq!(group.semantics, Semantics::Bundle);
|
|
||||||
assert_eq!(group.contents.len(), 2);
|
|
||||||
assert_eq!(
|
|
||||||
group.contents,
|
|
||||||
&[Content::new("voice"), Content::new("webcam")]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::ibb::{Stanza, StreamId};
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Describes an [In-Band Bytestream](https://xmpp.org/extensions/xep-0047.html)
|
|
||||||
/// Jingle transport, see also the [IBB module](../ibb.rs).
|
|
||||||
Transport, "transport", JINGLE_IBB,
|
|
||||||
attributes: [
|
|
||||||
/// Maximum size in bytes for each chunk.
|
|
||||||
block_size: Required<u16> = "block-size",
|
|
||||||
|
|
||||||
/// The identifier to be used to create a stream.
|
|
||||||
sid: Required<StreamId> = "sid",
|
|
||||||
|
|
||||||
/// Which stanza type to use to exchange data.
|
|
||||||
stanza: Default<Stanza> = "stanza",
|
|
||||||
]);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Transport, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Transport, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element =
|
|
||||||
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='3' sid='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let transport = Transport::try_from(elem).unwrap();
|
|
||||||
assert_eq!(transport.block_size, 3);
|
|
||||||
assert_eq!(transport.sid, StreamId(String::from("coucou")));
|
|
||||||
assert_eq!(transport.stanza, Stanza::Iq);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Transport::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'block-size' missing.");
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='65536'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Transport::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseIntError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
message.to_string(),
|
|
||||||
"number too large to fit in target type"
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='-5'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Transport::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseIntError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message.to_string(), "invalid digit found in string");
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='128'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Transport::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'sid' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_stanza() {
|
|
||||||
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='128' sid='coucou' stanza='fdsq'/>".parse().unwrap();
|
|
||||||
let error = Transport::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'stanza' attribute.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,243 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::jingle_dtls_srtp::Fingerprint;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Specifies the ability to multiplex RTP Data and Control Packets on a single port as
|
|
||||||
/// described in RFC 5761.
|
|
||||||
RtcpMux,
|
|
||||||
"rtcp-mux",
|
|
||||||
JINGLE_ICE_UDP
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Wrapper element for an ICE-UDP transport.
|
|
||||||
Transport, "transport", JINGLE_ICE_UDP,
|
|
||||||
attributes: [
|
|
||||||
/// A Password as defined in ICE-CORE.
|
|
||||||
pwd: Option<String> = "pwd",
|
|
||||||
|
|
||||||
/// A User Fragment as defined in ICE-CORE.
|
|
||||||
ufrag: Option<String> = "ufrag",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// List of candidates for this ICE-UDP session.
|
|
||||||
candidates: Vec<Candidate> = ("candidate", JINGLE_ICE_UDP) => Candidate,
|
|
||||||
|
|
||||||
/// Fingerprint of the key used for the DTLS handshake.
|
|
||||||
fingerprint: Option<Fingerprint> = ("fingerprint", JINGLE_DTLS) => Fingerprint,
|
|
||||||
|
|
||||||
/// Indicates that RTCP can be muxed
|
|
||||||
rtcp_mux: Option<RtcpMux> = ("rtcp-mux", JINGLE_ICE_UDP) => RtcpMux,
|
|
||||||
|
|
||||||
/// Details of the Colibri WebSocket
|
|
||||||
web_socket: Option<WebSocket> = ("web-socket", JITSI_COLIBRI) => WebSocket
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Transport {
|
|
||||||
/// Create a new ICE-UDP transport.
|
|
||||||
pub fn new() -> Transport {
|
|
||||||
Transport {
|
|
||||||
pwd: None,
|
|
||||||
ufrag: None,
|
|
||||||
candidates: Vec::new(),
|
|
||||||
fingerprint: None,
|
|
||||||
rtcp_mux: None,
|
|
||||||
web_socket: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a candidate to this transport.
|
|
||||||
pub fn add_candidate(mut self, candidate: Candidate) -> Self {
|
|
||||||
self.candidates.push(candidate);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the DTLS-SRTP fingerprint of this transport.
|
|
||||||
pub fn with_fingerprint(mut self, fingerprint: Fingerprint) -> Self {
|
|
||||||
self.fingerprint = Some(fingerprint);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Colibri WebSocket details
|
|
||||||
WebSocket, "web-socket", JITSI_COLIBRI,
|
|
||||||
attributes: [
|
|
||||||
/// The WebSocket URL
|
|
||||||
url: Required<String> = "url",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// A Candidate Type as defined in ICE-CORE.
|
|
||||||
Type, "type", {
|
|
||||||
/// Host candidate.
|
|
||||||
Host => "host",
|
|
||||||
|
|
||||||
/// Peer reflexive candidate.
|
|
||||||
Prflx => "prflx",
|
|
||||||
|
|
||||||
/// Relayed candidate.
|
|
||||||
Relay => "relay",
|
|
||||||
|
|
||||||
/// Server reflexive candidate.
|
|
||||||
Srflx => "srflx",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A candidate for an ICE-UDP session.
|
|
||||||
Candidate, "candidate", JINGLE_ICE_UDP,
|
|
||||||
attributes: [
|
|
||||||
/// A Component ID as defined in ICE-CORE.
|
|
||||||
component: Required<u8> = "component",
|
|
||||||
|
|
||||||
/// A Foundation as defined in ICE-CORE.
|
|
||||||
foundation: Required<String> = "foundation",
|
|
||||||
|
|
||||||
/// An index, starting at 0, that enables the parties to keep track of updates to the
|
|
||||||
/// candidate throughout the life of the session.
|
|
||||||
generation: Required<u8> = "generation",
|
|
||||||
|
|
||||||
/// A unique identifier for the candidate.
|
|
||||||
id: Required<String> = "id",
|
|
||||||
|
|
||||||
/// The Internet Protocol (IP) address for the candidate transport mechanism; this can be
|
|
||||||
/// either an IPv4 address or an IPv6 address.
|
|
||||||
ip: Required<IpAddr> = "ip",
|
|
||||||
|
|
||||||
/// The port at the candidate IP address.
|
|
||||||
port: Required<u16> = "port",
|
|
||||||
|
|
||||||
/// A Priority as defined in ICE-CORE.
|
|
||||||
priority: Required<u32> = "priority",
|
|
||||||
|
|
||||||
/// The protocol to be used. The only value defined by this specification is "udp".
|
|
||||||
protocol: Required<String> = "protocol",
|
|
||||||
|
|
||||||
/// A related address as defined in ICE-CORE.
|
|
||||||
rel_addr: Option<IpAddr> = "rel-addr",
|
|
||||||
|
|
||||||
/// A related port as defined in ICE-CORE.
|
|
||||||
rel_port: Option<u16> = "rel-port",
|
|
||||||
|
|
||||||
/// An index, starting at 0, referencing which network this candidate is on for a given
|
|
||||||
/// peer.
|
|
||||||
network: Option<u8> = "network",
|
|
||||||
|
|
||||||
/// A Candidate Type as defined in ICE-CORE.
|
|
||||||
type_: Required<Type> = "type",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::hashes::Algo;
|
|
||||||
use crate::jingle_dtls_srtp::Setup;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Transport, 68);
|
|
||||||
assert_size!(Type, 1);
|
|
||||||
assert_size!(Candidate, 92);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Transport, 136);
|
|
||||||
assert_size!(Type, 1);
|
|
||||||
assert_size!(Candidate, 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_gajim() {
|
|
||||||
let elem: Element = "
|
|
||||||
<transport xmlns='urn:xmpp:jingle:transports:ice-udp:1' pwd='wakMJ8Ydd5rqnPaFerws5o' ufrag='aeXX'>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='11b72719-6a1b-4c51-8ae6-9f1538047568' ip='192.168.0.12' network='0' port='56715' priority='1010828030' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='7e07b22d-db50-4e17-9ed9-eafeb96f4f63' ip='192.168.0.12' network='0' port='0' priority='1015022334' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='431de362-c45f-40a8-bf10-9ed898a71d86' ip='192.168.0.12' network='0' port='36480' priority='2013266428' protocol='udp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='b1197df3-abca-413b-99ee-3660d91bcfa7' ip='192.168.0.12' network='0' port='50387' priority='1010828031' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='adaf3a85-3a57-4df0-a2d8-0c7d28d3ca01' ip='192.168.0.12' network='0' port='0' priority='1015022335' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='ef4e0a62-81f2-4fe3-87ae-46cb5d1d1e1d' ip='192.168.0.12' network='0' port='43132' priority='2013266429' protocol='udp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='51891e8a-4c1e-4540-b173-8637aeb0143c' ip='fe80::24eb:646f:7d78:cb6' network='0' port='38881' priority='2013266431' protocol='udp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='73f82655-eb84-4fa1-b05c-1ea76f695d32' ip='fe80::24eb:646f:7d78:cb6' network='0' port='0' priority='1015023103' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='a2a8fa62-6f2e-41e8-b218-ba095540d60f' ip='fe80::24eb:646f:7d78:cb6' network='0' port='55819' priority='1010828799' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='23e66735-9515-414c-81ad-2455569a57f8' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='39967' priority='2013266430' protocol='udp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='9a8dff18-e138-4fb2-a956-89d71216da84' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='0' priority='1015022079' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='f0c73ac3-9b7d-4032-abe3-6dd9a57d0f03' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='37487' priority='1010827775' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='a6199a00-34df-46f5-a608-847b75c5250e' ip='fe80::24eb:646f:7d78:cb6' network='0' port='43521' priority='2013266430' protocol='udp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='83bc2600-39ce-4c9e-8b0b-cc7aa7e6a293' ip='fe80::24eb:646f:7d78:cb6' network='0' port='0' priority='1015023102' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='7e3606ca-46de-4de8-8802-068dd69ef01a' ip='fe80::24eb:646f:7d78:cb6' network='0' port='52279' priority='1010828798' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='a7c2472a-8462-412c-a64c-d3528f0abfa4' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='34088' priority='2013266429' protocol='udp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='5a12c345-9643-4d2c-b770-695ec6affcaf' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='0' priority='1015022078' protocol='tcp' type='host'/>
|
|
||||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='67f65b0b-8cee-421a-9f37-1f2ca2211c87' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='39431' priority='1010827774' protocol='tcp' type='host'/>
|
|
||||||
</transport>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let transport = Transport::try_from(elem).unwrap();
|
|
||||||
assert_eq!(transport.pwd.unwrap(), "wakMJ8Ydd5rqnPaFerws5o");
|
|
||||||
assert_eq!(transport.ufrag.unwrap(), "aeXX");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_jitsi_meet() {
|
|
||||||
let elem: Element = "
|
|
||||||
<transport ufrag='2acq51d4p07v2m' pwd='7lk9uul39gckit6t02oavv2r9j' xmlns='urn:xmpp:jingle:transports:ice-udp:1'>
|
|
||||||
<fingerprint hash='sha-1' setup='actpass' xmlns='urn:xmpp:jingle:apps:dtls:0'>97:F2:B5:BE:DB:A6:00:B1:3E:40:B2:41:3C:0D:FC:E0:BD:B2:A0:E8</fingerprint>
|
|
||||||
<candidate type='host' protocol='udp' id='186cb069513c2bbe546192c93cc4ab3b05ab0d426' ip='2a05:d014:fc7:54a1:8bfc:7248:3d1c:51a4' component='1' port='10000' foundation='1' generation='0' priority='2130706431' network='0'/>
|
|
||||||
<candidate type='host' protocol='udp' id='186cb069513c2bbe546192c93cc4ab3b063daeefd' ip='10.15.1.120' component='1' port='10000' foundation='2' generation='0' priority='2130706431' network='0'/>
|
|
||||||
<candidate rel-port='10000' type='srflx' protocol='udp' id='186cb069513c2bbe546192c93cc4ab3b05d449db8' ip='3.120.176.51' component='1' port='10000' foundation='3' generation='0' network='0' priority='1677724415' rel-addr='10.15.1.120'/>
|
|
||||||
</transport>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let transport = Transport::try_from(elem).unwrap();
|
|
||||||
assert_eq!(transport.pwd.unwrap(), "7lk9uul39gckit6t02oavv2r9j");
|
|
||||||
assert_eq!(transport.ufrag.unwrap(), "2acq51d4p07v2m");
|
|
||||||
|
|
||||||
let fingerprint = transport.fingerprint.unwrap();
|
|
||||||
assert_eq!(fingerprint.hash, Algo::Sha_1);
|
|
||||||
assert_eq!(fingerprint.setup, Setup::Actpass);
|
|
||||||
assert_eq!(
|
|
||||||
fingerprint.value,
|
|
||||||
[
|
|
||||||
151, 242, 181, 190, 219, 166, 0, 177, 62, 64, 178, 65, 60, 13, 252, 224, 189, 178,
|
|
||||||
160, 232
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_transport() {
|
|
||||||
let reference: Element =
|
|
||||||
"<transport xmlns='urn:xmpp:jingle:transports:ice-udp:1'><fingerprint xmlns='urn:xmpp:jingle:apps:dtls:0' hash='sha-256' setup='actpass'>02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2</fingerprint></transport>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<fingerprint xmlns='urn:xmpp:jingle:apps:dtls:0' hash='sha-256' setup='actpass'>02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2</fingerprint>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let fingerprint = Fingerprint::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let transport = Transport {
|
|
||||||
pwd: None,
|
|
||||||
ufrag: None,
|
|
||||||
candidates: vec![],
|
|
||||||
fingerprint: Some(fingerprint),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = transport.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::jingle::SessionId;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Defines a protocol for broadcasting Jingle requests to all of the clients
|
|
||||||
/// of a user.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum JingleMI {
|
|
||||||
/// Indicates we want to start a Jingle session.
|
|
||||||
Propose {
|
|
||||||
/// The generated session identifier, must be unique between two users.
|
|
||||||
sid: SessionId,
|
|
||||||
|
|
||||||
/// The application description of the proposed session.
|
|
||||||
// TODO: Use a more specialised type here.
|
|
||||||
description: Element,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Cancels a previously proposed session.
|
|
||||||
Retract(SessionId),
|
|
||||||
|
|
||||||
/// Accepts a session proposed by the other party.
|
|
||||||
Accept(SessionId),
|
|
||||||
|
|
||||||
/// Proceed with a previously proposed session.
|
|
||||||
Proceed(SessionId),
|
|
||||||
|
|
||||||
/// Rejects a session proposed by the other party.
|
|
||||||
Reject(SessionId),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sid(elem: Element) -> Result<SessionId, Error> {
|
|
||||||
check_no_unknown_attributes!(elem, "Jingle message", ["id"]);
|
|
||||||
Ok(SessionId(get_attr!(elem, "id", Required)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_empty_and_get_sid(elem: Element) -> Result<SessionId, Error> {
|
|
||||||
check_no_children!(elem, "Jingle message");
|
|
||||||
get_sid(elem)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for JingleMI {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<JingleMI, Error> {
|
|
||||||
if !elem.has_ns(ns::JINGLE_MESSAGE) {
|
|
||||||
return Err(Error::ParseError("This is not a Jingle message element."));
|
|
||||||
}
|
|
||||||
Ok(match elem.name() {
|
|
||||||
"propose" => {
|
|
||||||
let mut description = None;
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.name() != "description" {
|
|
||||||
return Err(Error::ParseError("Unknown child in propose element."));
|
|
||||||
}
|
|
||||||
if description.is_some() {
|
|
||||||
return Err(Error::ParseError("Too many children in propose element."));
|
|
||||||
}
|
|
||||||
description = Some(child.clone());
|
|
||||||
}
|
|
||||||
JingleMI::Propose {
|
|
||||||
sid: get_sid(elem)?,
|
|
||||||
description: description.ok_or(Error::ParseError(
|
|
||||||
"Propose element doesn’t contain a description.",
|
|
||||||
))?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"retract" => JingleMI::Retract(check_empty_and_get_sid(elem)?),
|
|
||||||
"accept" => JingleMI::Accept(check_empty_and_get_sid(elem)?),
|
|
||||||
"proceed" => JingleMI::Proceed(check_empty_and_get_sid(elem)?),
|
|
||||||
"reject" => JingleMI::Reject(check_empty_and_get_sid(elem)?),
|
|
||||||
_ => return Err(Error::ParseError("This is not a Jingle message element.")),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<JingleMI> for Element {
|
|
||||||
fn from(jingle_mi: JingleMI) -> Element {
|
|
||||||
match jingle_mi {
|
|
||||||
JingleMI::Propose { sid, description } => {
|
|
||||||
Element::builder("propose", ns::JINGLE_MESSAGE)
|
|
||||||
.attr("id", sid)
|
|
||||||
.append(description)
|
|
||||||
}
|
|
||||||
JingleMI::Retract(sid) => {
|
|
||||||
Element::builder("retract", ns::JINGLE_MESSAGE).attr("id", sid)
|
|
||||||
}
|
|
||||||
JingleMI::Accept(sid) => Element::builder("accept", ns::JINGLE_MESSAGE).attr("id", sid),
|
|
||||||
JingleMI::Proceed(sid) => {
|
|
||||||
Element::builder("proceed", ns::JINGLE_MESSAGE).attr("id", sid)
|
|
||||||
}
|
|
||||||
JingleMI::Reject(sid) => Element::builder("reject", ns::JINGLE_MESSAGE).attr("id", sid),
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(JingleMI, 92);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(JingleMI, 184);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<accept xmlns='urn:xmpp:jingle-message:0' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
JingleMI::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element =
|
|
||||||
"<propose xmlns='urn:xmpp:jingle-message:0' id='coucou'><coucou/></propose>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = JingleMI::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in propose element.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
// Copyright (c) 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::jingle_ice_udp::Type;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Wrapper element for an raw UDP transport.
|
|
||||||
Transport, "transport", JINGLE_RAW_UDP,
|
|
||||||
children: [
|
|
||||||
/// List of candidates for this raw UDP session.
|
|
||||||
candidates: Vec<Candidate> = ("candidate", JINGLE_RAW_UDP) => Candidate
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Transport {
|
|
||||||
/// Create a new ICE-UDP transport.
|
|
||||||
pub fn new() -> Transport {
|
|
||||||
Transport {
|
|
||||||
candidates: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a candidate to this transport.
|
|
||||||
pub fn add_candidate(mut self, candidate: Candidate) -> Self {
|
|
||||||
self.candidates.push(candidate);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A candidate for an ICE-UDP session.
|
|
||||||
Candidate, "candidate", JINGLE_RAW_UDP,
|
|
||||||
attributes: [
|
|
||||||
/// A Component ID as defined in ICE-CORE.
|
|
||||||
component: Required<u8> = "component",
|
|
||||||
|
|
||||||
/// An index, starting at 0, that enables the parties to keep track of updates to the
|
|
||||||
/// candidate throughout the life of the session.
|
|
||||||
generation: Required<u8> = "generation",
|
|
||||||
|
|
||||||
/// A unique identifier for the candidate.
|
|
||||||
id: Required<String> = "id",
|
|
||||||
|
|
||||||
/// The Internet Protocol (IP) address for the candidate transport mechanism; this can be
|
|
||||||
/// either an IPv4 address or an IPv6 address.
|
|
||||||
ip: Required<IpAddr> = "ip",
|
|
||||||
|
|
||||||
/// The port at the candidate IP address.
|
|
||||||
port: Required<u16> = "port",
|
|
||||||
|
|
||||||
/// A Candidate Type as defined in ICE-CORE.
|
|
||||||
type_: Option<Type> = "type",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Transport, 12);
|
|
||||||
assert_size!(Candidate, 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Transport, 24);
|
|
||||||
assert_size!(Candidate, 56);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn example_1() {
|
|
||||||
let elem: Element = "
|
|
||||||
<transport xmlns='urn:xmpp:jingle:transports:raw-udp:1'>
|
|
||||||
<candidate component='1'
|
|
||||||
generation='0'
|
|
||||||
id='a9j3mnbtu1'
|
|
||||||
ip='10.1.1.104'
|
|
||||||
port='13540'/>
|
|
||||||
</transport>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let mut transport = Transport::try_from(elem).unwrap();
|
|
||||||
assert_eq!(transport.candidates.len(), 1);
|
|
||||||
let candidate = transport.candidates.pop().unwrap();
|
|
||||||
assert_eq!(candidate.component, 1);
|
|
||||||
assert_eq!(candidate.generation, 0);
|
|
||||||
assert_eq!(candidate.id, "a9j3mnbtu1");
|
|
||||||
assert_eq!(candidate.ip, "10.1.1.104".parse::<IpAddr>().unwrap());
|
|
||||||
assert_eq!(candidate.port, 13540u16);
|
|
||||||
assert!(candidate.type_.is_none());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Wrapper element for a rtcp-fb.
|
|
||||||
RtcpFb, "rtcp-fb", JINGLE_RTCP_FB,
|
|
||||||
attributes: [
|
|
||||||
/// Type of this rtcp-fb.
|
|
||||||
type_: Required<String> = "type",
|
|
||||||
|
|
||||||
/// Subtype of this rtcp-fb, if relevant.
|
|
||||||
subtype: Option<String> = "subtype",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(RtcpFb, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(RtcpFb, 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_simple() {
|
|
||||||
let elem: Element =
|
|
||||||
"<rtcp-fb xmlns='urn:xmpp:jingle:apps:rtp:rtcp-fb:0' type='nack' subtype='sli'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let rtcp_fb = RtcpFb::try_from(elem).unwrap();
|
|
||||||
assert_eq!(rtcp_fb.type_, "nack");
|
|
||||||
assert_eq!(rtcp_fb.subtype.unwrap(), "sli");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,211 +0,0 @@
|
||||||
// Copyright (c) 2019-2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::jingle_rtcp_fb::RtcpFb;
|
|
||||||
use crate::jingle_rtp_hdrext::RtpHdrext;
|
|
||||||
use crate::jingle_ssma::{Group, Source};
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Specifies the ability to multiplex RTP Data and Control Packets on a single port as
|
|
||||||
/// described in RFC 5761.
|
|
||||||
RtcpMux,
|
|
||||||
"rtcp-mux",
|
|
||||||
JINGLE_RTP
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Wrapper element describing an RTP session.
|
|
||||||
Description, "description", JINGLE_RTP,
|
|
||||||
attributes: [
|
|
||||||
/// Namespace of the encryption scheme used.
|
|
||||||
media: Required<String> = "media",
|
|
||||||
|
|
||||||
/// ssrc?
|
|
||||||
ssrc: Option<String> = "ssrc",
|
|
||||||
|
|
||||||
/// maximum packet time
|
|
||||||
maxptime: Option<u32> = "maxptime",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// List of encodings that can be used for this RTP stream.
|
|
||||||
payload_types: Vec<PayloadType> = ("payload-type", JINGLE_RTP) => PayloadType,
|
|
||||||
|
|
||||||
/// Specifies the ability to multiplex RTP Data and Control Packets on a single port as
|
|
||||||
/// described in RFC 5761.
|
|
||||||
rtcp_mux: Option<RtcpMux> = ("rtcp-mux", JINGLE_RTP) => RtcpMux,
|
|
||||||
|
|
||||||
/// List of ssrc-group.
|
|
||||||
ssrc_groups: Vec<Group> = ("ssrc-group", JINGLE_SSMA) => Group,
|
|
||||||
|
|
||||||
/// List of ssrc.
|
|
||||||
ssrcs: Vec<Source> = ("source", JINGLE_SSMA) => Source,
|
|
||||||
|
|
||||||
/// List of header extensions.
|
|
||||||
hdrexts: Vec<RtpHdrext> = ("rtp-hdrext", JINGLE_RTP_HDREXT) => RtpHdrext
|
|
||||||
|
|
||||||
// TODO: Add support for <encryption/> and <bandwidth/>.
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Description {
|
|
||||||
/// Create a new RTP description.
|
|
||||||
pub fn new(media: String) -> Description {
|
|
||||||
Description {
|
|
||||||
media,
|
|
||||||
ssrc: None,
|
|
||||||
maxptime: None,
|
|
||||||
payload_types: Vec::new(),
|
|
||||||
rtcp_mux: None,
|
|
||||||
ssrc_groups: Vec::new(),
|
|
||||||
ssrcs: Vec::new(),
|
|
||||||
hdrexts: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The number of channels.
|
|
||||||
Channels,
|
|
||||||
"channels",
|
|
||||||
u8,
|
|
||||||
Default = 1
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// An encoding that can be used for an RTP stream.
|
|
||||||
PayloadType, "payload-type", JINGLE_RTP,
|
|
||||||
attributes: [
|
|
||||||
/// The number of channels.
|
|
||||||
channels: Default<Channels> = "channels",
|
|
||||||
|
|
||||||
/// The sampling frequency in Hertz.
|
|
||||||
clockrate: Option<u32> = "clockrate",
|
|
||||||
|
|
||||||
/// The payload identifier.
|
|
||||||
id: Required<u8> = "id",
|
|
||||||
|
|
||||||
/// Maximum packet time as specified in RFC 4566.
|
|
||||||
maxptime: Option<u32> = "maxptime",
|
|
||||||
|
|
||||||
/// The appropriate subtype of the MIME type.
|
|
||||||
name: Option<String> = "name",
|
|
||||||
|
|
||||||
/// Packet time as specified in RFC 4566.
|
|
||||||
ptime: Option<u32> = "ptime",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// List of parameters specifying this payload-type.
|
|
||||||
///
|
|
||||||
/// Their order MUST be ignored.
|
|
||||||
parameters: Vec<Parameter> = ("parameter", JINGLE_RTP) => Parameter,
|
|
||||||
|
|
||||||
/// List of rtcp-fb parameters from XEP-0293.
|
|
||||||
rtcp_fbs: Vec<RtcpFb> = ("rtcp-fb", JINGLE_RTCP_FB) => RtcpFb
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PayloadType {
|
|
||||||
/// Create a new RTP payload-type.
|
|
||||||
pub fn new(id: u8, name: String, clockrate: u32, channels: u8) -> PayloadType {
|
|
||||||
PayloadType {
|
|
||||||
channels: Channels(channels),
|
|
||||||
clockrate: Some(clockrate),
|
|
||||||
id,
|
|
||||||
maxptime: None,
|
|
||||||
name: Some(name),
|
|
||||||
ptime: None,
|
|
||||||
parameters: Vec::new(),
|
|
||||||
rtcp_fbs: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new RTP payload-type without a clockrate. Warning: this is invalid as per
|
|
||||||
/// RFC 4566!
|
|
||||||
pub fn without_clockrate(id: u8, name: String) -> PayloadType {
|
|
||||||
PayloadType {
|
|
||||||
channels: Default::default(),
|
|
||||||
clockrate: None,
|
|
||||||
id,
|
|
||||||
maxptime: None,
|
|
||||||
name: Some(name),
|
|
||||||
ptime: None,
|
|
||||||
parameters: Vec::new(),
|
|
||||||
rtcp_fbs: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Parameter related to a payload.
|
|
||||||
Parameter, "parameter", JINGLE_RTP,
|
|
||||||
attributes: [
|
|
||||||
/// The name of the parameter, from the list at
|
|
||||||
/// https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml
|
|
||||||
name: Required<String> = "name",
|
|
||||||
|
|
||||||
/// The value of this parameter.
|
|
||||||
value: Required<String> = "value",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Description, 76);
|
|
||||||
assert_size!(Channels, 1);
|
|
||||||
assert_size!(PayloadType, 64);
|
|
||||||
assert_size!(Parameter, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Description, 152);
|
|
||||||
assert_size!(Channels, 1);
|
|
||||||
assert_size!(PayloadType, 104);
|
|
||||||
assert_size!(Parameter, 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "
|
|
||||||
<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='2' clockrate='48000' id='96' name='OPUS'/>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='32000' id='105' name='SPEEX'/>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='9' name='G722'/>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='16000' id='106' name='SPEEX'/>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='8' name='PCMA'/>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='0' name='PCMU'/>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='107' name='SPEEX'/>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='99' name='AMR'>
|
|
||||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='octet-align' value='1'/>
|
|
||||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='crc' value='0'/>
|
|
||||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='robust-sorting' value='0'/>
|
|
||||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='interleaving' value='0'/>
|
|
||||||
</payload-type>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='48000' id='100' name='telephone-event'>
|
|
||||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
|
|
||||||
</payload-type>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='16000' id='101' name='telephone-event'>
|
|
||||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
|
|
||||||
</payload-type>
|
|
||||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='102' name='telephone-event'>
|
|
||||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
|
|
||||||
</payload-type>
|
|
||||||
</description>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let desc = Description::try_from(elem).unwrap();
|
|
||||||
assert_eq!(desc.media, "audio");
|
|
||||||
assert_eq!(desc.ssrc, None);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
// Copyright (c) 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Which party is allowed to send the negotiated RTP Header Extensions.
|
|
||||||
Senders, "senders", {
|
|
||||||
/// Both parties can send them.
|
|
||||||
Both => "both",
|
|
||||||
|
|
||||||
/// Only the initiator can send them.
|
|
||||||
Initiator => "initiator",
|
|
||||||
|
|
||||||
/// Only the responder can send them.
|
|
||||||
Responder => "responder",
|
|
||||||
}, Default = Both
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Header extensions to be used in a RTP description.
|
|
||||||
RtpHdrext, "rtp-hdrext", JINGLE_RTP_HDREXT,
|
|
||||||
attributes: [
|
|
||||||
/// The ID of the extensions.
|
|
||||||
id: Required<String> = "id",
|
|
||||||
|
|
||||||
/// The URI that defines the extension.
|
|
||||||
uri: Required<String> = "uri",
|
|
||||||
|
|
||||||
/// Which party is allowed to send the negotiated RTP Header Extensions.
|
|
||||||
senders: Default<Senders> = "senders",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl RtpHdrext {
|
|
||||||
/// Create a new RTP header extension element.
|
|
||||||
pub fn new(id: String, uri: String) -> RtpHdrext {
|
|
||||||
RtpHdrext {
|
|
||||||
id,
|
|
||||||
uri,
|
|
||||||
senders: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the senders.
|
|
||||||
pub fn with_senders(mut self, senders: Senders) -> RtpHdrext {
|
|
||||||
self.senders = senders;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Senders, 1);
|
|
||||||
assert_size!(RtpHdrext, 28);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Senders, 1);
|
|
||||||
assert_size!(RtpHdrext, 56);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_exthdr() {
|
|
||||||
let elem: Element = "
|
|
||||||
<rtp-hdrext xmlns='urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'
|
|
||||||
uri='urn:ietf:params:rtp-hdrext:toffset'
|
|
||||||
id='1'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let rtp_hdrext = RtpHdrext::try_from(elem).unwrap();
|
|
||||||
assert_eq!(rtp_hdrext.id, "1");
|
|
||||||
assert_eq!(rtp_hdrext.uri, "urn:ietf:params:rtp-hdrext:toffset");
|
|
||||||
assert_eq!(rtp_hdrext.senders, Senders::Both);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,353 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The type of the connection being proposed by this candidate.
|
|
||||||
Type, "type", {
|
|
||||||
/// Direct connection using NAT assisting technologies like NAT-PMP or
|
|
||||||
/// UPnP-IGD.
|
|
||||||
Assisted => "assisted",
|
|
||||||
|
|
||||||
/// Direct connection using the given interface.
|
|
||||||
Direct => "direct",
|
|
||||||
|
|
||||||
/// SOCKS5 relay.
|
|
||||||
Proxy => "proxy",
|
|
||||||
|
|
||||||
/// Tunnel protocol such as Teredo.
|
|
||||||
Tunnel => "tunnel",
|
|
||||||
}, Default = Direct
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Which mode to use for the connection.
|
|
||||||
Mode, "mode", {
|
|
||||||
/// Use TCP, which is the default.
|
|
||||||
Tcp => "tcp",
|
|
||||||
|
|
||||||
/// Use UDP.
|
|
||||||
Udp => "udp",
|
|
||||||
}, Default = Tcp
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// An identifier for a candidate.
|
|
||||||
CandidateId
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// An identifier for a stream.
|
|
||||||
StreamId
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A candidate for a connection.
|
|
||||||
Candidate, "candidate", JINGLE_S5B,
|
|
||||||
attributes: [
|
|
||||||
/// The identifier for this candidate.
|
|
||||||
cid: Required<CandidateId> = "cid",
|
|
||||||
|
|
||||||
/// The host to connect to.
|
|
||||||
host: Required<IpAddr> = "host",
|
|
||||||
|
|
||||||
/// The JID to request at the given end.
|
|
||||||
jid: Required<Jid> = "jid",
|
|
||||||
|
|
||||||
/// The port to connect to.
|
|
||||||
port: Option<u16> = "port",
|
|
||||||
|
|
||||||
/// The priority of this candidate, computed using this formula:
|
|
||||||
/// priority = (2^16)*(type preference) + (local preference)
|
|
||||||
priority: Required<u32> = "priority",
|
|
||||||
|
|
||||||
/// The type of the connection being proposed by this candidate.
|
|
||||||
type_: Default<Type> = "type",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Candidate {
|
|
||||||
/// Creates a new candidate with the given parameters.
|
|
||||||
pub fn new(cid: CandidateId, host: IpAddr, jid: Jid, priority: u32) -> Candidate {
|
|
||||||
Candidate {
|
|
||||||
cid,
|
|
||||||
host,
|
|
||||||
jid,
|
|
||||||
priority,
|
|
||||||
port: Default::default(),
|
|
||||||
type_: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the port of this candidate.
|
|
||||||
pub fn with_port(mut self, port: u16) -> Candidate {
|
|
||||||
self.port = Some(port);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the type of this candidate.
|
|
||||||
pub fn with_type(mut self, type_: Type) -> Candidate {
|
|
||||||
self.type_ = type_;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The payload of a transport.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum TransportPayload {
|
|
||||||
/// The responder informs the initiator that the bytestream pointed by this
|
|
||||||
/// candidate has been activated.
|
|
||||||
Activated(CandidateId),
|
|
||||||
|
|
||||||
/// A list of suggested candidates.
|
|
||||||
Candidates(Vec<Candidate>),
|
|
||||||
|
|
||||||
/// Both parties failed to use a candidate, they should fallback to another
|
|
||||||
/// transport.
|
|
||||||
CandidateError,
|
|
||||||
|
|
||||||
/// The candidate pointed here should be used by both parties.
|
|
||||||
CandidateUsed(CandidateId),
|
|
||||||
|
|
||||||
/// This entity can’t connect to the SOCKS5 proxy.
|
|
||||||
ProxyError,
|
|
||||||
|
|
||||||
/// XXX: Invalid, should not be found in the wild.
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes a Jingle transport using a direct or proxied connection.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Transport {
|
|
||||||
/// The stream identifier for this transport.
|
|
||||||
pub sid: StreamId,
|
|
||||||
|
|
||||||
/// The destination address.
|
|
||||||
pub dstaddr: Option<String>,
|
|
||||||
|
|
||||||
/// The mode to be used for the transfer.
|
|
||||||
pub mode: Mode,
|
|
||||||
|
|
||||||
/// The payload of this transport.
|
|
||||||
pub payload: TransportPayload,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Transport {
|
|
||||||
/// Creates a new transport element.
|
|
||||||
pub fn new(sid: StreamId) -> Transport {
|
|
||||||
Transport {
|
|
||||||
sid,
|
|
||||||
dstaddr: None,
|
|
||||||
mode: Default::default(),
|
|
||||||
payload: TransportPayload::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the destination address of this transport.
|
|
||||||
pub fn with_dstaddr(mut self, dstaddr: String) -> Transport {
|
|
||||||
self.dstaddr = Some(dstaddr);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the mode of this transport.
|
|
||||||
pub fn with_mode(mut self, mode: Mode) -> Transport {
|
|
||||||
self.mode = mode;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the payload of this transport.
|
|
||||||
pub fn with_payload(mut self, payload: TransportPayload) -> Transport {
|
|
||||||
self.payload = payload;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Transport {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Transport, Error> {
|
|
||||||
check_self!(elem, "transport", JINGLE_S5B);
|
|
||||||
check_no_unknown_attributes!(elem, "transport", ["sid", "dstaddr", "mode"]);
|
|
||||||
let sid = get_attr!(elem, "sid", Required);
|
|
||||||
let dstaddr = get_attr!(elem, "dstaddr", Option);
|
|
||||||
let mode = get_attr!(elem, "mode", Default);
|
|
||||||
|
|
||||||
let mut payload = None;
|
|
||||||
for child in elem.children() {
|
|
||||||
payload = Some(if child.is("candidate", ns::JINGLE_S5B) {
|
|
||||||
let mut candidates =
|
|
||||||
match payload {
|
|
||||||
Some(TransportPayload::Candidates(candidates)) => candidates,
|
|
||||||
Some(_) => return Err(Error::ParseError(
|
|
||||||
"Non-candidate child already present in JingleS5B transport element.",
|
|
||||||
)),
|
|
||||||
None => vec![],
|
|
||||||
};
|
|
||||||
candidates.push(Candidate::try_from(child.clone())?);
|
|
||||||
TransportPayload::Candidates(candidates)
|
|
||||||
} else if child.is("activated", ns::JINGLE_S5B) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Non-activated child already present in JingleS5B transport element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let cid = get_attr!(child, "cid", Required);
|
|
||||||
TransportPayload::Activated(cid)
|
|
||||||
} else if child.is("candidate-error", ns::JINGLE_S5B) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Non-candidate-error child already present in JingleS5B transport element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
TransportPayload::CandidateError
|
|
||||||
} else if child.is("candidate-used", ns::JINGLE_S5B) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Non-candidate-used child already present in JingleS5B transport element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let cid = get_attr!(child, "cid", Required);
|
|
||||||
TransportPayload::CandidateUsed(cid)
|
|
||||||
} else if child.is("proxy-error", ns::JINGLE_S5B) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Non-proxy-error child already present in JingleS5B transport element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
TransportPayload::ProxyError
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Unknown child in JingleS5B transport element.",
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let payload = payload.unwrap_or(TransportPayload::None);
|
|
||||||
Ok(Transport {
|
|
||||||
sid,
|
|
||||||
dstaddr,
|
|
||||||
mode,
|
|
||||||
payload,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Transport> for Element {
|
|
||||||
fn from(transport: Transport) -> Element {
|
|
||||||
Element::builder("transport", ns::JINGLE_S5B)
|
|
||||||
.attr("sid", transport.sid)
|
|
||||||
.attr("dstaddr", transport.dstaddr)
|
|
||||||
.attr("mode", transport.mode)
|
|
||||||
.append_all(match transport.payload {
|
|
||||||
TransportPayload::Candidates(candidates) => candidates
|
|
||||||
.into_iter()
|
|
||||||
.map(Element::from)
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
TransportPayload::Activated(cid) => {
|
|
||||||
vec![Element::builder("activated", ns::JINGLE_S5B)
|
|
||||||
.attr("cid", cid)
|
|
||||||
.build()]
|
|
||||||
}
|
|
||||||
TransportPayload::CandidateError => {
|
|
||||||
vec![Element::builder("candidate-error", ns::JINGLE_S5B).build()]
|
|
||||||
}
|
|
||||||
TransportPayload::CandidateUsed(cid) => {
|
|
||||||
vec![Element::builder("candidate-used", ns::JINGLE_S5B)
|
|
||||||
.attr("cid", cid)
|
|
||||||
.build()]
|
|
||||||
}
|
|
||||||
TransportPayload::ProxyError => {
|
|
||||||
vec![Element::builder("proxy-error", ns::JINGLE_S5B).build()]
|
|
||||||
}
|
|
||||||
TransportPayload::None => vec![],
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use jid::BareJid;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Type, 1);
|
|
||||||
assert_size!(Mode, 1);
|
|
||||||
assert_size!(CandidateId, 12);
|
|
||||||
assert_size!(StreamId, 12);
|
|
||||||
assert_size!(Candidate, 84);
|
|
||||||
assert_size!(TransportPayload, 16);
|
|
||||||
assert_size!(Transport, 44);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Type, 1);
|
|
||||||
assert_size!(Mode, 1);
|
|
||||||
assert_size!(CandidateId, 24);
|
|
||||||
assert_size!(StreamId, 24);
|
|
||||||
assert_size!(Candidate, 136);
|
|
||||||
assert_size!(TransportPayload, 32);
|
|
||||||
assert_size!(Transport, 88);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:s5b:1' sid='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let transport = Transport::try_from(elem).unwrap();
|
|
||||||
assert_eq!(transport.sid, StreamId(String::from("coucou")));
|
|
||||||
assert_eq!(transport.dstaddr, None);
|
|
||||||
assert_eq!(transport.mode, Mode::Tcp);
|
|
||||||
match transport.payload {
|
|
||||||
TransportPayload::None => (),
|
|
||||||
_ => panic!("Wrong element inside transport!"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise_activated() {
|
|
||||||
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:s5b:1' sid='coucou'><activated cid='coucou'/></transport>".parse().unwrap();
|
|
||||||
let transport = Transport {
|
|
||||||
sid: StreamId(String::from("coucou")),
|
|
||||||
dstaddr: None,
|
|
||||||
mode: Mode::Tcp,
|
|
||||||
payload: TransportPayload::Activated(CandidateId(String::from("coucou"))),
|
|
||||||
};
|
|
||||||
let elem2: Element = transport.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise_candidate() {
|
|
||||||
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:s5b:1' sid='coucou'><candidate cid='coucou' host='127.0.0.1' jid='coucou@coucou' priority='0'/></transport>".parse().unwrap();
|
|
||||||
let transport = Transport {
|
|
||||||
sid: StreamId(String::from("coucou")),
|
|
||||||
dstaddr: None,
|
|
||||||
mode: Mode::Tcp,
|
|
||||||
payload: TransportPayload::Candidates(vec![Candidate {
|
|
||||||
cid: CandidateId(String::from("coucou")),
|
|
||||||
host: IpAddr::from_str("127.0.0.1").unwrap(),
|
|
||||||
jid: Jid::Bare(BareJid::new("coucou", "coucou")),
|
|
||||||
port: None,
|
|
||||||
priority: 0u32,
|
|
||||||
type_: Type::Direct,
|
|
||||||
}]),
|
|
||||||
};
|
|
||||||
let elem2: Element = transport.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Source element for the ssrc SDP attribute.
|
|
||||||
Source, "source", JINGLE_SSMA,
|
|
||||||
attributes: [
|
|
||||||
/// Maps to the ssrc-id parameter.
|
|
||||||
id: Required<String> = "ssrc",
|
|
||||||
|
|
||||||
/// XXX: wtf is that? It can be either name='jvb-a0' or name='jvb-v0' at avstack’s jicofo.
|
|
||||||
name: Option<String> = "name",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// List of attributes for this source.
|
|
||||||
parameters: Vec<Parameter> = ("parameter", JINGLE_SSMA) => Parameter,
|
|
||||||
|
|
||||||
/// ssrc-info for this source.
|
|
||||||
info: Option<SsrcInfo> = ("ssrc-info", JITSI_MEET) => SsrcInfo
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Source {
|
|
||||||
/// Create a new SSMA Source element.
|
|
||||||
pub fn new(id: String) -> Source {
|
|
||||||
Source {
|
|
||||||
id,
|
|
||||||
parameters: Vec::new(),
|
|
||||||
info: None,
|
|
||||||
name: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Parameter associated with a ssrc.
|
|
||||||
Parameter, "parameter", JINGLE_SSMA,
|
|
||||||
attributes: [
|
|
||||||
/// The name of the parameter.
|
|
||||||
name: Required<String> = "name",
|
|
||||||
|
|
||||||
/// The optional value of the parameter.
|
|
||||||
value: Option<String> = "value",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// ssrc-info associated with a ssrc.
|
|
||||||
SsrcInfo, "ssrc-info", JITSI_MEET,
|
|
||||||
attributes: [
|
|
||||||
/// The owner of the ssrc.
|
|
||||||
owner: Required<String> = "owner"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Element grouping multiple ssrc.
|
|
||||||
Group, "ssrc-group", JINGLE_SSMA,
|
|
||||||
attributes: [
|
|
||||||
/// The semantics of this group.
|
|
||||||
semantics: Required<String> = "semantics",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The various ssrc concerned by this group.
|
|
||||||
sources: Vec<Source> = ("source", JINGLE_SSMA) => Source
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Source, 24);
|
|
||||||
assert_size!(Parameter, 24);
|
|
||||||
assert_size!(Group, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Source, 48);
|
|
||||||
assert_size!(Parameter, 48);
|
|
||||||
assert_size!(Group, 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_source() {
|
|
||||||
let elem: Element = "
|
|
||||||
<source ssrc='1656081975' xmlns='urn:xmpp:jingle:apps:rtp:ssma:0'>
|
|
||||||
<parameter name='cname' value='Yv/wvbCdsDW2Prgd'/>
|
|
||||||
<parameter name='msid' value='MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0'/>
|
|
||||||
</source>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let mut ssrc = Source::try_from(elem).unwrap();
|
|
||||||
assert_eq!(ssrc.id, "1656081975");
|
|
||||||
assert_eq!(ssrc.parameters.len(), 2);
|
|
||||||
let parameter = ssrc.parameters.pop().unwrap();
|
|
||||||
assert_eq!(parameter.name, "msid");
|
|
||||||
assert_eq!(
|
|
||||||
parameter.value.unwrap(),
|
|
||||||
"MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0"
|
|
||||||
);
|
|
||||||
let parameter = ssrc.parameters.pop().unwrap();
|
|
||||||
assert_eq!(parameter.name, "cname");
|
|
||||||
assert_eq!(parameter.value.unwrap(), "Yv/wvbCdsDW2Prgd");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_source_group() {
|
|
||||||
let elem: Element = "
|
|
||||||
<ssrc-group semantics='FID' xmlns='urn:xmpp:jingle:apps:rtp:ssma:0'>
|
|
||||||
<source ssrc='2301230316'/>
|
|
||||||
<source ssrc='386328120'/>
|
|
||||||
</ssrc-group>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let mut group = Group::try_from(elem).unwrap();
|
|
||||||
assert_eq!(group.semantics, "FID");
|
|
||||||
assert_eq!(group.sources.len(), 2);
|
|
||||||
let source = group.sources.pop().unwrap();
|
|
||||||
assert_eq!(source.id, "386328120");
|
|
||||||
let source = group.sources.pop().unwrap();
|
|
||||||
assert_eq!(source.id, "2301230316");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,229 +0,0 @@
|
||||||
//! A crate parsing common XMPP elements into Rust structures.
|
|
||||||
//!
|
|
||||||
//! Each module implements the `TryFrom<Element>` trait, which takes a
|
|
||||||
//! minidom [`Element`] and returns a `Result` whose value is `Ok` if the
|
|
||||||
//! element parsed correctly, `Err(error::Error)` otherwise.
|
|
||||||
//!
|
|
||||||
//! The returned structure can be manipuled as any Rust structure, with each
|
|
||||||
//! field being public. You can also create the same structure manually, with
|
|
||||||
//! some having `new()` and `with_*()` helper methods to create them.
|
|
||||||
//!
|
|
||||||
//! Once you are happy with your structure, you can serialise it back to an
|
|
||||||
//! [`Element`], using either `From` or `Into<Element>`, which give you what
|
|
||||||
//! you want to be sending on the wire.
|
|
||||||
//!
|
|
||||||
//! [`Element`]: ../minidom/element/struct.Element.html
|
|
||||||
|
|
||||||
// Copyright (c) 2017-2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
// Copyright (c) 2017-2019 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
#![deny(missing_docs)]
|
|
||||||
|
|
||||||
pub use crate::util::error::Error;
|
|
||||||
pub use jid::{BareJid, FullJid, Jid, JidParseError};
|
|
||||||
pub use minidom::Element;
|
|
||||||
|
|
||||||
/// XML namespace definitions used through XMPP.
|
|
||||||
pub mod ns;
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
mod util;
|
|
||||||
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub mod bind;
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub mod iq;
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub mod message;
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub mod presence;
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub mod sasl;
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub mod stanza_error;
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub mod stream;
|
|
||||||
|
|
||||||
/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
|
|
||||||
pub mod roster;
|
|
||||||
|
|
||||||
/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket
|
|
||||||
pub mod websocket;
|
|
||||||
|
|
||||||
/// XEP-0004: Data Forms
|
|
||||||
pub mod data_forms;
|
|
||||||
|
|
||||||
/// XEP-0030: Service Discovery
|
|
||||||
pub mod disco;
|
|
||||||
|
|
||||||
/// XEP-0045: Multi-User Chat
|
|
||||||
pub mod muc;
|
|
||||||
|
|
||||||
/// XEP-0047: In-Band Bytestreams
|
|
||||||
pub mod ibb;
|
|
||||||
|
|
||||||
/// XEP-0048: Bookmarks
|
|
||||||
pub mod bookmarks;
|
|
||||||
|
|
||||||
/// XEP-0059: Result Set Management
|
|
||||||
pub mod rsm;
|
|
||||||
|
|
||||||
/// XEP-0060: Publish-Subscribe
|
|
||||||
pub mod pubsub;
|
|
||||||
|
|
||||||
/// XEP-0071: XHTML-IM
|
|
||||||
pub mod xhtml;
|
|
||||||
|
|
||||||
/// XEP-0077: In-Band Registration
|
|
||||||
pub mod ibr;
|
|
||||||
|
|
||||||
/// XEP-0082: XMPP Date and Time Profiles
|
|
||||||
pub mod date;
|
|
||||||
|
|
||||||
/// XEP-0084: User Avatar
|
|
||||||
pub mod avatar;
|
|
||||||
|
|
||||||
/// XEP-0085: Chat State Notifications
|
|
||||||
pub mod chatstates;
|
|
||||||
|
|
||||||
/// XEP-0092: Software Version
|
|
||||||
pub mod version;
|
|
||||||
|
|
||||||
/// XEP-0107: User Mood
|
|
||||||
pub mod mood;
|
|
||||||
|
|
||||||
/// XEP-0114: Jabber Component Protocol
|
|
||||||
pub mod component;
|
|
||||||
|
|
||||||
/// XEP-0115: Entity Capabilities
|
|
||||||
pub mod caps;
|
|
||||||
|
|
||||||
/// XEP-0118: User Tune
|
|
||||||
pub mod tune;
|
|
||||||
|
|
||||||
/// XEP-0157: Contact Addresses for XMPP Services
|
|
||||||
pub mod server_info;
|
|
||||||
|
|
||||||
/// XEP-0166: Jingle
|
|
||||||
pub mod jingle;
|
|
||||||
|
|
||||||
/// XEP-0167: Jingle RTP Sessions
|
|
||||||
pub mod jingle_rtp;
|
|
||||||
|
|
||||||
/// XEP-0172: User Nickname
|
|
||||||
pub mod nick;
|
|
||||||
|
|
||||||
/// XEP-0176: Jingle ICE-UDP Transport Method
|
|
||||||
pub mod jingle_ice_udp;
|
|
||||||
|
|
||||||
/// XEP-0177: Jingle Raw UDP Transport Method
|
|
||||||
pub mod jingle_raw_udp;
|
|
||||||
|
|
||||||
/// XEP-0184: Message Delivery Receipts
|
|
||||||
pub mod receipts;
|
|
||||||
|
|
||||||
/// XEP-0191: Blocking Command
|
|
||||||
pub mod blocking;
|
|
||||||
|
|
||||||
/// XEP-0198: Stream Management
|
|
||||||
pub mod sm;
|
|
||||||
|
|
||||||
/// XEP-0199: XMPP Ping
|
|
||||||
pub mod ping;
|
|
||||||
|
|
||||||
/// XEP-0202: Entity Time
|
|
||||||
pub mod time;
|
|
||||||
|
|
||||||
/// XEP-0203: Delayed Delivery
|
|
||||||
pub mod delay;
|
|
||||||
|
|
||||||
/// XEP-0221: Data Forms Media Element
|
|
||||||
pub mod media_element;
|
|
||||||
|
|
||||||
/// XEP-0224: Attention
|
|
||||||
pub mod attention;
|
|
||||||
|
|
||||||
/// XEP-0231: Bits of Binary
|
|
||||||
pub mod bob;
|
|
||||||
|
|
||||||
/// XEP-0234: Jingle File Transfer
|
|
||||||
pub mod jingle_ft;
|
|
||||||
|
|
||||||
/// XEP-0257: Client Certificate Management for SASL EXTERNAL
|
|
||||||
pub mod cert_management;
|
|
||||||
|
|
||||||
/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
|
|
||||||
pub mod jingle_s5b;
|
|
||||||
|
|
||||||
/// XEP-0261: Jingle In-Band Bytestreams Transport Method
|
|
||||||
pub mod jingle_ibb;
|
|
||||||
|
|
||||||
/// XEP-0280: Message Carbons
|
|
||||||
pub mod carbons;
|
|
||||||
|
|
||||||
/// XEP-0293: Jingle RTP Feedback Negotiation
|
|
||||||
pub mod jingle_rtcp_fb;
|
|
||||||
|
|
||||||
/// XEP-0294: Jingle RTP Header Extensions Negociation
|
|
||||||
pub mod jingle_rtp_hdrext;
|
|
||||||
|
|
||||||
/// XEP-0297: Stanza Forwarding
|
|
||||||
pub mod forwarding;
|
|
||||||
|
|
||||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
|
||||||
pub mod hashes;
|
|
||||||
|
|
||||||
/// XEP-0308: Last Message Correction
|
|
||||||
pub mod message_correct;
|
|
||||||
|
|
||||||
/// XEP-0313: Message Archive Management
|
|
||||||
pub mod mam;
|
|
||||||
|
|
||||||
/// XEP-0319: Last User Interaction in Presence
|
|
||||||
pub mod idle;
|
|
||||||
|
|
||||||
/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions
|
|
||||||
pub mod jingle_dtls_srtp;
|
|
||||||
|
|
||||||
/// XEP-0328: JID Prep
|
|
||||||
pub mod jid_prep;
|
|
||||||
|
|
||||||
/// XEP-0338: Jingle Grouping Framework
|
|
||||||
pub mod jingle_grouping;
|
|
||||||
|
|
||||||
/// XEP-0339: Source-Specific Media Attributes in Jingle
|
|
||||||
pub mod jingle_ssma;
|
|
||||||
|
|
||||||
/// XEP-0352: Client State Indication
|
|
||||||
pub mod csi;
|
|
||||||
|
|
||||||
/// XEP-0353: Jingle Message Initiation
|
|
||||||
pub mod jingle_message;
|
|
||||||
|
|
||||||
/// XEP-0359: Unique and Stable Stanza IDs
|
|
||||||
pub mod stanza_id;
|
|
||||||
|
|
||||||
/// XEP-0369: Mediated Information eXchange (MIX)
|
|
||||||
pub mod mix;
|
|
||||||
|
|
||||||
/// XEP-0373: OpenPGP for XMPP
|
|
||||||
pub mod openpgp;
|
|
||||||
|
|
||||||
/// XEP-0380: Explicit Message Encryption
|
|
||||||
pub mod eme;
|
|
||||||
|
|
||||||
/// XEP-0390: Entity Capabilities 2.0
|
|
||||||
pub mod ecaps2;
|
|
||||||
|
|
||||||
/// XEP-0402: Bookmarks 2 (This Time it's Serious)
|
|
||||||
pub mod bookmarks2;
|
|
||||||
|
|
||||||
/// XEP-0421: Anonymous unique occupant identifiers for MUCs
|
|
||||||
pub mod occupant_id;
|
|
||||||
|
|
||||||
/// XEP-0441: Message Archive Management Preferences
|
|
||||||
pub mod mam_prefs;
|
|
|
@ -1,296 +0,0 @@
|
||||||
// Copyright (c) 2017-2021 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::data_forms::DataForm;
|
|
||||||
use crate::forwarding::Forwarded;
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
use crate::pubsub::NodeName;
|
|
||||||
use crate::rsm::{SetQuery, SetResult};
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// An identifier matching a result message to the query requesting it.
|
|
||||||
QueryId
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Starts a query to the archive.
|
|
||||||
Query, "query", MAM,
|
|
||||||
attributes: [
|
|
||||||
/// An optional identifier for matching forwarded messages to this
|
|
||||||
/// query.
|
|
||||||
queryid: Option<QueryId> = "queryid",
|
|
||||||
|
|
||||||
/// Must be set to Some when querying a PubSub node’s archive.
|
|
||||||
node: Option<NodeName> = "node"
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// Used for filtering the results.
|
|
||||||
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm,
|
|
||||||
|
|
||||||
/// Used for paging through results.
|
|
||||||
set: Option<SetQuery> = ("set", RSM) => SetQuery
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqGetPayload for Query {}
|
|
||||||
impl IqSetPayload for Query {}
|
|
||||||
impl IqResultPayload for Query {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The wrapper around forwarded stanzas.
|
|
||||||
Result_, "result", MAM,
|
|
||||||
attributes: [
|
|
||||||
/// The stanza-id under which the archive stored this stanza.
|
|
||||||
id: Required<String> = "id",
|
|
||||||
|
|
||||||
/// The same queryid as the one requested in the
|
|
||||||
/// [query](struct.Query.html).
|
|
||||||
queryid: Option<QueryId> = "queryid",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The actual stanza being forwarded.
|
|
||||||
forwarded: Required<Forwarded> = ("forwarded", FORWARD) => Forwarded
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Result_ {}
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// True when the end of a MAM query has been reached.
|
|
||||||
Complete,
|
|
||||||
"complete",
|
|
||||||
bool
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Notes the end of a page in a query.
|
|
||||||
Fin, "fin", MAM,
|
|
||||||
attributes: [
|
|
||||||
/// True when the end of a MAM query has been reached.
|
|
||||||
complete: Default<Complete> = "complete",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// Describes the current page, it should contain at least [first]
|
|
||||||
/// (with an [index]) and [last], and generally [count].
|
|
||||||
///
|
|
||||||
/// [first]: ../rsm/struct.SetResult.html#structfield.first
|
|
||||||
/// [index]: ../rsm/struct.SetResult.html#structfield.first_index
|
|
||||||
/// [last]: ../rsm/struct.SetResult.html#structfield.last
|
|
||||||
/// [count]: ../rsm/struct.SetResult.html#structfield.count
|
|
||||||
set: Required<SetResult> = ("set", RSM) => SetResult
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqResultPayload for Fin {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use minidom::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(QueryId, 12);
|
|
||||||
assert_size!(Query, 116);
|
|
||||||
assert_size!(Result_, 236);
|
|
||||||
assert_size!(Complete, 1);
|
|
||||||
assert_size!(Fin, 44);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(QueryId, 24);
|
|
||||||
assert_size!(Query, 232);
|
|
||||||
assert_size!(Result_, 456);
|
|
||||||
assert_size!(Complete, 1);
|
|
||||||
assert_size!(Fin, 88);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_query() {
|
|
||||||
let elem: Element = "<query xmlns='urn:xmpp:mam:2'/>".parse().unwrap();
|
|
||||||
Query::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_result() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = r#"
|
|
||||||
<result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
|
|
||||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
|
||||||
<delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
|
|
||||||
<message xmlns='jabber:client' from="witch@shakespeare.lit" to="macbeth@shakespeare.lit">
|
|
||||||
<body>Hail to thee</body>
|
|
||||||
</message>
|
|
||||||
</forwarded>
|
|
||||||
</result>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = r#"
|
|
||||||
<result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
|
|
||||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
|
||||||
<delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
|
|
||||||
<message xmlns='jabber:component:accept' from="witch@shakespeare.lit" to="macbeth@shakespeare.lit">
|
|
||||||
<body>Hail to thee</body>
|
|
||||||
</message>
|
|
||||||
</forwarded>
|
|
||||||
</result>
|
|
||||||
"#.parse().unwrap();
|
|
||||||
Result_::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fin() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<fin xmlns='urn:xmpp:mam:2'>
|
|
||||||
<set xmlns='http://jabber.org/protocol/rsm'>
|
|
||||||
<first index='0'>28482-98726-73623</first>
|
|
||||||
<last>09af3-cc343-b409f</last>
|
|
||||||
</set>
|
|
||||||
</fin>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Fin::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_query_x() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns='urn:xmpp:mam:2'>
|
|
||||||
<x xmlns='jabber:x:data' type='submit'>
|
|
||||||
<field var='FORM_TYPE' type='hidden'>
|
|
||||||
<value>urn:xmpp:mam:2</value>
|
|
||||||
</field>
|
|
||||||
<field var='with'>
|
|
||||||
<value>juliet@capulet.lit</value>
|
|
||||||
</field>
|
|
||||||
</x>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Query::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_query_x_set() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns='urn:xmpp:mam:2'>
|
|
||||||
<x xmlns='jabber:x:data' type='submit'>
|
|
||||||
<field var='FORM_TYPE' type='hidden'>
|
|
||||||
<value>urn:xmpp:mam:2</value>
|
|
||||||
</field>
|
|
||||||
<field var='start'>
|
|
||||||
<value>2010-08-07T00:00:00Z</value>
|
|
||||||
</field>
|
|
||||||
</x>
|
|
||||||
<set xmlns='http://jabber.org/protocol/rsm'>
|
|
||||||
<max>10</max>
|
|
||||||
</set>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Query::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<query xmlns='urn:xmpp:mam:2'><coucou/></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Query::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in query element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise_empty() {
|
|
||||||
let elem: Element = "<query xmlns='urn:xmpp:mam:2'/>".parse().unwrap();
|
|
||||||
let replace = Query {
|
|
||||||
queryid: None,
|
|
||||||
node: None,
|
|
||||||
form: None,
|
|
||||||
set: None,
|
|
||||||
};
|
|
||||||
let elem2 = replace.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_query_with_form() {
|
|
||||||
let reference: Element = "<query xmlns='urn:xmpp:mam:2'><x xmlns='jabber:x:data' type='submit'><field xmlns='jabber:x:data' var='FORM_TYPE' type='hidden'><value xmlns='jabber:x:data'>urn:xmpp:mam:2</value></field><field xmlns='jabber:x:data' var='with'><value xmlns='jabber:x:data'>juliet@capulet.lit</value></field></x></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<x xmlns='jabber:x:data' type='submit'><field xmlns='jabber:x:data' var='FORM_TYPE' type='hidden'><value xmlns='jabber:x:data'>urn:xmpp:mam:2</value></field><field xmlns='jabber:x:data' var='with'><value xmlns='jabber:x:data'>juliet@capulet.lit</value></field></x>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let form = DataForm::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let query = Query {
|
|
||||||
queryid: None,
|
|
||||||
node: None,
|
|
||||||
set: None,
|
|
||||||
form: Some(form),
|
|
||||||
};
|
|
||||||
let serialized: Element = query.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_result() {
|
|
||||||
let reference: Element = "<result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'><forwarded xmlns='urn:xmpp:forward:0'><delay xmlns='urn:xmpp:delay' stamp='2002-09-10T23:08:25+00:00'/><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded></result>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'><delay xmlns='urn:xmpp:delay' stamp='2002-09-10T23:08:25+00:00'/><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let forwarded = Forwarded::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let result = Result_ {
|
|
||||||
id: String::from("28482-98726-73623"),
|
|
||||||
queryid: Some(QueryId(String::from("f27"))),
|
|
||||||
forwarded: forwarded,
|
|
||||||
};
|
|
||||||
let serialized: Element = result.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_fin() {
|
|
||||||
let reference: Element = "<fin xmlns='urn:xmpp:mam:2'><set xmlns='http://jabber.org/protocol/rsm'><first index='0'>28482-98726-73623</first><last>09af3-cc343-b409f</last></set></fin>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'><first index='0'>28482-98726-73623</first><last>09af3-cc343-b409f</last></set>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let set = SetResult::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let fin = Fin {
|
|
||||||
set: set,
|
|
||||||
complete: Complete::default(),
|
|
||||||
};
|
|
||||||
let serialized: Element = fin.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,175 +0,0 @@
|
||||||
// Copyright (c) 2021 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use jid::Jid;
|
|
||||||
use minidom::{Element, Node};
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Notes the default archiving preference for the user.
|
|
||||||
DefaultPrefs, "default", {
|
|
||||||
/// The default is to always log messages in the archive.
|
|
||||||
Always => "always",
|
|
||||||
|
|
||||||
/// The default is to never log messages in the archive.
|
|
||||||
Never => "never",
|
|
||||||
|
|
||||||
/// The default is to log messages in the archive only for contacts
|
|
||||||
/// present in the user’s [roster](../roster/index.html).
|
|
||||||
Roster => "roster",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Controls the archiving preferences of the user.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Prefs {
|
|
||||||
/// The default preference for JIDs in neither
|
|
||||||
/// [always](#structfield.always) or [never](#structfield.never) lists.
|
|
||||||
pub default_: DefaultPrefs,
|
|
||||||
|
|
||||||
/// The set of JIDs for which to always store messages in the archive.
|
|
||||||
pub always: Vec<Jid>,
|
|
||||||
|
|
||||||
/// The set of JIDs for which to never store messages in the archive.
|
|
||||||
pub never: Vec<Jid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IqGetPayload for Prefs {}
|
|
||||||
impl IqSetPayload for Prefs {}
|
|
||||||
impl IqResultPayload for Prefs {}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Prefs {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Prefs, Error> {
|
|
||||||
check_self!(elem, "prefs", MAM);
|
|
||||||
check_no_unknown_attributes!(elem, "prefs", ["default"]);
|
|
||||||
let mut always = vec![];
|
|
||||||
let mut never = vec![];
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("always", ns::MAM) {
|
|
||||||
for jid_elem in child.children() {
|
|
||||||
if !jid_elem.is("jid", ns::MAM) {
|
|
||||||
return Err(Error::ParseError("Invalid jid element in always."));
|
|
||||||
}
|
|
||||||
always.push(jid_elem.text().parse()?);
|
|
||||||
}
|
|
||||||
} else if child.is("never", ns::MAM) {
|
|
||||||
for jid_elem in child.children() {
|
|
||||||
if !jid_elem.is("jid", ns::MAM) {
|
|
||||||
return Err(Error::ParseError("Invalid jid element in never."));
|
|
||||||
}
|
|
||||||
never.push(jid_elem.text().parse()?);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in prefs element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let default_ = get_attr!(elem, "default", Required);
|
|
||||||
Ok(Prefs {
|
|
||||||
default_,
|
|
||||||
always,
|
|
||||||
never,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialise_jid_list(name: &str, jids: Vec<Jid>) -> ::std::option::IntoIter<Node> {
|
|
||||||
if jids.is_empty() {
|
|
||||||
None.into_iter()
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
Element::builder(name, ns::MAM)
|
|
||||||
.append_all(
|
|
||||||
jids.into_iter()
|
|
||||||
.map(|jid| Element::builder("jid", ns::MAM).append(String::from(jid))),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Prefs> for Element {
|
|
||||||
fn from(prefs: Prefs) -> Element {
|
|
||||||
Element::builder("prefs", ns::MAM)
|
|
||||||
.attr("default", prefs.default_)
|
|
||||||
.append_all(serialise_jid_list("always", prefs.always))
|
|
||||||
.append_all(serialise_jid_list("never", prefs.never))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use jid::BareJid;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(DefaultPrefs, 1);
|
|
||||||
assert_size!(Prefs, 28);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(DefaultPrefs, 1);
|
|
||||||
assert_size!(Prefs, 56);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_prefs_get() {
|
|
||||||
let elem: Element = "<prefs xmlns='urn:xmpp:mam:2' default='always'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let prefs = Prefs::try_from(elem).unwrap();
|
|
||||||
assert!(prefs.always.is_empty());
|
|
||||||
assert!(prefs.never.is_empty());
|
|
||||||
|
|
||||||
let elem: Element = r#"
|
|
||||||
<prefs xmlns='urn:xmpp:mam:2' default='roster'>
|
|
||||||
<always/>
|
|
||||||
<never/>
|
|
||||||
</prefs>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let prefs = Prefs::try_from(elem).unwrap();
|
|
||||||
assert!(prefs.always.is_empty());
|
|
||||||
assert!(prefs.never.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_prefs_result() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<prefs xmlns='urn:xmpp:mam:2' default='roster'>
|
|
||||||
<always>
|
|
||||||
<jid>romeo@montague.lit</jid>
|
|
||||||
</always>
|
|
||||||
<never>
|
|
||||||
<jid>montague@montague.lit</jid>
|
|
||||||
</never>
|
|
||||||
</prefs>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let prefs = Prefs::try_from(elem).unwrap();
|
|
||||||
assert_eq!(prefs.always, [BareJid::new("romeo", "montague.lit")]);
|
|
||||||
assert_eq!(prefs.never, [BareJid::new("montague", "montague.lit")]);
|
|
||||||
|
|
||||||
let elem2 = Element::from(prefs.clone());
|
|
||||||
println!("{:?}", elem2);
|
|
||||||
let prefs2 = Prefs::try_from(elem2).unwrap();
|
|
||||||
assert_eq!(prefs.default_, prefs2.default_);
|
|
||||||
assert_eq!(prefs.always, prefs2.always);
|
|
||||||
assert_eq!(prefs.never, prefs2.never);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,249 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::util::helpers::TrimmedPlainText;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Represents an URI used in a media element.
|
|
||||||
URI, "uri", MEDIA_ELEMENT,
|
|
||||||
attributes: [
|
|
||||||
/// The MIME type of the URI referenced.
|
|
||||||
///
|
|
||||||
/// See the [IANA MIME Media Types Registry][1] for a list of
|
|
||||||
/// registered types, but unregistered or yet-to-be-registered are
|
|
||||||
/// accepted too.
|
|
||||||
///
|
|
||||||
/// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
|
||||||
type_: Required<String> = "type"
|
|
||||||
],
|
|
||||||
text: (
|
|
||||||
/// The actual URI contained.
|
|
||||||
uri: TrimmedPlainText<String>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// References a media element, to be used in [data
|
|
||||||
/// forms](../data_forms/index.html).
|
|
||||||
MediaElement, "media", MEDIA_ELEMENT,
|
|
||||||
attributes: [
|
|
||||||
/// The recommended display width in pixels.
|
|
||||||
width: Option<usize> = "width",
|
|
||||||
|
|
||||||
/// The recommended display height in pixels.
|
|
||||||
height: Option<usize> = "height"
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// A list of URIs referencing this media.
|
|
||||||
uris: Vec<URI> = ("uri", MEDIA_ELEMENT) => URI
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::data_forms::DataForm;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(URI, 24);
|
|
||||||
assert_size!(MediaElement, 28);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(URI, 48);
|
|
||||||
assert_size!(MediaElement, 56);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<media xmlns='urn:xmpp:media-element'/>".parse().unwrap();
|
|
||||||
let media = MediaElement::try_from(elem).unwrap();
|
|
||||||
assert!(media.width.is_none());
|
|
||||||
assert!(media.height.is_none());
|
|
||||||
assert!(media.uris.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_width_height() {
|
|
||||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' width='32' height='32'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let media = MediaElement::try_from(elem).unwrap();
|
|
||||||
assert_eq!(media.width.unwrap(), 32);
|
|
||||||
assert_eq!(media.height.unwrap(), 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_uri() {
|
|
||||||
let elem: Element = "<media xmlns='urn:xmpp:media-element'><uri type='text/html'>https://example.org/</uri></media>".parse().unwrap();
|
|
||||||
let media = MediaElement::try_from(elem).unwrap();
|
|
||||||
assert_eq!(media.uris.len(), 1);
|
|
||||||
assert_eq!(media.uris[0].type_, "text/html");
|
|
||||||
assert_eq!(media.uris[0].uri, "https://example.org/");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_width_height() {
|
|
||||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' width=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = MediaElement::try_from(elem).unwrap_err();
|
|
||||||
let error = match error {
|
|
||||||
Error::ParseIntError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(error.to_string(), "cannot parse integer from empty string");
|
|
||||||
|
|
||||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' width='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = MediaElement::try_from(elem).unwrap_err();
|
|
||||||
let error = match error {
|
|
||||||
Error::ParseIntError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(error.to_string(), "invalid digit found in string");
|
|
||||||
|
|
||||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' height=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = MediaElement::try_from(elem).unwrap_err();
|
|
||||||
let error = match error {
|
|
||||||
Error::ParseIntError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(error.to_string(), "cannot parse integer from empty string");
|
|
||||||
|
|
||||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' height='-10'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = MediaElement::try_from(elem).unwrap_err();
|
|
||||||
let error = match error {
|
|
||||||
Error::ParseIntError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(error.to_string(), "invalid digit found in string");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unknown_child() {
|
|
||||||
let elem: Element = "<media xmlns='urn:xmpp:media-element'><coucou/></media>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = MediaElement::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in media element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_bad_uri() {
|
|
||||||
let elem: Element =
|
|
||||||
"<media xmlns='urn:xmpp:media-element'><uri>https://example.org/</uri></media>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = MediaElement::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'type' missing.");
|
|
||||||
|
|
||||||
let elem: Element = "<media xmlns='urn:xmpp:media-element'><uri type='text/html'/></media>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = MediaElement::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "URI missing in uri.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_xep_ex1() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<media xmlns='urn:xmpp:media-element'>
|
|
||||||
<uri type='audio/x-wav'>
|
|
||||||
http://victim.example.com/challenges/speech.wav?F3A6292C
|
|
||||||
</uri>
|
|
||||||
<uri type='audio/ogg; codecs=speex'>
|
|
||||||
cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org
|
|
||||||
</uri>
|
|
||||||
<uri type='audio/mpeg'>
|
|
||||||
http://victim.example.com/challenges/speech.mp3?F3A6292C
|
|
||||||
</uri>
|
|
||||||
</media>"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let media = MediaElement::try_from(elem).unwrap();
|
|
||||||
assert!(media.width.is_none());
|
|
||||||
assert!(media.height.is_none());
|
|
||||||
assert_eq!(media.uris.len(), 3);
|
|
||||||
assert_eq!(media.uris[0].type_, "audio/x-wav");
|
|
||||||
assert_eq!(
|
|
||||||
media.uris[0].uri,
|
|
||||||
"http://victim.example.com/challenges/speech.wav?F3A6292C"
|
|
||||||
);
|
|
||||||
assert_eq!(media.uris[1].type_, "audio/ogg; codecs=speex");
|
|
||||||
assert_eq!(
|
|
||||||
media.uris[1].uri,
|
|
||||||
"cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org"
|
|
||||||
);
|
|
||||||
assert_eq!(media.uris[2].type_, "audio/mpeg");
|
|
||||||
assert_eq!(
|
|
||||||
media.uris[2].uri,
|
|
||||||
"http://victim.example.com/challenges/speech.mp3?F3A6292C"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_xep_ex2() {
|
|
||||||
let elem: Element = r#"
|
|
||||||
<x xmlns='jabber:x:data' type='form'>
|
|
||||||
[ ... ]
|
|
||||||
<field var='ocr'>
|
|
||||||
<media xmlns='urn:xmpp:media-element'
|
|
||||||
height='80'
|
|
||||||
width='290'>
|
|
||||||
<uri type='image/jpeg'>
|
|
||||||
http://www.victim.com/challenges/ocr.jpeg?F3A6292C
|
|
||||||
</uri>
|
|
||||||
<uri type='image/jpeg'>
|
|
||||||
cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org
|
|
||||||
</uri>
|
|
||||||
</media>
|
|
||||||
</field>
|
|
||||||
[ ... ]
|
|
||||||
</x>"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let form = DataForm::try_from(elem).unwrap();
|
|
||||||
assert_eq!(form.fields.len(), 1);
|
|
||||||
assert_eq!(form.fields[0].var, "ocr");
|
|
||||||
assert_eq!(form.fields[0].media[0].width, Some(290));
|
|
||||||
assert_eq!(form.fields[0].media[0].height, Some(80));
|
|
||||||
assert_eq!(form.fields[0].media[0].uris[0].type_, "image/jpeg");
|
|
||||||
assert_eq!(
|
|
||||||
form.fields[0].media[0].uris[0].uri,
|
|
||||||
"http://www.victim.com/challenges/ocr.jpeg?F3A6292C"
|
|
||||||
);
|
|
||||||
assert_eq!(form.fields[0].media[0].uris[1].type_, "image/jpeg");
|
|
||||||
assert_eq!(
|
|
||||||
form.fields[0].media[0].uris[1].uri,
|
|
||||||
"cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,407 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Should be implemented on every known payload of a `<message/>`.
|
|
||||||
pub trait MessagePayload: TryFrom<Element> + Into<Element> {}
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The type of a message.
|
|
||||||
MessageType, "type", {
|
|
||||||
/// Standard instant messaging message.
|
|
||||||
Chat => "chat",
|
|
||||||
|
|
||||||
/// Notifies that an error happened.
|
|
||||||
Error => "error",
|
|
||||||
|
|
||||||
/// Standard group instant messaging message.
|
|
||||||
Groupchat => "groupchat",
|
|
||||||
|
|
||||||
/// Used by servers to notify users when things happen.
|
|
||||||
Headline => "headline",
|
|
||||||
|
|
||||||
/// This is an email-like message, it usually contains a
|
|
||||||
/// [subject](struct.Subject.html).
|
|
||||||
Normal => "normal",
|
|
||||||
}, Default = Normal
|
|
||||||
);
|
|
||||||
|
|
||||||
type Lang = String;
|
|
||||||
|
|
||||||
generate_elem_id!(
|
|
||||||
/// Represents one `<body/>` element, that is the free form text content of
|
|
||||||
/// a message.
|
|
||||||
Body,
|
|
||||||
"body",
|
|
||||||
DEFAULT_NS
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_elem_id!(
|
|
||||||
/// Defines the subject of a room, or of an email-like normal message.
|
|
||||||
Subject,
|
|
||||||
"subject",
|
|
||||||
DEFAULT_NS
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_elem_id!(
|
|
||||||
/// A thread identifier, so that other people can specify to which message
|
|
||||||
/// they are replying.
|
|
||||||
Thread,
|
|
||||||
"thread",
|
|
||||||
DEFAULT_NS
|
|
||||||
);
|
|
||||||
|
|
||||||
/// The main structure representing the `<message/>` stanza.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Message {
|
|
||||||
/// The JID emitting this stanza.
|
|
||||||
pub from: Option<Jid>,
|
|
||||||
|
|
||||||
/// The recipient of this stanza.
|
|
||||||
pub to: Option<Jid>,
|
|
||||||
|
|
||||||
/// The @id attribute of this stanza, which is required in order to match a
|
|
||||||
/// request with its response.
|
|
||||||
pub id: Option<String>,
|
|
||||||
|
|
||||||
/// The type of this message.
|
|
||||||
pub type_: MessageType,
|
|
||||||
|
|
||||||
/// A list of bodies, sorted per language. Use
|
|
||||||
/// [get_best_body()](#method.get_best_body) to access them on reception.
|
|
||||||
pub bodies: BTreeMap<Lang, Body>,
|
|
||||||
|
|
||||||
/// A list of subjects, sorted per language. Use
|
|
||||||
/// [get_best_subject()](#method.get_best_subject) to access them on
|
|
||||||
/// reception.
|
|
||||||
pub subjects: BTreeMap<Lang, Subject>,
|
|
||||||
|
|
||||||
/// An optional thread identifier, so that other people can reply directly
|
|
||||||
/// to this message.
|
|
||||||
pub thread: Option<Thread>,
|
|
||||||
|
|
||||||
/// A list of the extension payloads contained in this stanza.
|
|
||||||
pub payloads: Vec<Element>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Message {
|
|
||||||
/// Creates a new `<message/>` stanza for the given recipient.
|
|
||||||
pub fn new<J: Into<Option<Jid>>>(to: J) -> Message {
|
|
||||||
Message {
|
|
||||||
from: None,
|
|
||||||
to: to.into(),
|
|
||||||
id: None,
|
|
||||||
type_: MessageType::Chat,
|
|
||||||
bodies: BTreeMap::new(),
|
|
||||||
subjects: BTreeMap::new(),
|
|
||||||
thread: None,
|
|
||||||
payloads: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_best<'a, T>(
|
|
||||||
map: &'a BTreeMap<Lang, T>,
|
|
||||||
preferred_langs: Vec<&str>,
|
|
||||||
) -> Option<(Lang, &'a T)> {
|
|
||||||
if map.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
for lang in preferred_langs {
|
|
||||||
if let Some(value) = map.get(lang) {
|
|
||||||
return Some((Lang::from(lang), value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(value) = map.get("") {
|
|
||||||
return Some((Lang::new(), value));
|
|
||||||
}
|
|
||||||
map.iter().map(|(lang, value)| (lang.clone(), value)).next()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the best matching body from a list of languages.
|
|
||||||
///
|
|
||||||
/// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English
|
|
||||||
/// body without an xml:lang attribute, and you pass ["fr", "en"] as your preferred languages,
|
|
||||||
/// `Some(("fr", the_second_body))` will be returned.
|
|
||||||
///
|
|
||||||
/// If no body matches, an undefined body will be returned.
|
|
||||||
pub fn get_best_body(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Body)> {
|
|
||||||
Message::get_best::<Body>(&self.bodies, preferred_langs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the best matching subject from a list of languages.
|
|
||||||
///
|
|
||||||
/// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English
|
|
||||||
/// subject without an xml:lang attribute, and you pass ["fr", "en"] as your preferred
|
|
||||||
/// languages, `Some(("fr", the_second_subject))` will be returned.
|
|
||||||
///
|
|
||||||
/// If no subject matches, an undefined subject will be returned.
|
|
||||||
pub fn get_best_subject(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Subject)> {
|
|
||||||
Message::get_best::<Subject>(&self.subjects, preferred_langs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Message {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(root: Element) -> Result<Message, Error> {
|
|
||||||
check_self!(root, "message", DEFAULT_NS);
|
|
||||||
let from = get_attr!(root, "from", Option);
|
|
||||||
let to = get_attr!(root, "to", Option);
|
|
||||||
let id = get_attr!(root, "id", Option);
|
|
||||||
let type_ = get_attr!(root, "type", Default);
|
|
||||||
let mut bodies = BTreeMap::new();
|
|
||||||
let mut subjects = BTreeMap::new();
|
|
||||||
let mut thread = None;
|
|
||||||
let mut payloads = vec![];
|
|
||||||
for elem in root.children() {
|
|
||||||
if elem.is("body", ns::DEFAULT_NS) {
|
|
||||||
check_no_children!(elem, "body");
|
|
||||||
let lang = get_attr!(elem, "xml:lang", Default);
|
|
||||||
let body = Body(elem.text());
|
|
||||||
if bodies.insert(lang, body).is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Body element present twice for the same xml:lang.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if elem.is("subject", ns::DEFAULT_NS) {
|
|
||||||
check_no_children!(elem, "subject");
|
|
||||||
let lang = get_attr!(elem, "xml:lang", Default);
|
|
||||||
let subject = Subject(elem.text());
|
|
||||||
if subjects.insert(lang, subject).is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Subject element present twice for the same xml:lang.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if elem.is("thread", ns::DEFAULT_NS) {
|
|
||||||
if thread.is_some() {
|
|
||||||
return Err(Error::ParseError("Thread element present twice."));
|
|
||||||
}
|
|
||||||
check_no_children!(elem, "thread");
|
|
||||||
thread = Some(Thread(elem.text()));
|
|
||||||
} else {
|
|
||||||
payloads.push(elem.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Message {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
id,
|
|
||||||
type_,
|
|
||||||
bodies,
|
|
||||||
subjects,
|
|
||||||
thread,
|
|
||||||
payloads,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Message> for Element {
|
|
||||||
fn from(message: Message) -> Element {
|
|
||||||
Element::builder("message", ns::DEFAULT_NS)
|
|
||||||
.attr("from", message.from)
|
|
||||||
.attr("to", message.to)
|
|
||||||
.attr("id", message.id)
|
|
||||||
.attr("type", message.type_)
|
|
||||||
.append_all(message.subjects.into_iter().map(|(lang, subject)| {
|
|
||||||
let mut subject = Element::from(subject);
|
|
||||||
subject.set_attr(
|
|
||||||
"xml:lang",
|
|
||||||
match lang.as_ref() {
|
|
||||||
"" => None,
|
|
||||||
lang => Some(lang),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
subject
|
|
||||||
}))
|
|
||||||
.append_all(message.bodies.into_iter().map(|(lang, body)| {
|
|
||||||
let mut body = Element::from(body);
|
|
||||||
body.set_attr(
|
|
||||||
"xml:lang",
|
|
||||||
match lang.as_ref() {
|
|
||||||
"" => None,
|
|
||||||
lang => Some(lang),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
body
|
|
||||||
}))
|
|
||||||
.append_all(message.payloads.into_iter())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use jid::BareJid;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(MessageType, 1);
|
|
||||||
assert_size!(Body, 12);
|
|
||||||
assert_size!(Subject, 12);
|
|
||||||
assert_size!(Thread, 12);
|
|
||||||
assert_size!(Message, 144);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(MessageType, 1);
|
|
||||||
assert_size!(Body, 24);
|
|
||||||
assert_size!(Subject, 24);
|
|
||||||
assert_size!(Thread, 24);
|
|
||||||
assert_size!(Message, 288);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<message xmlns='jabber:component:accept'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let message = Message::try_from(elem).unwrap();
|
|
||||||
assert_eq!(message.from, None);
|
|
||||||
assert_eq!(message.to, None);
|
|
||||||
assert_eq!(message.id, None);
|
|
||||||
assert_eq!(message.type_, MessageType::Normal);
|
|
||||||
assert!(message.payloads.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<message xmlns='jabber:component:accept'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let mut message = Message::new(None);
|
|
||||||
message.type_ = MessageType::Normal;
|
|
||||||
let elem2 = message.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_body() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let message = Message::try_from(elem).unwrap();
|
|
||||||
assert_eq!(message.bodies[""], Body::from_str("Hello world!").unwrap());
|
|
||||||
|
|
||||||
{
|
|
||||||
let (lang, body) = message.get_best_body(vec!["en"]).unwrap();
|
|
||||||
assert_eq!(lang, "");
|
|
||||||
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem2 = message.into();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise_body() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
|
||||||
let mut message = Message::new(Jid::Bare(BareJid::new("coucou", "example.org")));
|
|
||||||
message
|
|
||||||
.bodies
|
|
||||||
.insert(String::from(""), Body::from_str("Hello world!").unwrap());
|
|
||||||
let elem2 = message.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_subject() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><subject>Hello world!</subject></message>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><subject>Hello world!</subject></message>".parse().unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let message = Message::try_from(elem).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
message.subjects[""],
|
|
||||||
Subject::from_str("Hello world!").unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
|
||||||
let (lang, subject) = message.get_best_subject(vec!["en"]).unwrap();
|
|
||||||
assert_eq!(lang, "");
|
|
||||||
assert_eq!(subject, &Subject::from_str("Hello world!").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem2 = message.into();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_best_body() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
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();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
|
||||||
let message = Message::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
// Tests basic feature.
|
|
||||||
{
|
|
||||||
let (lang, body) = message.get_best_body(vec!["fr"]).unwrap();
|
|
||||||
assert_eq!(lang, "fr");
|
|
||||||
assert_eq!(body, &Body::from_str("Salut le monde !").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests order.
|
|
||||||
{
|
|
||||||
let (lang, body) = message.get_best_body(vec!["en", "de"]).unwrap();
|
|
||||||
assert_eq!(lang, "de");
|
|
||||||
assert_eq!(body, &Body::from_str("Hallo Welt!").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests fallback.
|
|
||||||
{
|
|
||||||
let (lang, body) = message.get_best_body(vec![]).unwrap();
|
|
||||||
assert_eq!(lang, "");
|
|
||||||
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests fallback.
|
|
||||||
{
|
|
||||||
let (lang, body) = message.get_best_body(vec!["ja"]).unwrap();
|
|
||||||
assert_eq!(lang, "");
|
|
||||||
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = Message::new(None);
|
|
||||||
|
|
||||||
// Tests without a body.
|
|
||||||
assert_eq!(message.get_best_body(vec!("ja")), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_attention() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let message = Message::try_from(elem).unwrap();
|
|
||||||
let elem2 = message.into();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Defines that the message containing this payload should replace a
|
|
||||||
/// previous message, identified by the id.
|
|
||||||
Replace, "replace", MESSAGE_CORRECT,
|
|
||||||
attributes: [
|
|
||||||
/// The 'id' attribute of the message getting corrected.
|
|
||||||
id: Required<String> = "id",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Replace {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Replace, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Replace, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Replace::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_attribute() {
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Replace::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in replace element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'><coucou/></replace>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Replace::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in replace element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_id() {
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Replace::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'id' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let replace = Replace {
|
|
||||||
id: String::from("coucou"),
|
|
||||||
};
|
|
||||||
let elem2 = replace.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,398 +0,0 @@
|
||||||
// Copyright (c) 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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 crate::iq::{IqResultPayload, IqSetPayload};
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
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<P: Into<String>>(participant: P) -> ParticipantId {
|
|
||||||
ParticipantId(participant.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// A MIX channel identifier.
|
|
||||||
ChannelId
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Represents a participant in a MIX channel, usually returned on the
|
|
||||||
/// urn:xmpp:mix:nodes:participants PubSub node.
|
|
||||||
Participant, "participant", MIX_CORE,
|
|
||||||
children: [
|
|
||||||
/// The nick of this participant.
|
|
||||||
nick: Required<String> = ("nick", MIX_CORE) => String,
|
|
||||||
|
|
||||||
/// The bare JID of this participant.
|
|
||||||
// TODO: should be a BareJid!
|
|
||||||
jid: Required<String> = ("jid", MIX_CORE) => String
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PubSubPayload for Participant {}
|
|
||||||
|
|
||||||
impl Participant {
|
|
||||||
/// Create a new MIX participant.
|
|
||||||
pub fn new<J: Into<String>, N: Into<String>>(jid: J, nick: N) -> Participant {
|
|
||||||
Participant {
|
|
||||||
nick: nick.into(),
|
|
||||||
jid: jid.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A node to subscribe to.
|
|
||||||
Subscribe, "subscribe", MIX_CORE,
|
|
||||||
attributes: [
|
|
||||||
/// The PubSub node to subscribe to.
|
|
||||||
node: Required<NodeName> = "node",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Subscribe {
|
|
||||||
/// Create a new Subscribe element.
|
|
||||||
pub fn new<N: Into<String>>(node: N) -> Subscribe {
|
|
||||||
Subscribe {
|
|
||||||
node: NodeName(node.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A request from a user’s server to join a MIX channel.
|
|
||||||
Join, "join", MIX_CORE,
|
|
||||||
attributes: [
|
|
||||||
/// The participant identifier returned by the MIX service on successful join.
|
|
||||||
id: Option<ParticipantId> = "id",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The nick requested by the user or set by the service.
|
|
||||||
nick: Required<String> = ("nick", MIX_CORE) => String,
|
|
||||||
|
|
||||||
/// Which MIX nodes to subscribe to.
|
|
||||||
subscribes: Vec<Subscribe> = ("subscribe", MIX_CORE) => Subscribe
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Join {}
|
|
||||||
impl IqResultPayload for Join {}
|
|
||||||
|
|
||||||
impl Join {
|
|
||||||
/// Create a new Join element.
|
|
||||||
pub fn from_nick_and_nodes<N: Into<String>>(nick: N, nodes: &[&str]) -> Join {
|
|
||||||
let subscribes = nodes
|
|
||||||
.into_iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|n| Subscribe::new(n))
|
|
||||||
.collect();
|
|
||||||
Join {
|
|
||||||
id: None,
|
|
||||||
nick: nick.into(),
|
|
||||||
subscribes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the JID on this update-subscription.
|
|
||||||
pub fn with_id<I: Into<String>>(mut self, id: I) -> Self {
|
|
||||||
self.id = Some(ParticipantId(id.into()));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Update a given subscription.
|
|
||||||
UpdateSubscription, "update-subscription", MIX_CORE,
|
|
||||||
attributes: [
|
|
||||||
/// The JID of the user to be affected.
|
|
||||||
// TODO: why is it not a participant id instead?
|
|
||||||
jid: Option<BareJid> = "jid",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The list of additional nodes to subscribe to.
|
|
||||||
// TODO: what happens when we are already subscribed? Also, how do we unsubscribe from
|
|
||||||
// just one?
|
|
||||||
subscribes: Vec<Subscribe> = ("subscribe", MIX_CORE) => Subscribe
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
.into_iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|n| Subscribe::new(n))
|
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Request to leave a given MIX channel. It will automatically unsubscribe the user from all
|
|
||||||
/// nodes on this channel.
|
|
||||||
Leave,
|
|
||||||
"leave",
|
|
||||||
MIX_CORE
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Leave {}
|
|
||||||
impl IqResultPayload for Leave {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A request to change the user’s nick.
|
|
||||||
SetNick, "setnick", MIX_CORE,
|
|
||||||
children: [
|
|
||||||
/// The new requested nick.
|
|
||||||
nick: Required<String> = ("nick", MIX_CORE) => String
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for SetNick {}
|
|
||||||
impl IqResultPayload for SetNick {}
|
|
||||||
|
|
||||||
impl SetNick {
|
|
||||||
/// Create a new SetNick element.
|
|
||||||
pub fn new<N: Into<String>>(nick: N) -> SetNick {
|
|
||||||
SetNick { nick: nick.into() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Message payload describing who actually sent the message, since unlike in MUC, all messages
|
|
||||||
/// are sent from the channel’s JID.
|
|
||||||
Mix, "mix", MIX_CORE,
|
|
||||||
children: [
|
|
||||||
/// The nick of the user who said something.
|
|
||||||
nick: Required<String> = ("nick", MIX_CORE) => String,
|
|
||||||
|
|
||||||
/// The JID of the user who said something.
|
|
||||||
// TODO: should be a BareJid!
|
|
||||||
jid: Required<String> = ("jid", MIX_CORE) => String
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Mix {}
|
|
||||||
|
|
||||||
impl Mix {
|
|
||||||
/// Create a new Mix element.
|
|
||||||
pub fn new<N: Into<String>, J: Into<String>>(nick: N, jid: J) -> Mix {
|
|
||||||
Mix {
|
|
||||||
nick: nick.into(),
|
|
||||||
jid: jid.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Create a new MIX channel.
|
|
||||||
Create, "create", MIX_CORE,
|
|
||||||
attributes: [
|
|
||||||
/// The requested channel identifier.
|
|
||||||
channel: Option<ChannelId> = "channel",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqSetPayload for Create {}
|
|
||||||
impl IqResultPayload for Create {}
|
|
||||||
|
|
||||||
impl Create {
|
|
||||||
/// Create a new ad-hoc Create element.
|
|
||||||
pub fn new() -> Create {
|
|
||||||
Create { channel: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new Create element with a channel identifier.
|
|
||||||
pub fn from_channel_id<C: Into<String>>(channel: C) -> Create {
|
|
||||||
Create {
|
|
||||||
channel: Some(ChannelId(channel.into())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Destroy a given MIX channel.
|
|
||||||
Destroy, "destroy", MIX_CORE,
|
|
||||||
attributes: [
|
|
||||||
/// The channel identifier to be destroyed.
|
|
||||||
channel: Required<ChannelId> = "channel",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: section 7.3.4, example 33, doesn’t mirror the <destroy/> in the iq result unlike every
|
|
||||||
// other section so far.
|
|
||||||
impl IqSetPayload for Destroy {}
|
|
||||||
|
|
||||||
impl Destroy {
|
|
||||||
/// Create a new Destroy element.
|
|
||||||
pub fn new<C: Into<String>>(channel: C) -> Destroy {
|
|
||||||
Destroy {
|
|
||||||
channel: ChannelId(channel.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn participant() {
|
|
||||||
let elem: Element = "<participant xmlns='urn:xmpp:mix:core:1'><jid>foo@bar</jid><nick>coucou</nick></participant>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let participant = Participant::try_from(elem).unwrap();
|
|
||||||
assert_eq!(participant.nick, "coucou");
|
|
||||||
assert_eq!(participant.jid, "foo@bar");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn join() {
|
|
||||||
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>"
|
|
||||||
.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 = "<update-subscription xmlns='urn:xmpp:mix:core:1'><subscribe node='urn:xmpp:mix:nodes:participants'/></update-subscription>"
|
|
||||||
.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 = "<leave xmlns='urn:xmpp:mix:core:1'/>".parse().unwrap();
|
|
||||||
Leave::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn setnick() {
|
|
||||||
let elem: Element = "<setnick xmlns='urn:xmpp:mix:core:1'><nick>coucou</nick></setnick>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let setnick = SetNick::try_from(elem).unwrap();
|
|
||||||
assert_eq!(setnick.nick, "coucou");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn message_mix() {
|
|
||||||
let elem: Element =
|
|
||||||
"<mix xmlns='urn:xmpp:mix:core:1'><jid>foo@bar</jid><nick>coucou</nick></mix>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let mix = Mix::try_from(elem).unwrap();
|
|
||||||
assert_eq!(mix.nick, "coucou");
|
|
||||||
assert_eq!(mix.jid, "foo@bar");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create() {
|
|
||||||
let elem: Element = "<create xmlns='urn:xmpp:mix:core:1' channel='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let create = Create::try_from(elem).unwrap();
|
|
||||||
assert_eq!(create.channel.unwrap().0, "coucou");
|
|
||||||
|
|
||||||
let elem: Element = "<create xmlns='urn:xmpp:mix:core:1'/>".parse().unwrap();
|
|
||||||
let create = Create::try_from(elem).unwrap();
|
|
||||||
assert_eq!(create.channel, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn destroy() {
|
|
||||||
let elem: Element = "<destroy xmlns='urn:xmpp:mix:core:1' channel='coucou'/>"
|
|
||||||
.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, "<join xmlns=\"urn:xmpp:mix:core:1\"><nick>coucou</nick><subscribe node=\"foo\"/><subscribe node=\"bar\"/></join>");
|
|
||||||
|
|
||||||
let elem: Element = UpdateSubscription::from_nodes(&["foo", "bar"]).into();
|
|
||||||
let xml = String::from(&elem);
|
|
||||||
assert_eq!(xml, "<update-subscription xmlns=\"urn:xmpp:mix:core:1\"><subscribe node=\"foo\"/><subscribe node=\"bar\"/></update-subscription>");
|
|
||||||
|
|
||||||
let elem: Element = Leave.into();
|
|
||||||
let xml = String::from(&elem);
|
|
||||||
assert_eq!(xml, "<leave xmlns=\"urn:xmpp:mix:core:1\"/>");
|
|
||||||
|
|
||||||
let elem: Element = SetNick::new("coucou").into();
|
|
||||||
let xml = String::from(&elem);
|
|
||||||
assert_eq!(
|
|
||||||
xml,
|
|
||||||
"<setnick xmlns=\"urn:xmpp:mix:core:1\"><nick>coucou</nick></setnick>"
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = Mix::new("coucou", "coucou@example").into();
|
|
||||||
let xml = String::from(&elem);
|
|
||||||
assert_eq!(
|
|
||||||
xml,
|
|
||||||
"<mix xmlns=\"urn:xmpp:mix:core:1\"><nick>coucou</nick><jid>coucou@example</jid></mix>"
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = Create::new().into();
|
|
||||||
let xml = String::from(&elem);
|
|
||||||
assert_eq!(xml, "<create xmlns=\"urn:xmpp:mix:core:1\"/>");
|
|
||||||
|
|
||||||
let elem: Element = Create::from_channel_id("coucou").into();
|
|
||||||
let xml = String::from(&elem);
|
|
||||||
assert_eq!(
|
|
||||||
xml,
|
|
||||||
"<create xmlns=\"urn:xmpp:mix:core:1\" channel=\"coucou\"/>"
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = Destroy::new("coucou").into();
|
|
||||||
let xml = String::from(&elem);
|
|
||||||
assert_eq!(
|
|
||||||
xml,
|
|
||||||
"<destroy xmlns=\"urn:xmpp:mix:core:1\" channel=\"coucou\"/>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,312 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
generate_element_enum!(
|
|
||||||
/// Enum representing all of the possible values of the XEP-0107 moods.
|
|
||||||
MoodEnum, "mood", MOOD, {
|
|
||||||
/// Impressed with fear or apprehension; in fear; apprehensive.
|
|
||||||
Afraid => "afraid",
|
|
||||||
|
|
||||||
/// Astonished; confounded with fear, surprise or wonder.
|
|
||||||
Amazed => "amazed",
|
|
||||||
|
|
||||||
/// Inclined to love; having a propensity to love, or to sexual enjoyment; loving, fond, affectionate, passionate, lustful, sexual, etc.
|
|
||||||
Amorous => "amorous",
|
|
||||||
|
|
||||||
/// Displaying or feeling anger, i.e., a strong feeling of displeasure, hostility or antagonism towards someone or something, usually combined with an urge to harm.
|
|
||||||
Angry => "angry",
|
|
||||||
|
|
||||||
/// To be disturbed or irritated, especially by continued or repeated acts.
|
|
||||||
Annoyed => "annoyed",
|
|
||||||
|
|
||||||
/// Full of anxiety or disquietude; greatly concerned or solicitous, esp. respecting something future or unknown; being in painful suspense.
|
|
||||||
Anxious => "anxious",
|
|
||||||
|
|
||||||
/// To be stimulated in one's feelings, especially to be sexually stimulated.
|
|
||||||
Aroused => "aroused",
|
|
||||||
|
|
||||||
/// Feeling shame or guilt.
|
|
||||||
Ashamed => "ashamed",
|
|
||||||
|
|
||||||
/// Suffering from boredom; uninterested, without attention.
|
|
||||||
Bored => "bored",
|
|
||||||
|
|
||||||
/// Strong in the face of fear; courageous.
|
|
||||||
Brave => "brave",
|
|
||||||
|
|
||||||
/// Peaceful, quiet.
|
|
||||||
Calm => "calm",
|
|
||||||
|
|
||||||
/// Taking care or caution; tentative.
|
|
||||||
Cautious => "cautious",
|
|
||||||
|
|
||||||
/// Feeling the sensation of coldness, especially to the point of discomfort.
|
|
||||||
Cold => "cold",
|
|
||||||
|
|
||||||
/// Feeling very sure of or positive about something, especially about one's own capabilities.
|
|
||||||
Confident => "confident",
|
|
||||||
|
|
||||||
/// Chaotic, jumbled or muddled.
|
|
||||||
Confused => "confused",
|
|
||||||
|
|
||||||
/// Feeling introspective or thoughtful.
|
|
||||||
Contemplative => "contemplative",
|
|
||||||
|
|
||||||
/// Pleased at the satisfaction of a want or desire; satisfied.
|
|
||||||
Contented => "contented",
|
|
||||||
|
|
||||||
/// Grouchy, irritable; easily upset.
|
|
||||||
Cranky => "cranky",
|
|
||||||
|
|
||||||
/// Feeling out of control; feeling overly excited or enthusiastic.
|
|
||||||
Crazy => "crazy",
|
|
||||||
|
|
||||||
/// Feeling original, expressive, or imaginative.
|
|
||||||
Creative => "creative",
|
|
||||||
|
|
||||||
/// Inquisitive; tending to ask questions, investigate, or explore.
|
|
||||||
Curious => "curious",
|
|
||||||
|
|
||||||
/// Feeling sad and dispirited.
|
|
||||||
Dejected => "dejected",
|
|
||||||
|
|
||||||
/// Severely despondent and unhappy.
|
|
||||||
Depressed => "depressed",
|
|
||||||
|
|
||||||
/// Defeated of expectation or hope; let down.
|
|
||||||
Disappointed => "disappointed",
|
|
||||||
|
|
||||||
/// Filled with disgust; irritated and out of patience.
|
|
||||||
Disgusted => "disgusted",
|
|
||||||
|
|
||||||
/// Feeling a sudden or complete loss of courage in the face of trouble or danger.
|
|
||||||
Dismayed => "dismayed",
|
|
||||||
|
|
||||||
/// Having one's attention diverted; preoccupied.
|
|
||||||
Distracted => "distracted",
|
|
||||||
|
|
||||||
/// Having a feeling of shameful discomfort.
|
|
||||||
Embarrassed => "embarrassed",
|
|
||||||
|
|
||||||
/// Feeling pain by the excellence or good fortune of another.
|
|
||||||
Envious => "envious",
|
|
||||||
|
|
||||||
/// Having great enthusiasm.
|
|
||||||
Excited => "excited",
|
|
||||||
|
|
||||||
/// In the mood for flirting.
|
|
||||||
Flirtatious => "flirtatious",
|
|
||||||
|
|
||||||
/// Suffering from frustration; dissatisfied, agitated, or discontented because one is unable to perform an action or fulfill a desire.
|
|
||||||
Frustrated => "frustrated",
|
|
||||||
|
|
||||||
/// Feeling appreciation or thanks.
|
|
||||||
Grateful => "grateful",
|
|
||||||
|
|
||||||
/// Feeling very sad about something, especially something lost; mournful; sorrowful.
|
|
||||||
Grieving => "grieving",
|
|
||||||
|
|
||||||
/// Unhappy and irritable.
|
|
||||||
Grumpy => "grumpy",
|
|
||||||
|
|
||||||
/// Feeling responsible for wrongdoing; feeling blameworthy.
|
|
||||||
Guilty => "guilty",
|
|
||||||
|
|
||||||
/// Experiencing the effect of favourable fortune; having the feeling arising from the consciousness of well-being or of enjoyment; enjoying good of any kind, as peace, tranquillity, comfort; contented; joyous.
|
|
||||||
Happy => "happy",
|
|
||||||
|
|
||||||
/// Having a positive feeling, belief, or expectation that something wished for can or will happen.
|
|
||||||
Hopeful => "hopeful",
|
|
||||||
|
|
||||||
/// Feeling the sensation of heat, especially to the point of discomfort.
|
|
||||||
Hot => "hot",
|
|
||||||
|
|
||||||
/// Having or showing a modest or low estimate of one's own importance; feeling lowered in dignity or importance.
|
|
||||||
Humbled => "humbled",
|
|
||||||
|
|
||||||
/// Feeling deprived of dignity or self-respect.
|
|
||||||
Humiliated => "humiliated",
|
|
||||||
|
|
||||||
/// Having a physical need for food.
|
|
||||||
Hungry => "hungry",
|
|
||||||
|
|
||||||
/// Wounded, injured, or pained, whether physically or emotionally.
|
|
||||||
Hurt => "hurt",
|
|
||||||
|
|
||||||
/// Favourably affected by something or someone.
|
|
||||||
Impressed => "impressed",
|
|
||||||
|
|
||||||
/// Feeling amazement at something or someone; or feeling a combination of fear and reverence.
|
|
||||||
InAwe => "in_awe",
|
|
||||||
|
|
||||||
/// Feeling strong affection, care, liking, or attraction..
|
|
||||||
InLove => "in_love",
|
|
||||||
|
|
||||||
/// Showing anger or indignation, especially at something unjust or wrong.
|
|
||||||
Indignant => "indignant",
|
|
||||||
|
|
||||||
/// Showing great attention to something or someone; having or showing interest.
|
|
||||||
Interested => "interested",
|
|
||||||
|
|
||||||
/// Under the influence of alcohol; drunk.
|
|
||||||
Intoxicated => "intoxicated",
|
|
||||||
|
|
||||||
/// Feeling as if one cannot be defeated, overcome or denied.
|
|
||||||
Invincible => "invincible",
|
|
||||||
|
|
||||||
/// Fearful of being replaced in position or affection.
|
|
||||||
Jealous => "jealous",
|
|
||||||
|
|
||||||
/// Feeling isolated, empty, or abandoned.
|
|
||||||
Lonely => "lonely",
|
|
||||||
|
|
||||||
/// Unable to find one's way, either physically or emotionally.
|
|
||||||
Lost => "lost",
|
|
||||||
|
|
||||||
/// Feeling as if one will be favored by luck.
|
|
||||||
Lucky => "lucky",
|
|
||||||
|
|
||||||
/// Causing or intending to cause intentional harm; bearing ill will towards another; cruel; malicious.
|
|
||||||
Mean => "mean",
|
|
||||||
|
|
||||||
/// Given to sudden or frequent changes of mind or feeling; temperamental.
|
|
||||||
Moody => "moody",
|
|
||||||
|
|
||||||
/// Easily agitated or alarmed; apprehensive or anxious.
|
|
||||||
Nervous => "nervous",
|
|
||||||
|
|
||||||
/// Not having a strong mood or emotional state.
|
|
||||||
Neutral => "neutral",
|
|
||||||
|
|
||||||
/// Feeling emotionally hurt, displeased, or insulted.
|
|
||||||
Offended => "offended",
|
|
||||||
|
|
||||||
/// Feeling resentful anger caused by an extremely violent or vicious attack, or by an offensive, immoral, or indecent act.
|
|
||||||
Outraged => "outraged",
|
|
||||||
|
|
||||||
/// Interested in play; fun, recreational, unserious, lighthearted; joking, silly.
|
|
||||||
Playful => "playful",
|
|
||||||
|
|
||||||
/// Feeling a sense of one's own worth or accomplishment.
|
|
||||||
Proud => "proud",
|
|
||||||
|
|
||||||
/// Having an easy-going mood; not stressed; calm.
|
|
||||||
Relaxed => "relaxed",
|
|
||||||
|
|
||||||
/// Feeling uplifted because of the removal of stress or discomfort.
|
|
||||||
Relieved => "relieved",
|
|
||||||
|
|
||||||
/// Feeling regret or sadness for doing something wrong.
|
|
||||||
Remorseful => "remorseful",
|
|
||||||
|
|
||||||
/// Without rest; unable to be still or quiet; uneasy; continually moving.
|
|
||||||
Restless => "restless",
|
|
||||||
|
|
||||||
/// Feeling sorrow; sorrowful, mournful.
|
|
||||||
Sad => "sad",
|
|
||||||
|
|
||||||
/// Mocking and ironical.
|
|
||||||
Sarcastic => "sarcastic",
|
|
||||||
|
|
||||||
/// Pleased at the fulfillment of a need or desire.
|
|
||||||
Satisfied => "satisfied",
|
|
||||||
|
|
||||||
/// Without humor or expression of happiness; grave in manner or disposition; earnest; thoughtful; solemn.
|
|
||||||
Serious => "serious",
|
|
||||||
|
|
||||||
/// Surprised, startled, confused, or taken aback.
|
|
||||||
Shocked => "shocked",
|
|
||||||
|
|
||||||
/// Feeling easily frightened or scared; timid; reserved or coy.
|
|
||||||
Shy => "shy",
|
|
||||||
|
|
||||||
/// Feeling in poor health; ill.
|
|
||||||
Sick => "sick",
|
|
||||||
|
|
||||||
/// Feeling the need for sleep.
|
|
||||||
Sleepy => "sleepy",
|
|
||||||
|
|
||||||
/// Acting without planning; natural; impulsive.
|
|
||||||
Spontaneous => "spontaneous",
|
|
||||||
|
|
||||||
/// Suffering emotional pressure.
|
|
||||||
Stressed => "stressed",
|
|
||||||
|
|
||||||
/// Capable of producing great physical force; or, emotionally forceful, able, determined, unyielding.
|
|
||||||
Strong => "strong",
|
|
||||||
|
|
||||||
/// Experiencing a feeling caused by something unexpected.
|
|
||||||
Surprised => "surprised",
|
|
||||||
|
|
||||||
/// Showing appreciation or gratitude.
|
|
||||||
Thankful => "thankful",
|
|
||||||
|
|
||||||
/// Feeling the need to drink.
|
|
||||||
Thirsty => "thirsty",
|
|
||||||
|
|
||||||
/// In need of rest or sleep.
|
|
||||||
Tired => "tired",
|
|
||||||
|
|
||||||
/// [Feeling any emotion not defined here.]
|
|
||||||
Undefined => "undefined",
|
|
||||||
|
|
||||||
/// Lacking in force or ability, either physical or emotional.
|
|
||||||
Weak => "weak",
|
|
||||||
|
|
||||||
/// Thinking about unpleasant things that have happened or that might happen; feeling afraid and unhappy.
|
|
||||||
Worried => "worried",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_elem_id!(
|
|
||||||
/// Free-form text description of the mood.
|
|
||||||
Text,
|
|
||||||
"text",
|
|
||||||
MOOD
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(MoodEnum, 1);
|
|
||||||
assert_size!(Text, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(MoodEnum, 1);
|
|
||||||
assert_size!(Text, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<happy xmlns='http://jabber.org/protocol/mood'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let mood = MoodEnum::try_from(elem).unwrap();
|
|
||||||
assert_eq!(mood, MoodEnum::Happy);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_text() {
|
|
||||||
let elem: Element = "<text xmlns='http://jabber.org/protocol/mood'>Yay!</text>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem2 = elem.clone();
|
|
||||||
let text = Text::try_from(elem).unwrap();
|
|
||||||
assert_eq!(text.0, String::from("Yay!"));
|
|
||||||
|
|
||||||
let elem3 = text.into();
|
|
||||||
assert_eq!(elem2, elem3);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
/// The http://jabber.org/protocol/muc protocol.
|
|
||||||
pub mod muc;
|
|
||||||
|
|
||||||
/// The http://jabber.org/protocol/muc#user protocol.
|
|
||||||
pub mod user;
|
|
||||||
|
|
||||||
pub use self::muc::Muc;
|
|
||||||
pub use self::user::MucUser;
|
|
|
@ -1,194 +0,0 @@
|
||||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::date::DateTime;
|
|
||||||
use crate::presence::PresencePayload;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Represents the query for messages before our join.
|
|
||||||
#[derive(Default)]
|
|
||||||
History, "history", MUC,
|
|
||||||
attributes: [
|
|
||||||
/// How many characters of history to send, in XML characters.
|
|
||||||
maxchars: Option<u32> = "maxchars",
|
|
||||||
|
|
||||||
/// How many messages to send.
|
|
||||||
maxstanzas: Option<u32> = "maxstanzas",
|
|
||||||
|
|
||||||
/// Only send messages received in these last seconds.
|
|
||||||
seconds: Option<u32> = "seconds",
|
|
||||||
|
|
||||||
/// Only send messages after this date.
|
|
||||||
since: Option<DateTime> = "since",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl History {
|
|
||||||
/// Create a new empty history element.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
History::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set how many characters of history to send.
|
|
||||||
pub fn with_maxchars(mut self, maxchars: u32) -> Self {
|
|
||||||
self.maxchars = Some(maxchars);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set how many messages to send.
|
|
||||||
pub fn with_maxstanzas(mut self, maxstanzas: u32) -> Self {
|
|
||||||
self.maxstanzas = Some(maxstanzas);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Only send messages received in these last seconds.
|
|
||||||
pub fn with_seconds(mut self, seconds: u32) -> Self {
|
|
||||||
self.seconds = Some(seconds);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Only send messages received since this date.
|
|
||||||
pub fn with_since(mut self, since: DateTime) -> Self {
|
|
||||||
self.since = Some(since);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Represents a room join request.
|
|
||||||
#[derive(Default)]
|
|
||||||
Muc, "x", MUC, children: [
|
|
||||||
/// Password to use when the room is protected by a password.
|
|
||||||
password: Option<String> = ("password", MUC) => String,
|
|
||||||
|
|
||||||
/// Controls how much and how old we want to receive history on join.
|
|
||||||
history: Option<History> = ("history", MUC) => History
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PresencePayload for Muc {}
|
|
||||||
|
|
||||||
impl Muc {
|
|
||||||
/// Create a new MUC join element.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Muc::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Join a room with this password.
|
|
||||||
pub fn with_password(mut self, password: String) -> Self {
|
|
||||||
self.password = Some(password);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Join a room with only that much history.
|
|
||||||
pub fn with_history(mut self, history: History) -> Self {
|
|
||||||
self.history = Some(history);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_muc_simple() {
|
|
||||||
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Muc::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_muc_invalid_child() {
|
|
||||||
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc'><coucou/></x>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Muc::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in x element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_muc_serialise() {
|
|
||||||
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let muc = Muc {
|
|
||||||
password: None,
|
|
||||||
history: None,
|
|
||||||
};
|
|
||||||
let elem2 = muc.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_muc_invalid_attribute() {
|
|
||||||
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Muc::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in x element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_muc_simple_password() {
|
|
||||||
let elem: Element =
|
|
||||||
"<x xmlns='http://jabber.org/protocol/muc'><password>coucou</password></x>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let muc = Muc::try_from(elem).unwrap();
|
|
||||||
assert_eq!(muc.password, Some("coucou".to_owned()));
|
|
||||||
|
|
||||||
let elem2 = Element::from(muc);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn history() {
|
|
||||||
let elem: Element = "
|
|
||||||
<x xmlns='http://jabber.org/protocol/muc'>
|
|
||||||
<history maxstanzas='0'/>
|
|
||||||
</x>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let muc = Muc::try_from(elem).unwrap();
|
|
||||||
let muc2 = Muc::new().with_history(History::new().with_maxstanzas(0));
|
|
||||||
assert_eq!(muc, muc2);
|
|
||||||
|
|
||||||
let history = muc.history.unwrap();
|
|
||||||
assert_eq!(history.maxstanzas, Some(0));
|
|
||||||
assert_eq!(history.maxchars, None);
|
|
||||||
assert_eq!(history.seconds, None);
|
|
||||||
assert_eq!(history.since, None);
|
|
||||||
|
|
||||||
let elem: Element = "
|
|
||||||
<x xmlns='http://jabber.org/protocol/muc'>
|
|
||||||
<history since='1970-01-01T00:00:00Z'/>
|
|
||||||
</x>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let muc = Muc::try_from(elem).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
muc.history.unwrap().since.unwrap(),
|
|
||||||
DateTime::from_str("1970-01-01T00:00:00+00:00").unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,725 +0,0 @@
|
||||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::FullJid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
generate_attribute_enum!(
|
|
||||||
/// Lists all of the possible status codes used in MUC presences.
|
|
||||||
Status, "status", MUC_USER, "code", {
|
|
||||||
/// Inform user that any occupant is allowed to see the user's full JID
|
|
||||||
NonAnonymousRoom => 100,
|
|
||||||
|
|
||||||
/// Inform user that his or her affiliation changed while not in the room
|
|
||||||
AffiliationChange => 101,
|
|
||||||
|
|
||||||
/// Inform occupants that room now shows unavailable members
|
|
||||||
ConfigShowsUnavailableMembers => 102,
|
|
||||||
|
|
||||||
/// Inform occupants that room now does not show unavailable members
|
|
||||||
ConfigHidesUnavailableMembers => 103,
|
|
||||||
|
|
||||||
/// Inform occupants that a non-privacy-related room configuration change has occurred
|
|
||||||
ConfigNonPrivacyRelated => 104,
|
|
||||||
|
|
||||||
/// Inform user that presence refers to itself
|
|
||||||
SelfPresence => 110,
|
|
||||||
|
|
||||||
/// Inform occupants that room logging is now enabled
|
|
||||||
ConfigRoomLoggingEnabled => 170,
|
|
||||||
|
|
||||||
/// Inform occupants that room logging is now disabled
|
|
||||||
ConfigRoomLoggingDisabled => 171,
|
|
||||||
|
|
||||||
/// Inform occupants that the room is now non-anonymous
|
|
||||||
ConfigRoomNonAnonymous => 172,
|
|
||||||
|
|
||||||
/// Inform occupants that the room is now semi-anonymous
|
|
||||||
ConfigRoomSemiAnonymous => 173,
|
|
||||||
|
|
||||||
/// Inform user that a new room has been created
|
|
||||||
RoomHasBeenCreated => 201,
|
|
||||||
|
|
||||||
/// Inform user that service has assigned or modified occupant's roomnick
|
|
||||||
AssignedNick => 210,
|
|
||||||
|
|
||||||
/// Inform user that he or she has been banned from the room
|
|
||||||
Banned => 301,
|
|
||||||
|
|
||||||
/// Inform all occupants of new room nickname
|
|
||||||
NewNick => 303,
|
|
||||||
|
|
||||||
/// Inform user that he or she has been kicked from the room
|
|
||||||
Kicked => 307,
|
|
||||||
|
|
||||||
/// Inform user that he or she is being removed from the room
|
|
||||||
/// because of an affiliation change
|
|
||||||
RemovalFromRoom => 321,
|
|
||||||
|
|
||||||
/// Inform user that he or she is being removed from the room
|
|
||||||
/// because the room has been changed to members-only and the
|
|
||||||
/// user is not a member
|
|
||||||
ConfigMembersOnly => 322,
|
|
||||||
|
|
||||||
/// Inform user that he or she is being removed from the room
|
|
||||||
/// because the MUC service is being shut down
|
|
||||||
ServiceShutdown => 332,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Optional <actor/> element used in <item/> elements inside presence stanzas of type
|
|
||||||
/// "unavailable" that are sent to users who are kick or banned, as well as within IQs for tracking
|
|
||||||
/// purposes. -- CHANGELOG 0.17 (2002-10-23)
|
|
||||||
///
|
|
||||||
/// Possesses a 'jid' and a 'nick' attribute, so that an action can be attributed either to a real
|
|
||||||
/// JID or to a roomnick. -- CHANGELOG 1.25 (2012-02-08)
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum Actor {
|
|
||||||
/// The full JID associated with this user.
|
|
||||||
Jid(FullJid),
|
|
||||||
|
|
||||||
/// The nickname of this user.
|
|
||||||
Nick(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Actor {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Actor, Error> {
|
|
||||||
check_self!(elem, "actor", MUC_USER);
|
|
||||||
check_no_unknown_attributes!(elem, "actor", ["jid", "nick"]);
|
|
||||||
check_no_children!(elem, "actor");
|
|
||||||
let jid: Option<FullJid> = get_attr!(elem, "jid", Option);
|
|
||||||
let nick = get_attr!(elem, "nick", Option);
|
|
||||||
|
|
||||||
match (jid, nick) {
|
|
||||||
(Some(_), Some(_)) | (None, None) => Err(Error::ParseError(
|
|
||||||
"Either 'jid' or 'nick' attribute is required.",
|
|
||||||
)),
|
|
||||||
(Some(jid), _) => Ok(Actor::Jid(jid)),
|
|
||||||
(_, Some(nick)) => Ok(Actor::Nick(nick)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Actor> for Element {
|
|
||||||
fn from(actor: Actor) -> Element {
|
|
||||||
let elem = Element::builder("actor", ns::MUC_USER);
|
|
||||||
|
|
||||||
(match actor {
|
|
||||||
Actor::Jid(jid) => elem.attr("jid", jid),
|
|
||||||
Actor::Nick(nick) => elem.attr("nick", nick),
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Used to continue a one-to-one discussion in a room, with more than one
|
|
||||||
/// participant.
|
|
||||||
Continue, "continue", MUC_USER,
|
|
||||||
attributes: [
|
|
||||||
/// The thread to continue in this room.
|
|
||||||
thread: Option<String> = "thread",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_elem_id!(
|
|
||||||
/// A reason for inviting, declining, etc. a request.
|
|
||||||
Reason,
|
|
||||||
"reason",
|
|
||||||
MUC_USER
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The affiliation of an entity with a room, which isn’t tied to its
|
|
||||||
/// presence in it.
|
|
||||||
Affiliation, "affiliation", {
|
|
||||||
/// The user who created the room, or who got appointed by its creator
|
|
||||||
/// to be their equal.
|
|
||||||
Owner => "owner",
|
|
||||||
|
|
||||||
/// A user who has been empowered by an owner to do administrative
|
|
||||||
/// operations.
|
|
||||||
Admin => "admin",
|
|
||||||
|
|
||||||
/// A user who is whitelisted to speak in moderated rooms, or to join a
|
|
||||||
/// member-only room.
|
|
||||||
Member => "member",
|
|
||||||
|
|
||||||
/// A user who has been banned from this room.
|
|
||||||
Outcast => "outcast",
|
|
||||||
|
|
||||||
/// A normal participant.
|
|
||||||
None => "none",
|
|
||||||
}, Default = None
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The current role of an entity in a room, it can be changed by an owner
|
|
||||||
/// or an administrator but will be lost once they leave the room.
|
|
||||||
Role, "role", {
|
|
||||||
/// This user can kick other participants, as well as grant and revoke
|
|
||||||
/// them voice.
|
|
||||||
Moderator => "moderator",
|
|
||||||
|
|
||||||
/// A user who can speak in this room.
|
|
||||||
Participant => "participant",
|
|
||||||
|
|
||||||
/// A user who cannot speak in this room, and must request voice before
|
|
||||||
/// doing so.
|
|
||||||
Visitor => "visitor",
|
|
||||||
|
|
||||||
/// A user who is absent from the room.
|
|
||||||
None => "none",
|
|
||||||
}, Default = None
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// An item representing a user in a room.
|
|
||||||
Item, "item", MUC_USER, attributes: [
|
|
||||||
/// The affiliation of this user with the room.
|
|
||||||
affiliation: Required<Affiliation> = "affiliation",
|
|
||||||
|
|
||||||
/// The real JID of this user, if you are allowed to see it.
|
|
||||||
jid: Option<FullJid> = "jid",
|
|
||||||
|
|
||||||
/// The current nickname of this user.
|
|
||||||
nick: Option<String> = "nick",
|
|
||||||
|
|
||||||
/// The current role of this user.
|
|
||||||
role: Required<Role> = "role",
|
|
||||||
], children: [
|
|
||||||
/// The actor affected by this item.
|
|
||||||
actor: Option<Actor> = ("actor", MUC_USER) => Actor,
|
|
||||||
|
|
||||||
/// Whether this continues a one-to-one discussion.
|
|
||||||
continue_: Option<Continue> = ("continue", MUC_USER) => Continue,
|
|
||||||
|
|
||||||
/// A reason for this item.
|
|
||||||
reason: Option<Reason> = ("reason", MUC_USER) => Reason
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Item {
|
|
||||||
/// Creates a new item with the given affiliation and role.
|
|
||||||
pub fn new(affiliation: Affiliation, role: Role) -> Item {
|
|
||||||
Item {
|
|
||||||
affiliation,
|
|
||||||
role,
|
|
||||||
jid: None,
|
|
||||||
nick: None,
|
|
||||||
actor: None,
|
|
||||||
continue_: None,
|
|
||||||
reason: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The main muc#user element.
|
|
||||||
MucUser, "x", MUC_USER, children: [
|
|
||||||
/// List of statuses applying to this item.
|
|
||||||
status: Vec<Status> = ("status", MUC_USER) => Status,
|
|
||||||
|
|
||||||
/// List of items.
|
|
||||||
items: Vec<Item> = ("item", MUC_USER) => Item
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "
|
|
||||||
<x xmlns='http://jabber.org/protocol/muc#user'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
MucUser::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn statuses_and_items() {
|
|
||||||
let elem: Element = "
|
|
||||||
<x xmlns='http://jabber.org/protocol/muc#user'>
|
|
||||||
<status code='101'/>
|
|
||||||
<status code='102'/>
|
|
||||||
<item affiliation='member' role='moderator'/>
|
|
||||||
</x>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let muc_user = MucUser::try_from(elem).unwrap();
|
|
||||||
assert_eq!(muc_user.status.len(), 2);
|
|
||||||
assert_eq!(muc_user.status[0], Status::AffiliationChange);
|
|
||||||
assert_eq!(muc_user.status[1], Status::ConfigShowsUnavailableMembers);
|
|
||||||
assert_eq!(muc_user.items.len(), 1);
|
|
||||||
assert_eq!(muc_user.items[0].affiliation, Affiliation::Member);
|
|
||||||
assert_eq!(muc_user.items[0].role, Role::Moderator);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "
|
|
||||||
<x xmlns='http://jabber.org/protocol/muc#user'>
|
|
||||||
<coucou/>
|
|
||||||
</x>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = MucUser::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in x element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "
|
|
||||||
<x xmlns='http://jabber.org/protocol/muc#user'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let muc = MucUser {
|
|
||||||
status: vec![],
|
|
||||||
items: vec![],
|
|
||||||
};
|
|
||||||
let elem2 = muc.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_attribute() {
|
|
||||||
let elem: Element = "
|
|
||||||
<x xmlns='http://jabber.org/protocol/muc#user' coucou=''/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = MucUser::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in x element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_status_simple() {
|
|
||||||
let elem: Element = "
|
|
||||||
<status xmlns='http://jabber.org/protocol/muc#user' code='110'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Status::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_status_invalid() {
|
|
||||||
let elem: Element = "
|
|
||||||
<status xmlns='http://jabber.org/protocol/muc#user'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Status::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'code' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_status_invalid_child() {
|
|
||||||
let elem: Element = "
|
|
||||||
<status xmlns='http://jabber.org/protocol/muc#user' code='110'>
|
|
||||||
<foo/>
|
|
||||||
</status>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Status::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in status element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_status_simple_code() {
|
|
||||||
let elem: Element = "
|
|
||||||
<status xmlns='http://jabber.org/protocol/muc#user' code='307'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let status = Status::try_from(elem).unwrap();
|
|
||||||
assert_eq!(status, Status::Kicked);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_status_invalid_code() {
|
|
||||||
let elem: Element = "
|
|
||||||
<status xmlns='http://jabber.org/protocol/muc#user' code='666'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Status::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Invalid status code value.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_status_invalid_code2() {
|
|
||||||
let elem: Element = "
|
|
||||||
<status xmlns='http://jabber.org/protocol/muc#user' code='coucou'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Status::try_from(elem).unwrap_err();
|
|
||||||
let error = match error {
|
|
||||||
Error::ParseIntError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(error.to_string(), "invalid digit found in string");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_actor_required_attributes() {
|
|
||||||
let elem: Element = "
|
|
||||||
<actor xmlns='http://jabber.org/protocol/muc#user'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Actor::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_actor_required_attributes2() {
|
|
||||||
let elem: Element = "
|
|
||||||
<actor xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
jid='foo@bar/baz'
|
|
||||||
nick='baz'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Actor::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_actor_jid() {
|
|
||||||
let elem: Element = "
|
|
||||||
<actor xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
jid='foo@bar/baz'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let actor = Actor::try_from(elem).unwrap();
|
|
||||||
let jid = match actor {
|
|
||||||
Actor::Jid(jid) => jid,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(jid, "foo@bar/baz".parse::<FullJid>().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_actor_nick() {
|
|
||||||
let elem: Element = "
|
|
||||||
<actor xmlns='http://jabber.org/protocol/muc#user' nick='baz'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let actor = Actor::try_from(elem).unwrap();
|
|
||||||
let nick = match actor {
|
|
||||||
Actor::Nick(nick) => nick,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(nick, "baz".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_continue_simple() {
|
|
||||||
let elem: Element = "
|
|
||||||
<continue xmlns='http://jabber.org/protocol/muc#user'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Continue::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_continue_thread_attribute() {
|
|
||||||
let elem: Element = "
|
|
||||||
<continue xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
thread='foo'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let continue_ = Continue::try_from(elem).unwrap();
|
|
||||||
assert_eq!(continue_.thread, Some("foo".to_owned()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_continue_invalid() {
|
|
||||||
let elem: Element = "
|
|
||||||
<continue xmlns='http://jabber.org/protocol/muc#user'>
|
|
||||||
<foobar/>
|
|
||||||
</continue>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let continue_ = Continue::try_from(elem).unwrap_err();
|
|
||||||
let message = match continue_ {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in continue element.".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_reason_simple() {
|
|
||||||
let elem: Element = "
|
|
||||||
<reason xmlns='http://jabber.org/protocol/muc#user'>Reason</reason>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem2 = elem.clone();
|
|
||||||
let reason = Reason::try_from(elem).unwrap();
|
|
||||||
assert_eq!(reason.0, "Reason".to_owned());
|
|
||||||
|
|
||||||
let elem3 = reason.into();
|
|
||||||
assert_eq!(elem2, elem3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_reason_invalid_attribute() {
|
|
||||||
let elem: Element = "
|
|
||||||
<reason xmlns='http://jabber.org/protocol/muc#user' foo='bar'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Reason::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in reason element.".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_reason_invalid() {
|
|
||||||
let elem: Element = "
|
|
||||||
<reason xmlns='http://jabber.org/protocol/muc#user'>
|
|
||||||
<foobar/>
|
|
||||||
</reason>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Reason::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in reason element.".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_item_invalid_attr() {
|
|
||||||
let elem: Element = "
|
|
||||||
<item xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
foo='bar'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Item::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in item element.".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_item_affiliation_role_attr() {
|
|
||||||
let elem: Element = "
|
|
||||||
<item xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
affiliation='member'
|
|
||||||
role='moderator'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Item::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_item_affiliation_role_invalid_attr() {
|
|
||||||
let elem: Element = "
|
|
||||||
<item xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
affiliation='member'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Item::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'role' missing.".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_item_nick_attr() {
|
|
||||||
let elem: Element = "
|
|
||||||
<item xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
affiliation='member'
|
|
||||||
role='moderator'
|
|
||||||
nick='foobar'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let item = Item::try_from(elem).unwrap();
|
|
||||||
match item {
|
|
||||||
Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_item_affiliation_role_invalid_attr2() {
|
|
||||||
let elem: Element = "
|
|
||||||
<item xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
role='moderator'/>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Item::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
message,
|
|
||||||
"Required attribute 'affiliation' missing.".to_owned()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_item_role_actor_child() {
|
|
||||||
let elem: Element = "
|
|
||||||
<item xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
affiliation='member'
|
|
||||||
role='moderator'>
|
|
||||||
<actor nick='foobar'/>
|
|
||||||
</item>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let item = Item::try_from(elem).unwrap();
|
|
||||||
match item {
|
|
||||||
Item { actor, .. } => assert_eq!(actor, Some(Actor::Nick("foobar".to_owned()))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_item_role_continue_child() {
|
|
||||||
let elem: Element = "
|
|
||||||
<item xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
affiliation='member'
|
|
||||||
role='moderator'>
|
|
||||||
<continue thread='foobar'/>
|
|
||||||
</item>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let item = Item::try_from(elem).unwrap();
|
|
||||||
let continue_1 = Continue {
|
|
||||||
thread: Some("foobar".to_owned()),
|
|
||||||
};
|
|
||||||
match item {
|
|
||||||
Item {
|
|
||||||
continue_: Some(continue_2),
|
|
||||||
..
|
|
||||||
} => assert_eq!(continue_2.thread, continue_1.thread),
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_item_role_reason_child() {
|
|
||||||
let elem: Element = "
|
|
||||||
<item xmlns='http://jabber.org/protocol/muc#user'
|
|
||||||
affiliation='member'
|
|
||||||
role='moderator'>
|
|
||||||
<reason>foobar</reason>
|
|
||||||
</item>
|
|
||||||
"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let item = Item::try_from(elem).unwrap();
|
|
||||||
match item {
|
|
||||||
Item { reason, .. } => assert_eq!(reason, Some(Reason("foobar".to_owned()))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_item() {
|
|
||||||
let reference: Element = "<item xmlns='http://jabber.org/protocol/muc#user' affiliation='member' role='moderator'><actor nick='foobar'/><continue thread='foobar'/><reason>foobar</reason></item>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<actor xmlns='http://jabber.org/protocol/muc#user' nick='foobar'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let actor = Actor::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<continue xmlns='http://jabber.org/protocol/muc#user' thread='foobar'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let continue_ = Continue::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<reason xmlns='http://jabber.org/protocol/muc#user'>foobar</reason>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let reason = Reason::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let item = Item {
|
|
||||||
affiliation: Affiliation::Member,
|
|
||||||
role: Role::Moderator,
|
|
||||||
jid: None,
|
|
||||||
nick: None,
|
|
||||||
actor: Some(actor),
|
|
||||||
reason: Some(reason),
|
|
||||||
continue_: Some(continue_),
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = item.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
generate_elem_id!(
|
|
||||||
/// Represents a global, memorable, friendly or informal name chosen by a user.
|
|
||||||
Nick,
|
|
||||||
"nick",
|
|
||||||
NICK
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Nick, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Nick, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let nick = Nick::try_from(elem).unwrap();
|
|
||||||
assert_eq!(&nick.0, "Link Mauve");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem1 = Element::from(Nick(String::from("Link Mauve")));
|
|
||||||
let elem2: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'><coucou/></nick>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Nick::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in nick element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_attribute() {
|
|
||||||
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Nick::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in nick element.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,271 +0,0 @@
|
||||||
// Copyright (c) 2017-2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub const JABBER_CLIENT: &str = "jabber:client";
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub const XMPP_STANZAS: &str = "urn:ietf:params:xml:ns:xmpp-stanzas";
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub const STREAM: &str = "http://etherx.jabber.org/streams";
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub const TLS: &str = "urn:ietf:params:xml:ns:xmpp-tls";
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub const SASL: &str = "urn:ietf:params:xml:ns:xmpp-sasl";
|
|
||||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
|
||||||
pub const BIND: &str = "urn:ietf:params:xml:ns:xmpp-bind";
|
|
||||||
|
|
||||||
/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
|
|
||||||
pub const ROSTER: &str = "jabber:iq:roster";
|
|
||||||
|
|
||||||
/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket
|
|
||||||
pub const WEBSOCKET: &str = "urn:ietf:params:xml:ns:xmpp-framing";
|
|
||||||
|
|
||||||
/// XEP-0004: Data Forms
|
|
||||||
pub const DATA_FORMS: &str = "jabber:x:data";
|
|
||||||
|
|
||||||
/// XEP-0030: Service Discovery
|
|
||||||
pub const DISCO_INFO: &str = "http://jabber.org/protocol/disco#info";
|
|
||||||
/// XEP-0030: Service Discovery
|
|
||||||
pub const DISCO_ITEMS: &str = "http://jabber.org/protocol/disco#items";
|
|
||||||
|
|
||||||
/// XEP-0045: Multi-User Chat
|
|
||||||
pub const MUC: &str = "http://jabber.org/protocol/muc";
|
|
||||||
/// XEP-0045: Multi-User Chat
|
|
||||||
pub const MUC_USER: &str = "http://jabber.org/protocol/muc#user";
|
|
||||||
|
|
||||||
/// XEP-0047: In-Band Bytestreams
|
|
||||||
pub const IBB: &str = "http://jabber.org/protocol/ibb";
|
|
||||||
|
|
||||||
/// XEP-0048: Bookmarks
|
|
||||||
pub const BOOKMARKS: &str = "storage:bookmarks";
|
|
||||||
|
|
||||||
/// XEP-0059: Result Set Management
|
|
||||||
pub const RSM: &str = "http://jabber.org/protocol/rsm";
|
|
||||||
|
|
||||||
/// XEP-0060: Publish-Subscribe
|
|
||||||
pub const PUBSUB: &str = "http://jabber.org/protocol/pubsub";
|
|
||||||
/// XEP-0060: Publish-Subscribe
|
|
||||||
pub const PUBSUB_ERRORS: &str = "http://jabber.org/protocol/pubsub#errors";
|
|
||||||
/// XEP-0060: Publish-Subscribe
|
|
||||||
pub const PUBSUB_EVENT: &str = "http://jabber.org/protocol/pubsub#event";
|
|
||||||
/// XEP-0060: Publish-Subscribe
|
|
||||||
pub const PUBSUB_OWNER: &str = "http://jabber.org/protocol/pubsub#owner";
|
|
||||||
/// XEP-0060: Publish-Subscribe node configuration
|
|
||||||
pub const PUBSUB_CONFIGURE: &str = "http://jabber.org/protocol/pubsub#node_config";
|
|
||||||
|
|
||||||
/// XEP-0071: XHTML-IM
|
|
||||||
pub const XHTML_IM: &str = "http://jabber.org/protocol/xhtml-im";
|
|
||||||
/// XEP-0071: XHTML-IM
|
|
||||||
pub const XHTML: &str = "http://www.w3.org/1999/xhtml";
|
|
||||||
|
|
||||||
/// XEP-0077: In-Band Registration
|
|
||||||
pub const REGISTER: &str = "jabber:iq:register";
|
|
||||||
|
|
||||||
/// XEP-0084: User Avatar
|
|
||||||
pub const AVATAR_DATA: &str = "urn:xmpp:avatar:data";
|
|
||||||
/// XEP-0084: User Avatar
|
|
||||||
pub const AVATAR_METADATA: &str = "urn:xmpp:avatar:metadata";
|
|
||||||
|
|
||||||
/// XEP-0085: Chat State Notifications
|
|
||||||
pub const CHATSTATES: &str = "http://jabber.org/protocol/chatstates";
|
|
||||||
|
|
||||||
/// XEP-0092: Software Version
|
|
||||||
pub const VERSION: &str = "jabber:iq:version";
|
|
||||||
|
|
||||||
/// XEP-0107: User Mood
|
|
||||||
pub const MOOD: &str = "http://jabber.org/protocol/mood";
|
|
||||||
|
|
||||||
/// XEP-0114: Jabber Component Protocol
|
|
||||||
pub const COMPONENT_ACCEPT: &str = "jabber:component:accept";
|
|
||||||
|
|
||||||
/// XEP-0114: Jabber Component Protocol
|
|
||||||
pub const COMPONENT: &str = "jabber:component:accept";
|
|
||||||
|
|
||||||
/// XEP-0115: Entity Capabilities
|
|
||||||
pub const CAPS: &str = "http://jabber.org/protocol/caps";
|
|
||||||
|
|
||||||
/// XEP-0118: User Tune
|
|
||||||
pub const TUNE: &str = "http://jabber.org/protocol/tune";
|
|
||||||
|
|
||||||
/// XEP-0157: Contact Addresses for XMPP Services
|
|
||||||
pub const SERVER_INFO: &str = "http://jabber.org/network/serverinfo";
|
|
||||||
|
|
||||||
/// XEP-0166: Jingle
|
|
||||||
pub const JINGLE: &str = "urn:xmpp:jingle:1";
|
|
||||||
|
|
||||||
/// XEP-0167: Jingle RTP Sessions
|
|
||||||
pub const JINGLE_RTP: &str = "urn:xmpp:jingle:apps:rtp:1";
|
|
||||||
/// XEP-0167: Jingle RTP Sessions
|
|
||||||
pub const JINGLE_RTP_AUDIO: &str = "urn:xmpp:jingle:apps:rtp:audio";
|
|
||||||
/// XEP-0167: Jingle RTP Sessions
|
|
||||||
pub const JINGLE_RTP_VIDEO: &str = "urn:xmpp:jingle:apps:rtp:video";
|
|
||||||
|
|
||||||
/// XEP-0172: User Nickname
|
|
||||||
pub const NICK: &str = "http://jabber.org/protocol/nick";
|
|
||||||
|
|
||||||
/// XEP-0176: Jingle ICE-UDP Transport Method
|
|
||||||
pub const JINGLE_ICE_UDP: &str = "urn:xmpp:jingle:transports:ice-udp:1";
|
|
||||||
|
|
||||||
/// XEP-0177: Jingle Raw UDP Transport Method
|
|
||||||
pub const JINGLE_RAW_UDP: &str = "urn:xmpp:jingle:transports:raw-udp:1";
|
|
||||||
|
|
||||||
/// XEP-0184: Message Delivery Receipts
|
|
||||||
pub const RECEIPTS: &str = "urn:xmpp:receipts";
|
|
||||||
|
|
||||||
/// XEP-0191: Blocking Command
|
|
||||||
pub const BLOCKING: &str = "urn:xmpp:blocking";
|
|
||||||
/// XEP-0191: Blocking Command
|
|
||||||
pub const BLOCKING_ERRORS: &str = "urn:xmpp:blocking:errors";
|
|
||||||
|
|
||||||
/// XEP-0198: Stream Management
|
|
||||||
pub const SM: &str = "urn:xmpp:sm:3";
|
|
||||||
|
|
||||||
/// XEP-0199: XMPP Ping
|
|
||||||
pub const PING: &str = "urn:xmpp:ping";
|
|
||||||
|
|
||||||
/// XEP-0202: Entity Time
|
|
||||||
pub const TIME: &str = "urn:xmpp:time";
|
|
||||||
|
|
||||||
/// XEP-0203: Delayed Delivery
|
|
||||||
pub const DELAY: &str = "urn:xmpp:delay";
|
|
||||||
|
|
||||||
/// XEP-0221: Data Forms Media Element
|
|
||||||
pub const MEDIA_ELEMENT: &str = "urn:xmpp:media-element";
|
|
||||||
|
|
||||||
/// XEP-0224: Attention
|
|
||||||
pub const ATTENTION: &str = "urn:xmpp:attention:0";
|
|
||||||
|
|
||||||
/// XEP-0231: Bits of Binary
|
|
||||||
pub const BOB: &str = "urn:xmpp:bob";
|
|
||||||
|
|
||||||
/// XEP-0234: Jingle File Transfer
|
|
||||||
pub const JINGLE_FT: &str = "urn:xmpp:jingle:apps:file-transfer:5";
|
|
||||||
/// XEP-0234: Jingle File Transfer
|
|
||||||
pub const JINGLE_FT_ERROR: &str = "urn:xmpp:jingle:apps:file-transfer:errors:0";
|
|
||||||
|
|
||||||
/// XEP-0257: Client Certificate Management for SASL EXTERNAL
|
|
||||||
pub const SASL_CERT: &str = "urn:xmpp:saslcert:1";
|
|
||||||
|
|
||||||
/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
|
|
||||||
pub const JINGLE_S5B: &str = "urn:xmpp:jingle:transports:s5b:1";
|
|
||||||
|
|
||||||
/// XEP-0261: Jingle In-Band Bytestreams Transport Method
|
|
||||||
pub const JINGLE_IBB: &str = "urn:xmpp:jingle:transports:ibb:1";
|
|
||||||
|
|
||||||
/// XEP-0277: Microblogging over XMPP
|
|
||||||
pub const MICROBLOG: &str = "urn:xmpp:microblog:0";
|
|
||||||
|
|
||||||
/// XEP-0280: Message Carbons
|
|
||||||
pub const CARBONS: &str = "urn:xmpp:carbons:2";
|
|
||||||
|
|
||||||
/// XEP-0293: Jingle RTP Feedback Negotiation
|
|
||||||
pub const JINGLE_RTCP_FB: &str = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0";
|
|
||||||
|
|
||||||
/// XEP-0294: Jingle RTP Header Extensions Negociation
|
|
||||||
pub const JINGLE_RTP_HDREXT: &str = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0";
|
|
||||||
|
|
||||||
/// XEP-0297: Stanza Forwarding
|
|
||||||
pub const FORWARD: &str = "urn:xmpp:forward:0";
|
|
||||||
|
|
||||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
|
||||||
pub const HASHES: &str = "urn:xmpp:hashes:2";
|
|
||||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
|
||||||
pub const HASH_ALGO_SHA_256: &str = "urn:xmpp:hash-function-text-names:sha-256";
|
|
||||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
|
||||||
pub const HASH_ALGO_SHA_512: &str = "urn:xmpp:hash-function-text-names:sha-512";
|
|
||||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
|
||||||
pub const HASH_ALGO_SHA3_256: &str = "urn:xmpp:hash-function-text-names:sha3-256";
|
|
||||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
|
||||||
pub const HASH_ALGO_SHA3_512: &str = "urn:xmpp:hash-function-text-names:sha3-512";
|
|
||||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
|
||||||
pub const HASH_ALGO_BLAKE2B_256: &str = "urn:xmpp:hash-function-text-names:id-blake2b256";
|
|
||||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
|
||||||
pub const HASH_ALGO_BLAKE2B_512: &str = "urn:xmpp:hash-function-text-names:id-blake2b512";
|
|
||||||
|
|
||||||
/// XEP-0308: Last Message Correction
|
|
||||||
pub const MESSAGE_CORRECT: &str = "urn:xmpp:message-correct:0";
|
|
||||||
|
|
||||||
/// XEP-0313: Message Archive Management
|
|
||||||
pub const MAM: &str = "urn:xmpp:mam:2";
|
|
||||||
|
|
||||||
/// XEP-0319: Last User Interaction in Presence
|
|
||||||
pub const IDLE: &str = "urn:xmpp:idle:1";
|
|
||||||
|
|
||||||
/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions
|
|
||||||
pub const JINGLE_DTLS: &str = "urn:xmpp:jingle:apps:dtls:0";
|
|
||||||
|
|
||||||
/// XEP-0328: JID Prep
|
|
||||||
pub const JID_PREP: &str = "urn:xmpp:jidprep:0";
|
|
||||||
|
|
||||||
/// XEP-0338: Jingle Grouping Framework
|
|
||||||
pub const JINGLE_GROUPING: &str = "urn:xmpp:jingle:apps:grouping:0";
|
|
||||||
|
|
||||||
/// XEP-0339: Source-Specific Media Attributes in Jingle
|
|
||||||
pub const JINGLE_SSMA: &str = "urn:xmpp:jingle:apps:rtp:ssma:0";
|
|
||||||
|
|
||||||
/// XEP-0352: Client State Indication
|
|
||||||
pub const CSI: &str = "urn:xmpp:csi:0";
|
|
||||||
|
|
||||||
/// XEP-0353: Jingle Message Initiation
|
|
||||||
pub const JINGLE_MESSAGE: &str = "urn:xmpp:jingle-message:0";
|
|
||||||
|
|
||||||
/// XEP-0359: Unique and Stable Stanza IDs
|
|
||||||
pub const SID: &str = "urn:xmpp:sid:0";
|
|
||||||
|
|
||||||
/// XEP-0369: Mediated Information eXchange (MIX)
|
|
||||||
pub const MIX_CORE: &str = "urn:xmpp:mix:core:1";
|
|
||||||
/// XEP-0369: Mediated Information eXchange (MIX)
|
|
||||||
pub const MIX_CORE_SEARCHABLE: &str = "urn:xmpp:mix:core:1#searchable";
|
|
||||||
/// XEP-0369: Mediated Information eXchange (MIX)
|
|
||||||
pub const MIX_CORE_CREATE_CHANNEL: &str = "urn:xmpp:mix:core:1#create-channel";
|
|
||||||
/// XEP-0369: Mediated Information eXchange (MIX)
|
|
||||||
pub const MIX_NODES_PRESENCE: &str = "urn:xmpp:mix:nodes:presence";
|
|
||||||
/// XEP-0369: Mediated Information eXchange (MIX)
|
|
||||||
pub const MIX_NODES_PARTICIPANTS: &str = "urn:xmpp:mix:nodes:participants";
|
|
||||||
/// XEP-0369: Mediated Information eXchange (MIX)
|
|
||||||
pub const MIX_NODES_MESSAGES: &str = "urn:xmpp:mix:nodes:messages";
|
|
||||||
/// XEP-0369: Mediated Information eXchange (MIX)
|
|
||||||
pub const MIX_NODES_CONFIG: &str = "urn:xmpp:mix:nodes:config";
|
|
||||||
/// XEP-0369: Mediated Information eXchange (MIX)
|
|
||||||
pub const MIX_NODES_INFO: &str = "urn:xmpp:mix:nodes:info";
|
|
||||||
|
|
||||||
/// XEP-0373: OpenPGP for XMPP
|
|
||||||
pub const OX: &str = "urn:xmpp:openpgp:0";
|
|
||||||
/// XEP-0373: OpenPGP for XMPP
|
|
||||||
pub const OX_PUBKEYS: &str = "urn:xmpp:openpgp:0:public-keys";
|
|
||||||
|
|
||||||
/// XEP-0380: Explicit Message Encryption
|
|
||||||
pub const EME: &str = "urn:xmpp:eme:0";
|
|
||||||
|
|
||||||
/// XEP-0390: Entity Capabilities 2.0
|
|
||||||
pub const ECAPS2: &str = "urn:xmpp:caps";
|
|
||||||
/// XEP-0390: Entity Capabilities 2.0
|
|
||||||
pub const ECAPS2_OPTIMIZE: &str = "urn:xmpp:caps:optimize";
|
|
||||||
|
|
||||||
/// XEP-0402: Bookmarks 2 (This Time it's Serious)
|
|
||||||
pub const BOOKMARKS2: &str = "urn:xmpp:bookmarks:1";
|
|
||||||
/// XEP-0402: Bookmarks 2 (This Time it's Serious)
|
|
||||||
pub const BOOKMARKS2_COMPAT: &str = "urn:xmpp:bookmarks:0#compat";
|
|
||||||
|
|
||||||
/// XEP-0421: Anonymous unique occupant identifiers for MUCs
|
|
||||||
pub const OID: &str = "urn:xmpp:occupant-id:0";
|
|
||||||
|
|
||||||
/// Alias for the main namespace of the stream, that is "jabber:client" when
|
|
||||||
/// the component feature isn’t enabled.
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
pub const DEFAULT_NS: &str = JABBER_CLIENT;
|
|
||||||
|
|
||||||
/// Alias for the main namespace of the stream, that is
|
|
||||||
/// "jabber:component:accept" when the component feature is enabled.
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
pub const DEFAULT_NS: &str = COMPONENT_ACCEPT;
|
|
||||||
|
|
||||||
/// Jitsi Meet general namespace
|
|
||||||
pub const JITSI_MEET: &str = "http://jitsi.org/jitmeet";
|
|
||||||
|
|
||||||
/// Jitsi Meet Colibri namespace
|
|
||||||
pub const JITSI_COLIBRI: &str = "http://jitsi.org/protocol/colibri";
|
|
|
@ -1,91 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
use crate::presence::PresencePayload;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Unique identifier given to a MUC participant.
|
|
||||||
///
|
|
||||||
/// It allows clients to identify a MUC participant across reconnects and
|
|
||||||
/// renames. It thus prevents impersonification of anonymous users.
|
|
||||||
OccupantId, "occupant-id", OID,
|
|
||||||
|
|
||||||
attributes: [
|
|
||||||
/// The id associated to the sending user by the MUC service.
|
|
||||||
id: Required<String> = "id",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for OccupantId {}
|
|
||||||
impl PresencePayload for OccupantId {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(OccupantId, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(OccupantId, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let origin_id = OccupantId::try_from(elem).unwrap();
|
|
||||||
assert_eq!(origin_id.id, "coucou");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0'><coucou/></occupant-id>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = OccupantId::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in occupant-id element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_id() {
|
|
||||||
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = OccupantId::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'id' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let occupant_id = OccupantId {
|
|
||||||
id: String::from("coucou"),
|
|
||||||
};
|
|
||||||
let elem2 = occupant_id.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
// Copyright (c) 2019 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::date::DateTime;
|
|
||||||
use crate::pubsub::PubSubPayload;
|
|
||||||
use crate::util::helpers::Base64;
|
|
||||||
|
|
||||||
// TODO: Merge this container with the PubKey struct
|
|
||||||
generate_element!(
|
|
||||||
/// Data contained in the PubKey element
|
|
||||||
PubKeyData, "data", OX,
|
|
||||||
text: (
|
|
||||||
/// Base64 data
|
|
||||||
data: Base64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Pubkey element to be used in PubSub publish payloads.
|
|
||||||
PubKey, "pubkey", OX,
|
|
||||||
attributes: [
|
|
||||||
/// Last updated date
|
|
||||||
date: Option<DateTime> = "date"
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// Public key as base64 data
|
|
||||||
data: Required<PubKeyData> = ("data", OX) => PubKeyData
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PubSubPayload for PubKey {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Public key metadata
|
|
||||||
PubKeyMeta, "pubkey-metadata", OX,
|
|
||||||
attributes: [
|
|
||||||
/// OpenPGP v4 fingerprint
|
|
||||||
v4fingerprint: Required<String> = "v4-fingerprint",
|
|
||||||
/// Time the key was published or updated
|
|
||||||
date: Required<DateTime> = "date",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// List of public key metadata
|
|
||||||
PubKeysMeta, "public-key-list", OX,
|
|
||||||
children: [
|
|
||||||
/// Public keys
|
|
||||||
pubkeys: Vec<PubKeyMeta> = ("pubkey-metadata", OX) => PubKeyMeta
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl PubSubPayload for PubKeysMeta {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::pubsub::{
|
|
||||||
pubsub::{Item as PubSubItem, Publish},
|
|
||||||
Item, NodeName,
|
|
||||||
};
|
|
||||||
use crate::Element;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pubsub_publish_pubkey_data() {
|
|
||||||
let pubkey = PubKey {
|
|
||||||
date: None,
|
|
||||||
data: PubKeyData {
|
|
||||||
data: (&"Foo").as_bytes().to_vec(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
println!("Foo1: {:?}", pubkey);
|
|
||||||
|
|
||||||
let pubsub = Publish {
|
|
||||||
node: NodeName(format!("{}:{}", ns::OX_PUBKEYS, "some-fingerprint")),
|
|
||||||
items: vec![PubSubItem(Item::new(None, None, Some(pubkey)))],
|
|
||||||
};
|
|
||||||
println!("Foo2: {:?}", pubsub);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pubsub_publish_pubkey_meta() {
|
|
||||||
let pubkeymeta = PubKeysMeta {
|
|
||||||
pubkeys: vec![PubKeyMeta {
|
|
||||||
v4fingerprint: "some-fingerprint".to_owned(),
|
|
||||||
date: DateTime::from_str("2019-03-30T18:30:25Z").unwrap(),
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
println!("Foo1: {:?}", pubkeymeta);
|
|
||||||
|
|
||||||
let pubsub = Publish {
|
|
||||||
node: NodeName("foo".to_owned()),
|
|
||||||
items: vec![PubSubItem(Item::new(None, None, Some(pubkeymeta)))],
|
|
||||||
};
|
|
||||||
println!("Foo2: {:?}", pubsub);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_pubkey() {
|
|
||||||
let reference: Element = "<pubkey xmlns='urn:xmpp:openpgp:0'><data>AAAA</data></pubkey>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let pubkey = PubKey {
|
|
||||||
date: None,
|
|
||||||
data: PubKeyData {
|
|
||||||
data: b"\0\0\0".to_vec(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let serialized: Element = pubkey.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::iq::IqGetPayload;
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Represents a ping to the recipient, which must be answered with an
|
|
||||||
/// empty `<iq/>` or with an error.
|
|
||||||
Ping,
|
|
||||||
"ping",
|
|
||||||
PING
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqGetPayload for Ping {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Ping, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
|
|
||||||
Ping::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem1 = Element::from(Ping);
|
|
||||||
let elem2: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element = "<ping xmlns='urn:xmpp:ping'><coucou/></ping>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Ping::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in ping element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_attribute() {
|
|
||||||
let elem: Element = "<ping xmlns='urn:xmpp:ping' coucou=''/>".parse().unwrap();
|
|
||||||
let error = Ping::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in ping element.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,655 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use jid::Jid;
|
|
||||||
use minidom::{Element, IntoAttributeValue, Node};
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// Should be implemented on every known payload of a `<presence/>`.
|
|
||||||
pub trait PresencePayload: TryFrom<Element> + Into<Element> {}
|
|
||||||
|
|
||||||
/// Specifies the availability of an entity or resource.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum Show {
|
|
||||||
/// The entity or resource is temporarily away.
|
|
||||||
Away,
|
|
||||||
|
|
||||||
/// The entity or resource is actively interested in chatting.
|
|
||||||
Chat,
|
|
||||||
|
|
||||||
/// The entity or resource is busy (dnd = "Do Not Disturb").
|
|
||||||
Dnd,
|
|
||||||
|
|
||||||
/// The entity or resource is away for an extended period (xa = "eXtended
|
|
||||||
/// Away").
|
|
||||||
Xa,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Show {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Show, Error> {
|
|
||||||
Ok(match s {
|
|
||||||
"away" => Show::Away,
|
|
||||||
"chat" => Show::Chat,
|
|
||||||
"dnd" => Show::Dnd,
|
|
||||||
"xa" => Show::Xa,
|
|
||||||
|
|
||||||
_ => return Err(Error::ParseError("Invalid value for show.")),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Into<Node> for Show {
|
|
||||||
fn into(self) -> Node {
|
|
||||||
Element::builder("show", ns::DEFAULT_NS)
|
|
||||||
.append(match self {
|
|
||||||
Show::Away => "away",
|
|
||||||
Show::Chat => "chat",
|
|
||||||
Show::Dnd => "dnd",
|
|
||||||
Show::Xa => "xa",
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Lang = String;
|
|
||||||
type Status = String;
|
|
||||||
|
|
||||||
type Priority = i8;
|
|
||||||
|
|
||||||
///
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum Type {
|
|
||||||
/// This value is not an acceptable 'type' attribute, it is only used
|
|
||||||
/// internally to signal the absence of 'type'.
|
|
||||||
None,
|
|
||||||
|
|
||||||
/// An error has occurred regarding processing of a previously sent
|
|
||||||
/// presence stanza; if the presence stanza is of type "error", it MUST
|
|
||||||
/// include an <error/> child element (refer to [XMPP‑CORE]).
|
|
||||||
Error,
|
|
||||||
|
|
||||||
/// A request for an entity's current presence; SHOULD be generated only by
|
|
||||||
/// a server on behalf of a user.
|
|
||||||
Probe,
|
|
||||||
|
|
||||||
/// The sender wishes to subscribe to the recipient's presence.
|
|
||||||
Subscribe,
|
|
||||||
|
|
||||||
/// The sender has allowed the recipient to receive their presence.
|
|
||||||
Subscribed,
|
|
||||||
|
|
||||||
/// The sender is no longer available for communication.
|
|
||||||
Unavailable,
|
|
||||||
|
|
||||||
/// The sender is unsubscribing from the receiver's presence.
|
|
||||||
Unsubscribe,
|
|
||||||
|
|
||||||
/// The subscription request has been denied or a previously granted
|
|
||||||
/// subscription has been canceled.
|
|
||||||
Unsubscribed,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Type {
|
|
||||||
fn default() -> Type {
|
|
||||||
Type::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Type {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Type, Error> {
|
|
||||||
Ok(match s {
|
|
||||||
"error" => Type::Error,
|
|
||||||
"probe" => Type::Probe,
|
|
||||||
"subscribe" => Type::Subscribe,
|
|
||||||
"subscribed" => Type::Subscribed,
|
|
||||||
"unavailable" => Type::Unavailable,
|
|
||||||
"unsubscribe" => Type::Unsubscribe,
|
|
||||||
"unsubscribed" => Type::Unsubscribed,
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Invalid 'type' attribute on presence element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoAttributeValue for Type {
|
|
||||||
fn into_attribute_value(self) -> Option<String> {
|
|
||||||
Some(
|
|
||||||
match self {
|
|
||||||
Type::None => return None,
|
|
||||||
|
|
||||||
Type::Error => "error",
|
|
||||||
Type::Probe => "probe",
|
|
||||||
Type::Subscribe => "subscribe",
|
|
||||||
Type::Subscribed => "subscribed",
|
|
||||||
Type::Unavailable => "unavailable",
|
|
||||||
Type::Unsubscribe => "unsubscribe",
|
|
||||||
Type::Unsubscribed => "unsubscribed",
|
|
||||||
}
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main structure representing the `<presence/>` stanza.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Presence {
|
|
||||||
/// The sender of this presence.
|
|
||||||
pub from: Option<Jid>,
|
|
||||||
|
|
||||||
/// The recipient of this presence.
|
|
||||||
pub to: Option<Jid>,
|
|
||||||
|
|
||||||
/// The identifier, unique on this stream, of this stanza.
|
|
||||||
pub id: Option<String>,
|
|
||||||
|
|
||||||
/// The type of this presence stanza.
|
|
||||||
pub type_: Type,
|
|
||||||
|
|
||||||
/// The availability of the sender of this presence.
|
|
||||||
pub show: Option<Show>,
|
|
||||||
|
|
||||||
/// A localised list of statuses defined in this presence.
|
|
||||||
pub statuses: BTreeMap<Lang, Status>,
|
|
||||||
|
|
||||||
/// The sender’s resource priority, if negative it won’t receive messages
|
|
||||||
/// that haven’t been directed to it.
|
|
||||||
pub priority: Priority,
|
|
||||||
|
|
||||||
/// A list of payloads contained in this presence.
|
|
||||||
pub payloads: Vec<Element>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Presence {
|
|
||||||
/// Create a new presence of this type.
|
|
||||||
pub fn new(type_: Type) -> Presence {
|
|
||||||
Presence {
|
|
||||||
from: None,
|
|
||||||
to: None,
|
|
||||||
id: None,
|
|
||||||
type_,
|
|
||||||
show: None,
|
|
||||||
statuses: BTreeMap::new(),
|
|
||||||
priority: 0i8,
|
|
||||||
payloads: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the emitter of this presence, this should only be useful for
|
|
||||||
/// servers and components, as clients can only send presences from their
|
|
||||||
/// own resource (which is implicit).
|
|
||||||
pub fn with_from<J: Into<Jid>>(mut self, from: J) -> Presence {
|
|
||||||
self.from = Some(from.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the recipient of this presence, this is only useful for directed
|
|
||||||
/// presences.
|
|
||||||
pub fn with_to<J: Into<Jid>>(mut self, to: J) -> Presence {
|
|
||||||
self.to = Some(to.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the identifier for this presence.
|
|
||||||
pub fn with_id(mut self, id: String) -> Presence {
|
|
||||||
self.id = Some(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the availability information of this presence.
|
|
||||||
pub fn with_show(mut self, show: Show) -> Presence {
|
|
||||||
self.show = Some(show);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the priority of this presence.
|
|
||||||
pub fn with_priority(mut self, priority: i8) -> Presence {
|
|
||||||
self.priority = priority;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the payloads of this presence.
|
|
||||||
pub fn with_payloads(mut self, payloads: Vec<Element>) -> Presence {
|
|
||||||
self.payloads = payloads;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the availability information of this presence.
|
|
||||||
pub fn set_status<L, S>(&mut self, lang: L, status: S)
|
|
||||||
where
|
|
||||||
L: Into<Lang>,
|
|
||||||
S: Into<Status>,
|
|
||||||
{
|
|
||||||
self.statuses.insert(lang.into(), status.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a payload to this presence.
|
|
||||||
pub fn add_payload<P: PresencePayload>(&mut self, payload: P) {
|
|
||||||
self.payloads.push(payload.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Presence {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(root: Element) -> Result<Presence, Error> {
|
|
||||||
check_self!(root, "presence", DEFAULT_NS);
|
|
||||||
let mut show = None;
|
|
||||||
let mut priority = None;
|
|
||||||
let mut presence = Presence {
|
|
||||||
from: get_attr!(root, "from", Option),
|
|
||||||
to: get_attr!(root, "to", Option),
|
|
||||||
id: get_attr!(root, "id", Option),
|
|
||||||
type_: get_attr!(root, "type", Default),
|
|
||||||
show: None,
|
|
||||||
statuses: BTreeMap::new(),
|
|
||||||
priority: 0i8,
|
|
||||||
payloads: vec![],
|
|
||||||
};
|
|
||||||
for elem in root.children() {
|
|
||||||
if elem.is("show", ns::DEFAULT_NS) {
|
|
||||||
if show.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"More than one show element in a presence.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
check_no_attributes!(elem, "show");
|
|
||||||
check_no_children!(elem, "show");
|
|
||||||
show = Some(Show::from_str(elem.text().as_ref())?);
|
|
||||||
} else if elem.is("status", ns::DEFAULT_NS) {
|
|
||||||
check_no_unknown_attributes!(elem, "status", ["xml:lang"]);
|
|
||||||
check_no_children!(elem, "status");
|
|
||||||
let lang = get_attr!(elem, "xml:lang", Default);
|
|
||||||
if presence.statuses.insert(lang, elem.text()).is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Status element present twice for the same xml:lang.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if elem.is("priority", ns::DEFAULT_NS) {
|
|
||||||
if priority.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"More than one priority element in a presence.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
check_no_attributes!(elem, "priority");
|
|
||||||
check_no_children!(elem, "priority");
|
|
||||||
priority = Some(Priority::from_str(elem.text().as_ref())?);
|
|
||||||
} else {
|
|
||||||
presence.payloads.push(elem.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
presence.show = show;
|
|
||||||
if let Some(priority) = priority {
|
|
||||||
presence.priority = priority;
|
|
||||||
}
|
|
||||||
Ok(presence)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Presence> for Element {
|
|
||||||
fn from(presence: Presence) -> Element {
|
|
||||||
Element::builder("presence", ns::DEFAULT_NS)
|
|
||||||
.attr("from", presence.from)
|
|
||||||
.attr("to", presence.to)
|
|
||||||
.attr("id", presence.id)
|
|
||||||
.attr("type", presence.type_)
|
|
||||||
.append_all(presence.show.into_iter())
|
|
||||||
.append_all(presence.statuses.into_iter().map(|(lang, status)| {
|
|
||||||
Element::builder("status", ns::DEFAULT_NS)
|
|
||||||
.attr(
|
|
||||||
"xml:lang",
|
|
||||||
match lang.as_ref() {
|
|
||||||
"" => None,
|
|
||||||
lang => Some(lang),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.append(status)
|
|
||||||
}))
|
|
||||||
.append_all(if presence.priority == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
Element::builder("priority", ns::DEFAULT_NS)
|
|
||||||
.append(format!("{}", presence.priority)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.append_all(presence.payloads.into_iter())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use jid::{BareJid, FullJid};
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Show, 1);
|
|
||||||
assert_size!(Type, 1);
|
|
||||||
assert_size!(Presence, 120);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Show, 1);
|
|
||||||
assert_size!(Type, 1);
|
|
||||||
assert_size!(Presence, 240);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'/>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:component:accept'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let presence = Presence::try_from(elem).unwrap();
|
|
||||||
assert_eq!(presence.from, None);
|
|
||||||
assert_eq!(presence.to, None);
|
|
||||||
assert_eq!(presence.id, None);
|
|
||||||
assert_eq!(presence.type_, Type::None);
|
|
||||||
assert!(presence.payloads.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client' type='unavailable'/>/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:component:accept' type='unavailable'/>/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let presence = Presence::new(Type::Unavailable);
|
|
||||||
let elem2 = presence.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_show() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><show>chat</show></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element =
|
|
||||||
"<presence xmlns='jabber:component:accept'><show>chat</show></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let presence = Presence::try_from(elem).unwrap();
|
|
||||||
assert_eq!(presence.payloads.len(), 0);
|
|
||||||
assert_eq!(presence.show, Some(Show::Chat));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_empty_show_value() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'/>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:component:accept'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let presence = Presence::try_from(elem).unwrap();
|
|
||||||
assert_eq!(presence.show, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_missing_show_value() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><show/></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:component:accept'><show/></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Presence::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Invalid value for show.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_show() {
|
|
||||||
// "online" used to be a pretty common mistake.
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><show>online</show></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element =
|
|
||||||
"<presence xmlns='jabber:component:accept'><show>online</show></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Presence::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Invalid value for show.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_empty_status() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><status/></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:component:accept'><status/></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let presence = Presence::try_from(elem).unwrap();
|
|
||||||
assert_eq!(presence.payloads.len(), 0);
|
|
||||||
assert_eq!(presence.statuses.len(), 1);
|
|
||||||
assert_eq!(presence.statuses[""], "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_status() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element =
|
|
||||||
"<presence xmlns='jabber:component:accept'><status>Here!</status></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let presence = Presence::try_from(elem).unwrap();
|
|
||||||
assert_eq!(presence.payloads.len(), 0);
|
|
||||||
assert_eq!(presence.statuses.len(), 1);
|
|
||||||
assert_eq!(presence.statuses[""], "Here!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiple_statuses() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:component:accept'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
|
||||||
let presence = Presence::try_from(elem).unwrap();
|
|
||||||
assert_eq!(presence.payloads.len(), 0);
|
|
||||||
assert_eq!(presence.statuses.len(), 2);
|
|
||||||
assert_eq!(presence.statuses[""], "Here!");
|
|
||||||
assert_eq!(presence.statuses["fr"], "Là!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_multiple_statuses() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:component:accept'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
|
||||||
let error = Presence::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
message,
|
|
||||||
"Status element present twice for the same xml:lang."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_priority() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><priority>-1</priority></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element =
|
|
||||||
"<presence xmlns='jabber:component:accept'><priority>-1</priority></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let presence = Presence::try_from(elem).unwrap();
|
|
||||||
assert_eq!(presence.payloads.len(), 0);
|
|
||||||
assert_eq!(presence.priority, -1i8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_priority() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><priority>128</priority></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element =
|
|
||||||
"<presence xmlns='jabber:component:accept'><priority>128</priority></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Presence::try_from(elem).unwrap_err();
|
|
||||||
match error {
|
|
||||||
Error::ParseIntError(_) => (),
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unknown_child() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><test xmlns='invalid'/></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element =
|
|
||||||
"<presence xmlns='jabber:component:accept'><test xmlns='invalid'/></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let presence = Presence::try_from(elem).unwrap();
|
|
||||||
let payload = &presence.payloads[0];
|
|
||||||
assert!(payload.is("test", "invalid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_status_child() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><status><coucou/></status></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element =
|
|
||||||
"<presence xmlns='jabber:component:accept'><status><coucou/></status></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Presence::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in status element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_attribute() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<presence xmlns='jabber:client'><status coucou=''/></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element =
|
|
||||||
"<presence xmlns='jabber:component:accept'><status coucou=''/></presence>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Presence::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in status element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise_status() {
|
|
||||||
let status = Status::from("Hello world!");
|
|
||||||
let mut presence = Presence::new(Type::Unavailable);
|
|
||||||
presence.statuses.insert(String::from(""), status);
|
|
||||||
let elem: Element = presence.into();
|
|
||||||
assert!(elem.is("presence", ns::DEFAULT_NS));
|
|
||||||
assert!(elem.children().next().unwrap().is("status", ns::DEFAULT_NS));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise_priority() {
|
|
||||||
let presence = Presence::new(Type::None).with_priority(42);
|
|
||||||
let elem: Element = presence.into();
|
|
||||||
assert!(elem.is("presence", ns::DEFAULT_NS));
|
|
||||||
let priority = elem.children().next().unwrap();
|
|
||||||
assert!(priority.is("priority", ns::DEFAULT_NS));
|
|
||||||
assert_eq!(priority.text(), "42");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn presence_with_to() {
|
|
||||||
let presence = Presence::new(Type::None);
|
|
||||||
let elem: Element = presence.into();
|
|
||||||
assert_eq!(elem.attr("to"), None);
|
|
||||||
|
|
||||||
let presence = Presence::new(Type::None).with_to(Jid::Bare(BareJid::domain("localhost")));
|
|
||||||
let elem: Element = presence.into();
|
|
||||||
assert_eq!(elem.attr("to"), Some("localhost"));
|
|
||||||
|
|
||||||
let presence = Presence::new(Type::None).with_to(BareJid::domain("localhost"));
|
|
||||||
let elem: Element = presence.into();
|
|
||||||
assert_eq!(elem.attr("to"), Some("localhost"));
|
|
||||||
|
|
||||||
let presence = Presence::new(Type::None).with_to(Jid::Full(FullJid::new(
|
|
||||||
"test",
|
|
||||||
"localhost",
|
|
||||||
"coucou",
|
|
||||||
)));
|
|
||||||
let elem: Element = presence.into();
|
|
||||||
assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
|
|
||||||
|
|
||||||
let presence =
|
|
||||||
Presence::new(Type::None).with_to(FullJid::new("test", "localhost", "coucou"));
|
|
||||||
let elem: Element = presence.into();
|
|
||||||
assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,416 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::data_forms::DataForm;
|
|
||||||
use crate::date::DateTime;
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::pubsub::{Item as PubSubItem, ItemId, NodeName, Subscription, SubscriptionId};
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Event wrapper for a PubSub `<item/>`.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Item(pub PubSubItem);
|
|
||||||
|
|
||||||
impl_pubsub_item!(Item, PUBSUB_EVENT);
|
|
||||||
|
|
||||||
/// Represents an event happening to a PubSub node.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum PubSubEvent {
|
|
||||||
/*
|
|
||||||
Collection {
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
/// This node’s configuration changed.
|
|
||||||
Configuration {
|
|
||||||
/// The node affected.
|
|
||||||
node: NodeName,
|
|
||||||
|
|
||||||
/// The new configuration of this node.
|
|
||||||
form: Option<DataForm>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// This node has been deleted, with an optional redirect to another node.
|
|
||||||
Delete {
|
|
||||||
/// The node affected.
|
|
||||||
node: NodeName,
|
|
||||||
|
|
||||||
/// The xmpp: URI of another node replacing this one.
|
|
||||||
redirect: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Some items have been published on this node.
|
|
||||||
PublishedItems {
|
|
||||||
/// The node affected.
|
|
||||||
node: NodeName,
|
|
||||||
|
|
||||||
/// The list of published items.
|
|
||||||
items: Vec<Item>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Some items have been removed from this node.
|
|
||||||
RetractedItems {
|
|
||||||
/// The node affected.
|
|
||||||
node: NodeName,
|
|
||||||
|
|
||||||
/// The list of retracted items.
|
|
||||||
items: Vec<ItemId>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// All items of this node just got removed at once.
|
|
||||||
Purge {
|
|
||||||
/// The node affected.
|
|
||||||
node: NodeName,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The user’s subscription to this node has changed.
|
|
||||||
Subscription {
|
|
||||||
/// The node affected.
|
|
||||||
node: NodeName,
|
|
||||||
|
|
||||||
/// The time at which this subscription will expire.
|
|
||||||
expiry: Option<DateTime>,
|
|
||||||
|
|
||||||
/// The JID of the user affected.
|
|
||||||
jid: Option<Jid>,
|
|
||||||
|
|
||||||
/// An identifier for this subscription.
|
|
||||||
subid: Option<SubscriptionId>,
|
|
||||||
|
|
||||||
/// The state of this subscription.
|
|
||||||
subscription: Option<Subscription>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_items(elem: Element, node: NodeName) -> Result<PubSubEvent, Error> {
|
|
||||||
let mut is_retract = None;
|
|
||||||
let mut items = vec![];
|
|
||||||
let mut retracts = vec![];
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("item", ns::PUBSUB_EVENT) {
|
|
||||||
match is_retract {
|
|
||||||
None => is_retract = Some(false),
|
|
||||||
Some(false) => (),
|
|
||||||
Some(true) => {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Mix of item and retract in items element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items.push(Item::try_from(child.clone())?);
|
|
||||||
} else if child.is("retract", ns::PUBSUB_EVENT) {
|
|
||||||
match is_retract {
|
|
||||||
None => is_retract = Some(true),
|
|
||||||
Some(true) => (),
|
|
||||||
Some(false) => {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Mix of item and retract in items element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check_no_children!(child, "retract");
|
|
||||||
check_no_unknown_attributes!(child, "retract", ["id"]);
|
|
||||||
let id = get_attr!(child, "id", Required);
|
|
||||||
retracts.push(id);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Invalid child in items element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(match is_retract {
|
|
||||||
Some(false) => PubSubEvent::PublishedItems { node, items },
|
|
||||||
Some(true) => PubSubEvent::RetractedItems {
|
|
||||||
node,
|
|
||||||
items: retracts,
|
|
||||||
},
|
|
||||||
None => return Err(Error::ParseError("Missing children in items element.")),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for PubSubEvent {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<PubSubEvent, Error> {
|
|
||||||
check_self!(elem, "event", PUBSUB_EVENT);
|
|
||||||
check_no_attributes!(elem, "event");
|
|
||||||
|
|
||||||
let mut payload = None;
|
|
||||||
for child in elem.children() {
|
|
||||||
let node = get_attr!(child, "node", Required);
|
|
||||||
if child.is("configuration", ns::PUBSUB_EVENT) {
|
|
||||||
let mut payloads = child.children().cloned().collect::<Vec<_>>();
|
|
||||||
let item = payloads.pop();
|
|
||||||
if !payloads.is_empty() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"More than a single payload in configuration element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let form = match item {
|
|
||||||
None => None,
|
|
||||||
Some(payload) => Some(DataForm::try_from(payload)?),
|
|
||||||
};
|
|
||||||
payload = Some(PubSubEvent::Configuration { node, form });
|
|
||||||
} else if child.is("delete", ns::PUBSUB_EVENT) {
|
|
||||||
let mut redirect = None;
|
|
||||||
for item in child.children() {
|
|
||||||
if item.is("redirect", ns::PUBSUB_EVENT) {
|
|
||||||
if redirect.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"More than one redirect in delete element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let uri = get_attr!(item, "uri", Required);
|
|
||||||
redirect = Some(uri);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in delete element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
payload = Some(PubSubEvent::Delete { node, redirect });
|
|
||||||
} else if child.is("items", ns::PUBSUB_EVENT) {
|
|
||||||
payload = Some(parse_items(child.clone(), node)?);
|
|
||||||
} else if child.is("purge", ns::PUBSUB_EVENT) {
|
|
||||||
check_no_children!(child, "purge");
|
|
||||||
payload = Some(PubSubEvent::Purge { node });
|
|
||||||
} else if child.is("subscription", ns::PUBSUB_EVENT) {
|
|
||||||
check_no_children!(child, "subscription");
|
|
||||||
payload = Some(PubSubEvent::Subscription {
|
|
||||||
node,
|
|
||||||
expiry: get_attr!(child, "expiry", Option),
|
|
||||||
jid: get_attr!(child, "jid", Option),
|
|
||||||
subid: get_attr!(child, "subid", Option),
|
|
||||||
subscription: get_attr!(child, "subscription", Option),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in event element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(payload.ok_or(Error::ParseError("No payload in event element."))?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PubSubEvent> for Element {
|
|
||||||
fn from(event: PubSubEvent) -> Element {
|
|
||||||
let payload = match event {
|
|
||||||
PubSubEvent::Configuration { node, form } => {
|
|
||||||
Element::builder("configuration", ns::PUBSUB_EVENT)
|
|
||||||
.attr("node", node)
|
|
||||||
.append_all(form.map(Element::from))
|
|
||||||
}
|
|
||||||
PubSubEvent::Delete { node, redirect } => Element::builder("purge", ns::PUBSUB_EVENT)
|
|
||||||
.attr("node", node)
|
|
||||||
.append_all(redirect.map(|redirect| {
|
|
||||||
Element::builder("redirect", ns::PUBSUB_EVENT).attr("uri", redirect)
|
|
||||||
})),
|
|
||||||
PubSubEvent::PublishedItems { node, items } => {
|
|
||||||
Element::builder("items", ns::PUBSUB_EVENT)
|
|
||||||
.attr("node", node)
|
|
||||||
.append_all(items.into_iter())
|
|
||||||
}
|
|
||||||
PubSubEvent::RetractedItems { node, items } => {
|
|
||||||
Element::builder("items", ns::PUBSUB_EVENT)
|
|
||||||
.attr("node", node)
|
|
||||||
.append_all(
|
|
||||||
items
|
|
||||||
.into_iter()
|
|
||||||
.map(|id| Element::builder("retract", ns::PUBSUB_EVENT).attr("id", id)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
PubSubEvent::Purge { node } => {
|
|
||||||
Element::builder("purge", ns::PUBSUB_EVENT).attr("node", node)
|
|
||||||
}
|
|
||||||
PubSubEvent::Subscription {
|
|
||||||
node,
|
|
||||||
expiry,
|
|
||||||
jid,
|
|
||||||
subid,
|
|
||||||
subscription,
|
|
||||||
} => Element::builder("subscription", ns::PUBSUB_EVENT)
|
|
||||||
.attr("node", node)
|
|
||||||
.attr("expiry", expiry)
|
|
||||||
.attr("jid", jid)
|
|
||||||
.attr("subid", subid)
|
|
||||||
.attr("subscription", subscription),
|
|
||||||
};
|
|
||||||
Element::builder("event", ns::PUBSUB_EVENT)
|
|
||||||
.append(payload)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessagePayload for PubSubEvent {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use jid::BareJid;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_items() {
|
|
||||||
let elem: Element =
|
|
||||||
"<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'/></event>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = PubSubEvent::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Missing children in items element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_items() {
|
|
||||||
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'><item id='test' publisher='test@coucou'/></items></event>".parse().unwrap();
|
|
||||||
let event = PubSubEvent::try_from(elem).unwrap();
|
|
||||||
match event {
|
|
||||||
PubSubEvent::PublishedItems { node, items } => {
|
|
||||||
assert_eq!(node, NodeName(String::from("coucou")));
|
|
||||||
assert_eq!(items[0].id, Some(ItemId(String::from("test"))));
|
|
||||||
assert_eq!(
|
|
||||||
items[0].publisher.clone().unwrap(),
|
|
||||||
BareJid::new("test", "coucou")
|
|
||||||
);
|
|
||||||
assert_eq!(items[0].payload, None);
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_pep() {
|
|
||||||
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><item><foreign xmlns='example:namespace'/></item></items></event>".parse().unwrap();
|
|
||||||
let event = PubSubEvent::try_from(elem).unwrap();
|
|
||||||
match event {
|
|
||||||
PubSubEvent::PublishedItems { node, items } => {
|
|
||||||
assert_eq!(node, NodeName(String::from("something")));
|
|
||||||
assert_eq!(items[0].id, None);
|
|
||||||
assert_eq!(items[0].publisher, None);
|
|
||||||
match items[0].payload {
|
|
||||||
Some(ref elem) => assert!(elem.is("foreign", "example:namespace")),
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_retract() {
|
|
||||||
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><retract id='coucou'/><retract id='test'/></items></event>".parse().unwrap();
|
|
||||||
let event = PubSubEvent::try_from(elem).unwrap();
|
|
||||||
match event {
|
|
||||||
PubSubEvent::RetractedItems { node, items } => {
|
|
||||||
assert_eq!(node, NodeName(String::from("something")));
|
|
||||||
assert_eq!(items[0], ItemId(String::from("coucou")));
|
|
||||||
assert_eq!(items[1], ItemId(String::from("test")));
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_delete() {
|
|
||||||
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><delete node='coucou'><redirect uri='hello'/></delete></event>".parse().unwrap();
|
|
||||||
let event = PubSubEvent::try_from(elem).unwrap();
|
|
||||||
match event {
|
|
||||||
PubSubEvent::Delete { node, redirect } => {
|
|
||||||
assert_eq!(node, NodeName(String::from("coucou")));
|
|
||||||
assert_eq!(redirect, Some(String::from("hello")));
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_purge() {
|
|
||||||
let elem: Element =
|
|
||||||
"<event xmlns='http://jabber.org/protocol/pubsub#event'><purge node='coucou'/></event>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let event = PubSubEvent::try_from(elem).unwrap();
|
|
||||||
match event {
|
|
||||||
PubSubEvent::Purge { node } => {
|
|
||||||
assert_eq!(node, NodeName(String::from("coucou")));
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple_configure() {
|
|
||||||
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();
|
|
||||||
let event = PubSubEvent::try_from(elem).unwrap();
|
|
||||||
match event {
|
|
||||||
PubSubEvent::Configuration { node, form: _ } => {
|
|
||||||
assert_eq!(node, NodeName(String::from("coucou")));
|
|
||||||
//assert_eq!(form.type_, Result_);
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element =
|
|
||||||
"<event xmlns='http://jabber.org/protocol/pubsub#event'><coucou node='test'/></event>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = PubSubEvent::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in event element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_attribute() {
|
|
||||||
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = PubSubEvent::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in event element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ex221_subscription() {
|
|
||||||
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>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let event = PubSubEvent::try_from(elem.clone()).unwrap();
|
|
||||||
match event.clone() {
|
|
||||||
PubSubEvent::Subscription {
|
|
||||||
node,
|
|
||||||
expiry,
|
|
||||||
jid,
|
|
||||||
subid,
|
|
||||||
subscription,
|
|
||||||
} => {
|
|
||||||
assert_eq!(node, NodeName(String::from("princely_musings")));
|
|
||||||
assert_eq!(
|
|
||||||
subid,
|
|
||||||
Some(SubscriptionId(String::from(
|
|
||||||
"ba49252aaa4f5d320c24d3766f0bdcade78c78d3"
|
|
||||||
)))
|
|
||||||
);
|
|
||||||
assert_eq!(subscription, Some(Subscription::Subscribed));
|
|
||||||
assert_eq!(jid.unwrap(), BareJid::new("francisco", "denmark.lit"));
|
|
||||||
assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap()));
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem2: Element = event.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
/// The `http://jabber.org/protocol/pubsub#event` protocol.
|
|
||||||
pub mod event;
|
|
||||||
|
|
||||||
/// The `http://jabber.org/protocol/pubsub#owner` protocol.
|
|
||||||
pub mod owner;
|
|
||||||
|
|
||||||
/// The `http://jabber.org/protocol/pubsub` protocol.
|
|
||||||
pub mod pubsub;
|
|
||||||
|
|
||||||
pub use self::event::PubSubEvent;
|
|
||||||
pub use self::owner::PubSubOwner;
|
|
||||||
pub use self::pubsub::PubSub;
|
|
||||||
|
|
||||||
use crate::{Element, Jid};
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// The name of a PubSub node, used to identify it on a JID.
|
|
||||||
NodeName
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// The identifier of an item, which is unique per node.
|
|
||||||
ItemId
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// The identifier of a subscription to a PubSub node.
|
|
||||||
SubscriptionId
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The state of a subscription to a node.
|
|
||||||
Subscription, "subscription", {
|
|
||||||
/// The user is not subscribed to this node.
|
|
||||||
None => "none",
|
|
||||||
|
|
||||||
/// The user’s subscription to this node is still pending.
|
|
||||||
Pending => "pending",
|
|
||||||
|
|
||||||
/// The user is subscribed to this node.
|
|
||||||
Subscribed => "subscribed",
|
|
||||||
|
|
||||||
/// The user’s subscription to this node will only be valid once
|
|
||||||
/// configured.
|
|
||||||
Unconfigured => "unconfigured",
|
|
||||||
}, Default = None
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// A list of possible affiliations to a node.
|
|
||||||
AffiliationAttribute, "affiliation", {
|
|
||||||
/// You are a member of this node, you can subscribe and retrieve items.
|
|
||||||
Member => "member",
|
|
||||||
|
|
||||||
/// You don’t have a specific affiliation with this node, you can only subscribe to it.
|
|
||||||
None => "none",
|
|
||||||
|
|
||||||
/// You are banned from this node.
|
|
||||||
Outcast => "outcast",
|
|
||||||
|
|
||||||
/// You are an owner of this node, and can do anything with it.
|
|
||||||
Owner => "owner",
|
|
||||||
|
|
||||||
/// You are a publisher on this node, you can publish and retract items to it.
|
|
||||||
Publisher => "publisher",
|
|
||||||
|
|
||||||
/// You can publish and retract items on this node, but not subscribe or retrieve items.
|
|
||||||
PublishOnly => "publish-only",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/// An item from a PubSub node.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Item {
|
|
||||||
/// The identifier for this item, unique per node.
|
|
||||||
pub id: Option<ItemId>,
|
|
||||||
|
|
||||||
/// The JID of the entity who published this item.
|
|
||||||
pub publisher: Option<Jid>,
|
|
||||||
|
|
||||||
/// The payload of this item, in an arbitrary namespace.
|
|
||||||
pub payload: Option<Element>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Item {
|
|
||||||
/// Create a new item, accepting only payloads implementing `PubSubPayload`.
|
|
||||||
pub fn new<P: PubSubPayload>(
|
|
||||||
id: Option<ItemId>,
|
|
||||||
publisher: Option<Jid>,
|
|
||||||
payload: Option<P>,
|
|
||||||
) -> Item {
|
|
||||||
Item {
|
|
||||||
id,
|
|
||||||
publisher,
|
|
||||||
payload: payload.map(Into::into),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This trait should be implemented on any element which can be included as a PubSub payload.
|
|
||||||
pub trait PubSubPayload: ::std::convert::TryFrom<crate::Element> + Into<crate::Element> {}
|
|
|
@ -1,364 +0,0 @@
|
||||||
// Copyright (c) 2020 Paul Fariello <paul@fariello.eu>
|
|
||||||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::data_forms::DataForm;
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::pubsub::{AffiliationAttribute, NodeName, Subscription};
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A list of affiliations you have on a service, or on a node.
|
|
||||||
Affiliations, "affiliations", PUBSUB_OWNER,
|
|
||||||
attributes: [
|
|
||||||
/// The node name this request pertains to.
|
|
||||||
node: Required<NodeName> = "node",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The actual list of affiliation elements.
|
|
||||||
affiliations: Vec<Affiliation> = ("affiliation", PUBSUB_OWNER) => Affiliation
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// An affiliation element.
|
|
||||||
Affiliation, "affiliation", PUBSUB_OWNER,
|
|
||||||
attributes: [
|
|
||||||
/// The node this affiliation pertains to.
|
|
||||||
jid: Required<Jid> = "jid",
|
|
||||||
|
|
||||||
/// The affiliation you currently have on this node.
|
|
||||||
affiliation: Required<AffiliationAttribute> = "affiliation",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request to configure a node.
|
|
||||||
Configure, "configure", PUBSUB_OWNER,
|
|
||||||
attributes: [
|
|
||||||
/// The node to be configured.
|
|
||||||
node: Option<NodeName> = "node",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The form to configure it.
|
|
||||||
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request to change default configuration.
|
|
||||||
Default, "default", PUBSUB_OWNER,
|
|
||||||
children: [
|
|
||||||
/// The form to configure it.
|
|
||||||
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request to delete a node.
|
|
||||||
Delete, "delete", PUBSUB_OWNER,
|
|
||||||
attributes: [
|
|
||||||
/// The node to be configured.
|
|
||||||
node: Required<NodeName> = "node",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// Redirection to replace the deleted node.
|
|
||||||
redirect: Option<Redirect> = ("redirect", PUBSUB_OWNER) => Redirect
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A redirect element.
|
|
||||||
Redirect, "redirect", PUBSUB_OWNER,
|
|
||||||
attributes: [
|
|
||||||
/// The node this node will be redirected to.
|
|
||||||
uri: Required<String> = "uri",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request to delete a node.
|
|
||||||
Purge, "purge", PUBSUB_OWNER,
|
|
||||||
attributes: [
|
|
||||||
/// The node to be configured.
|
|
||||||
node: Required<NodeName> = "node",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A request for current subscriptions.
|
|
||||||
Subscriptions, "subscriptions", PUBSUB_OWNER,
|
|
||||||
attributes: [
|
|
||||||
/// The node to query.
|
|
||||||
node: Required<NodeName> = "node",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The list of subscription elements returned.
|
|
||||||
subscriptions: Vec<SubscriptionElem> = ("subscription", PUBSUB_OWNER) => SubscriptionElem
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A subscription element, describing the state of a subscription.
|
|
||||||
SubscriptionElem, "subscription", PUBSUB_OWNER,
|
|
||||||
attributes: [
|
|
||||||
/// The JID affected by this subscription.
|
|
||||||
jid: Required<Jid> = "jid",
|
|
||||||
|
|
||||||
/// The state of the subscription.
|
|
||||||
subscription: Required<Subscription> = "subscription",
|
|
||||||
|
|
||||||
/// Subscription unique id.
|
|
||||||
subid: Option<String> = "subid",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Main payload used to communicate with a PubSubOwner service.
|
|
||||||
///
|
|
||||||
/// `<pubsub xmlns="http://jabber.org/protocol/pubsub#owner"/>`
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum PubSubOwner {
|
|
||||||
/// Manage the affiliations of a node.
|
|
||||||
Affiliations(Affiliations),
|
|
||||||
/// Request to configure a node, with optional suggested name and suggested configuration.
|
|
||||||
Configure(Configure),
|
|
||||||
/// Request the default node configuration.
|
|
||||||
Default(Default),
|
|
||||||
/// Delete a node.
|
|
||||||
Delete(Delete),
|
|
||||||
/// Purge all items from node.
|
|
||||||
Purge(Purge),
|
|
||||||
/// Request subscriptions of a node.
|
|
||||||
Subscriptions(Subscriptions),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IqGetPayload for PubSubOwner {}
|
|
||||||
impl IqSetPayload for PubSubOwner {}
|
|
||||||
impl IqResultPayload for PubSubOwner {}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for PubSubOwner {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<PubSubOwner, Error> {
|
|
||||||
check_self!(elem, "pubsub", PUBSUB_OWNER);
|
|
||||||
check_no_attributes!(elem, "pubsub");
|
|
||||||
|
|
||||||
let mut payload = None;
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("configure", ns::PUBSUB_OWNER) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub owner element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let configure = Configure::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSubOwner::Configure(configure));
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in pubsub element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PubSubOwner> for Element {
|
|
||||||
fn from(pubsub: PubSubOwner) -> Element {
|
|
||||||
Element::builder("pubsub", ns::PUBSUB_OWNER)
|
|
||||||
.append_all(match pubsub {
|
|
||||||
PubSubOwner::Affiliations(affiliations) => vec![Element::from(affiliations)],
|
|
||||||
PubSubOwner::Configure(configure) => vec![Element::from(configure)],
|
|
||||||
PubSubOwner::Default(default) => vec![Element::from(default)],
|
|
||||||
PubSubOwner::Delete(delete) => vec![Element::from(delete)],
|
|
||||||
PubSubOwner::Purge(purge) => vec![Element::from(purge)],
|
|
||||||
PubSubOwner::Subscriptions(subscriptions) => vec![Element::from(subscriptions)],
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
|
|
||||||
use jid::BareJid;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn affiliations() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><affiliations node='foo'><affiliation jid='hamlet@denmark.lit' affiliation='owner'/><affiliation jid='polonius@denmark.lit' affiliation='outcast'/></affiliations></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
|
|
||||||
let pubsub = PubSubOwner::Affiliations(Affiliations {
|
|
||||||
node: NodeName(String::from("foo")),
|
|
||||||
affiliations: vec![
|
|
||||||
Affiliation {
|
|
||||||
jid: Jid::Bare(BareJid::from_str("hamlet@denmark.lit").unwrap()),
|
|
||||||
affiliation: AffiliationAttribute::Owner,
|
|
||||||
},
|
|
||||||
Affiliation {
|
|
||||||
jid: Jid::Bare(BareJid::from_str("polonius@denmark.lit").unwrap()),
|
|
||||||
affiliation: AffiliationAttribute::Outcast,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn configure() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><configure node='foo'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model' type='list-single'><value>whitelist</value></field></x></configure></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
|
|
||||||
let pubsub = PubSubOwner::Configure(Configure {
|
|
||||||
node: Some(NodeName(String::from("foo"))),
|
|
||||||
form: Some(DataForm {
|
|
||||||
type_: DataFormType::Submit,
|
|
||||||
form_type: Some(String::from(ns::PUBSUB_CONFIGURE)),
|
|
||||||
title: None,
|
|
||||||
instructions: None,
|
|
||||||
fields: vec![Field {
|
|
||||||
var: String::from("pubsub#access_model"),
|
|
||||||
type_: FieldType::ListSingle,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![String::from("whitelist")],
|
|
||||||
media: vec![],
|
|
||||||
}],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_configure() {
|
|
||||||
let reference: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><configure node='foo'><x xmlns='jabber:x:data' type='submit'/></configure></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<x xmlns='jabber:x:data' type='submit'/>".parse().unwrap();
|
|
||||||
|
|
||||||
let form = DataForm::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let configure = PubSubOwner::Configure(Configure {
|
|
||||||
node: Some(NodeName(String::from("foo"))),
|
|
||||||
form: Some(form),
|
|
||||||
});
|
|
||||||
let serialized: Element = configure.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn default() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><default><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model' type='list-single'><value>whitelist</value></field></x></default></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
|
|
||||||
let pubsub = PubSubOwner::Default(Default {
|
|
||||||
form: Some(DataForm {
|
|
||||||
type_: DataFormType::Submit,
|
|
||||||
form_type: Some(String::from(ns::PUBSUB_CONFIGURE)),
|
|
||||||
title: None,
|
|
||||||
instructions: None,
|
|
||||||
fields: vec![Field {
|
|
||||||
var: String::from("pubsub#access_model"),
|
|
||||||
type_: FieldType::ListSingle,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![String::from("whitelist")],
|
|
||||||
media: vec![],
|
|
||||||
}],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn delete() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><delete node='foo'><redirect uri='xmpp:hamlet@denmark.lit?;node=blog'/></delete></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
|
|
||||||
let pubsub = PubSubOwner::Delete(Delete {
|
|
||||||
node: NodeName(String::from("foo")),
|
|
||||||
redirect: Some(Redirect {
|
|
||||||
uri: String::from("xmpp:hamlet@denmark.lit?;node=blog"),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn purge() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><purge node='foo'></purge></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
|
|
||||||
let pubsub = PubSubOwner::Purge(Purge {
|
|
||||||
node: NodeName(String::from("foo")),
|
|
||||||
});
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn subscriptions() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><subscriptions node='foo'><subscription jid='hamlet@denmark.lit' subscription='subscribed'/><subscription jid='polonius@denmark.lit' subscription='unconfigured'/><subscription jid='bernardo@denmark.lit' subscription='subscribed' subid='123-abc'/><subscription jid='bernardo@denmark.lit' subscription='subscribed' subid='004-yyy'/></subscriptions></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
|
|
||||||
let pubsub = PubSubOwner::Subscriptions(Subscriptions {
|
|
||||||
node: NodeName(String::from("foo")),
|
|
||||||
subscriptions: vec![
|
|
||||||
SubscriptionElem {
|
|
||||||
jid: Jid::Bare(BareJid::from_str("hamlet@denmark.lit").unwrap()),
|
|
||||||
subscription: Subscription::Subscribed,
|
|
||||||
subid: None,
|
|
||||||
},
|
|
||||||
SubscriptionElem {
|
|
||||||
jid: Jid::Bare(BareJid::from_str("polonius@denmark.lit").unwrap()),
|
|
||||||
subscription: Subscription::Unconfigured,
|
|
||||||
subid: None,
|
|
||||||
},
|
|
||||||
SubscriptionElem {
|
|
||||||
jid: Jid::Bare(BareJid::from_str("bernardo@denmark.lit").unwrap()),
|
|
||||||
subscription: Subscription::Subscribed,
|
|
||||||
subid: Some(String::from("123-abc")),
|
|
||||||
},
|
|
||||||
SubscriptionElem {
|
|
||||||
jid: Jid::Bare(BareJid::from_str("bernardo@denmark.lit").unwrap()),
|
|
||||||
subscription: Subscription::Subscribed,
|
|
||||||
subid: Some(String::from("004-yyy")),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,772 +0,0 @@
|
||||||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::data_forms::DataForm;
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::pubsub::{
|
|
||||||
AffiliationAttribute, Item as PubSubItem, NodeName, Subscription, SubscriptionId,
|
|
||||||
};
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
// TODO: a better solution would be to split this into a query and a result elements, like for
|
|
||||||
// XEP-0030.
|
|
||||||
generate_element!(
|
|
||||||
/// A list of affiliations you have on a service, or on a node.
|
|
||||||
Affiliations, "affiliations", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The optional node name this request pertains to.
|
|
||||||
node: Option<NodeName> = "node",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The actual list of affiliation elements.
|
|
||||||
affiliations: Vec<Affiliation> = ("affiliation", PUBSUB) => Affiliation
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// An affiliation element.
|
|
||||||
Affiliation, "affiliation", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The node this affiliation pertains to.
|
|
||||||
node: Required<NodeName> = "node",
|
|
||||||
|
|
||||||
/// The affiliation you currently have on this node.
|
|
||||||
affiliation: Required<AffiliationAttribute> = "affiliation",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request to configure a new node.
|
|
||||||
Configure, "configure", PUBSUB,
|
|
||||||
children: [
|
|
||||||
/// The form to configure it.
|
|
||||||
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request to create a new node.
|
|
||||||
Create, "create", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The node name to create, if `None` the service will generate one.
|
|
||||||
node: Option<NodeName> = "node",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request for a default node configuration.
|
|
||||||
Default, "default", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The node targeted by this request, otherwise the entire service.
|
|
||||||
node: Option<NodeName> = "node",
|
|
||||||
|
|
||||||
// TODO: do we really want to support collection nodes?
|
|
||||||
// type: Option<String> = "type",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A request for a list of items.
|
|
||||||
Items, "items", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
// TODO: should be an xs:positiveInteger, that is, an unbounded int ≥ 1.
|
|
||||||
/// Maximum number of items returned.
|
|
||||||
max_items: Option<u32> = "max_items",
|
|
||||||
|
|
||||||
/// The node queried by this request.
|
|
||||||
node: Required<NodeName> = "node",
|
|
||||||
|
|
||||||
/// The subscription identifier related to this request.
|
|
||||||
subid: Option<SubscriptionId> = "subid",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The actual list of items returned.
|
|
||||||
items: Vec<Item> = ("item", PUBSUB) => Item
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Items {
|
|
||||||
/// Create a new items request.
|
|
||||||
pub fn new(node: &str) -> Items {
|
|
||||||
Items {
|
|
||||||
node: NodeName(String::from(node)),
|
|
||||||
max_items: None,
|
|
||||||
subid: None,
|
|
||||||
items: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Response wrapper for a PubSub `<item/>`.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct Item(pub PubSubItem);
|
|
||||||
|
|
||||||
impl_pubsub_item!(Item, PUBSUB);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The options associated to a subscription request.
|
|
||||||
Options, "options", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The JID affected by this request.
|
|
||||||
jid: Required<Jid> = "jid",
|
|
||||||
|
|
||||||
/// The node affected by this request.
|
|
||||||
node: Option<NodeName> = "node",
|
|
||||||
|
|
||||||
/// The subscription identifier affected by this request.
|
|
||||||
subid: Option<SubscriptionId> = "subid",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The form describing the subscription.
|
|
||||||
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Request to publish items to a node.
|
|
||||||
Publish, "publish", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The target node for this operation.
|
|
||||||
node: Required<NodeName> = "node",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The items you want to publish.
|
|
||||||
items: Vec<Item> = ("item", PUBSUB) => Item
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The options associated to a publish request.
|
|
||||||
PublishOptions, "publish-options", PUBSUB,
|
|
||||||
children: [
|
|
||||||
/// The form describing these options.
|
|
||||||
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Whether a retract request should notify subscribers or not.
|
|
||||||
Notify,
|
|
||||||
"notify",
|
|
||||||
bool
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A request to retract some items from a node.
|
|
||||||
Retract, "retract", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The node affected by this request.
|
|
||||||
node: Required<NodeName> = "node",
|
|
||||||
|
|
||||||
/// Whether a retract request should notify subscribers or not.
|
|
||||||
notify: Default<Notify> = "notify",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The items affected by this request.
|
|
||||||
items: Vec<Item> = ("item", PUBSUB) => Item
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Indicate that the subscription can be configured.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct SubscribeOptions {
|
|
||||||
/// If `true`, the configuration is actually required.
|
|
||||||
required: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for SubscribeOptions {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<Self, Error> {
|
|
||||||
check_self!(elem, "subscribe-options", PUBSUB);
|
|
||||||
check_no_attributes!(elem, "subscribe-options");
|
|
||||||
let mut required = false;
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("required", ns::PUBSUB) {
|
|
||||||
if required {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"More than one required element in subscribe-options.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
required = true;
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Unknown child in subscribe-options element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(SubscribeOptions { required })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SubscribeOptions> for Element {
|
|
||||||
fn from(subscribe_options: SubscribeOptions) -> Element {
|
|
||||||
Element::builder("subscribe-options", ns::PUBSUB)
|
|
||||||
.append_all(if subscribe_options.required {
|
|
||||||
Some(Element::builder("required", ns::PUBSUB))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A request to subscribe a JID to a node.
|
|
||||||
Subscribe, "subscribe", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The JID being subscribed.
|
|
||||||
jid: Required<Jid> = "jid",
|
|
||||||
|
|
||||||
/// The node to subscribe to.
|
|
||||||
node: Option<NodeName> = "node",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A request for current subscriptions.
|
|
||||||
Subscriptions, "subscriptions", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The node to query.
|
|
||||||
node: Option<NodeName> = "node",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The list of subscription elements returned.
|
|
||||||
subscription: Vec<SubscriptionElem> = ("subscription", PUBSUB) => SubscriptionElem
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A subscription element, describing the state of a subscription.
|
|
||||||
SubscriptionElem, "subscription", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The JID affected by this subscription.
|
|
||||||
jid: Required<Jid> = "jid",
|
|
||||||
|
|
||||||
/// The node affected by this subscription.
|
|
||||||
node: Option<NodeName> = "node",
|
|
||||||
|
|
||||||
/// The subscription identifier for this subscription.
|
|
||||||
subid: Option<SubscriptionId> = "subid",
|
|
||||||
|
|
||||||
/// The state of the subscription.
|
|
||||||
subscription: Option<Subscription> = "subscription",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The options related to this subscription.
|
|
||||||
subscribe_options: Option<SubscribeOptions> = ("subscribe-options", PUBSUB) => SubscribeOptions
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// An unsubscribe request.
|
|
||||||
Unsubscribe, "unsubscribe", PUBSUB,
|
|
||||||
attributes: [
|
|
||||||
/// The JID affected by this request.
|
|
||||||
jid: Required<Jid> = "jid",
|
|
||||||
|
|
||||||
/// The node affected by this request.
|
|
||||||
node: Option<NodeName> = "node",
|
|
||||||
|
|
||||||
/// The subscription identifier for this subscription.
|
|
||||||
subid: Option<SubscriptionId> = "subid",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Main payload used to communicate with a PubSub service.
|
|
||||||
///
|
|
||||||
/// `<pubsub xmlns="http://jabber.org/protocol/pubsub"/>`
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum PubSub {
|
|
||||||
/// Request to create a new node, with optional suggested name and suggested configuration.
|
|
||||||
Create {
|
|
||||||
/// The create request.
|
|
||||||
create: Create,
|
|
||||||
|
|
||||||
/// The configure request for the new node.
|
|
||||||
configure: Option<Configure>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A subcribe request.
|
|
||||||
Subscribe {
|
|
||||||
/// The subscribe request.
|
|
||||||
subscribe: Option<Subscribe>,
|
|
||||||
|
|
||||||
/// The options related to this subscribe request.
|
|
||||||
options: Option<Options>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Request to publish items to a node, with optional options.
|
|
||||||
Publish {
|
|
||||||
/// The publish request.
|
|
||||||
publish: Publish,
|
|
||||||
|
|
||||||
/// The options related to this publish request.
|
|
||||||
publish_options: Option<PublishOptions>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A list of affiliations you have on a service, or on a node.
|
|
||||||
Affiliations(Affiliations),
|
|
||||||
|
|
||||||
/// Request for a default node configuration.
|
|
||||||
Default(Default),
|
|
||||||
|
|
||||||
/// A request for a list of items.
|
|
||||||
Items(Items),
|
|
||||||
|
|
||||||
/// A request to retract some items from a node.
|
|
||||||
Retract(Retract),
|
|
||||||
|
|
||||||
/// A request about a subscription.
|
|
||||||
Subscription(SubscriptionElem),
|
|
||||||
|
|
||||||
/// A request for current subscriptions.
|
|
||||||
Subscriptions(Subscriptions),
|
|
||||||
|
|
||||||
/// An unsubscribe request.
|
|
||||||
Unsubscribe(Unsubscribe),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IqGetPayload for PubSub {}
|
|
||||||
impl IqSetPayload for PubSub {}
|
|
||||||
impl IqResultPayload for PubSub {}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for PubSub {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<PubSub, Error> {
|
|
||||||
check_self!(elem, "pubsub", PUBSUB);
|
|
||||||
check_no_attributes!(elem, "pubsub");
|
|
||||||
|
|
||||||
let mut payload = None;
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("create", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let create = Create::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Create {
|
|
||||||
create,
|
|
||||||
configure: None,
|
|
||||||
});
|
|
||||||
} else if child.is("subscribe", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let subscribe = Subscribe::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Subscribe {
|
|
||||||
subscribe: Some(subscribe),
|
|
||||||
options: None,
|
|
||||||
});
|
|
||||||
} else if child.is("options", ns::PUBSUB) {
|
|
||||||
if let Some(PubSub::Subscribe { subscribe, options }) = payload {
|
|
||||||
if options.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Options is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let options = Some(Options::try_from(child.clone())?);
|
|
||||||
payload = Some(PubSub::Subscribe { subscribe, options });
|
|
||||||
} else if payload.is_none() {
|
|
||||||
let options = Options::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Subscribe {
|
|
||||||
subscribe: None,
|
|
||||||
options: Some(options),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if child.is("configure", ns::PUBSUB) {
|
|
||||||
if let Some(PubSub::Create { create, configure }) = payload {
|
|
||||||
if configure.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Configure is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let configure = Some(Configure::try_from(child.clone())?);
|
|
||||||
payload = Some(PubSub::Create { create, configure });
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if child.is("publish", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let publish = Publish::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Publish {
|
|
||||||
publish,
|
|
||||||
publish_options: None,
|
|
||||||
});
|
|
||||||
} else if child.is("publish-options", ns::PUBSUB) {
|
|
||||||
if let Some(PubSub::Publish {
|
|
||||||
publish,
|
|
||||||
publish_options,
|
|
||||||
}) = payload
|
|
||||||
{
|
|
||||||
if publish_options.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Publish-options are already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let publish_options = Some(PublishOptions::try_from(child.clone())?);
|
|
||||||
payload = Some(PubSub::Publish {
|
|
||||||
publish,
|
|
||||||
publish_options,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if child.is("affiliations", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let affiliations = Affiliations::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Affiliations(affiliations));
|
|
||||||
} else if child.is("default", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let default = Default::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Default(default));
|
|
||||||
} else if child.is("items", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let items = Items::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Items(items));
|
|
||||||
} else if child.is("retract", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let retract = Retract::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Retract(retract));
|
|
||||||
} else if child.is("subscription", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let subscription = SubscriptionElem::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Subscription(subscription));
|
|
||||||
} else if child.is("subscriptions", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let subscriptions = Subscriptions::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Subscriptions(subscriptions));
|
|
||||||
} else if child.is("unsubscribe", ns::PUBSUB) {
|
|
||||||
if payload.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Payload is already defined in pubsub element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let unsubscribe = Unsubscribe::try_from(child.clone())?;
|
|
||||||
payload = Some(PubSub::Unsubscribe(unsubscribe));
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in pubsub element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PubSub> for Element {
|
|
||||||
fn from(pubsub: PubSub) -> Element {
|
|
||||||
Element::builder("pubsub", ns::PUBSUB)
|
|
||||||
.append_all(match pubsub {
|
|
||||||
PubSub::Create { create, configure } => {
|
|
||||||
let mut elems = vec![Element::from(create)];
|
|
||||||
if let Some(configure) = configure {
|
|
||||||
elems.push(Element::from(configure));
|
|
||||||
}
|
|
||||||
elems
|
|
||||||
}
|
|
||||||
PubSub::Subscribe { subscribe, options } => {
|
|
||||||
let mut elems = vec![];
|
|
||||||
if let Some(subscribe) = subscribe {
|
|
||||||
elems.push(Element::from(subscribe));
|
|
||||||
}
|
|
||||||
if let Some(options) = options {
|
|
||||||
elems.push(Element::from(options));
|
|
||||||
}
|
|
||||||
elems
|
|
||||||
}
|
|
||||||
PubSub::Publish {
|
|
||||||
publish,
|
|
||||||
publish_options,
|
|
||||||
} => {
|
|
||||||
let mut elems = vec![Element::from(publish)];
|
|
||||||
if let Some(publish_options) = publish_options {
|
|
||||||
elems.push(Element::from(publish_options));
|
|
||||||
}
|
|
||||||
elems
|
|
||||||
}
|
|
||||||
PubSub::Affiliations(affiliations) => vec![Element::from(affiliations)],
|
|
||||||
PubSub::Default(default) => vec![Element::from(default)],
|
|
||||||
PubSub::Items(items) => vec![Element::from(items)],
|
|
||||||
PubSub::Retract(retract) => vec![Element::from(retract)],
|
|
||||||
PubSub::Subscription(subscription) => vec![Element::from(subscription)],
|
|
||||||
PubSub::Subscriptions(subscriptions) => vec![Element::from(subscriptions)],
|
|
||||||
PubSub::Unsubscribe(unsubscribe) => vec![Element::from(unsubscribe)],
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
|
|
||||||
use jid::FullJid;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let pubsub = PubSub::try_from(elem).unwrap();
|
|
||||||
match pubsub.clone() {
|
|
||||||
PubSub::Create { create, configure } => {
|
|
||||||
assert!(create.node.is_none());
|
|
||||||
assert!(configure.is_none());
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><create node='coucou'/></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let pubsub = PubSub::try_from(elem).unwrap();
|
|
||||||
match pubsub.clone() {
|
|
||||||
PubSub::Create { create, configure } => {
|
|
||||||
assert_eq!(&create.node.unwrap().0, "coucou");
|
|
||||||
assert!(configure.is_none());
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_and_configure_empty() {
|
|
||||||
let elem: Element =
|
|
||||||
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/><configure/></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let pubsub = PubSub::try_from(elem).unwrap();
|
|
||||||
match pubsub.clone() {
|
|
||||||
PubSub::Create { create, configure } => {
|
|
||||||
assert!(create.node.is_none());
|
|
||||||
assert!(configure.unwrap().form.is_none());
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_and_configure_simple() {
|
|
||||||
// XXX: Do we want xmpp-parsers to always specify the field type in the output Element?
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create node='foo'/><configure><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model' type='list-single'><value>whitelist</value></field></x></configure></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
|
|
||||||
let pubsub = PubSub::Create {
|
|
||||||
create: Create {
|
|
||||||
node: Some(NodeName(String::from("foo"))),
|
|
||||||
},
|
|
||||||
configure: Some(Configure {
|
|
||||||
form: Some(DataForm {
|
|
||||||
type_: DataFormType::Submit,
|
|
||||||
form_type: Some(String::from(ns::PUBSUB_CONFIGURE)),
|
|
||||||
title: None,
|
|
||||||
instructions: None,
|
|
||||||
fields: vec![Field {
|
|
||||||
var: String::from("pubsub#access_model"),
|
|
||||||
type_: FieldType::ListSingle,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![String::from("whitelist")],
|
|
||||||
media: vec![],
|
|
||||||
}],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn publish() {
|
|
||||||
let elem: Element =
|
|
||||||
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/></pubsub>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let pubsub = PubSub::try_from(elem).unwrap();
|
|
||||||
match pubsub.clone() {
|
|
||||||
PubSub::Publish {
|
|
||||||
publish,
|
|
||||||
publish_options,
|
|
||||||
} => {
|
|
||||||
assert_eq!(&publish.node.0, "coucou");
|
|
||||||
assert!(publish_options.is_none());
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn publish_with_publish_options() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/><publish-options/></pubsub>".parse().unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let pubsub = PubSub::try_from(elem).unwrap();
|
|
||||||
match pubsub.clone() {
|
|
||||||
PubSub::Publish {
|
|
||||||
publish,
|
|
||||||
publish_options,
|
|
||||||
} => {
|
|
||||||
assert_eq!(&publish.node.0, "coucou");
|
|
||||||
assert!(publish_options.unwrap().form.is_none());
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_empty_pubsub() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = PubSub::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "No payload in pubsub element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn publish_option() {
|
|
||||||
let elem: Element = "<publish-options xmlns='http://jabber.org/protocol/pubsub'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#publish-options</value></field></x></publish-options>".parse().unwrap();
|
|
||||||
let publish_options = PublishOptions::try_from(elem).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
&publish_options.form.unwrap().form_type.unwrap(),
|
|
||||||
"http://jabber.org/protocol/pubsub#publish-options"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn subscribe_options() {
|
|
||||||
let elem1: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let subscribe_options1 = SubscribeOptions::try_from(elem1).unwrap();
|
|
||||||
assert_eq!(subscribe_options1.required, false);
|
|
||||||
|
|
||||||
let elem2: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'><required/></subscribe-options>".parse().unwrap();
|
|
||||||
let subscribe_options2 = SubscribeOptions::try_from(elem2).unwrap();
|
|
||||||
assert_eq!(subscribe_options2.required, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_options_without_subscribe() {
|
|
||||||
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><options xmlns='http://jabber.org/protocol/pubsub' jid='juliet@capulet.lit/balcony'><x xmlns='jabber:x:data' type='submit'/></options></pubsub>".parse().unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let pubsub = PubSub::try_from(elem).unwrap();
|
|
||||||
match pubsub.clone() {
|
|
||||||
PubSub::Subscribe { subscribe, options } => {
|
|
||||||
assert!(subscribe.is_none());
|
|
||||||
assert!(options.is_some());
|
|
||||||
}
|
|
||||||
_ => panic!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
let elem2 = Element::from(pubsub);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_options() {
|
|
||||||
let reference: Element = "<options xmlns='http://jabber.org/protocol/pubsub' jid='juliet@capulet.lit/balcony'><x xmlns='jabber:x:data' type='submit'/></options>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<x xmlns='jabber:x:data' type='submit'/>".parse().unwrap();
|
|
||||||
|
|
||||||
let form = DataForm::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let options = Options {
|
|
||||||
jid: Jid::Full(FullJid::new("juliet", "capulet.lit", "balcony")),
|
|
||||||
node: None,
|
|
||||||
subid: None,
|
|
||||||
form: Some(form),
|
|
||||||
};
|
|
||||||
let serialized: Element = options.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_publish_options() {
|
|
||||||
let reference: Element = "<publish-options xmlns='http://jabber.org/protocol/pubsub'><x xmlns='jabber:x:data' type='submit'/></publish-options>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<x xmlns='jabber:x:data' type='submit'/>".parse().unwrap();
|
|
||||||
|
|
||||||
let form = DataForm::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let options = PublishOptions { form: Some(form) };
|
|
||||||
let serialized: Element = options.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Requests that this message is acked by the final recipient once
|
|
||||||
/// received.
|
|
||||||
Request,
|
|
||||||
"request",
|
|
||||||
RECEIPTS
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Request {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Notes that a previous message has correctly been received, it is
|
|
||||||
/// referenced by its 'id' attribute.
|
|
||||||
Received, "received", RECEIPTS,
|
|
||||||
attributes: [
|
|
||||||
/// The 'id' attribute of the received message.
|
|
||||||
id: Required<String> = "id",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for Received {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Request, 0);
|
|
||||||
assert_size!(Received, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Request, 0);
|
|
||||||
assert_size!(Received, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<request xmlns='urn:xmpp:receipts'/>".parse().unwrap();
|
|
||||||
Request::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<received xmlns='urn:xmpp:receipts' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
Received::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_missing_id() {
|
|
||||||
let elem: Element = "<received xmlns='urn:xmpp:receipts'/>".parse().unwrap();
|
|
||||||
let error = Received::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'id' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let receipt = Request;
|
|
||||||
let elem: Element = receipt.into();
|
|
||||||
assert!(elem.is("request", ns::RECEIPTS));
|
|
||||||
assert_eq!(elem.attrs().count(), 0);
|
|
||||||
|
|
||||||
let receipt = Received {
|
|
||||||
id: String::from("coucou"),
|
|
||||||
};
|
|
||||||
let elem: Element = receipt.into();
|
|
||||||
assert!(elem.is("received", ns::RECEIPTS));
|
|
||||||
assert_eq!(elem.attr("id"), Some("coucou"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,313 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
|
||||||
use jid::BareJid;
|
|
||||||
|
|
||||||
generate_elem_id!(
|
|
||||||
/// Represents a group a contact is part of.
|
|
||||||
Group,
|
|
||||||
"group",
|
|
||||||
ROSTER
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The state of your mutual subscription with a contact.
|
|
||||||
Subscription, "subscription", {
|
|
||||||
/// The user doesn’t have any subscription to this contact’s presence,
|
|
||||||
/// and neither does this contact.
|
|
||||||
None => "none",
|
|
||||||
|
|
||||||
/// Only this contact has a subscription with you, not the opposite.
|
|
||||||
From => "from",
|
|
||||||
|
|
||||||
/// Only you have a subscription with this contact, not the opposite.
|
|
||||||
To => "to",
|
|
||||||
|
|
||||||
/// Both you and your contact are subscribed to each other’s presence.
|
|
||||||
Both => "both",
|
|
||||||
|
|
||||||
/// In a roster set, this asks the server to remove this contact item
|
|
||||||
/// from your roster.
|
|
||||||
Remove => "remove",
|
|
||||||
}, Default = None
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The sub-state of subscription with a contact.
|
|
||||||
Ask, "ask", (
|
|
||||||
/// Pending sub-state of the 'none' subscription state.
|
|
||||||
Subscribe => "subscribe"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Contact from the user’s contact list.
|
|
||||||
Item, "item", ROSTER,
|
|
||||||
attributes: [
|
|
||||||
/// JID of this contact.
|
|
||||||
jid: Required<BareJid> = "jid",
|
|
||||||
|
|
||||||
/// Name of this contact.
|
|
||||||
name: OptionEmpty<String> = "name",
|
|
||||||
|
|
||||||
/// Subscription status of this contact.
|
|
||||||
subscription: Default<Subscription> = "subscription",
|
|
||||||
|
|
||||||
/// Indicates “Pending Out” sub-states for this contact.
|
|
||||||
ask: Default<Ask> = "ask",
|
|
||||||
],
|
|
||||||
|
|
||||||
children: [
|
|
||||||
/// Groups this contact is part of.
|
|
||||||
groups: Vec<Group> = ("group", ROSTER) => Group
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The contact list of the user.
|
|
||||||
Roster, "query", ROSTER,
|
|
||||||
attributes: [
|
|
||||||
/// Version of the contact list.
|
|
||||||
///
|
|
||||||
/// This is an opaque string that should only be sent back to the server on
|
|
||||||
/// a new connection, if this client is storing the contact list between
|
|
||||||
/// connections.
|
|
||||||
ver: Option<String> = "ver"
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// List of the contacts of the user.
|
|
||||||
items: Vec<Item> = ("item", ROSTER) => Item
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqGetPayload for Roster {}
|
|
||||||
impl IqSetPayload for Roster {}
|
|
||||||
impl IqResultPayload for Roster {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Group, 12);
|
|
||||||
assert_size!(Subscription, 1);
|
|
||||||
assert_size!(Ask, 1);
|
|
||||||
assert_size!(Item, 52);
|
|
||||||
assert_size!(Roster, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Group, 24);
|
|
||||||
assert_size!(Subscription, 1);
|
|
||||||
assert_size!(Ask, 1);
|
|
||||||
assert_size!(Item, 104);
|
|
||||||
assert_size!(Roster, 48);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get() {
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:roster'/>".parse().unwrap();
|
|
||||||
let roster = Roster::try_from(elem).unwrap();
|
|
||||||
assert!(roster.ver.is_none());
|
|
||||||
assert!(roster.items.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_result() {
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:roster' ver='ver7'><item jid='nurse@example.com'/><item jid='romeo@example.net'/></query>".parse().unwrap();
|
|
||||||
let roster = Roster::try_from(elem).unwrap();
|
|
||||||
assert_eq!(roster.ver, Some(String::from("ver7")));
|
|
||||||
assert_eq!(roster.items.len(), 2);
|
|
||||||
|
|
||||||
let elem2: Element = "<query xmlns='jabber:iq:roster' ver='ver7'><item jid='nurse@example.com'/><item jid='romeo@example.net' name=''/></query>".parse().unwrap();
|
|
||||||
let roster2 = Roster::try_from(elem2).unwrap();
|
|
||||||
assert_eq!(roster.items, roster2.items);
|
|
||||||
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:roster' ver='ver9'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let roster = Roster::try_from(elem).unwrap();
|
|
||||||
assert_eq!(roster.ver, Some(String::from("ver9")));
|
|
||||||
assert!(roster.items.is_empty());
|
|
||||||
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns='jabber:iq:roster' ver='ver11'>
|
|
||||||
<item jid='romeo@example.net'
|
|
||||||
name='Romeo'
|
|
||||||
subscription='both'>
|
|
||||||
<group>Friends</group>
|
|
||||||
</item>
|
|
||||||
<item jid='mercutio@example.com'
|
|
||||||
name='Mercutio'
|
|
||||||
subscription='from'/>
|
|
||||||
<item jid='benvolio@example.net'
|
|
||||||
name='Benvolio'
|
|
||||||
subscription='both'/>
|
|
||||||
<item jid='contact@example.org'
|
|
||||||
subscription='none'
|
|
||||||
ask='subscribe'
|
|
||||||
name='MyContact'>
|
|
||||||
<group>MyBuddies</group>
|
|
||||||
</item>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let roster = Roster::try_from(elem).unwrap();
|
|
||||||
assert_eq!(roster.ver, Some(String::from("ver11")));
|
|
||||||
assert_eq!(roster.items.len(), 4);
|
|
||||||
assert_eq!(roster.items[0].jid, BareJid::new("romeo", "example.net"));
|
|
||||||
assert_eq!(roster.items[0].name, Some(String::from("Romeo")));
|
|
||||||
assert_eq!(roster.items[0].subscription, Subscription::Both);
|
|
||||||
assert_eq!(roster.items[0].ask, Ask::None);
|
|
||||||
assert_eq!(
|
|
||||||
roster.items[0].groups,
|
|
||||||
vec!(Group::from_str("Friends").unwrap())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(roster.items[3].jid, BareJid::new("contact", "example.org"));
|
|
||||||
assert_eq!(roster.items[3].name, Some(String::from("MyContact")));
|
|
||||||
assert_eq!(roster.items[3].subscription, Subscription::None);
|
|
||||||
assert_eq!(roster.items[3].ask, Ask::Subscribe);
|
|
||||||
assert_eq!(
|
|
||||||
roster.items[3].groups,
|
|
||||||
vec!(Group::from_str("MyBuddies").unwrap())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiple_groups() {
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:roster'><item jid='test@example.org'><group>A</group><group>B</group></item></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let roster = Roster::try_from(elem).unwrap();
|
|
||||||
assert!(roster.ver.is_none());
|
|
||||||
assert_eq!(roster.items.len(), 1);
|
|
||||||
assert_eq!(roster.items[0].jid, BareJid::new("test", "example.org"));
|
|
||||||
assert_eq!(roster.items[0].name, None);
|
|
||||||
assert_eq!(roster.items[0].groups.len(), 2);
|
|
||||||
assert_eq!(roster.items[0].groups[0], Group::from_str("A").unwrap());
|
|
||||||
assert_eq!(roster.items[0].groups[1], Group::from_str("B").unwrap());
|
|
||||||
let elem2 = roster.into();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_set() {
|
|
||||||
let elem: Element =
|
|
||||||
"<query xmlns='jabber:iq:roster'><item jid='nurse@example.com'/></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let roster = Roster::try_from(elem).unwrap();
|
|
||||||
assert!(roster.ver.is_none());
|
|
||||||
assert_eq!(roster.items.len(), 1);
|
|
||||||
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns='jabber:iq:roster'>
|
|
||||||
<item jid='nurse@example.com'
|
|
||||||
name='Nurse'>
|
|
||||||
<group>Servants</group>
|
|
||||||
</item>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let roster = Roster::try_from(elem).unwrap();
|
|
||||||
assert!(roster.ver.is_none());
|
|
||||||
assert_eq!(roster.items.len(), 1);
|
|
||||||
assert_eq!(roster.items[0].jid, BareJid::new("nurse", "example.com"));
|
|
||||||
assert_eq!(roster.items[0].name, Some(String::from("Nurse")));
|
|
||||||
assert_eq!(roster.items[0].groups.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
roster.items[0].groups[0],
|
|
||||||
Group::from_str("Servants").unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let elem: Element = r#"
|
|
||||||
<query xmlns='jabber:iq:roster'>
|
|
||||||
<item jid='nurse@example.com'
|
|
||||||
subscription='remove'/>
|
|
||||||
</query>
|
|
||||||
"#
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let roster = Roster::try_from(elem).unwrap();
|
|
||||||
assert!(roster.ver.is_none());
|
|
||||||
assert_eq!(roster.items.len(), 1);
|
|
||||||
assert_eq!(roster.items[0].jid, BareJid::new("nurse", "example.com"));
|
|
||||||
assert!(roster.items[0].name.is_none());
|
|
||||||
assert!(roster.items[0].groups.is_empty());
|
|
||||||
assert_eq!(roster.items[0].subscription, Subscription::Remove);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "disable-validation"))]
|
|
||||||
#[test]
|
|
||||||
fn test_invalid() {
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:roster'><coucou/></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Roster::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in query element.");
|
|
||||||
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:roster' coucou=''/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Roster::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown attribute in query element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_item() {
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:roster'><item/></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Roster::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'jid' missing.");
|
|
||||||
|
|
||||||
/*
|
|
||||||
let elem: Element = "<query xmlns='jabber:iq:roster'><item jid=''/></query>".parse().unwrap();
|
|
||||||
let error = Roster::try_from(elem).unwrap_err();
|
|
||||||
let error = match error {
|
|
||||||
Error::JidParseError(error) => error,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(error.description(), "Invalid JID, I guess?");
|
|
||||||
*/
|
|
||||||
|
|
||||||
let elem: Element =
|
|
||||||
"<query xmlns='jabber:iq:roster'><item jid='coucou'><coucou/></item></query>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = Roster::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in item element.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,302 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Requests paging through a potentially big set of items (represented by an
|
|
||||||
/// UID).
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct SetQuery {
|
|
||||||
/// Limit the number of items, or use the recipient’s defaults if None.
|
|
||||||
pub max: Option<usize>,
|
|
||||||
|
|
||||||
/// The UID after which to give results, or if None it is the element
|
|
||||||
/// “before” the first item, effectively an index of negative one.
|
|
||||||
pub after: Option<String>,
|
|
||||||
|
|
||||||
/// The UID before which to give results, or if None it starts with the
|
|
||||||
/// last page of the full set.
|
|
||||||
pub before: Option<String>,
|
|
||||||
|
|
||||||
/// Numerical index of the page (deprecated).
|
|
||||||
pub index: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for SetQuery {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<SetQuery, Error> {
|
|
||||||
check_self!(elem, "set", RSM, "RSM set");
|
|
||||||
let mut set = SetQuery {
|
|
||||||
max: None,
|
|
||||||
after: None,
|
|
||||||
before: None,
|
|
||||||
index: None,
|
|
||||||
};
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("max", ns::RSM) {
|
|
||||||
if set.max.is_some() {
|
|
||||||
return Err(Error::ParseError("Set can’t have more than one max."));
|
|
||||||
}
|
|
||||||
set.max = Some(child.text().parse()?);
|
|
||||||
} else if child.is("after", ns::RSM) {
|
|
||||||
if set.after.is_some() {
|
|
||||||
return Err(Error::ParseError("Set can’t have more than one after."));
|
|
||||||
}
|
|
||||||
set.after = Some(child.text());
|
|
||||||
} else if child.is("before", ns::RSM) {
|
|
||||||
if set.before.is_some() {
|
|
||||||
return Err(Error::ParseError("Set can’t have more than one before."));
|
|
||||||
}
|
|
||||||
set.before = Some(child.text());
|
|
||||||
} else if child.is("index", ns::RSM) {
|
|
||||||
if set.index.is_some() {
|
|
||||||
return Err(Error::ParseError("Set can’t have more than one index."));
|
|
||||||
}
|
|
||||||
set.index = Some(child.text().parse()?);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in set element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(set)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SetQuery> for Element {
|
|
||||||
fn from(set: SetQuery) -> Element {
|
|
||||||
Element::builder("set", ns::RSM)
|
|
||||||
.append_all(
|
|
||||||
set.max
|
|
||||||
.map(|max| Element::builder("max", ns::RSM).append(format!("{}", max))),
|
|
||||||
)
|
|
||||||
.append_all(
|
|
||||||
set.after
|
|
||||||
.map(|after| Element::builder("after", ns::RSM).append(after)),
|
|
||||||
)
|
|
||||||
.append_all(
|
|
||||||
set.before
|
|
||||||
.map(|before| Element::builder("before", ns::RSM).append(before)),
|
|
||||||
)
|
|
||||||
.append_all(
|
|
||||||
set.index
|
|
||||||
.map(|index| Element::builder("index", ns::RSM).append(format!("{}", index))),
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes the paging result of a [query](struct.SetQuery.html).
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct SetResult {
|
|
||||||
/// The UID of the first item of the page.
|
|
||||||
pub first: Option<String>,
|
|
||||||
|
|
||||||
/// The position of the [first item](#structfield.first) in the full set
|
|
||||||
/// (which may be approximate).
|
|
||||||
pub first_index: Option<usize>,
|
|
||||||
|
|
||||||
/// The UID of the last item of the page.
|
|
||||||
pub last: Option<String>,
|
|
||||||
|
|
||||||
/// How many items there are in the full set (which may be approximate).
|
|
||||||
pub count: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for SetResult {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<SetResult, Error> {
|
|
||||||
check_self!(elem, "set", RSM, "RSM set");
|
|
||||||
let mut set = SetResult {
|
|
||||||
first: None,
|
|
||||||
first_index: None,
|
|
||||||
last: None,
|
|
||||||
count: None,
|
|
||||||
};
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("first", ns::RSM) {
|
|
||||||
if set.first.is_some() {
|
|
||||||
return Err(Error::ParseError("Set can’t have more than one first."));
|
|
||||||
}
|
|
||||||
set.first_index = get_attr!(child, "index", Option);
|
|
||||||
set.first = Some(child.text());
|
|
||||||
} else if child.is("last", ns::RSM) {
|
|
||||||
if set.last.is_some() {
|
|
||||||
return Err(Error::ParseError("Set can’t have more than one last."));
|
|
||||||
}
|
|
||||||
set.last = Some(child.text());
|
|
||||||
} else if child.is("count", ns::RSM) {
|
|
||||||
if set.count.is_some() {
|
|
||||||
return Err(Error::ParseError("Set can’t have more than one count."));
|
|
||||||
}
|
|
||||||
set.count = Some(child.text().parse()?);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in set element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(set)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SetResult> for Element {
|
|
||||||
fn from(set: SetResult) -> Element {
|
|
||||||
let first = set.first.clone().map(|first| {
|
|
||||||
Element::builder("first", ns::RSM)
|
|
||||||
.attr("index", set.first_index)
|
|
||||||
.append(first)
|
|
||||||
});
|
|
||||||
Element::builder("set", ns::RSM)
|
|
||||||
.append_all(first)
|
|
||||||
.append_all(
|
|
||||||
set.last
|
|
||||||
.map(|last| Element::builder("last", ns::RSM).append(last)),
|
|
||||||
)
|
|
||||||
.append_all(
|
|
||||||
set.count
|
|
||||||
.map(|count| Element::builder("count", ns::RSM).append(format!("{}", count))),
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(SetQuery, 40);
|
|
||||||
assert_size!(SetResult, 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(SetQuery, 80);
|
|
||||||
assert_size!(SetResult, 80);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let set = SetQuery::try_from(elem).unwrap();
|
|
||||||
assert_eq!(set.max, None);
|
|
||||||
assert_eq!(set.after, None);
|
|
||||||
assert_eq!(set.before, None);
|
|
||||||
assert_eq!(set.index, None);
|
|
||||||
|
|
||||||
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let set = SetResult::try_from(elem).unwrap();
|
|
||||||
match set.first {
|
|
||||||
Some(_) => panic!(),
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
assert_eq!(set.last, None);
|
|
||||||
assert_eq!(set.count, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unknown() {
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = SetQuery::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "This is not a RSM set element.");
|
|
||||||
|
|
||||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = SetResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "This is not a RSM set element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'><coucou/></set>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = SetQuery::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in set element.");
|
|
||||||
|
|
||||||
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'><coucou/></set>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = SetResult::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in set element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let rsm = SetQuery {
|
|
||||||
max: None,
|
|
||||||
after: None,
|
|
||||||
before: None,
|
|
||||||
index: None,
|
|
||||||
};
|
|
||||||
let elem2 = rsm.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
|
|
||||||
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let rsm = SetResult {
|
|
||||||
first: None,
|
|
||||||
first_index: None,
|
|
||||||
last: None,
|
|
||||||
count: None,
|
|
||||||
};
|
|
||||||
let elem2 = rsm.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_first_index() {
|
|
||||||
let elem: Element =
|
|
||||||
"<set xmlns='http://jabber.org/protocol/rsm'><first index='4'>coucou</first></set>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let set = SetResult::try_from(elem).unwrap();
|
|
||||||
assert_eq!(set.first, Some(String::from("coucou")));
|
|
||||||
assert_eq!(set.first_index, Some(4));
|
|
||||||
|
|
||||||
let set2 = SetResult {
|
|
||||||
first: Some(String::from("coucou")),
|
|
||||||
first_index: Some(4),
|
|
||||||
last: None,
|
|
||||||
count: None,
|
|
||||||
};
|
|
||||||
let elem2 = set2.into();
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,301 +0,0 @@
|
||||||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::util::helpers::Base64;
|
|
||||||
use crate::Element;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The list of available SASL mechanisms.
|
|
||||||
Mechanism, "mechanism", {
|
|
||||||
/// Uses no hashing mechanism and transmit the password in clear to the
|
|
||||||
/// server, using a single step.
|
|
||||||
Plain => "PLAIN",
|
|
||||||
|
|
||||||
/// Challenge-based mechanism using HMAC and SHA-1, allows both the
|
|
||||||
/// client and the server to avoid having to store the password in
|
|
||||||
/// clear.
|
|
||||||
///
|
|
||||||
/// See https://tools.ietf.org/html/rfc5802
|
|
||||||
ScramSha1 => "SCRAM-SHA-1",
|
|
||||||
|
|
||||||
/// Same as [ScramSha1](#structfield.ScramSha1), with the addition of
|
|
||||||
/// channel binding.
|
|
||||||
ScramSha1Plus => "SCRAM-SHA-1-PLUS",
|
|
||||||
|
|
||||||
/// Same as [ScramSha1](#structfield.ScramSha1), but using SHA-256
|
|
||||||
/// instead of SHA-1 as the hash function.
|
|
||||||
ScramSha256 => "SCRAM-SHA-256",
|
|
||||||
|
|
||||||
/// Same as [ScramSha256](#structfield.ScramSha256), with the addition
|
|
||||||
/// of channel binding.
|
|
||||||
ScramSha256Plus => "SCRAM-SHA-256-PLUS",
|
|
||||||
|
|
||||||
/// Creates a temporary JID on login, which will be destroyed on
|
|
||||||
/// disconnect.
|
|
||||||
Anonymous => "ANONYMOUS",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The first step of the SASL process, selecting the mechanism and sending
|
|
||||||
/// the first part of the handshake.
|
|
||||||
Auth, "auth", SASL,
|
|
||||||
attributes: [
|
|
||||||
/// The mechanism used.
|
|
||||||
mechanism: Required<Mechanism> = "mechanism"
|
|
||||||
],
|
|
||||||
text: (
|
|
||||||
/// The content of the handshake.
|
|
||||||
data: Base64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// In case the mechanism selected at the [auth](struct.Auth.html) step
|
|
||||||
/// requires a second step, the server sends this element with additional
|
|
||||||
/// data.
|
|
||||||
Challenge, "challenge", SASL,
|
|
||||||
text: (
|
|
||||||
/// The challenge data.
|
|
||||||
data: Base64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// In case the mechanism selected at the [auth](struct.Auth.html) step
|
|
||||||
/// requires a second step, this contains the client’s response to the
|
|
||||||
/// server’s [challenge](struct.Challenge.html).
|
|
||||||
Response, "response", SASL,
|
|
||||||
text: (
|
|
||||||
/// The response data.
|
|
||||||
data: Base64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Sent by the client at any point after [auth](struct.Auth.html) if it
|
|
||||||
/// wants to cancel the current authentication process.
|
|
||||||
Abort,
|
|
||||||
"abort",
|
|
||||||
SASL
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Sent by the server on SASL success.
|
|
||||||
Success, "success", SASL,
|
|
||||||
text: (
|
|
||||||
/// Possible data sent on success.
|
|
||||||
data: Base64<Vec<u8>>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element_enum!(
|
|
||||||
/// List of possible failure conditions for SASL.
|
|
||||||
DefinedCondition, "defined-condition", SASL, {
|
|
||||||
/// The client aborted the authentication with
|
|
||||||
/// [abort](struct.Abort.html).
|
|
||||||
Aborted => "aborted",
|
|
||||||
|
|
||||||
/// The account the client is trying to authenticate against has been
|
|
||||||
/// disabled.
|
|
||||||
AccountDisabled => "account-disabled",
|
|
||||||
|
|
||||||
/// The credentials for this account have expired.
|
|
||||||
CredentialsExpired => "credentials-expired",
|
|
||||||
|
|
||||||
/// You must enable StartTLS or use direct TLS before using this
|
|
||||||
/// authentication mechanism.
|
|
||||||
EncryptionRequired => "encryption-required",
|
|
||||||
|
|
||||||
/// The base64 data sent by the client is invalid.
|
|
||||||
IncorrectEncoding => "incorrect-encoding",
|
|
||||||
|
|
||||||
/// The authzid provided by the client is invalid.
|
|
||||||
InvalidAuthzid => "invalid-authzid",
|
|
||||||
|
|
||||||
/// The client tried to use an invalid mechanism, or none.
|
|
||||||
InvalidMechanism => "invalid-mechanism",
|
|
||||||
|
|
||||||
/// The client sent a bad request.
|
|
||||||
MalformedRequest => "malformed-request",
|
|
||||||
|
|
||||||
/// The mechanism selected is weaker than what the server allows.
|
|
||||||
MechanismTooWeak => "mechanism-too-weak",
|
|
||||||
|
|
||||||
/// The credentials provided are invalid.
|
|
||||||
NotAuthorized => "not-authorized",
|
|
||||||
|
|
||||||
/// The server encountered an issue which may be fixed later, the
|
|
||||||
/// client should retry at some point.
|
|
||||||
TemporaryAuthFailure => "temporary-auth-failure",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
type Lang = String;
|
|
||||||
|
|
||||||
/// Sent by the server on SASL failure.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Failure {
|
|
||||||
/// One of the allowed defined-conditions for SASL.
|
|
||||||
pub defined_condition: DefinedCondition,
|
|
||||||
|
|
||||||
/// A human-readable explanation for the failure.
|
|
||||||
pub texts: BTreeMap<Lang, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for Failure {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(root: Element) -> Result<Failure, Error> {
|
|
||||||
check_self!(root, "failure", SASL);
|
|
||||||
check_no_attributes!(root, "failure");
|
|
||||||
|
|
||||||
let mut defined_condition = None;
|
|
||||||
let mut texts = BTreeMap::new();
|
|
||||||
|
|
||||||
for child in root.children() {
|
|
||||||
if child.is("text", ns::SASL) {
|
|
||||||
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
|
|
||||||
check_no_children!(child, "text");
|
|
||||||
let lang = get_attr!(child, "xml:lang", Default);
|
|
||||||
if texts.insert(lang, child.text()).is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Text element present twice for the same xml:lang in failure element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if child.has_ns(ns::SASL) {
|
|
||||||
if defined_condition.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Failure must not have more than one defined-condition.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
check_no_attributes!(child, "defined-condition");
|
|
||||||
check_no_children!(child, "defined-condition");
|
|
||||||
let condition = match DefinedCondition::try_from(child.clone()) {
|
|
||||||
Ok(condition) => condition,
|
|
||||||
// TODO: do we really want to eat this error?
|
|
||||||
Err(_) => DefinedCondition::NotAuthorized,
|
|
||||||
};
|
|
||||||
defined_condition = Some(condition);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown element in Failure."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let defined_condition =
|
|
||||||
defined_condition.ok_or(Error::ParseError("Failure must have a defined-condition."))?;
|
|
||||||
|
|
||||||
Ok(Failure {
|
|
||||||
defined_condition,
|
|
||||||
texts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Failure> for Element {
|
|
||||||
fn from(failure: Failure) -> Element {
|
|
||||||
Element::builder("failure", ns::SASL)
|
|
||||||
.append(failure.defined_condition)
|
|
||||||
.append_all(failure.texts.into_iter().map(|(lang, text)| {
|
|
||||||
Element::builder("text", ns::SASL)
|
|
||||||
.attr("xml:lang", lang)
|
|
||||||
.append(text)
|
|
||||||
}))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Mechanism, 1);
|
|
||||||
assert_size!(Auth, 16);
|
|
||||||
assert_size!(Challenge, 12);
|
|
||||||
assert_size!(Response, 12);
|
|
||||||
assert_size!(Abort, 0);
|
|
||||||
assert_size!(Success, 12);
|
|
||||||
assert_size!(DefinedCondition, 1);
|
|
||||||
assert_size!(Failure, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Mechanism, 1);
|
|
||||||
assert_size!(Auth, 32);
|
|
||||||
assert_size!(Challenge, 24);
|
|
||||||
assert_size!(Response, 24);
|
|
||||||
assert_size!(Abort, 0);
|
|
||||||
assert_size!(Success, 24);
|
|
||||||
assert_size!(DefinedCondition, 1);
|
|
||||||
assert_size!(Failure, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let auth = Auth::try_from(elem).unwrap();
|
|
||||||
assert_eq!(auth.mechanism, Mechanism::Plain);
|
|
||||||
assert!(auth.data.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn section_6_5_1() {
|
|
||||||
let elem: Element =
|
|
||||||
"<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><aborted/></failure>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let failure = Failure::try_from(elem).unwrap();
|
|
||||||
assert_eq!(failure.defined_condition, DefinedCondition::Aborted);
|
|
||||||
assert!(failure.texts.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn section_6_5_2() {
|
|
||||||
let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
|
|
||||||
<account-disabled/>
|
|
||||||
<text xml:lang='en'>Call 212-555-1212 for assistance.</text>
|
|
||||||
</failure>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let failure = Failure::try_from(elem).unwrap();
|
|
||||||
assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled);
|
|
||||||
assert_eq!(
|
|
||||||
failure.texts["en"],
|
|
||||||
String::from("Call 212-555-1212 for assistance.")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Some servers apparently use a non-namespaced 'lang' attribute, which is invalid as not part
|
|
||||||
/// of the schema. This tests whether we can parse it when disabling validation.
|
|
||||||
#[cfg(feature = "disable-validation")]
|
|
||||||
#[test]
|
|
||||||
fn invalid_failure_with_non_prefixed_text_lang() {
|
|
||||||
let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
|
|
||||||
<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
|
|
||||||
<text xmlns='urn:ietf:params:xml:ns:xmpp-sasl' lang='en'>Invalid username or password</text>
|
|
||||||
</failure>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let failure = Failure::try_from(elem).unwrap();
|
|
||||||
assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized);
|
|
||||||
assert_eq!(
|
|
||||||
failure.texts[""],
|
|
||||||
String::from("Invalid username or password")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,209 +0,0 @@
|
||||||
// Copyright (C) 2019 Maxime “pep” Buquet <pep@bouah.net>
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
/// Structure representing a `http://jabber.org/network/serverinfo` form type.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
|
||||||
pub struct ServerInfo {
|
|
||||||
/// Abuse addresses
|
|
||||||
pub abuse: Vec<String>,
|
|
||||||
|
|
||||||
/// Admin addresses
|
|
||||||
pub admin: Vec<String>,
|
|
||||||
|
|
||||||
/// Feedback addresses
|
|
||||||
pub feedback: Vec<String>,
|
|
||||||
|
|
||||||
/// Sales addresses
|
|
||||||
pub sales: Vec<String>,
|
|
||||||
|
|
||||||
/// Security addresses
|
|
||||||
pub security: Vec<String>,
|
|
||||||
|
|
||||||
/// Support addresses
|
|
||||||
pub support: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<DataForm> for ServerInfo {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(form: DataForm) -> Result<ServerInfo, Error> {
|
|
||||||
if form.type_ != DataFormType::Result_ {
|
|
||||||
return Err(Error::ParseError("Wrong type of form."));
|
|
||||||
}
|
|
||||||
if form.form_type != Some(String::from(ns::SERVER_INFO)) {
|
|
||||||
return Err(Error::ParseError("Wrong FORM_TYPE for form."));
|
|
||||||
}
|
|
||||||
let mut server_info = ServerInfo::default();
|
|
||||||
for field in form.fields {
|
|
||||||
if field.type_ != FieldType::ListMulti {
|
|
||||||
return Err(Error::ParseError("Field is not of the required type."));
|
|
||||||
}
|
|
||||||
if field.var == "abuse-addresses" {
|
|
||||||
server_info.abuse = field.values;
|
|
||||||
} else if field.var == "admin-addresses" {
|
|
||||||
server_info.admin = field.values;
|
|
||||||
} else if field.var == "feedback-addresses" {
|
|
||||||
server_info.feedback = field.values;
|
|
||||||
} else if field.var == "sales-addresses" {
|
|
||||||
server_info.sales = field.values;
|
|
||||||
} else if field.var == "security-addresses" {
|
|
||||||
server_info.security = field.values;
|
|
||||||
} else if field.var == "support-addresses" {
|
|
||||||
server_info.support = field.values;
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown form field var."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(server_info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ServerInfo> for DataForm {
|
|
||||||
fn from(server_info: ServerInfo) -> DataForm {
|
|
||||||
DataForm {
|
|
||||||
type_: DataFormType::Result_,
|
|
||||||
form_type: Some(String::from(ns::SERVER_INFO)),
|
|
||||||
title: None,
|
|
||||||
instructions: None,
|
|
||||||
fields: vec![
|
|
||||||
generate_address_field("abuse-addresses", server_info.abuse),
|
|
||||||
generate_address_field("admin-addresses", server_info.admin),
|
|
||||||
generate_address_field("feedback-addresses", server_info.feedback),
|
|
||||||
generate_address_field("sales-addresses", server_info.sales),
|
|
||||||
generate_address_field("security-addresses", server_info.security),
|
|
||||||
generate_address_field("support-addresses", server_info.support),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate `Field` for addresses
|
|
||||||
pub fn generate_address_field<S: Into<String>>(var: S, values: Vec<String>) -> Field {
|
|
||||||
Field {
|
|
||||||
var: var.into(),
|
|
||||||
type_: FieldType::ListMulti,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values,
|
|
||||||
media: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ServerInfo, 72);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ServerInfo, 144);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let form = DataForm {
|
|
||||||
type_: DataFormType::Result_,
|
|
||||||
form_type: Some(String::from(ns::SERVER_INFO)),
|
|
||||||
title: None,
|
|
||||||
instructions: None,
|
|
||||||
fields: vec![
|
|
||||||
Field {
|
|
||||||
var: String::from("abuse-addresses"),
|
|
||||||
type_: FieldType::ListMulti,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![],
|
|
||||||
media: vec![],
|
|
||||||
},
|
|
||||||
Field {
|
|
||||||
var: String::from("admin-addresses"),
|
|
||||||
type_: FieldType::ListMulti,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![
|
|
||||||
String::from("xmpp:admin@foo.bar"),
|
|
||||||
String::from("https://foo.bar/chat/"),
|
|
||||||
String::from("mailto:admin@foo.bar"),
|
|
||||||
],
|
|
||||||
media: vec![],
|
|
||||||
},
|
|
||||||
Field {
|
|
||||||
var: String::from("feedback-addresses"),
|
|
||||||
type_: FieldType::ListMulti,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![],
|
|
||||||
media: vec![],
|
|
||||||
},
|
|
||||||
Field {
|
|
||||||
var: String::from("sales-addresses"),
|
|
||||||
type_: FieldType::ListMulti,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![],
|
|
||||||
media: vec![],
|
|
||||||
},
|
|
||||||
Field {
|
|
||||||
var: String::from("security-addresses"),
|
|
||||||
type_: FieldType::ListMulti,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![
|
|
||||||
String::from("xmpp:security@foo.bar"),
|
|
||||||
String::from("mailto:security@foo.bar"),
|
|
||||||
],
|
|
||||||
media: vec![],
|
|
||||||
},
|
|
||||||
Field {
|
|
||||||
var: String::from("support-addresses"),
|
|
||||||
type_: FieldType::ListMulti,
|
|
||||||
label: None,
|
|
||||||
required: false,
|
|
||||||
options: vec![],
|
|
||||||
values: vec![String::from("mailto:support@foo.bar")],
|
|
||||||
media: vec![],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
let server_info = ServerInfo {
|
|
||||||
abuse: vec![],
|
|
||||||
admin: vec![
|
|
||||||
String::from("xmpp:admin@foo.bar"),
|
|
||||||
String::from("https://foo.bar/chat/"),
|
|
||||||
String::from("mailto:admin@foo.bar"),
|
|
||||||
],
|
|
||||||
feedback: vec![],
|
|
||||||
sales: vec![],
|
|
||||||
security: vec![
|
|
||||||
String::from("xmpp:security@foo.bar"),
|
|
||||||
String::from("mailto:security@foo.bar"),
|
|
||||||
],
|
|
||||||
support: vec![String::from("mailto:support@foo.bar")],
|
|
||||||
};
|
|
||||||
|
|
||||||
// assert_eq!(DataForm::from(server_info), form);
|
|
||||||
assert_eq!(ServerInfo::try_from(form).unwrap(), server_info);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,247 +0,0 @@
|
||||||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::stanza_error::DefinedCondition;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Acknowledgement of the currently received stanzas.
|
|
||||||
A, "a", SM,
|
|
||||||
attributes: [
|
|
||||||
/// The last handled stanza.
|
|
||||||
h: Required<u32> = "h",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl A {
|
|
||||||
/// Generates a new `<a/>` element.
|
|
||||||
pub fn new(h: u32) -> A {
|
|
||||||
A { h }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// Whether to allow resumption of a previous stream.
|
|
||||||
ResumeAttr,
|
|
||||||
"resume",
|
|
||||||
bool
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Client request for enabling stream management.
|
|
||||||
#[derive(Default)]
|
|
||||||
Enable, "enable", SM,
|
|
||||||
attributes: [
|
|
||||||
/// The preferred resumption time in seconds by the client.
|
|
||||||
// TODO: should be the infinite integer set ≥ 1.
|
|
||||||
max: Option<u32> = "max",
|
|
||||||
|
|
||||||
/// Whether the client wants to be allowed to resume the stream.
|
|
||||||
resume: Default<ResumeAttr> = "resume",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Enable {
|
|
||||||
/// Generates a new `<enable/>` element.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Enable::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the preferred resumption time in seconds.
|
|
||||||
pub fn with_max(mut self, max: u32) -> Self {
|
|
||||||
self.max = Some(max);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Asks for resumption to be possible.
|
|
||||||
pub fn with_resume(mut self) -> Self {
|
|
||||||
self.resume = ResumeAttr::True;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_id!(
|
|
||||||
/// A random identifier used for stream resumption.
|
|
||||||
StreamId
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Server response once stream management is enabled.
|
|
||||||
Enabled, "enabled", SM,
|
|
||||||
attributes: [
|
|
||||||
/// A random identifier used for stream resumption.
|
|
||||||
id: Option<StreamId> = "id",
|
|
||||||
|
|
||||||
/// The preferred IP, domain, IP:port or domain:port location for
|
|
||||||
/// resumption.
|
|
||||||
location: Option<String> = "location",
|
|
||||||
|
|
||||||
/// The preferred resumption time in seconds by the server.
|
|
||||||
// TODO: should be the infinite integer set ≥ 1.
|
|
||||||
max: Option<u32> = "max",
|
|
||||||
|
|
||||||
/// Whether stream resumption is allowed.
|
|
||||||
resume: Default<ResumeAttr> = "resume",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A stream management error happened.
|
|
||||||
Failed, "failed", SM,
|
|
||||||
attributes: [
|
|
||||||
/// The last handled stanza.
|
|
||||||
h: Option<u32> = "h",
|
|
||||||
],
|
|
||||||
children: [
|
|
||||||
/// The error returned.
|
|
||||||
// XXX: implement the * handling.
|
|
||||||
error: Option<DefinedCondition> = ("*", XMPP_STANZAS) => DefinedCondition
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Requests the currently received stanzas by the other party.
|
|
||||||
R,
|
|
||||||
"r",
|
|
||||||
SM
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Requests a stream resumption.
|
|
||||||
Resume, "resume", SM,
|
|
||||||
attributes: [
|
|
||||||
/// The last handled stanza.
|
|
||||||
h: Required<u32> = "h",
|
|
||||||
|
|
||||||
/// The previous id given by the server on
|
|
||||||
/// [enabled](struct.Enabled.html).
|
|
||||||
previd: Required<StreamId> = "previd",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The response by the server for a successfully resumed stream.
|
|
||||||
Resumed, "resumed", SM,
|
|
||||||
attributes: [
|
|
||||||
/// The last handled stanza.
|
|
||||||
h: Required<u32> = "h",
|
|
||||||
|
|
||||||
/// The previous id given by the server on
|
|
||||||
/// [enabled](struct.Enabled.html).
|
|
||||||
previd: Required<StreamId> = "previd",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: add support for optional and required.
|
|
||||||
generate_empty_element!(
|
|
||||||
/// Represents availability of Stream Management in `<stream:features/>`.
|
|
||||||
StreamManagement,
|
|
||||||
"sm",
|
|
||||||
SM
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(A, 4);
|
|
||||||
assert_size!(ResumeAttr, 1);
|
|
||||||
assert_size!(Enable, 12);
|
|
||||||
assert_size!(StreamId, 12);
|
|
||||||
assert_size!(Enabled, 36);
|
|
||||||
assert_size!(Failed, 12);
|
|
||||||
assert_size!(R, 0);
|
|
||||||
assert_size!(Resume, 16);
|
|
||||||
assert_size!(Resumed, 16);
|
|
||||||
assert_size!(StreamManagement, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(A, 4);
|
|
||||||
assert_size!(ResumeAttr, 1);
|
|
||||||
assert_size!(Enable, 12);
|
|
||||||
assert_size!(StreamId, 24);
|
|
||||||
assert_size!(Enabled, 64);
|
|
||||||
assert_size!(Failed, 12);
|
|
||||||
assert_size!(R, 0);
|
|
||||||
assert_size!(Resume, 32);
|
|
||||||
assert_size!(Resumed, 32);
|
|
||||||
assert_size!(StreamManagement, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a() {
|
|
||||||
let elem: Element = "<a xmlns='urn:xmpp:sm:3' h='5'".parse().unwrap();
|
|
||||||
let a = A::try_from(elem).unwrap();
|
|
||||||
assert_eq!(a.h, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stream_feature() {
|
|
||||||
let elem: Element = "<sm xmlns='urn:xmpp:sm:3'/>".parse().unwrap();
|
|
||||||
StreamManagement::try_from(elem).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resume() {
|
|
||||||
let elem: Element = "<enable xmlns='urn:xmpp:sm:3' resume='true'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let enable = Enable::try_from(elem).unwrap();
|
|
||||||
assert_eq!(enable.max, None);
|
|
||||||
assert_eq!(enable.resume, ResumeAttr::True);
|
|
||||||
|
|
||||||
let elem: Element = "<enabled xmlns='urn:xmpp:sm:3' resume='true' id='coucou' max='600'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let enabled = Enabled::try_from(elem).unwrap();
|
|
||||||
let previd = enabled.id.unwrap();
|
|
||||||
assert_eq!(enabled.resume, ResumeAttr::True);
|
|
||||||
assert_eq!(previd, StreamId(String::from("coucou")));
|
|
||||||
assert_eq!(enabled.max, Some(600));
|
|
||||||
assert_eq!(enabled.location, None);
|
|
||||||
|
|
||||||
let elem: Element = "<resume xmlns='urn:xmpp:sm:3' h='5' previd='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let resume = Resume::try_from(elem).unwrap();
|
|
||||||
assert_eq!(resume.h, 5);
|
|
||||||
assert_eq!(resume.previd, previd);
|
|
||||||
|
|
||||||
let elem: Element = "<resumed xmlns='urn:xmpp:sm:3' h='5' previd='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let resumed = Resumed::try_from(elem).unwrap();
|
|
||||||
assert_eq!(resumed.h, 5);
|
|
||||||
assert_eq!(resumed.previd, previd);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_failed() {
|
|
||||||
let reference: Element = "<failed xmlns='urn:xmpp:sm:3'><unexpected-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></failed>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let elem: Element = "<unexpected-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let error = DefinedCondition::try_from(elem).unwrap();
|
|
||||||
|
|
||||||
let failed = Failed {
|
|
||||||
h: None,
|
|
||||||
error: Some(error),
|
|
||||||
};
|
|
||||||
let serialized: Element = failed.into();
|
|
||||||
assert_eq!(serialized, reference);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,392 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
use crate::ns;
|
|
||||||
use crate::presence::PresencePayload;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::Jid;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
generate_attribute!(
|
|
||||||
/// The type of the error.
|
|
||||||
ErrorType, "type", {
|
|
||||||
/// Retry after providing credentials.
|
|
||||||
Auth => "auth",
|
|
||||||
|
|
||||||
/// Do not retry (the error cannot be remedied).
|
|
||||||
Cancel => "cancel",
|
|
||||||
|
|
||||||
/// Proceed (the condition was only a warning).
|
|
||||||
Continue => "continue",
|
|
||||||
|
|
||||||
/// Retry after changing the data sent.
|
|
||||||
Modify => "modify",
|
|
||||||
|
|
||||||
/// Retry after waiting (the error is temporary).
|
|
||||||
Wait => "wait",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
generate_element_enum!(
|
|
||||||
/// List of valid error conditions.
|
|
||||||
DefinedCondition, "condition", XMPP_STANZAS, {
|
|
||||||
/// The sender has sent a stanza containing XML that does not conform
|
|
||||||
/// to the appropriate schema or that cannot be processed (e.g., an IQ
|
|
||||||
/// stanza that includes an unrecognized value of the 'type' attribute,
|
|
||||||
/// or an element that is qualified by a recognized namespace but that
|
|
||||||
/// violates the defined syntax for the element); the associated error
|
|
||||||
/// type SHOULD be "modify".
|
|
||||||
BadRequest => "bad-request",
|
|
||||||
|
|
||||||
/// Access cannot be granted because an existing resource exists with
|
|
||||||
/// the same name or address; the associated error type SHOULD be
|
|
||||||
/// "cancel".
|
|
||||||
Conflict => "conflict",
|
|
||||||
|
|
||||||
/// The feature represented in the XML stanza is not implemented by the
|
|
||||||
/// intended recipient or an intermediate server and therefore the
|
|
||||||
/// stanza cannot be processed (e.g., the entity understands the
|
|
||||||
/// namespace but does not recognize the element name); the associated
|
|
||||||
/// error type SHOULD be "cancel" or "modify".
|
|
||||||
FeatureNotImplemented => "feature-not-implemented",
|
|
||||||
|
|
||||||
/// The requesting entity does not possess the necessary permissions to
|
|
||||||
/// perform an action that only certain authorized roles or individuals
|
|
||||||
/// are allowed to complete (i.e., it typically relates to
|
|
||||||
/// authorization rather than authentication); the associated error
|
|
||||||
/// type SHOULD be "auth".
|
|
||||||
Forbidden => "forbidden",
|
|
||||||
|
|
||||||
/// The recipient or server can no longer be contacted at this address,
|
|
||||||
/// typically on a permanent basis (as opposed to the <redirect/> error
|
|
||||||
/// condition, which is used for temporary addressing failures); the
|
|
||||||
/// associated error type SHOULD be "cancel" and the error stanza
|
|
||||||
/// SHOULD include a new address (if available) as the XML character
|
|
||||||
/// data of the <gone/> element (which MUST be a Uniform Resource
|
|
||||||
/// Identifier [URI] or Internationalized Resource Identifier [IRI] at
|
|
||||||
/// which the entity can be contacted, typically an XMPP IRI as
|
|
||||||
/// specified in [XMPP‑URI]).
|
|
||||||
Gone => "gone",
|
|
||||||
|
|
||||||
/// The server has experienced a misconfiguration or other internal
|
|
||||||
/// error that prevents it from processing the stanza; the associated
|
|
||||||
/// error type SHOULD be "cancel".
|
|
||||||
InternalServerError => "internal-server-error",
|
|
||||||
|
|
||||||
/// The addressed JID or item requested cannot be found; the associated
|
|
||||||
/// error type SHOULD be "cancel".
|
|
||||||
ItemNotFound => "item-not-found",
|
|
||||||
|
|
||||||
/// The sending entity has provided (e.g., during resource binding) or
|
|
||||||
/// communicated (e.g., in the 'to' address of a stanza) an XMPP
|
|
||||||
/// address or aspect thereof that violates the rules defined in
|
|
||||||
/// [XMPP‑ADDR]; the associated error type SHOULD be "modify".
|
|
||||||
JidMalformed => "jid-malformed",
|
|
||||||
|
|
||||||
/// The recipient or server understands the request but cannot process
|
|
||||||
/// it because the request does not meet criteria defined by the
|
|
||||||
/// recipient or server (e.g., a request to subscribe to information
|
|
||||||
/// that does not simultaneously include configuration parameters
|
|
||||||
/// needed by the recipient); the associated error type SHOULD be
|
|
||||||
/// "modify".
|
|
||||||
NotAcceptable => "not-acceptable",
|
|
||||||
|
|
||||||
/// The recipient or server does not allow any entity to perform the
|
|
||||||
/// action (e.g., sending to entities at a blacklisted domain); the
|
|
||||||
/// associated error type SHOULD be "cancel".
|
|
||||||
NotAllowed => "not-allowed",
|
|
||||||
|
|
||||||
/// The sender needs to provide credentials before being allowed to
|
|
||||||
/// perform the action, or has provided improper credentials (the name
|
|
||||||
/// "not-authorized", which was borrowed from the "401 Unauthorized"
|
|
||||||
/// error of [HTTP], might lead the reader to think that this condition
|
|
||||||
/// relates to authorization, but instead it is typically used in
|
|
||||||
/// relation to authentication); the associated error type SHOULD be
|
|
||||||
/// "auth".
|
|
||||||
NotAuthorized => "not-authorized",
|
|
||||||
|
|
||||||
/// The entity has violated some local service policy (e.g., a message
|
|
||||||
/// contains words that are prohibited by the service) and the server
|
|
||||||
/// MAY choose to specify the policy in the <text/> element or in an
|
|
||||||
/// application-specific condition element; the associated error type
|
|
||||||
/// SHOULD be "modify" or "wait" depending on the policy being
|
|
||||||
/// violated.
|
|
||||||
PolicyViolation => "policy-violation",
|
|
||||||
|
|
||||||
/// The intended recipient is temporarily unavailable, undergoing
|
|
||||||
/// maintenance, etc.; the associated error type SHOULD be "wait".
|
|
||||||
RecipientUnavailable => "recipient-unavailable",
|
|
||||||
|
|
||||||
/// The recipient or server is redirecting requests for this
|
|
||||||
/// information to another entity, typically in a temporary fashion (as
|
|
||||||
/// opposed to the <gone/> error condition, which is used for permanent
|
|
||||||
/// addressing failures); the associated error type SHOULD be "modify"
|
|
||||||
/// and the error stanza SHOULD contain the alternate address in the
|
|
||||||
/// XML character data of the <redirect/> element (which MUST be a URI
|
|
||||||
/// or IRI with which the sender can communicate, typically an XMPP IRI
|
|
||||||
/// as specified in [XMPP‑URI]).
|
|
||||||
Redirect => "redirect",
|
|
||||||
|
|
||||||
/// The requesting entity is not authorized to access the requested
|
|
||||||
/// service because prior registration is necessary (examples of prior
|
|
||||||
/// registration include members-only rooms in XMPP multi-user chat
|
|
||||||
/// [XEP‑0045] and gateways to non-XMPP instant messaging services,
|
|
||||||
/// which traditionally required registration in order to use the
|
|
||||||
/// gateway [XEP‑0100]); the associated error type SHOULD be "auth".
|
|
||||||
RegistrationRequired => "registration-required",
|
|
||||||
|
|
||||||
/// A remote server or service specified as part or all of the JID of
|
|
||||||
/// the intended recipient does not exist or cannot be resolved (e.g.,
|
|
||||||
/// there is no _xmpp-server._tcp DNS SRV record, the A or AAAA
|
|
||||||
/// fallback resolution fails, or A/AAAA lookups succeed but there is
|
|
||||||
/// no response on the IANA-registered port 5269); the associated error
|
|
||||||
/// type SHOULD be "cancel".
|
|
||||||
RemoteServerNotFound => "remote-server-not-found",
|
|
||||||
|
|
||||||
/// A remote server or service specified as part or all of the JID of
|
|
||||||
/// the intended recipient (or needed to fulfill a request) was
|
|
||||||
/// resolved but communications could not be established within a
|
|
||||||
/// reasonable amount of time (e.g., an XML stream cannot be
|
|
||||||
/// established at the resolved IP address and port, or an XML stream
|
|
||||||
/// can be established but stream negotiation fails because of problems
|
|
||||||
/// with TLS, SASL, Server Dialback, etc.); the associated error type
|
|
||||||
/// SHOULD be "wait" (unless the error is of a more permanent nature,
|
|
||||||
/// e.g., the remote server is found but it cannot be authenticated or
|
|
||||||
/// it violates security policies).
|
|
||||||
RemoteServerTimeout => "remote-server-timeout",
|
|
||||||
|
|
||||||
/// The server or recipient is busy or lacks the system resources
|
|
||||||
/// necessary to service the request; the associated error type SHOULD
|
|
||||||
/// be "wait".
|
|
||||||
ResourceConstraint => "resource-constraint",
|
|
||||||
|
|
||||||
/// The server or recipient does not currently provide the requested
|
|
||||||
/// service; the associated error type SHOULD be "cancel".
|
|
||||||
ServiceUnavailable => "service-unavailable",
|
|
||||||
|
|
||||||
/// The requesting entity is not authorized to access the requested
|
|
||||||
/// service because a prior subscription is necessary (examples of
|
|
||||||
/// prior subscription include authorization to receive presence
|
|
||||||
/// information as defined in [XMPP‑IM] and opt-in data feeds for XMPP
|
|
||||||
/// publish-subscribe as defined in [XEP‑0060]); the associated error
|
|
||||||
/// type SHOULD be "auth".
|
|
||||||
SubscriptionRequired => "subscription-required",
|
|
||||||
|
|
||||||
/// The error condition is not one of those defined by the other
|
|
||||||
/// conditions in this list; any error type can be associated with this
|
|
||||||
/// condition, and it SHOULD NOT be used except in conjunction with an
|
|
||||||
/// application-specific condition.
|
|
||||||
UndefinedCondition => "undefined-condition",
|
|
||||||
|
|
||||||
/// The recipient or server understood the request but was not
|
|
||||||
/// expecting it at this time (e.g., the request was out of order); the
|
|
||||||
/// associated error type SHOULD be "wait" or "modify".
|
|
||||||
UnexpectedRequest => "unexpected-request",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
type Lang = String;
|
|
||||||
|
|
||||||
/// The representation of a stanza error.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct StanzaError {
|
|
||||||
/// The type of this error.
|
|
||||||
pub type_: ErrorType,
|
|
||||||
|
|
||||||
/// The JID of the entity who set this error.
|
|
||||||
pub by: Option<Jid>,
|
|
||||||
|
|
||||||
/// One of the defined conditions for this error to happen.
|
|
||||||
pub defined_condition: DefinedCondition,
|
|
||||||
|
|
||||||
/// Human-readable description of this error.
|
|
||||||
pub texts: BTreeMap<Lang, String>,
|
|
||||||
|
|
||||||
/// A protocol-specific extension for this error.
|
|
||||||
pub other: Option<Element>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessagePayload for StanzaError {}
|
|
||||||
impl PresencePayload for StanzaError {}
|
|
||||||
|
|
||||||
impl StanzaError {
|
|
||||||
/// Create a new `<error/>` with the according content.
|
|
||||||
pub fn new<L, T>(
|
|
||||||
type_: ErrorType,
|
|
||||||
defined_condition: DefinedCondition,
|
|
||||||
lang: L,
|
|
||||||
text: T,
|
|
||||||
) -> StanzaError
|
|
||||||
where
|
|
||||||
L: Into<Lang>,
|
|
||||||
T: Into<String>,
|
|
||||||
{
|
|
||||||
StanzaError {
|
|
||||||
type_,
|
|
||||||
by: None,
|
|
||||||
defined_condition,
|
|
||||||
texts: {
|
|
||||||
let mut map = BTreeMap::new();
|
|
||||||
map.insert(lang.into(), text.into());
|
|
||||||
map
|
|
||||||
},
|
|
||||||
other: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for StanzaError {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<StanzaError, Error> {
|
|
||||||
check_self!(elem, "error", DEFAULT_NS);
|
|
||||||
check_no_unknown_attributes!(elem, "error", ["type", "by"]);
|
|
||||||
|
|
||||||
let mut stanza_error = StanzaError {
|
|
||||||
type_: get_attr!(elem, "type", Required),
|
|
||||||
by: get_attr!(elem, "by", Option),
|
|
||||||
defined_condition: DefinedCondition::UndefinedCondition,
|
|
||||||
texts: BTreeMap::new(),
|
|
||||||
other: None,
|
|
||||||
};
|
|
||||||
let mut defined_condition = None;
|
|
||||||
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("text", ns::XMPP_STANZAS) {
|
|
||||||
check_no_children!(child, "text");
|
|
||||||
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
|
|
||||||
let lang = get_attr!(elem, "xml:lang", Default);
|
|
||||||
if stanza_error.texts.insert(lang, child.text()).is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Text element present twice for the same xml:lang.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else if child.has_ns(ns::XMPP_STANZAS) {
|
|
||||||
if defined_condition.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Error must not have more than one defined-condition.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
check_no_children!(child, "defined-condition");
|
|
||||||
check_no_attributes!(child, "defined-condition");
|
|
||||||
let condition = DefinedCondition::try_from(child.clone())?;
|
|
||||||
defined_condition = Some(condition);
|
|
||||||
} else {
|
|
||||||
if stanza_error.other.is_some() {
|
|
||||||
return Err(Error::ParseError(
|
|
||||||
"Error must not have more than one other element.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
stanza_error.other = Some(child.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stanza_error.defined_condition =
|
|
||||||
defined_condition.ok_or(Error::ParseError("Error must have a defined-condition."))?;
|
|
||||||
|
|
||||||
Ok(stanza_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<StanzaError> for Element {
|
|
||||||
fn from(err: StanzaError) -> Element {
|
|
||||||
Element::builder("error", ns::DEFAULT_NS)
|
|
||||||
.attr("type", err.type_)
|
|
||||||
.attr("by", err.by)
|
|
||||||
.append(err.defined_condition)
|
|
||||||
.append_all(err.texts.into_iter().map(|(lang, text)| {
|
|
||||||
Element::builder("text", ns::XMPP_STANZAS)
|
|
||||||
.attr("xml:lang", lang)
|
|
||||||
.append(text)
|
|
||||||
}))
|
|
||||||
.append_all(err.other)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ErrorType, 1);
|
|
||||||
assert_size!(DefinedCondition, 1);
|
|
||||||
assert_size!(StanzaError, 132);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(ErrorType, 1);
|
|
||||||
assert_size!(DefinedCondition, 1);
|
|
||||||
assert_size!(StanzaError, 264);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<error xmlns='jabber:client' type='cancel'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>".parse().unwrap();
|
|
||||||
let error = StanzaError::try_from(elem).unwrap();
|
|
||||||
assert_eq!(error.type_, ErrorType::Cancel);
|
|
||||||
assert_eq!(
|
|
||||||
error.defined_condition,
|
|
||||||
DefinedCondition::UndefinedCondition
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_type() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<error xmlns='jabber:client'/>".parse().unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<error xmlns='jabber:component:accept'/>".parse().unwrap();
|
|
||||||
let error = StanzaError::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'type' missing.");
|
|
||||||
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<error xmlns='jabber:client' type='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<error xmlns='jabber:component:accept' type='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = StanzaError::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown value for 'type' attribute.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_condition() {
|
|
||||||
#[cfg(not(feature = "component"))]
|
|
||||||
let elem: Element = "<error xmlns='jabber:client' type='cancel'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
#[cfg(feature = "component")]
|
|
||||||
let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = StanzaError::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Error must have a defined-condition.");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,124 +0,0 @@
|
||||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::message::MessagePayload;
|
|
||||||
use jid::Jid;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// Gives the identifier a service has stamped on this stanza, often in
|
|
||||||
/// order to identify it inside of [an archive](../mam/index.html).
|
|
||||||
StanzaId, "stanza-id", SID,
|
|
||||||
attributes: [
|
|
||||||
/// The id associated to this stanza by another entity.
|
|
||||||
id: Required<String> = "id",
|
|
||||||
|
|
||||||
/// The entity who stamped this stanza-id.
|
|
||||||
by: Required<Jid> = "by",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for StanzaId {}
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// A hack for MUC before version 1.31 to track a message which may have
|
|
||||||
/// its 'id' attribute changed.
|
|
||||||
OriginId, "origin-id", SID,
|
|
||||||
attributes: [
|
|
||||||
/// The id this client set for this stanza.
|
|
||||||
id: Required<String> = "id",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl MessagePayload for OriginId {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use jid::BareJid;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(StanzaId, 52);
|
|
||||||
assert_size!(OriginId, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(StanzaId, 104);
|
|
||||||
assert_size!(OriginId, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0' id='coucou' by='coucou@coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let stanza_id = StanzaId::try_from(elem).unwrap();
|
|
||||||
assert_eq!(stanza_id.id, String::from("coucou"));
|
|
||||||
assert_eq!(stanza_id.by, BareJid::new("coucou", "coucou"));
|
|
||||||
|
|
||||||
let elem: Element = "<origin-id xmlns='urn:xmpp:sid:0' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let origin_id = OriginId::try_from(elem).unwrap();
|
|
||||||
assert_eq!(origin_id.id, String::from("coucou"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_child() {
|
|
||||||
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0'><coucou/></stanza-id>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = StanzaId::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Unknown child in stanza-id element.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_id() {
|
|
||||||
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0'/>".parse().unwrap();
|
|
||||||
let error = StanzaId::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'id' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_invalid_by() {
|
|
||||||
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0' id='coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let error = StanzaId::try_from(elem).unwrap_err();
|
|
||||||
let message = match error {
|
|
||||||
Error::ParseError(string) => string,
|
|
||||||
_ => panic!(),
|
|
||||||
};
|
|
||||||
assert_eq!(message, "Required attribute 'by' missing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialise() {
|
|
||||||
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0' id='coucou' by='coucou@coucou'/>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let stanza_id = StanzaId {
|
|
||||||
id: String::from("coucou"),
|
|
||||||
by: Jid::Bare(BareJid::new("coucou", "coucou")),
|
|
||||||
};
|
|
||||||
let elem2 = stanza_id.into();
|
|
||||||
assert_eq!(elem, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use jid::BareJid;
|
|
||||||
|
|
||||||
generate_element!(
|
|
||||||
/// The stream opening for client-server communications.
|
|
||||||
Stream, "stream", STREAM,
|
|
||||||
attributes: [
|
|
||||||
/// The JID of the entity opening this stream.
|
|
||||||
from: Option<BareJid> = "from",
|
|
||||||
|
|
||||||
/// The JID of the entity receiving this stream opening.
|
|
||||||
to: Option<BareJid> = "to",
|
|
||||||
|
|
||||||
/// The id of the stream, used for authentication challenges.
|
|
||||||
id: Option<String> = "id",
|
|
||||||
|
|
||||||
/// The XMPP version used during this stream.
|
|
||||||
version: Option<String> = "version",
|
|
||||||
|
|
||||||
/// The default human language for all subsequent stanzas, which will
|
|
||||||
/// be transmitted to other entities for better localisation.
|
|
||||||
xml_lang: Option<String> = "xml:lang",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Stream {
|
|
||||||
/// Creates a simple client→server `<stream:stream>` element.
|
|
||||||
pub fn new(to: BareJid) -> Stream {
|
|
||||||
Stream {
|
|
||||||
from: None,
|
|
||||||
to: Some(to),
|
|
||||||
id: None,
|
|
||||||
version: Some(String::from("1.0")),
|
|
||||||
xml_lang: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the [@from](#structfield.from) attribute on this `<stream:stream>`
|
|
||||||
/// element.
|
|
||||||
pub fn with_from(mut self, from: BareJid) -> Stream {
|
|
||||||
self.from = Some(from);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the [@id](#structfield.id) attribute on this `<stream:stream>`
|
|
||||||
/// element.
|
|
||||||
pub fn with_id(mut self, id: String) -> Stream {
|
|
||||||
self.id = Some(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the [@xml:lang](#structfield.xml_lang) attribute on this
|
|
||||||
/// `<stream:stream>` element.
|
|
||||||
pub fn with_lang(mut self, xml_lang: String) -> Stream {
|
|
||||||
self.xml_lang = Some(xml_lang);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks whether the version matches the expected one.
|
|
||||||
pub fn is_version(&self, version: &str) -> bool {
|
|
||||||
match self.version {
|
|
||||||
None => false,
|
|
||||||
Some(ref self_version) => self_version == &String::from(version),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::Element;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Stream, 84);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(Stream, 168);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_simple() {
|
|
||||||
let elem: Element = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' xml:lang='en' version='1.0' id='abc' from='some-server.example'/>".parse().unwrap();
|
|
||||||
let stream = Stream::try_from(elem).unwrap();
|
|
||||||
assert_eq!(stream.from, Some(BareJid::domain("some-server.example")));
|
|
||||||
assert_eq!(stream.to, None);
|
|
||||||
assert_eq!(stream.id, Some(String::from("abc")));
|
|
||||||
assert_eq!(stream.version, Some(String::from("1.0")));
|
|
||||||
assert_eq!(stream.xml_lang, Some(String::from("en")));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
|
||||||
//
|
|
||||||
// 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/.
|
|
||||||
|
|
||||||
use crate::date::DateTime;
|
|
||||||
use crate::iq::{IqGetPayload, IqResultPayload};
|
|
||||||
use crate::ns;
|
|
||||||
use crate::util::error::Error;
|
|
||||||
use crate::Element;
|
|
||||||
use chrono::FixedOffset;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
generate_empty_element!(
|
|
||||||
/// An entity time query.
|
|
||||||
TimeQuery,
|
|
||||||
"time",
|
|
||||||
TIME
|
|
||||||
);
|
|
||||||
|
|
||||||
impl IqGetPayload for TimeQuery {}
|
|
||||||
|
|
||||||
/// An entity time result, containing an unique DateTime.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TimeResult(pub DateTime);
|
|
||||||
|
|
||||||
impl IqResultPayload for TimeResult {}
|
|
||||||
|
|
||||||
impl TryFrom<Element> for TimeResult {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(elem: Element) -> Result<TimeResult, Error> {
|
|
||||||
check_self!(elem, "time", TIME);
|
|
||||||
check_no_attributes!(elem, "time");
|
|
||||||
|
|
||||||
let mut tzo = None;
|
|
||||||
let mut utc = None;
|
|
||||||
|
|
||||||
for child in elem.children() {
|
|
||||||
if child.is("tzo", ns::TIME) {
|
|
||||||
if tzo.is_some() {
|
|
||||||
return Err(Error::ParseError("More than one tzo element in time."));
|
|
||||||
}
|
|
||||||
check_no_children!(child, "tzo");
|
|
||||||
check_no_attributes!(child, "tzo");
|
|
||||||
// TODO: Add a FromStr implementation to FixedOffset to avoid this hack.
|
|
||||||
let fake_date = String::from("2019-04-22T11:38:00") + &child.text();
|
|
||||||
let date_time = DateTime::from_str(&fake_date)?;
|
|
||||||
tzo = Some(date_time.timezone());
|
|
||||||
} else if child.is("utc", ns::TIME) {
|
|
||||||
if utc.is_some() {
|
|
||||||
return Err(Error::ParseError("More than one utc element in time."));
|
|
||||||
}
|
|
||||||
check_no_children!(child, "utc");
|
|
||||||
check_no_attributes!(child, "utc");
|
|
||||||
let date_time = DateTime::from_str(&child.text())?;
|
|
||||||
if date_time.timezone() != FixedOffset::east(0) {
|
|
||||||
return Err(Error::ParseError("Non-UTC timezone for utc element."));
|
|
||||||
}
|
|
||||||
utc = Some(date_time);
|
|
||||||
} else {
|
|
||||||
return Err(Error::ParseError("Unknown child in time element."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tzo = tzo.ok_or(Error::ParseError("Missing tzo child in time element."))?;
|
|
||||||
let utc = utc.ok_or(Error::ParseError("Missing utc child in time element."))?;
|
|
||||||
let date = utc.with_timezone(tzo);
|
|
||||||
|
|
||||||
Ok(TimeResult(date))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TimeResult> for Element {
|
|
||||||
fn from(time: TimeResult) -> Element {
|
|
||||||
Element::builder("time", ns::TIME)
|
|
||||||
.append(Element::builder("tzo", ns::TIME).append(format!("{}", time.0.timezone())))
|
|
||||||
.append(
|
|
||||||
Element::builder("utc", ns::TIME)
|
|
||||||
.append(time.0.with_timezone(FixedOffset::east(0)).format("%FT%TZ")),
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
// DateTime’s size doesn’t depend on the architecture.
|
|
||||||
#[test]
|
|
||||||
fn test_size() {
|
|
||||||
assert_size!(TimeQuery, 0);
|
|
||||||
assert_size!(TimeResult, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_response() {
|
|
||||||
let elem: Element =
|
|
||||||
"<time xmlns='urn:xmpp:time'><tzo>-06:00</tzo><utc>2006-12-19T17:58:35Z</utc></time>"
|
|
||||||
.parse()
|
|
||||||
.unwrap();
|
|
||||||
let elem1 = elem.clone();
|
|
||||||
let time = TimeResult::try_from(elem).unwrap();
|
|
||||||
assert_eq!(time.0.timezone(), FixedOffset::west(6 * 3600));
|
|
||||||
assert_eq!(
|
|
||||||
time.0,
|
|
||||||
DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap()
|
|
||||||
);
|
|
||||||
let elem2 = Element::from(time);
|
|
||||||
assert_eq!(elem1, elem2);
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue