xmpp-parsers: Add Message Reactions (XEP-0444) support

Emmanuel Gil Peyrot created

Change summary

parsers/ChangeLog        | 12 +++--
parsers/src/lib.rs       |  3 +
parsers/src/ns.rs        |  3 +
parsers/src/reactions.rs | 96 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 109 insertions(+), 5 deletions(-)

Detailed changes

parsers/ChangeLog πŸ”—

@@ -1,8 +1,10 @@
 Version xxx:
 xxx
+    * New parsers/serialisers:
+        - Message Reactions (XEP-0444)
     * Improvements:
-      - muc::user::Item: Added with_ helpers
-      - Correct cargo doc warnings
+        - muc::user::Item: Added with_ helpers
+        - Correct cargo doc warnings
 
 Version 0.19.2:
 2022-12-17  Maxime β€œpep” Buquet <pep@bouah.net>
@@ -17,10 +19,10 @@ Version 0.19.2:
 Version 0.19.1:
 2022-07-13  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
     * New parsers/serialisers:
-      - Add In-Band Real Time Text support
-      - Add OMEMO support
+        - Add In-Band Real Time Text support
+        - Add OMEMO support
     * Improvements:
-      - bookmarks 2: uncomment test
+        - bookmarks 2: uncomment test
 
 Version 0.19.0:
 2022-03-07  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>

parsers/src/lib.rs πŸ”—

@@ -239,3 +239,6 @@ pub mod occupant_id;
 
 /// XEP-0441: Message Archive Management Preferences
 pub mod mam_prefs;
+
+/// XEP-0444: Message Reactions
+pub mod reactions;

parsers/src/ns.rs πŸ”—

@@ -272,6 +272,9 @@ pub const BOOKMARKS2_COMPAT_PEP: &str = "urn:xmpp:bookmarks:1#compat-pep";
 /// XEP-0421: Anonymous unique occupant identifiers for MUCs
 pub const OID: &str = "urn:xmpp:occupant-id:0";
 
+/// XEP-0444: Message Reactions
+pub const REACTIONS: &str = "urn:xmpp:reactions:0";
+
 /// Alias for the main namespace of the stream, that is "jabber:client" when
 /// the component feature isn’t enabled.
 #[cfg(not(feature = "component"))]

parsers/src/reactions.rs πŸ”—

@@ -0,0 +1,96 @@
+// 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::message::MessagePayload;
+use crate::util::helpers::Text;
+
+generate_element!(
+    /// Container for a set of reactions.
+    Reactions, "reactions", REACTIONS,
+    attributes: [
+        /// The id of the message these reactions apply to.
+        id: Required<String> = "id",
+    ],
+    children: [
+        /// The list of reactions.
+        reactions: Vec<Reaction> = ("reaction", REACTIONS) => Reaction,
+    ]
+);
+
+impl MessagePayload for Reactions {}
+
+generate_element!(
+    /// One emoji reaction.
+    Reaction, "reaction", REACTIONS,
+    text: (
+        /// The text of this reaction.
+        emoji: Text<String>
+    )
+);
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::Element;
+    use std::convert::{TryFrom, TryInto};
+
+    #[cfg(target_pointer_width = "32")]
+    #[test]
+    fn test_size() {
+        assert_size!(Reactions, 24);
+        assert_size!(Reaction, 12);
+    }
+
+    #[cfg(target_pointer_width = "64")]
+    #[test]
+    fn test_size() {
+        assert_size!(Reactions, 48);
+        assert_size!(Reaction, 24);
+    }
+
+    #[test]
+    fn test_empty() {
+        let elem: Element = "<reactions xmlns='urn:xmpp:reactions:0' id='foo'/>"
+            .parse()
+            .unwrap();
+        let elem2 = elem.clone();
+        let reactions = Reactions::try_from(elem2).unwrap();
+        assert_eq!(reactions.id, "foo");
+        assert_eq!(reactions.reactions.len(), 0);
+    }
+
+    #[test]
+    fn test_multi() {
+        let elem: Element =
+            "<reactions xmlns='urn:xmpp:reactions:0' id='foo'><reaction>πŸ‘‹</reaction><reaction>🐒</reaction></reactions>"
+                .parse()
+                .unwrap();
+        let elem2 = elem.clone();
+        let reactions = Reactions::try_from(elem2).unwrap();
+        assert_eq!(reactions.id, "foo");
+        assert_eq!(reactions.reactions.len(), 2);
+        let [hand, turtle]: [Reaction; 2] = reactions.reactions.try_into().unwrap();
+        assert_eq!(hand.emoji, "πŸ‘‹");
+        assert_eq!(turtle.emoji, "🐒");
+    }
+
+    #[test]
+    fn test_zwj_emoji() {
+        let elem: Element =
+            "<reactions xmlns='urn:xmpp:reactions:0' id='foo'><reaction>πŸ‘©πŸΎβ€β€οΈβ€πŸ‘©πŸΌ</reaction></reactions>"
+                .parse()
+                .unwrap();
+        let elem2 = elem.clone();
+        let mut reactions = Reactions::try_from(elem2).unwrap();
+        assert_eq!(reactions.id, "foo");
+        assert_eq!(reactions.reactions.len(), 1);
+        let reaction = reactions.reactions.pop().unwrap();
+        assert_eq!(
+            reaction.emoji,
+            "\u{1F469}\u{1F3FE}\u{200D}\u{2764}\u{FE0F}\u{200D}\u{1F469}\u{1F3FC}"
+        );
+    }
+}