git » sdk » commit b3edbde

Store hash after fetch

author Stephen Paul Weber
2026-07-21 15:47:59 UTC
committer Stephen Paul Weber
2026-07-21 15:47:59 UTC
parent d32dd886f6d413b3a3d302a3f6b5be4d505e122f

Store hash after fetch

If a ChatAttachment has no hash at all, update the message so that it
does have a hash after download, allowing the cache to work.

borogove/ChatMessage.hx +53 -20
borogove/Client.hx +16 -15
borogove/Stanza.hx +2 -1

diff --git a/borogove/ChatMessage.hx b/borogove/ChatMessage.hx
index 81c40a2..3418802 100644
--- a/borogove/ChatMessage.hx
+++ b/borogove/ChatMessage.hx
@@ -80,7 +80,7 @@ class ChatAttachment {
 	/**
 		Hashes of data
 	**/
-	public final hashes: ReadOnlyArray<Hash>;
+	public final hashes: Array<Hash>;
 
 	#if cpp
 	@:allow(borogove)
@@ -130,6 +130,30 @@ class ChatAttachment {
 			return Promise.resolve(id);
 		});
 	}
+
+	@:allow(borogove)
+	private function sims() {
+		final stanza = new Stanza("reference", { xmlns: "urn:xmpp:reference:0", type: "data" })
+			.tag("media-sharing", { xmlns: "urn:xmpp:sims:1" });
+
+		stanza.tag("file", { xmlns: "urn:xmpp:jingle:apps:file-transfer:5" });
+		if (name != null) stanza.textTag("name", name);
+		stanza.textTag("media-type", mime);
+		if (size != null) stanza.textTag("size", Std.string(size));
+		for (hash in hashes) {
+			stanza.textTag("hash", Base64.encode(Bytes.ofData(hash.hash)), { xmlns: "urn:xmpp:hashes:2", algo: hash.algorithm });
+		}
+		stanza.up();
+
+		stanza.tag("sources");
+		for (uri in uris) {
+			stanza.tag("reference", { xmlns: "urn:xmpp:reference:0", type: "data", uri: uri }).up();
+		}
+
+		stanza.up().up().up();
+
+		return stanza;
+	}
 }
 
 @:expose
@@ -547,6 +571,33 @@ class ChatMessage {
 		}
 	}
 
+	/**
+		Fetch data for a ChatAttachment into local media cache
+
+		If cachedAt is already filled in for this ChatAttachment, it is simply returned.
+
+		@param attachment ChatAttachment to fetch
+		@param client Client to use when fetching
+		@returns Promise resolving to a ChatAttachment with cachedAt filled in, if possible
+	**/
+	public function fetchAttachment(attachment: ChatAttachment, client: Client) {
+		final hasNoHashes = attachment.hashes.length < 1;
+		return client.fetchAttachment(attachment).then(r -> {
+			if (hasNoHashes && r.hashes.length > 0) {
+				if (stanza != null) {
+					final sims = stanza.allTags().find(child -> switch child.find("{urn:xmpp:sims:1}media-sharing/sources/{urn:xmpp:reference:0}reference@uri") {
+						case CData(txt): txt.content == attachment.uris[0];
+						default: false;
+					});
+					if (sims != null && stanza != null) stanza.removeChild(sims);
+					if (stanza != null) stanza.addChild(r.sims());
+				}
+				return client.storeMessages([this]).then(_ -> r);
+			}
+			return Promise.resolve(r);
+		});
+	}
+
 	@:allow(borogove)
 	private function asStanza():Stanza {
 		if (stanza != null) return stanza;
@@ -613,25 +664,7 @@ class ChatMessage {
 		}
 
 		for (attachment in attachments) {
-			stanza
-				.tag("reference", { xmlns: "urn:xmpp:reference:0", type: "data" })
-				.tag("media-sharing", { xmlns: "urn:xmpp:sims:1" });
-
-			stanza.tag("file", { xmlns: "urn:xmpp:jingle:apps:file-transfer:5" });
-			if (attachment.name != null) stanza.textTag("name", attachment.name);
-			stanza.textTag("media-type", attachment.mime);
-			if (attachment.size != null) stanza.textTag("size", Std.string(attachment.size));
-			for (hash in attachment.hashes) {
-				stanza.textTag("hash", Base64.encode(Bytes.ofData(hash.hash)), { xmlns: "urn:xmpp:hashes:2", algo: hash.algorithm });
-			}
-			stanza.up();
-
-			stanza.tag("sources");
-			for (uri in attachment.uris) {
-				stanza.tag("reference", { xmlns: "urn:xmpp:reference:0", type: "data", uri: uri }).up();
-			}
-
-			stanza.up().up().up();
+			stanza.addChild(attachment.sims());
 
 			if (attachment.uris.length > 0) {
 				stanza.tag("x", { xmlns: "jabber:x:oob" }).textTag("url", attachment.uris[0]).up();
diff --git a/borogove/Client.hx b/borogove/Client.hx
index 80f8811..82d9393 100644
--- a/borogove/Client.hx
+++ b/borogove/Client.hx
@@ -1108,32 +1108,26 @@ class Client extends EventEmitter {
 		return EventHandled;
 	}
 
-	/**
-		Fetch data for a ChatAttachment into local media cache
-
-		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, if possible
-	**/
-	public function fetchAttachment(attachment: ChatAttachment): Promise<ChatAttachment> {
+	@:allow(borogove)
+	private function fetchAttachment(attachment: ChatAttachment): Promise<ChatAttachment> {
 		// We already have it
 		if (attachment.cachedAt != null) return Promise.resolve(attachment);
 
-		return fetchUris( attachment.uris.copy()).then(id -> {
-			attachment.cachedAt = id;
+		return fetchUris( attachment.uris.copy()).then(r -> {
+			attachment.cachedAt = r.id;
+			if (attachment.hashes.length < 1) attachment.hashes.push(r.hash);
 			return attachment;
 		});
 	}
 
-	private function fetchUris(uris: Array<String>): Promise<Null<String>> {
+	private function fetchUris(uris: Array<String>): Promise<Null<{ id: String, hash: Hash }>> {
 		if (uris.length < 1) return Promise.resolve(null);
 
 		final uri = uris.shift();
 		final aesgcm = XEP0454.parse(uri);
 		if (aesgcm != null) {
 			return XEP0454.fetch(aesgcm).then(
-				data -> persistence.storeMedia(aesgcm.mime, data),
+				data -> persistence.storeMedia(aesgcm.mime, data).then(id -> { id: id, hash: Hash.sha256(data) }),
 				e -> {
 					trace("fetchAttachment", e);
 					return fetchUris(uris);
@@ -1142,6 +1136,7 @@ class Client extends EventEmitter {
 		}
 
 		if (uri.startsWith("http://") || uri.startsWith("https://")) {
+			final sha256 = Hash.sha256incr();
 			return new Promise((resolve, reject) -> {
 				tink.http.Client.fetch(uri).handle((rOrErr) -> switch (rOrErr) {
 				case Success(r):
@@ -1152,13 +1147,19 @@ class Client extends EventEmitter {
 							case Success(ct): ct.toString();
 							default: "application/octet-stream";
 						};
-						resolve({ mime: mime, body: r.body });
+						resolve({
+							mime: mime,
+							body: Source.ofTinkSource(r.body.chunked().map(chunk -> {
+								sha256.update((chunk : Bytes).getData());
+								return chunk;
+							}))
+						});
 					}
 				case Failure(e):
 					reject(e);
 				});
 			}).then(
-				r -> persistence.storeMedia(r.mime, r.body),
+				r -> persistence.storeMedia(r.mime, r.body).then(id -> { id: id, hash: sha256.digest() }),
 				e -> fetchUris(uris)
 			);
 		}
diff --git a/borogove/Stanza.hx b/borogove/Stanza.hx
index 280a49d..8d42b7e 100644
--- a/borogove/Stanza.hx
+++ b/borogove/Stanza.hx
@@ -347,7 +347,8 @@ class Stanza {
 					}
 					return CData(new TextNode(text));
 				}
-				return Element(cursor.getChild(name, xmlns));
+				final got = cursor.getChild(name, xmlns);
+				return got == null ? null : Element(got);
 			}
 			cursor = cursor.getChild(name, xmlns);
 		} while (cursor != null);