@@ -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
@@ -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"/>
@@ -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;
@@ -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";
@@ -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);
+ }
+}