xmpp_parsers: Add Push Notifications (XEP-0357)

Saarko created

Add support for Push Notification parsing according to https://xmpp.org/extensions/xep-0357.html version 0.4.1.

Change summary

parsers/ChangeLog   |   1 
parsers/doap.xml    |   8 ++
parsers/src/lib.rs  |   3 
parsers/src/ns.rs   |   3 
parsers/src/push.rs | 169 +++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 184 insertions(+)

Detailed changes

parsers/ChangeLog 🔗

@@ -73,6 +73,7 @@ XXXX-YY-ZZ RELEASER <admin@example.com>
       - Message Displayed Synchronization (XEP-0490)
       - RFC 6120 stream errors
       - XEP-0045 mediated invites
+      - Push Notifications (XEP-0357) (!543)
     * Improvements:
       - Add support for `<optional/> in XEP-0198 feature advertisment
       - Add support application-specific error conditions in XEP-0198

parsers/doap.xml 🔗

@@ -546,6 +546,14 @@
             <xmpp:since>0.7.0</xmpp:since>
         </xmpp:SupportedXep>
     </implements>
+    <implements>
+        <xmpp:SupportedXep>
+            <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0357.html"/>
+            <xmpp:status>complete</xmpp:status>
+            <xmpp:version>0.4.1</xmpp:version>
+            <xmpp:since>NEXT</xmpp:since>
+        </xmpp:SupportedXep>
+    </implements>
     <implements>
         <xmpp:SupportedXep>
             <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html"/>

parsers/src/lib.rs 🔗

@@ -245,6 +245,9 @@ pub mod csi;
 /// XEP-0353: Jingle Message Initiation
 pub mod jingle_message;
 
+/// XEP-0357: Push Notifications
+pub mod push;
+
 /// XEP-0359: Unique and Stable Stanza IDs
 pub mod stanza_id;
 

parsers/src/ns.rs 🔗

@@ -237,6 +237,9 @@ pub const CSI: &str = "urn:xmpp:csi:0";
 /// XEP-0353: Jingle Message Initiation
 pub const JINGLE_MESSAGE: &str = "urn:xmpp:jingle-message:0";
 
+/// XEP-0357: Push Notifications
+pub const PUSH: &str = "urn:xmpp:push:0";
+
 /// XEP-0359: Unique and Stable Stanza IDs
 pub const SID: &str = "urn:xmpp:sid:0";
 

parsers/src/push.rs 🔗

@@ -0,0 +1,169 @@
+// Copyright (c) 2025 saarko <saarko@tutanota.com>
+//
+// 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::IqSetPayload;
+use crate::jid::BareJid;
+use crate::ns;
+use crate::pubsub::PubSubPayload;
+use minidom::Element;
+use xso::{AsXml, FromXml};
+
+/// An enable element for push notifications
+#[derive(Debug, Clone, FromXml, AsXml)]
+#[xml(namespace = ns::PUSH, name = "enable")]
+pub struct Enable {
+    /// The 'jid' attribute of the XMPP Push Service being enabled.
+    #[xml(attribute)]
+    pub jid: BareJid,
+
+    /// The 'node' attribute which is set to the provisioned node specified by the App Server.
+    #[xml(attribute(default))]
+    pub node: Option<String>,
+
+    /// Optional additional information to be provided with each published notification, such as authentication credentials.
+    #[xml(child(default))]
+    pub form: Option<DataForm>,
+}
+
+impl IqSetPayload for Enable {}
+
+/// A disable element for push notifications
+#[derive(Debug, Clone, FromXml, AsXml)]
+#[xml(namespace = ns::PUSH, name = "disable")]
+pub struct Disable {
+    /// The 'jid' attribute of the XMPP Push Service being disabled.
+    #[xml(attribute)]
+    pub jid: BareJid,
+
+    /// The 'node' attribute which was set to the provisioned node specified by the App Server.
+    #[xml(attribute(default))]
+    pub node: Option<String>,
+}
+
+impl IqSetPayload for Disable {}
+
+/// A notification element containing push notification data
+#[derive(Debug, Clone, FromXml, AsXml)]
+#[xml(namespace = ns::PUSH, name = "notification")]
+pub struct Notification {
+    /// The 'form' to provide summarized information such as the number of unread messages or number of pending subscription requests.
+    #[xml(child(default))]
+    pub form: Option<DataForm>,
+
+    /// Child elements for the notification that are not part of the summary data form.
+    #[xml(element(n = ..))]
+    pub payloads: Vec<Element>,
+}
+
+impl PubSubPayload for Notification {}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::str::FromStr;
+
+    #[test]
+    fn test_enable() {
+        let test_enable = r#"<enable xmlns='urn:xmpp:push:0'
+            jid='push-5.client.example'
+            node='yxs32uqsflafdk3iuqo'>
+            <x xmlns='jabber:x:data' type='submit'>
+                <field var='FORM_TYPE'><value>http://jabber.org/protocol/pubsub#publish-options</value></field>
+                <field var='secret'><value>eruio234vzxc2kla-91</value></field>
+            </x>
+        </enable>"#;
+
+        let elem = Element::from_str(test_enable).expect("Failed to parse XML");
+        let enable = Enable::try_from(elem).expect("Failed to parse enable");
+
+        assert_eq!(
+            enable.jid,
+            BareJid::from_str("push-5.client.example").unwrap()
+        );
+        assert_eq!(enable.node.unwrap(), "yxs32uqsflafdk3iuqo");
+        assert!(enable.form.is_some());
+    }
+
+    #[test]
+    fn test_enable_only_with_required_fields() {
+        let test_enable = r#"<enable xmlns='urn:xmpp:push:0'
+            jid='push-5.client.example' />"#;
+
+        let elem = Element::from_str(test_enable).expect("Failed to parse XML");
+        let enable = Enable::try_from(elem).expect("Failed to parse enable");
+
+        assert_eq!(
+            enable.jid,
+            BareJid::from_str("push-5.client.example").unwrap()
+        );
+        assert!(enable.node.is_none());
+        assert!(enable.form.is_none());
+    }
+
+    #[test]
+    fn test_disable() {
+        let test_disable = r#"<disable xmlns='urn:xmpp:push:0'
+            jid='push-5.client.example'
+            node='yxs32uqsflafdk3iuqo' />"#;
+
+        let elem = Element::from_str(test_disable).expect("Failed to parse XML");
+        let disable = Disable::try_from(elem).expect("Failed to parse disable");
+
+        assert_eq!(
+            disable.jid,
+            BareJid::from_str("push-5.client.example").unwrap()
+        );
+        assert_eq!(disable.node.unwrap(), "yxs32uqsflafdk3iuqo");
+    }
+
+    #[test]
+    fn test_disable_only_with_required_fields() {
+        let test_disable = r#"<disable xmlns='urn:xmpp:push:0'
+            jid='push-5.client.example' />"#;
+
+        let elem = Element::from_str(test_disable).expect("Failed to parse XML");
+        let disable = Disable::try_from(elem).expect("Failed to parse disable");
+
+        assert_eq!(
+            disable.jid,
+            BareJid::from_str("push-5.client.example").unwrap()
+        );
+        assert!(disable.node.is_none());
+    }
+
+    #[test]
+    fn test_notification() {
+        let test_notification = r#"<notification xmlns='urn:xmpp:push:0'>
+            <x xmlns='jabber:x:data' type='result'>
+                <field var='FORM_TYPE'><value>urn:xmpp:push:summary</value></field>
+                <field var='message-count'><value>1</value></field>
+                <field var='last-message-sender'><value>juliet@capulet.example/balcony</value></field>
+                <field var='last-message-body'><value>Wherefore art thou, Romeo?</value></field>
+            </x>
+            <additional xmlns='http://example.com/custom'>Additional custom elements</additional>
+        </notification>"#;
+
+        let elem = Element::from_str(test_notification).expect("Failed to parse XML");
+        let notification = Notification::try_from(elem).expect("Failed to parse notification");
+
+        assert!(notification.form.is_some());
+        assert_eq!(notification.payloads.len(), 1);
+    }
+
+    #[test]
+    fn test_notification_only_with_required_fields() {
+        let test_notification = r#"<notification xmlns='urn:xmpp:push:0'>
+            <additional xmlns='http://example.com/custom'>Additional custom elements</additional>
+        </notification>"#;
+
+        let elem = Element::from_str(test_notification).expect("Failed to parse XML");
+        let notification = Notification::try_from(elem).expect("Failed to parse notification");
+
+        assert!(notification.form.is_none());
+        assert_eq!(notification.payloads.len(), 1);
+    }
+}