git » sdk » commit 8b138a6

Fetch, cache, and use real avatar

author Stephen Paul Weber
2023-09-13 17:11:40 UTC
committer Stephen Paul Weber
2023-09-13 18:53:11 UTC
parent b88c0dad82317744f7dc6d01f953cbcc07518c06

Fetch, cache, and use real avatar

When told about one, which we need to implement caps for most clients to
bother doing.

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;
+	}
+}