git » sdk » commit e972d81

Rudimentary support for MUC avatars

author Stephen Paul Weber
2025-10-27 03:37:45 UTC
committer Stephen Paul Weber
2025-10-27 03:37:45 UTC
parent 3bf6e6bf9760713e5cfa2805faa68ae63f16cd0d

Rudimentary support for MUC avatars

borogove/Chat.hx +2 -2
borogove/Client.hx +25 -23
borogove/Presence.hx +5 -1
borogove/persistence/IDB.js +2 -2
borogove/persistence/Sqlite.hx +6 -2

diff --git a/borogove/Chat.hx b/borogove/Chat.hx
index fed5314..a0e6ddd 100644
--- a/borogove/Chat.hx
+++ b/borogove/Chat.hx
@@ -524,7 +524,7 @@ abstract class Chat {
 			presence.caps = caps;
 			setPresence(resource, presence);
 		} else {
-			setPresence(resource, new Presence(caps, null));
+			setPresence(resource, new Presence(caps, null, null));
 		}
 	}
 
@@ -1336,7 +1336,7 @@ class Channel extends Chat {
 		} else {
 			final nick = JID.parse(participantId).resource;
 			final placeholderUri = Color.defaultPhoto(participantId, nick == null ? " " : nick.charAt(0));
-			return new Participant(nick ?? "", null, placeholderUri, false);
+			return new Participant(nick ?? "", presence[nick]?.avatarHash?.toUri(), placeholderUri, false);
 		}
 	}
 
diff --git a/borogove/Client.hx b/borogove/Client.hx
index 02973d8..33355ec 100644
--- a/borogove/Client.hx
+++ b/borogove/Client.hx
@@ -320,7 +320,6 @@ class Client extends EventEmitter {
 		stream.on("presence", function(event) {
 			final stanza:Stanza = event.stanza;
 			final c = stanza.getChild("c", "http://jabber.org/protocol/caps");
-			final mucUser = stanza.getChild("x", "http://jabber.org/protocol/muc#user");
 			if (stanza.attr.get("from") != null && stanza.attr.get("type") == null) {
 				final from = JID.parse(stanza.attr.get("from"));
 				final chat = getChat(from.asBare().asString());
@@ -328,13 +327,18 @@ class Client extends EventEmitter {
 					trace("Presence for unknown JID: " + stanza.attr.get("from"));
 					return EventUnhandled;
 				}
+
+				final mucUser = stanza.getChild("x", "http://jabber.org/protocol/muc#user");
+				final avatarSha1Hex = stanza.findText("{vcard-temp:x:update}x/photo#");
+				final avatarSha1 = avatarSha1Hex == null ? null : Hash.fromHex("sha-1", avatarSha1Hex);
+
 				if (c == null) {
-					chat.setPresence(JID.parse(stanza.attr.get("from")).resource, new Presence(null, mucUser));
+					chat.setPresence(JID.parse(stanza.attr.get("from")).resource, new Presence(null, mucUser, avatarSha1));
 					persistence.storeChats(accountId(), [chat]);
 					if (chat.livePresence()) this.trigger("chats/update", [chat]);
 				} else {
 					final handleCaps = (caps) -> {
-						chat.setPresence(JID.parse(stanza.attr.get("from")).resource, new Presence(caps, mucUser));
+						chat.setPresence(JID.parse(stanza.attr.get("from")).resource, new Presence(caps, mucUser, avatarSha1));
 						if (mucUser == null || chat.livePresence()) persistence.storeChats(accountId(), [chat]);
 						return chat;
 					};
@@ -365,28 +369,26 @@ class Client extends EventEmitter {
 						}
 					});
 				}
-				if (from.isBare()) {
-					final avatarSha1Hex = stanza.findText("{vcard-temp:x:update}x/photo#");
-					if (avatarSha1Hex != null) {
-						final avatarSha1 = Hash.fromHex("sha-1", avatarSha1Hex)?.hash;
-						chat.setAvatarSha1(avatarSha1);
+				if (avatarSha1 != null) {
+					if (from.isBare()) {
+						chat.setAvatarSha1(avatarSha1.hash);
 						persistence.storeChats(accountId(), [chat]);
-						persistence.hasMedia("sha-1", avatarSha1).then((has) -> {
-							if (has) {
-								if (chat.livePresence()) this.trigger("chats/update", [chat]);
-							} else {
-								final vcardGet = new VcardTempGet(from);
-								vcardGet.onFinished(() -> {
-									final vcard = vcardGet.getResult();
-									if (vcard.photo == null) return;
-									persistence.storeMedia(vcard.photo.mime, vcard.photo.data.getData()).then(_ -> {
-										this.trigger("chats/update", [chat]);
-									});
-								});
-								sendQuery(vcardGet);
-							}
-						});
 					}
+					persistence.hasMedia("sha-1", avatarSha1.hash).then((has) -> {
+						if (has) {
+							if (chat.livePresence()) this.trigger("chats/update", [chat]);
+						} else {
+							final vcardGet = new VcardTempGet(from);
+							vcardGet.onFinished(() -> {
+								final vcard = vcardGet.getResult();
+								if (vcard.photo == null) return;
+								persistence.storeMedia(vcard.photo.mime, vcard.photo.data.getData()).then(_ -> {
+									this.trigger("chats/update", [chat]);
+								});
+							});
+							sendQuery(vcardGet);
+						}
+					});
 				}
 				return EventHandled;
 			}
diff --git a/borogove/Presence.hx b/borogove/Presence.hx
index cb45e4a..64b9261 100644
--- a/borogove/Presence.hx
+++ b/borogove/Presence.hx
@@ -1,12 +1,16 @@
 package borogove;
 
+import borogove.Hash;
+
 @:expose
 class Presence {
 	public var caps:Null<Caps>;
 	public final mucUser:Null<Stanza>;
+	public final avatarHash:Null<Hash>;
 
-	public function new(caps: Null<Caps>, mucUser: Null<Stanza>) {
+	public function new(caps: Null<Caps>, mucUser: Null<Stanza>, avatarHash: Null<Hash>) {
 		this.caps = caps;
 		this.mucUser = mucUser;
+		this.avatarHash = avatarHash;
 	}
 }
diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index f6da818..4c7c3ba 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -272,7 +272,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 					chatId: chat.chatId,
 					trusted: chat.trusted,
 					avatarSha1: chat.avatarSha1,
-					presence: new Map([...chat.presence.entries()].map(([k, p]) => [k, { caps: p.caps?.ver(), mucUser: p.mucUser?.toString() }])),
+					presence: new Map([...chat.presence.entries()].map(([k, p]) => [k, { caps: p.caps?.ver(), mucUser: p.mucUser?.toString(), avatarHash: p.avatarHash?.serializeUri() }])),
 					displayName: chat.displayName,
 					uiState: chat.uiState,
 					isBlocked: chat.isBlocked,
@@ -297,7 +297,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 				r.trusted,
 				r.avatarSha1,
 				new Map(await Promise.all((r.presence instanceof Map ? [...r.presence.entries()] : Object.entries(r.presence)).map(
-					async ([k, p]) => [k, new borogove.Presence(p.caps && await this.getCaps(p.caps), p.mucUser && borogove.Stanza.parse(p.mucUser))]
+					async ([k, p]) => [k, new borogove.Presence(p.caps && await this.getCaps(p.caps), p.mucUser && borogove.Stanza.parse(p.mucUser), p.avatarHash && borogove.Hash.fromUri(p.avatarHash))]
 				))),
 				r.displayName,
 				r.uiState,
diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx
index 68994c8..7b0884a 100644
--- a/borogove/persistence/Sqlite.hx
+++ b/borogove/persistence/Sqlite.hx
@@ -184,7 +184,7 @@ class Sqlite implements Persistence implements KeyValueStore {
 
 		storeChatTimer = haxe.Timer.delay(() -> {
 			final mapPresence = (chat: Chat) -> {
-				final storePresence: DynamicAccess<{ ?caps: String, ?mucUser: String }> = {};
+				final storePresence: DynamicAccess<{ ?caps: String, ?mucUser: String, ?avatarHash: String }> = {};
 				final caps: Map<BytesData, Caps> = [];
 				for (resource => presence in chat.presence) {
 					if (storePresence[resource ?? ""] == null) storePresence[resource ?? ""] = {};
@@ -195,6 +195,9 @@ class Sqlite implements Persistence implements KeyValueStore {
 					if (presence.mucUser != null) {
 						storePresence[resource ?? ""].mucUser = presence.mucUser.toString();
 					}
+					if (presence.avatarHash != null) {
+						storePresence[resource ?? ""].avatarHash = presence.avatarHash.serializeUri();
+					}
 				}
 				storeCapsSet(caps);
 				return storePresence;
@@ -268,7 +271,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 					presenceJson.remove(resource);
 					presenceMap[resource] = new Presence(
 						presence.caps == null ? null : capsMap[presence.caps],
-						presence.mucUser == null || Config.constrainedMemoryMode ? null : Stanza.parse(presence.mucUser)
+						presence.mucUser == null || Config.constrainedMemoryMode ? null : Stanza.parse(presence.mucUser),
+						presence.avatarHash == null ? null : Hash.fromUri(presence.avatarHash)
 					);
 				}
 				// FIXME: Empty OMEMO contact device ids hardcoded in next line