git » sdk » commit e49c535

Allow checking if attachment is already cached

author Stephen Paul Weber
2026-07-20 17:32:29 UTC
committer Stephen Paul Weber
2026-07-20 17:32:29 UTC
parent 83d5246f630dee101c6911d46144af61e483362f

Allow checking if attachment is already cached

borogove/Chat.hx +2 -2
borogove/ChatMessage.hx +22 -0
borogove/Client.hx +9 -9
borogove/Persistence.hx +3 -4
borogove/persistence/Dummy.hx +2 -2
borogove/persistence/IDB.js +7 -3
borogove/persistence/MediaStore.hx +1 -1
borogove/persistence/MediaStoreCache.js +6 -3
borogove/persistence/MediaStoreFS.hx +2 -3
borogove/persistence/Sqlite.hx +7 -3

diff --git a/borogove/Chat.hx b/borogove/Chat.hx
index 3fa6cd6..e98fd62 100644
--- a/borogove/Chat.hx
+++ b/borogove/Chat.hx
@@ -1965,8 +1965,8 @@ class Channel extends Chat {
 			if (avatarSha1Hex != null && avatarSha1Hex != "") {
 				final hash = Hash.fromHex("sha-1", avatarSha1Hex);
 				avatarSha1 = hash.hash;
-				persistence.hasMedia("sha-1", avatarSha1).then((has) -> {
-					if (!has) {
+				persistence.hasMedia(hash).then((has) -> {
+					if (has == null) {
 						final vcardGet = new VcardTempGet(JID.parse(chatId));
 						vcardGet.onFinished(() -> {
 							final vcard = vcardGet.getResult();
diff --git a/borogove/ChatMessage.hx b/borogove/ChatMessage.hx
index e58d406..81c40a2 100644
--- a/borogove/ChatMessage.hx
+++ b/borogove/ChatMessage.hx
@@ -6,6 +6,7 @@ import haxe.crypto.Base64;
 import haxe.ds.ReadOnlyArray;
 import haxe.io.Bytes;
 import haxe.io.BytesData;
+import thenshim.Promise;
 using Lambda;
 using StringTools;
 
@@ -108,6 +109,27 @@ class ChatAttachment {
 		return new ChatAttachment(name, mime, size > 0 ? size : null, [uri], []);
 	}
 	#end
+
+	@:allow(borogove)
+	private function lookup(persistence: Persistence): Promise<ChatAttachment> {
+		if (cachedAt != null) return Promise.resolve(this);
+
+		return lookupHashes(persistence, hashes.copy()).then(id -> {
+			cachedAt = id;
+			return this;
+		});
+	}
+
+	private function lookupHashes(persistence: Persistence, hashes: Array<Hash>): Promise<Null<String>> {
+		final hash = hashes.shift();
+		if (hash == null) return Promise.resolve(cast null);
+
+		return persistence.hasMedia(hash).then(id -> {
+			if (id == null) return lookupHashes(persistence, hashes);
+
+			return Promise.resolve(id);
+		});
+	}
 }
 
 @:expose
diff --git a/borogove/Client.hx b/borogove/Client.hx
index aa30687..8a14c8f 100644
--- a/borogove/Client.hx
+++ b/borogove/Client.hx
@@ -404,8 +404,8 @@ class Client extends EventEmitter {
 						chat.setAvatarSha1(avatarSha1.hash);
 						persistence.storeChats(this.accountId(), [chat]);
 					}
-					persistence.hasMedia("sha-1", avatarSha1.hash).then((has) -> {
-						if (has) {
+					persistence.hasMedia(avatarSha1).then((has) -> {
+						if (has != null) {
 							if (chat.livePresence()) this.trigger("chats/update", [chat]);
 						} else {
 							final vcardGet = new VcardTempGet(from);
@@ -674,7 +674,7 @@ class Client extends EventEmitter {
 		if (pubsubEvent != null && pubsubEvent.getFrom() != null && pubsubEvent.getNode() == "urn:xmpp:avatar:metadata" && pubsubEvent.getItems().length > 0) {
 			final item = pubsubEvent.getItems()[0];
 			final avatarSha1Hex = pubsubEvent.getItems()[0].attr.get("id");
-			final avatarSha1 = Hash.fromHex("sha-1", avatarSha1Hex)?.hash;
+			final avatarSha1 = Hash.fromHex("sha-1", avatarSha1Hex);
 			final metadata = item.getChild("metadata", "urn:xmpp:avatar:metadata");
 			var mime = "image/png";
 			if (metadata != null) {
@@ -685,10 +685,10 @@ class Client extends EventEmitter {
 			}
 			if (avatarSha1 != null) {
 				final chat = this.getDirectChat(JID.parse(pubsubEvent.getFrom()).asBare().asString(), false);
-				chat.setAvatarSha1(avatarSha1);
+				chat.setAvatarSha1(avatarSha1.hash);
 				persistence.storeChats(accountId(), [chat]);
-				persistence.hasMedia("sha-1", avatarSha1).then((has) -> {
-					if (has) {
+				persistence.hasMedia(avatarSha1).then((has) -> {
+					if (has != null) {
 						this.trigger("chats/update", [chat]);
 					} else {
 						final pubsubGet = new PubsubGet(pubsubEvent.getFrom(), "urn:xmpp:avatar:data", avatarSha1Hex);
@@ -1114,7 +1114,7 @@ class Client extends EventEmitter {
 		If cachedAt is already filled in for this ChatAttachment, it is simply returned.
 
 		@param attachment ChatAttachment to fetch
-		@returns Promise resolving to a ChatAttachment with cachedAt filled in
+		@returns Promise resolving to a ChatAttachment with cachedAt filled in, if possible
 	**/
 	public function fetchAttachment(attachment: ChatAttachment): Promise<ChatAttachment> {
 		// We already have it
@@ -1739,8 +1739,8 @@ class Client extends EventEmitter {
 	private function fetchMediaByHashOneCounterpart(hashes: Array<Hash>, counterpart: JID) {
 		if (hashes.length < 1) return thenshim.Promise.reject("no hashes left");
 
-		return persistence.hasMedia(hashes[0].algorithm, hashes[0].hash).then (has -> {
-			if (has) return Promise.resolve(null);
+		return persistence.hasMedia(hashes[0]).then (has -> {
+			if (has != null) return Promise.resolve(null);
 
 			return new Promise((resolve, reject) -> {
 				final q = BoB.forHash(counterpart.asString(), hashes[0]);
diff --git a/borogove/Persistence.hx b/borogove/Persistence.hx
index 01767c8..5964b43 100644
--- a/borogove/Persistence.hx
+++ b/borogove/Persistence.hx
@@ -214,11 +214,10 @@ interface Persistence {
 	/**
 		Check whether a media blob is already stored
 
-		@param hashAlgorithm hash algorithm for the content ID
-		@param hash raw hash bytes
-		@returns Promise resolving to true when the media exists
+		@param hash hash of the media we're looking for
+		@returns Promise resolving to an ID when the media exists, null when it does not
 	**/
-	public function hasMedia(hashAlgorithm:String, hash:BytesData): Promise<Bool>;
+	public function hasMedia(hash: Hash): Promise<Null<String>>;
 
 	/**
 		Store media bytes and any metadata needed to retrieve them later
diff --git a/borogove/persistence/Dummy.hx b/borogove/persistence/Dummy.hx
index b40883f..83950f6 100644
--- a/borogove/persistence/Dummy.hx
+++ b/borogove/persistence/Dummy.hx
@@ -122,8 +122,8 @@ class Dummy implements Persistence {
 	}
 
 	@HaxeCBridge.noemit
-	public function hasMedia(hashAlgorithm:String, hash:BytesData): Promise<Bool> {
-		return Promise.resolve(false);
+	public function hasMedia(hash: Hash): Promise<Null<String>> {
+		return Promise.resolve(null);
 	}
 
 	@HaxeCBridge.noemit
diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js
index e3f5884..44c2f1f 100644
--- a/borogove/persistence/IDB.js
+++ b/borogove/persistence/IDB.js
@@ -6,6 +6,7 @@ import {
 	borogove_AvailableChat,
 	borogove_Caps,
 	borogove_Channel,
+	borogove_ChatAttachment,
 	borogove_ChatMessageBuilder,
 	borogove_CustomEmojiReaction,
 	borogove_DirectChat,
@@ -336,7 +337,7 @@ export default async (dbname, media, tokenize, stemmer) => {
 		message.to = value.to ? borogove_JID.parse(value.to) : message.recipients[0];
 		message.replyTo = value.replyTo.map((r) => borogove_JID.parse(r));
 		message.threadId = value.threadId;
-		message.attachments = value.attachments;
+		message.attachments = (value.attachments ?? []).map(a => new borogove_ChatAttachment(a.name, a.mime, a.size, a.uris, a.hashes));
 		message.linkMetadata = value.linkMetadata ?? [];
 		message.reactions = hydrateReactions(value.reactions, message.timestamp);
 		message.text = value.text;
@@ -368,6 +369,9 @@ export default async (dbname, media, tokenize, stemmer) => {
 			v.versions = []; // No need for nested versions...
 			return hydrateMessage(v, store);
 		}));
+
+		await Promise.all(message.attachments.map(a => a.lookup(obj)));
+
 		return message;
 	}
 
@@ -1073,8 +1077,8 @@ tx.onerror = console.error;
 			return result.sort((a, b) => a.timestamp < b.timestamp ? -1 : (a.timestamp > b.timestamp ? 1 : 0));
 		},
 
-		hasMedia: function(hashAlgorithm, hash) {
-			return media.hasMedia(hashAlgorithm, hash);
+		hasMedia: function(hash) {
+			return media.hasMedia(hash);
 		},
 
 		removeMedia: function(hashAlgorithm, hash) {
diff --git a/borogove/persistence/MediaStore.hx b/borogove/persistence/MediaStore.hx
index 539d048..cb6beb6 100644
--- a/borogove/persistence/MediaStore.hx
+++ b/borogove/persistence/MediaStore.hx
@@ -12,7 +12,7 @@ import HaxeCBridge;
 @:build(HaxeSwiftBridge.expose())
 #end
 interface MediaStore {
-	public function hasMedia(hashAlgorithm:String, hash:BytesData): Promise<Bool>;
+	public function hasMedia(hash: Hash): Promise<Null<String>>;
 	public function removeMedia(hashAlgorithm:String, hash:BytesData): Promise<Bool>;
 	public function storeMedia(mime:String, source:Source): Promise<String>;
 	@:allow(borogove)
diff --git a/borogove/persistence/MediaStoreCache.js b/borogove/persistence/MediaStoreCache.js
index 268b9e7..b970dc1 100644
--- a/borogove/persistence/MediaStoreCache.js
+++ b/borogove/persistence/MediaStoreCache.js
@@ -69,9 +69,12 @@ export default (cacheName, { routeHashPath } = { routeHashPath: null }) => {
 			return await cache.match(niUrl);
 		},
 
-		async hasMedia(hashAlgorithm, hash) {
-			const response = await this.getMediaResponse(mkNiUrl(hashAlgorithm, hash));
-			return !!response;
+		async hasMedia(hash) {
+			const niUrl = mkNiUrl(hash.algorithm, hash.hash);
+			const response = await this.getMediaResponse(niUrl);
+			if (!response) return null;
+
+			return niUrl;
 		}
 	};
 
diff --git a/borogove/persistence/MediaStoreFS.hx b/borogove/persistence/MediaStoreFS.hx
index 93fbfc2..b4b654c 100644
--- a/borogove/persistence/MediaStoreFS.hx
+++ b/borogove/persistence/MediaStoreFS.hx
@@ -63,9 +63,8 @@ class MediaStoreFS implements MediaStore {
 	}
 
 	@HaxeCBridge.noemit
-	public function hasMedia(hashAlgorithm:String, hash:BytesData): Promise<Bool> {
-		final hash = new Hash(hashAlgorithm, hash);
-		return getMediaPath(hash.toUri()).then(path -> path != null);
+	public function hasMedia(hash: Hash): Promise<Null<String>> {
+		return getMediaPath(hash.toUri());
 	}
 
 	@HaxeCBridge.noemit
diff --git a/borogove/persistence/Sqlite.hx b/borogove/persistence/Sqlite.hx
index 1fe033b..f59a7e5 100644
--- a/borogove/persistence/Sqlite.hx
+++ b/borogove/persistence/Sqlite.hx
@@ -909,13 +909,17 @@ class Sqlite implements Persistence implements KeyValueStore {
 			if (op == "<" || op == "<=") {
 				messages.reverse();
 			}
+			final ps = [];
 			final replyTos = [];
 			for (message in messages) {
 				if (message.replyToMessage != null && message.replyToMessage.stanza == null) {
 					replyTos.push({ chatId: message.chatId(), serverId: message.replyToMessage.serverId, localId: message.replyToMessage.localId });
 				}
+				for (attachment in message.attachments) {
+					ps.push(attachment.lookup(this));
+				}
 			}
-			return hydrateReplyTo(accountId, messages, replyTos);
+			return thenshim.PromiseTools.all(ps).then(_ -> hydrateReplyTo(accountId, messages, replyTos));
 		}).then(messages -> hydrateReactions(accountId, messages));
 	}
 
@@ -994,8 +998,8 @@ class Sqlite implements Persistence implements KeyValueStore {
 	}
 
 	@HaxeCBridge.noemit
-	public function hasMedia(hashAlgorithm:String, hash:BytesData): Promise<Bool> {
-		return media.hasMedia(hashAlgorithm, hash);
+	public function hasMedia(hash: Hash): Promise<Null<String>> {
+		return media.hasMedia(hash);
 	}
 
 	@HaxeCBridge.noemit