git » sdk » commit f87f7b8

Helpers for push notifications

author Stephen Paul Weber
2023-09-25 17:08:59 UTC
committer Stephen Paul Weber
2023-09-25 17:10:23 UTC
parent d6ca04c9c62429db89a3439685a35693b56f423c

Helpers for push notifications

xmpp/ChatMessage.hx +11 -2
xmpp/Client.hx +40 -4
xmpp/Notification.hx +60 -0
xmpp/Push.hx +29 -0
xmpp/queries/Push2Enable.hx +50 -0
xmpp/streams/XmppJsStream.hx +5 -1

diff --git a/xmpp/ChatMessage.hx b/xmpp/ChatMessage.hx
index 9b149f5..ea24a27 100644
--- a/xmpp/ChatMessage.hx
+++ b/xmpp/ChatMessage.hx
@@ -27,9 +27,10 @@ class ChatMessage {
 	var threadId (default, null): String = null;
 	var replyTo (default, null): String = null;
 
-	var attachments : Array<ChatAttachment> = null;
+	public var attachments : Array<ChatAttachment> = [];
 
 	public var text (default, null): String = null;
+	public var lang (default, null): Null<String> = null;
 
 	private var direction:MessageDirection = null;
 
@@ -37,7 +38,11 @@ class ChatMessage {
 
 	public static function fromStanza(stanza:Stanza, localJidStr:String):Null<ChatMessage> {
 		var msg = new ChatMessage();
+		msg.lang = stanza.attr.get("xml:lang");
 		msg.text = stanza.getChildText("body");
+		if (msg.text != null && (msg.lang == null || msg.lang == "")) {
+			msg.lang = stanza.getChild("body").attr.get("xml:lang");
+		}
 		msg.to = stanza.attr.get("to");
 		msg.from = stanza.attr.get("from");
 		final localJid = JID.parse(localJidStr);
@@ -85,7 +90,11 @@ class ChatMessage {
 	}
 
 	public function conversation():String {
-		return direction == MessageReceived ? JID.parse(from).asBare().asString() : JID.parse(to).asBare().asString();
+		return isIncoming() ? JID.parse(from).asBare().asString() : JID.parse(to).asBare().asString();
+	}
+
+	public function account():String {
+		return !isIncoming() ? JID.parse(from).asBare().asString() : JID.parse(to).asBare().asString();
 	}
 
 	public function isIncoming():Bool {
diff --git a/xmpp/Client.hx b/xmpp/Client.hx
index b735c52..730dbcd 100644
--- a/xmpp/Client.hx
+++ b/xmpp/Client.hx
@@ -6,13 +6,15 @@ import haxe.io.BytesData;
 import xmpp.Caps;
 import xmpp.Chat;
 import xmpp.EventEmitter;
+import xmpp.EventHandler;
+import xmpp.PubsubEvent;
 import xmpp.Stream;
-import xmpp.queries.GenericQuery;
-import xmpp.queries.RosterGet;
-import xmpp.queries.PubsubGet;
 import xmpp.queries.DiscoInfoGet;
+import xmpp.queries.GenericQuery;
 import xmpp.queries.JabberIqGatewayGet;
-import xmpp.PubsubEvent;
+import xmpp.queries.PubsubGet;
+import xmpp.queries.Push2Enable;
+import xmpp.queries.RosterGet;
 
 typedef ChatList = Array<Chat>;
 
@@ -266,6 +268,40 @@ class Client extends xmpp.EventEmitter {
 		stream.sendStanza(stanza);
 	}
 
+	#if js
+	public function subscribePush(reg: js.html.ServiceWorkerRegistration, push_service: String, vapid_key: { publicKey: js.html.CryptoKey, privateKey: js.html.CryptoKey}) {
+		js.Browser.window.crypto.subtle.exportKey("raw", vapid_key.publicKey).then((vapid_public_raw) -> {
+			reg.pushManager.subscribe(untyped {
+				userVisibleOnly: true,
+				applicationServerKey: vapid_public_raw
+			}).then((pushSubscription) -> {
+				enablePush(
+					push_service,
+					vapid_key.privateKey,
+					pushSubscription.endpoint,
+					pushSubscription.getKey(js.html.push.PushEncryptionKeyName.P256DH),
+					pushSubscription.getKey(js.html.push.PushEncryptionKeyName.AUTH)
+				);
+			});
+		});
+	}
+
+	public function enablePush(push_service: String, vapid_private_key: js.html.CryptoKey, endpoint: String, p256dh: BytesData, auth: BytesData) {
+		js.Browser.window.crypto.subtle.exportKey("pkcs8", vapid_private_key).then((vapid_private_pkcs8) -> {
+			sendQuery(new Push2Enable(
+				jid,
+				push_service,
+				endpoint,
+				Bytes.ofData(p256dh),
+				Bytes.ofData(auth),
+				"ES256",
+				Bytes.ofData(vapid_private_pkcs8),
+				[ "aud" => new js.html.URL(endpoint).origin ]
+			));
+		});
+	}
+	#end
+
 	private function rosterGet() {
 		var rosterGet = new RosterGet();
 		rosterGet.onFinished(() -> {
diff --git a/xmpp/Notification.hx b/xmpp/Notification.hx
new file mode 100644
index 0000000..e220753
--- /dev/null
+++ b/xmpp/Notification.hx
@@ -0,0 +1,60 @@
+package xmpp;
+
+import xmpp.ChatMessage;
+import xmpp.JID;
+
+@:expose
+class Notification {
+	public var title (default, null) : String;
+	public var body (default, null) : String;
+	public var accountId (default, null) : String;
+	public var chatId (default, null) : String;
+	public var messageId (default, null) : String;
+	public var imageUri (default, null) : Null<String>;
+	public var lang (default, null) : Null<String>;
+	public var timestamp (default, null) : Null<String>;
+
+	public function new(title: String, body: String, accountId: String, chatId: String, messageId: String, imageUri: Null<String>, lang: Null<String>, timestamp: Null<String>) {
+		this.title = title;
+		this.body = body;
+		this.accountId = accountId;
+		this.chatId = chatId;
+		this.messageId = messageId;
+		this.imageUri = imageUri;
+		this.lang = lang;
+		this.timestamp = timestamp;
+	}
+
+	public static function fromChatMessage(m: ChatMessage) {
+		var imageUri = null;
+		final attachment = m.attachments[0];
+		if (attachment != null) {
+			imageUri = attachment.url;
+		}
+		return new Notification(
+			"New Message",
+			m.text,
+			m.account(),
+			m.conversation(),
+			m.serverId,
+			imageUri,
+			m.lang,
+			m.timestamp
+		);
+	}
+
+	// Sometimes a stanza has not much in it, so make something generic
+	// Assume it is an incoming message of some kind
+	public static function fromThinStanza(stanza: Stanza) {
+		return new Notification(
+			"New Message",
+			"",
+			JID.parse(stanza.attr.get("to")).asBare().asString(),
+			JID.parse(stanza.attr.get("from")).asBare().asString(),
+			stanza.getChildText("stanza-id", "urn:xmpp:sid:0"),
+			null,
+			null,
+			null
+		);
+	}
+}
diff --git a/xmpp/Push.hx b/xmpp/Push.hx
new file mode 100644
index 0000000..4d002a7
--- /dev/null
+++ b/xmpp/Push.hx
@@ -0,0 +1,29 @@
+package xmpp;
+
+import xmpp.ChatMessage;
+import xmpp.JID;
+import xmpp.Notification;
+import xmpp.Persistence;
+import xmpp.Stream;
+
+// this code should expect to be called from a different context to the app
+
+@:expose
+function receive(data: String, persistence: Persistence) {
+	var stanza = Stream.parse(data);
+	if (stanza == null) return null;
+	if (stanza.name == "envelope" && stanza.attr.get("xmlns") == "urn:xmpp:sce:1") {
+		stanza = stanza.getChild("content").getFirstChild();
+	}
+	if (stanza.name == "forwarded" && stanza.attr.get("xmlns") == "urn:xmpp:forward:0") {
+		stanza = stanza.getChild("message", "jabber:client");
+	}
+	if (stanza.attr.get("to") == null) return null;
+	// Assume incoming message
+	final message = ChatMessage.fromStanza(stanza, JID.parse(stanza.attr.get("to")).asBare().asString());
+	if (message != null) {
+		return Notification.fromChatMessage(message);
+	} else {
+		return Notification.fromThinStanza(stanza);
+	}
+}
diff --git a/xmpp/queries/Push2Enable.hx b/xmpp/queries/Push2Enable.hx
new file mode 100644
index 0000000..649e423
--- /dev/null
+++ b/xmpp/queries/Push2Enable.hx
@@ -0,0 +1,50 @@
+package xmpp.queries;
+
+import haxe.io.Bytes;
+import haxe.crypto.Base64;
+
+import xmpp.ID;
+import xmpp.Stanza;
+import xmpp.queries.GenericQuery;
+
+class Push2Enable extends GenericQuery {
+	public var xmlns(default, null) = "urn:xmpp:push2:0";
+	public var queryId:String = null;
+	public var ver:String = null;
+	private var responseStanza:Stanza;
+
+	public function new(to: String, service: String, client: String, ua_public: Bytes, auth_secret: Bytes, jwt_alg: Null<String>, jwt_key: Bytes, jwt_claims: Map<String, String>) {
+		queryId = ID.short();
+		queryStanza = new Stanza(
+			"iq",
+			{ to: to, type: "set", id: queryId }
+		);
+		final enable = queryStanza.tag("enable", { xmlns: xmlns });
+		enable.textTag("service", service);
+		enable.textTag("client", client);
+		final match = enable.tag("match", { profile: "urn:xmpp:push2:match:important" });
+		final send = match.tag("send", { xmlns: "urn:xmpp:push2:send:sce+rfc8291+rfc8292:0" });
+		send.textTag("ua-public", Base64.encode(ua_public));
+		send.textTag("auth-secret", Base64.encode(auth_secret));
+		if (jwt_alg != null) {
+			send.textTag("jwt-alg", jwt_alg);
+			send.textTag("jwt-key", Base64.encode(jwt_key));
+			for (claim in jwt_claims.keyValueIterator()) {
+				send.textTag("jwt-claim", claim.value, { name: claim.key });
+			}
+		}
+		enable.up().up().up();
+	}
+
+	public function handleResponse(stanza:Stanza) {
+		responseStanza = stanza;
+		finish();
+	}
+
+	public function getResult() {
+		if (responseStanza == null) {
+			return null;
+		}
+		return { type: responseStanza.attr.get("type") };
+	}
+}
diff --git a/xmpp/streams/XmppJsStream.hx b/xmpp/streams/XmppJsStream.hx
index 1eb5a76..201e78c 100644
--- a/xmpp/streams/XmppJsStream.hx
+++ b/xmpp/streams/XmppJsStream.hx
@@ -177,7 +177,7 @@ class XmppJsStream extends GenericStream {
 		return xml;
 	}
 
-	private function convertToStanza(el:XmppJsXml):Stanza {
+	private static function convertToStanza(el:XmppJsXml):Stanza {
 		var stanza = new Stanza(el.name, el.attrs);
 		for (child in el.children) {
 			if(XmppJsLtx.isText(child)) {
@@ -189,6 +189,10 @@ class XmppJsStream extends GenericStream {
 		return stanza;
 	}
 
+	public static function parse(input:String):Stanza {
+		return convertToStanza(XmppJsLtx.parse(input));
+	}
+
 	public function sendStanza(stanza:Stanza) {
 		client.send(convertFromStanza(stanza));
 	}