| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-11-29 16:42:20 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-12-11 15:59:51 UTC |
| parent | 049e71c5e90731750bb054d17c264b7cfa22dbc8 |
| xmpp/Client.hx | +42 | -0 |
| xmpp/queries/HttpUploadSlot.hx | +61 | -0 |
diff --git a/xmpp/Client.hx b/xmpp/Client.hx index f9a659e..8ff6cfb 100644 --- a/xmpp/Client.hx +++ b/xmpp/Client.hx @@ -1,5 +1,7 @@ package xmpp; +import sha.SHA256; + import haxe.crypto.Base64; import haxe.io.Bytes; import haxe.io.BytesData; @@ -16,6 +18,7 @@ import xmpp.queries.DiscoInfoGet; import xmpp.queries.DiscoItemsGet; import xmpp.queries.ExtDiscoGet; import xmpp.queries.GenericQuery; +import xmpp.queries.HttpUploadSlot; import xmpp.queries.JabberIqGatewayGet; import xmpp.queries.PubsubGet; import xmpp.queries.Push2Enable; @@ -473,6 +476,45 @@ class Client extends xmpp.EventEmitter { this.stream.trigger("auth/password", { password: password, requestToken: fastMechanism }); } + public function prepareAttachment(source: js.html.File, callback: (Null<ChatAttachment>)->Void) { // TODO: abstract with filename, mime, and ability to convert to tink.io.Source + persistence.findServicesWithFeature(accountId(), "urn:xmpp:http:upload:0", (services) -> { + final sha256 = new sha.SHA256(); + tink.io.Source.ofJsFile(source.name, source).chunked().forEach((chunk) -> { + sha256.update(chunk); + return tink.streams.Stream.Handled.Resume; + }).handle((o) -> switch o { + case Depleted: + prepareAttachmentFor(source, services, [{ algo: "sha-256", hash: sha256.digest().getData() }], callback); + default: + trace("Error computing attachment hash", o); + callback(null); + }); + }); + } + + private function prepareAttachmentFor(source: js.html.File, services: Array<{ serviceId: String }>, hashes: Array<{algo: String, hash: BytesData}>, callback: (Null<ChatAttachment>)->Void) { + if (services.length < 1) { + callback(null); + return; + } + final httpUploadSlot = new HttpUploadSlot(services[0].serviceId, source.name, source.size, source.type, hashes); + httpUploadSlot.onFinished(() -> { + final slot = httpUploadSlot.getResult(); + if (slot == null) { + prepareAttachmentFor(source, services.slice(1), hashes, callback); + } else { + tink.http.Client.fetch(slot.put, { method: PUT, headers: slot.putHeaders, body: tink.io.Source.RealSourceTools.idealize(tink.io.Source.ofJsFile(source.name, source), (e) -> throw e) }).all() + .handle((o) -> switch o { + case Success(res) if (res.header.statusCode == 201): + callback(new ChatAttachment(source.name, source.type, source.size, [slot.get], hashes)); + default: + prepareAttachmentFor(source, services.slice(1), hashes, callback); + }); + } + }); + sendQuery(httpUploadSlot); + } + /* Return array of chats, sorted by last activity */ public function getChats():Array<Chat> { return chats.filter((chat) -> chat.uiState != Closed); diff --git a/xmpp/queries/HttpUploadSlot.hx b/xmpp/queries/HttpUploadSlot.hx new file mode 100644 index 0000000..881a346 --- /dev/null +++ b/xmpp/queries/HttpUploadSlot.hx @@ -0,0 +1,61 @@ +package xmpp.queries; + +import haxe.DynamicAccess; +import haxe.Exception; +import haxe.crypto.Base64; +import haxe.io.Bytes; +import haxe.io.BytesData; + +import xmpp.ID; +import xmpp.JID; +import xmpp.ResultSet; +import xmpp.Stanza; +import xmpp.Stream; +import xmpp.queries.GenericQuery; + +class HttpUploadSlot extends GenericQuery { + public var xmlns(default, null) = "urn:xmpp:http:upload:0"; + public var queryId:String = null; + public var responseStanza(default, null):Stanza; + private var result: { put: String, putHeaders: Array<tink.http.Header.HeaderField>, get: String }; + + public function new(to: String, filename: String, size: Int, mime: String, hashes: Array<{algo:String,hash:BytesData}>) { + /* Build basic query */ + queryId = ID.short(); + queryStanza = new Stanza( + "iq", + { to: to, type: "get", id: queryId } + ).tag("request", { xmlns: xmlns, filename: filename, size: Std.string(size), "content-type": mime }); + for (hash in hashes) { + queryStanza.textTag("hash", Base64.encode(Bytes.ofData(hash.hash)), { xmlns: "urn:xmpp:hashes:2", algo: hash.algo }); + } + queryStanza.up(); + } + + public function handleResponse(stanza:Stanza) { + responseStanza = stanza; + finish(); + } + + public function getResult() { + if (responseStanza == null) { + return null; + } + if(result == null) { + final q = responseStanza.getChild("slot", xmlns); + if(q == null) { + return null; + } + final get = q.findText("get@url"); + if (get == null) return null; + final put = q.findText("put@url"); + if (put == null) return null; + final headers = []; + for (header in q.getChild("put").allTags("header")) { + headers.push(new tink.http.Header.HeaderField(header.attr.get("name"), header.getText())); + } + result = { get: get, put: put, putHeaders: headers }; + } + return result; + } +}