| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-09-13 17:11:40 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-09-13 18:53:11 UTC |
| parent | b88c0dad82317744f7dc6d01f953cbcc07518c06 |
| xmpp/Chat.hx | +17 | -1 |
| xmpp/Client.hx | +49 | -0 |
| xmpp/Persistence.hx | +3 | -0 |
| xmpp/Stanza.hx | +1 | -1 |
| xmpp/persistence/browser.js | +42 | -0 |
| xmpp/queries/PubsubGet.hx | +58 | -0 |
diff --git a/xmpp/Chat.hx b/xmpp/Chat.hx index c365e05..7d0da2c 100644 --- a/xmpp/Chat.hx +++ b/xmpp/Chat.hx @@ -1,5 +1,6 @@ package xmpp; +import haxe.io.BytesData; import xmpp.MessageSync; import xmpp.ChatMessage; import xmpp.Chat; @@ -17,6 +18,7 @@ abstract class Chat { private var client:Client; private var stream:GenericStream; private var persistence:Persistence; + private var avatarSha1:Null<BytesData> = null; public var chatId(default, null):String; public var type(default, null):Null<ChatType>; @@ -78,7 +80,21 @@ class DirectChat extends Chat { client.sendStanza(message.asStanza()); } + public function setAvatarSha1(sha1: BytesData) { + this.avatarSha1 = sha1; + } + public function getPhoto(callback:(String)->Void) { - callback(Color.defaultPhoto(chatId, chatId.charAt(0))); + if (avatarSha1 != null) { + persistence.getMediaUri("sha-1", avatarSha1, (uri) -> { + if (uri != null) { + callback(uri); + } else { + callback(Color.defaultPhoto(chatId, chatId.charAt(0))); + } + }); + } else { + callback(Color.defaultPhoto(chatId, chatId.charAt(0))); + } } } diff --git a/xmpp/Client.hx b/xmpp/Client.hx index 19d2980..4bb7a20 100644 --- a/xmpp/Client.hx +++ b/xmpp/Client.hx @@ -1,10 +1,15 @@ package xmpp; +import haxe.crypto.Base64; +import haxe.io.Bytes; +import haxe.io.BytesData; import xmpp.Chat; import xmpp.EventEmitter; import xmpp.Stream; import xmpp.queries.GenericQuery; import xmpp.queries.RosterGet; +import xmpp.queries.PubsubGet; +import xmpp.PubsubEvent; typedef ChatList = Array<Chat>; @@ -45,9 +50,53 @@ class Client extends xmpp.EventEmitter { } } + final pubsubEvent = PubsubEvent.fromStanza(stanza); + if (pubsubEvent != null && pubsubEvent.getFrom() != null && pubsubEvent.getNode() == "urn:xmpp:avatar:metadata" && pubsubEvent.getItems().length > 0) { + final avatarSha1Hex = pubsubEvent.getItems()[0].attr.get("id"); + final avatarSha1 = Bytes.ofHex(avatarSha1Hex).getData(); + final chat = this.getDirectChat(JID.parse(pubsubEvent.getFrom()).asBare().asString(), false); + chat.setAvatarSha1(avatarSha1); + persistence.getMediaUri("sha-1", avatarSha1, (uri) -> { + if (uri == null) { + final pubsubGet = new PubsubGet(pubsubEvent.getFrom(), "urn:xmpp:avatar:data", avatarSha1Hex); + pubsubGet.onFinished(() -> { + final item = pubsubGet.getResult()[0]; + if (item == null) return; + final dataNode = item.getChild("data", "urn:xmpp:avatar:data"); + if (dataNode == null) return; + persistence.storeMedia(Base64.decode(dataNode.getText()).getData(), () -> { + this.trigger("chats/update", [chat]); + }); + }); + sendQuery(pubsubGet); + } else { + this.trigger("chats/update", [chat]); + } + }); + } + return EventUnhandled; // Allow others to get this event as well }); + this.stream.on("iq", function(event) { + final stanza:Stanza = event.stanza; + if (stanza.attr.get("type") == "get" && stanza.getChild("query", "http://jabber.org/protocol/disco#info") != null) { + stream.sendStanza( + new Stanza("iq", { + type: "result", + id: stanza.attr.get("id"), + to: stanza.attr.get("from") + }) + .tag("query", { xmlns: "http://jabber.org/protocol/disco#info" }) + .tag("feature", { "var": "urn:xmpp:avatar:metadata+notify"}).up() + .up() + ); + return EventHandled; + } + + return EventUnhandled; + }); + stream.sendStanza(new Stanza("presence")); // Set self to online rosterGet(); sync(); diff --git a/xmpp/Persistence.hx b/xmpp/Persistence.hx index ffdd84a..af1396f 100644 --- a/xmpp/Persistence.hx +++ b/xmpp/Persistence.hx @@ -1,9 +1,12 @@ package xmpp; +import haxe.io.BytesData; import xmpp.ChatMessage; abstract class Persistence { abstract public function lastId(accountId: String, chatId: Null<String>, callback:(serverId:Null<String>)->Void):Void; abstract public function storeMessage(accountId: String, message: ChatMessage):Void; abstract public function getMessages(accountId: String, chatId: String, beforeId: Null<String>, beforeTime: Null<String>, callback: (messages:Array<ChatMessage>)->Void):Void; + abstract public function getMediaUri(hashAlgorithm:String, hash:BytesData, callback: (uri:Null<String>)->Void):Void; + abstract public function storeMedia(bytes:BytesData, callback: ()->Void):Void; } diff --git a/xmpp/Stanza.hx b/xmpp/Stanza.hx index 6a77f2b..eb9fe80 100644 --- a/xmpp/Stanza.hx +++ b/xmpp/Stanza.hx @@ -137,7 +137,7 @@ class Stanza implements NodeInterface { tags = tags.filter(function (child:Stanza):Bool { var childXmlns = child.attr.get("xmlns"); return (name == null || child.name == name - && ((xmlns == null && ourXmlns == childXmlns) + && ((xmlns == null && (ourXmlns == childXmlns || childXmlns == null)) || childXmlns == xmlns)); }); } diff --git a/xmpp/persistence/browser.js b/xmpp/persistence/browser.js index 1481c88..3bc9412 100644 --- a/xmpp/persistence/browser.js +++ b/xmpp/persistence/browser.js @@ -16,6 +16,14 @@ exports.xmpp.persistence = { db = event.target.result; }; + var cache = null; + caches.open(dbname).then((c) => cache = c); + + function mkNiUrl(hashAlgorithm, hashBytes) { + const b64url = btoa(Array.from(new Uint8Array(hashBytes), (x) => String.fromCodePoint(x)).join("")).replace(/\+/, "-").replace(/\//, "_").replace(/=/, ""); + return "/.well-known/ni/" + hashAlgorithm + "/" + b64url; + } + return { lastId: function(account, jid, callback) { const tx = db.transaction(["messages"], "readonly"); @@ -91,6 +99,40 @@ exports.xmpp.persistence = { console.error(event); callback([]); } + }, + + getMediaUri: function(hashAlgorithm, hash, callback) { + var niUrl; + if (hashAlgorithm == "sha-256") { + niUrl = mkNiUrl(hashAlgorithm, hash); + } else { + niUrl = localStorage.getItem(mkNiUrl(hashAlgorithm, hash)); + if (!niUrl) { + callback(null); + return; + } + } + cache.match(niUrl).then((response) => { + if (response) { + response.blob().then((blob) => { + callback(URL.createObjectURL(blob)); + }); + } else { + callback(null); + } + }); + }, + + storeMedia: function(buffer, callback) { + (async function() { + const sha256 = await crypto.subtle.digest("SHA-256", buffer); + const sha512 = await crypto.subtle.digest("SHA-512", buffer); + const sha1 = await crypto.subtle.digest("SHA-1", buffer); + const sha256NiUrl = mkNiUrl("sha-256", sha256); + await cache.put(sha256NiUrl, new Response(buffer)); + localStorage.setItem(mkNiUrl("sha-1", sha1), sha256NiUrl); + localStorage.setItem(mkNiUrl("sha-512", sha512), sha256NiUrl); + })().then(callback); } } } diff --git a/xmpp/queries/PubsubGet.hx b/xmpp/queries/PubsubGet.hx new file mode 100644 index 0000000..29407f8 --- /dev/null +++ b/xmpp/queries/PubsubGet.hx @@ -0,0 +1,58 @@ +package xmpp.queries; + +import haxe.DynamicAccess; +import haxe.Exception; + +import xmpp.ID; +import xmpp.ResultSet; +import xmpp.Stanza; +import xmpp.Stream; +import xmpp.queries.GenericQuery; + +class PubsubGet extends GenericQuery { + public var xmlns(default, null) = "http://jabber.org/protocol/pubsub"; + public var queryId:String = null; + public var ver:String = null; + private var responseStanza:Stanza; + private var result: Array<Stanza>; + + public function new(to: String, node: String, ?itemId: String) { + var attr: DynamicAccess<String> = { node: node }; + if (ver != null) attr["ver"] = ver; + /* Build basic query */ + queryId = ID.short(); + queryStanza = new Stanza("iq", { to: to, type: "get", id: queryId }); + final items = queryStanza + .tag("pubsub", { xmlns: xmlns }) + .tag("items", { node: node }); + if (itemId != null) { + items.tag("item", { id: itemId }).up(); + } + queryStanza.up().up(); + } + + public function handleResponse(stanza:Stanza) { + responseStanza = stanza; + finish(); + } + + public function getResult() { + if (responseStanza == null) { + return []; + } + if(result == null) { + final q = responseStanza.getChild("pubsub", xmlns); + if(q == null) { + return []; + } + final items = q.getChild("items"); // same xmlns as pubsub + if (items == null) { + return []; + } + if (items.attr.get("xmlns") == null) items.attr.set("xmlns", xmlns); + // TODO: cannot specify namespace here due to bugs in namespace handling in allTags + result = items.allTags("item"); + } + return result; + } +}