xmpp-parsers: Add In-Band Real Time Text support

Emmanuel Gil Peyrot created

Change summary

parsers/doap.xml   |   8 +
parsers/src/lib.rs |   3 
parsers/src/ns.rs  |   3 
parsers/src/rtt.rs | 262 ++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 276 insertions(+)

Detailed changes

parsers/doap.xml 🔗

@@ -411,6 +411,14 @@
             <xmpp:since>0.1.0</xmpp:since>
         </xmpp:SupportedXep>
     </implements>
+    <implements>
+        <xmpp:SupportedXep>
+            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0301.html"/>
+            <xmpp:status>complete</xmpp:status>
+            <xmpp:version>1.0</xmpp:version>
+            <xmpp:since>NEXT</xmpp:since>
+        </xmpp:SupportedXep>
+    </implements>
     <implements>
         <xmpp:SupportedXep>
             <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/>

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;
 

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";
 

parsers/src/rtt.rs 🔗

@@ -0,0 +1,262 @@
+// Copyright (c) 2022 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::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<u32> = "p",
+    ],
+    text: (
+        text: PlainText<Option<String>>
+    )
+);
+
+impl TryFrom<Action> for Insert {
+    type Error = Error;
+
+    fn try_from(action: Action) -> Result<Insert, Error> {
+        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<u32> = "p",
+        num: Default<u32> = "n",
+    ]
+);
+*/
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Erase {
+    pub pos: Option<u32>,
+    pub num: u32,
+}
+
+impl TryFrom<Element> for Erase {
+    type Error = Error;
+    fn try_from(elem: Element) -> Result<Erase, Error> {
+        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<Erase> for Element {
+    fn from(elem: Erase) -> Element {
+        Element::builder("e", ns::RTT)
+            .attr("p", elem.pos)
+            .attr("n", elem.num)
+            .build()
+    }
+}
+
+impl TryFrom<Action> for Erase {
+    type Error = Error;
+
+    fn try_from(action: Action) -> Result<Erase, Error> {
+        match action {
+            Action::Erase(erase) => Ok(erase),
+            _ => Err(Error::ParseError("This is not an erase action.")),
+        }
+    }
+}
+
+generate_element!(
+    Wait,
+    "w",
+    RTT,
+    attributes: [
+        time: Required<u32> = "n",
+    ]
+);
+
+impl TryFrom<Action> for Wait {
+    type Error = Error;
+
+    fn try_from(action: Action) -> Result<Wait, Error> {
+        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<Element> for Action {
+    type Error = Error;
+
+    fn try_from(elem: Element) -> Result<Action, Error> {
+        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<Action> 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<u32> = "seq",
+        event: Default<Event> = "event",
+        id: Option<String> = "id",
+    ],
+    children: [
+        actions: Vec<Action> = (*, RTT) => Action,
+    ]
+);
+*/
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Rtt {
+    pub seq: u32,
+    pub event: Event,
+    pub id: Option<String>,
+    pub actions: Vec<Action>,
+}
+
+impl TryFrom<Element> for Rtt {
+    type Error = Error;
+    fn try_from(elem: Element) -> Result<Rtt, Error> {
+        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<Rtt> 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 = "<rtt xmlns='urn:xmpp:rtt:0' seq='0'/>".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 = "<rtt xmlns='urn:xmpp:rtt:0' seq='0' event='new'><t>Hello,</t><w n='50'/><e/><t>!</t></rtt>"
+            .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("!")));
+    }
+}