| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-07-20 17:32:29 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-07-20 17:32:29 UTC |
| parent | 83d5246f630dee101c6911d46144af61e483362f |
| 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