| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-09-25 17:08:59 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-09-25 17:10:23 UTC |
| parent | d6ca04c9c62429db89a3439685a35693b56f423c |
| 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)); }