From e076ba52e29a6a44b098312d85ed666d4d3b87c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Tue, 22 Mar 2022 16:01:47 +0100 Subject: [PATCH] xmpp-parsers: Add In-Band Real Time Text support --- parsers/doap.xml | 8 ++ parsers/src/lib.rs | 3 + parsers/src/ns.rs | 3 + parsers/src/rtt.rs | 262 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 276 insertions(+) create mode 100644 parsers/src/rtt.rs diff --git a/parsers/doap.xml b/parsers/doap.xml index 57da3fa2..125ad858 100644 --- a/parsers/doap.xml +++ b/parsers/doap.xml @@ -411,6 +411,14 @@ 0.1.0 + + + + complete + 1.0 + NEXT + + diff --git a/parsers/src/lib.rs b/parsers/src/lib.rs index 8b85cd13..f17727a7 100644 --- a/parsers/src/lib.rs +++ b/parsers/src/lib.rs @@ -180,6 +180,9 @@ pub mod forwarding; /// XEP-0300: Use of Cryptographic Hash Functions in XMPP pub mod hashes; +/// XEP-0301: In-Band Real Time Text +pub mod rtt; + /// XEP-0308: Last Message Correction pub mod message_correct; diff --git a/parsers/src/ns.rs b/parsers/src/ns.rs index 00f8ce02..cfb16d72 100644 --- a/parsers/src/ns.rs +++ b/parsers/src/ns.rs @@ -189,6 +189,9 @@ pub const HASH_ALGO_BLAKE2B_256: &str = "urn:xmpp:hash-function-text-names:id-bl /// 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-0301: In-Band Real Time Text +pub const RTT: &str = "urn:xmpp:rtt:0"; + /// XEP-0308: Last Message Correction pub const MESSAGE_CORRECT: &str = "urn:xmpp:message-correct:0"; diff --git a/parsers/src/rtt.rs b/parsers/src/rtt.rs new file mode 100644 index 00000000..69e8be18 --- /dev/null +++ b/parsers/src/rtt.rs @@ -0,0 +1,262 @@ +// Copyright (c) 2022 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ns; +use crate::util::error::Error; +use crate::util::helpers::PlainText; +use crate::Element; +use std::convert::TryFrom; + +generate_attribute!( + Event, "event", { + New => "new", + Reset => "reset", + Edit => "edit", + Init => "init", + Cancel => "cancel", + }, Default = Edit +); + +generate_element!( + Insert, + "t", + RTT, + attributes: [ + pos: Option = "p", + ], + text: ( + text: PlainText> + ) +); + +impl TryFrom for Insert { + type Error = Error; + + fn try_from(action: Action) -> Result { + match action { + Action::Insert(insert) => Ok(insert), + _ => Err(Error::ParseError("This is not an insert action.")), + } + } +} + +// TODO: add a way in the macro to set a default value. +/* +generate_element!( + Erase, + "e", + RTT, + attributes: [ + pos: Option = "p", + num: Default = "n", + ] +); +*/ + +#[derive(Debug, Clone, PartialEq)] +pub struct Erase { + pub pos: Option, + pub num: u32, +} + +impl TryFrom for Erase { + type Error = Error; + fn try_from(elem: Element) -> Result { + check_self!(elem, "e", RTT); + check_no_unknown_attributes!(elem, "e", ["p", "n"]); + let pos = get_attr!(elem, "p", Option); + let num = get_attr!(elem, "n", Option).unwrap_or(1); + check_no_children!(elem, "e"); + Ok(Erase { pos, num }) + } +} + +impl From for Element { + fn from(elem: Erase) -> Element { + Element::builder("e", ns::RTT) + .attr("p", elem.pos) + .attr("n", elem.num) + .build() + } +} + +impl TryFrom for Erase { + type Error = Error; + + fn try_from(action: Action) -> Result { + match action { + Action::Erase(erase) => Ok(erase), + _ => Err(Error::ParseError("This is not an erase action.")), + } + } +} + +generate_element!( + Wait, + "w", + RTT, + attributes: [ + time: Required = "n", + ] +); + +impl TryFrom for Wait { + type Error = Error; + + fn try_from(action: Action) -> Result { + match action { + Action::Wait(wait) => Ok(wait), + _ => Err(Error::ParseError("This is not a wait action.")), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Action { + Insert(Insert), + Erase(Erase), + Wait(Wait), +} + +impl TryFrom for Action { + type Error = Error; + + fn try_from(elem: Element) -> Result { + match elem.name() { + "t" => Insert::try_from(elem).map(|insert| Action::Insert(insert)), + "e" => Erase::try_from(elem).map(|erase| Action::Erase(erase)), + "w" => Wait::try_from(elem).map(|wait| Action::Wait(wait)), + _ => Err(Error::ParseError("This is not a rtt action element.")), + } + } +} + +impl From for Element { + fn from(action: Action) -> Element { + match action { + Action::Insert(insert) => Element::from(insert), + Action::Erase(erase) => Element::from(erase), + Action::Wait(wait) => Element::from(wait), + } + } +} + +// TODO: Allow a wildcard name to the macro, to simplify the following code: +/* +generate_element!( + Rtt, "rtt", RTT, + attributes: [ + seq: Required = "seq", + event: Default = "event", + id: Option = "id", + ], + children: [ + actions: Vec = (*, RTT) => Action, + ] +); +*/ + +#[derive(Debug, Clone, PartialEq)] +pub struct Rtt { + pub seq: u32, + pub event: Event, + pub id: Option, + pub actions: Vec, +} + +impl TryFrom for Rtt { + type Error = Error; + fn try_from(elem: Element) -> Result { + check_self!(elem, "rtt", RTT); + + check_no_unknown_attributes!(elem, "rtt", ["seq", "event", "id"]); + let seq = get_attr!(elem, "seq", Required); + let event = get_attr!(elem, "event", Default); + let id = get_attr!(elem, "id", Option); + + let mut actions = Vec::new(); + for child in elem.children() { + if child.ns() != ns::RTT { + return Err(Error::ParseError("Unknown child in rtt element.")); + } + actions.push(Action::try_from(child.clone())?); + } + + Ok(Rtt { + seq, + event, + id, + actions: actions, + }) + } +} + +impl From for Element { + fn from(elem: Rtt) -> Element { + Element::builder("rtt", ns::RTT) + .attr("seq", elem.seq) + .attr("event", elem.event) + .attr("id", elem.id) + .append_all(elem.actions) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::convert::TryInto; + + #[test] + fn test_size() { + assert_size!(Event, 1); + assert_size!(Insert, 32); + assert_size!(Erase, 12); + assert_size!(Wait, 4); + assert_size!(Action, 40); + assert_size!(Rtt, 56); + } + + #[test] + fn simple() { + let elem: Element = "".parse().unwrap(); + let rtt = Rtt::try_from(elem).unwrap(); + assert_eq!(rtt.seq, 0); + assert_eq!(rtt.event, Event::Edit); + assert_eq!(rtt.id, None); + assert_eq!(rtt.actions.len(), 0); + } + + #[test] + fn sequence() { + let elem: Element = "Hello,!" + .parse() + .unwrap(); + + let rtt = Rtt::try_from(elem).unwrap(); + assert_eq!(rtt.seq, 0); + assert_eq!(rtt.event, Event::New); + assert_eq!(rtt.id, None); + + let mut actions = rtt.actions.into_iter(); + assert_eq!(actions.len(), 4); + + let t: Insert = actions.next().unwrap().try_into().unwrap(); + assert_eq!(t.pos, None); + assert_eq!(t.text, Some(String::from("Hello,"))); + + let w: Wait = actions.next().unwrap().try_into().unwrap(); + assert_eq!(w.time, 50); + + let e: Erase = actions.next().unwrap().try_into().unwrap(); + assert_eq!(e.pos, None); + assert_eq!(e.num, 1); + + let t: Insert = actions.next().unwrap().try_into().unwrap(); + assert_eq!(t.pos, None); + assert_eq!(t.text, Some(String::from("!"))); + } +}