diff --git a/deno.json b/deno.json index ef4ea803..43a33de8 100644 --- a/deno.json +++ b/deno.json @@ -41,7 +41,6 @@ "@gfx/canvas-wasm": "jsr:@gfx/canvas-wasm@^0.4.2", "@hono/hono": "jsr:@hono/hono@^4.4.6", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", - "@jcayzac/image-information": "npm:@jcayzac/image-information@1.1.1", "@lambdalisue/async": "jsr:@lambdalisue/async@^2.1.1", "@negrel/webpush": "jsr:@negrel/webpush@^0.3.0", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", @@ -69,6 +68,7 @@ "entities": "npm:entities@^4.5.0", "fast-stable-stringify": "npm:fast-stable-stringify@^1.0.0", "formdata-helper": "npm:formdata-helper@^0.3.0", + "get-pixels": "npm:get-pixels@3.3.3", "hono-rate-limiter": "npm:hono-rate-limiter@^0.3.0", "iso-639-1": "npm:iso-639-1@2.1.15", "isomorphic-dompurify": "npm:isomorphic-dompurify@^2.16.0", @@ -87,6 +87,7 @@ "postgres": "https://gitlab.com/soapbox-pub/postgres.js/-/raw/e79d7d2039446fbf7a37d4eca0d17e94a94b8b53/deno/mod.js", "prom-client": "npm:prom-client@^15.1.2", "question-deno": "https://raw.githubusercontent.com/ocpu/question-deno/10022b8e52555335aa510adb08b0a300df3cf904/mod.ts", + "sharp": "npm:sharp@^0.33.5", "tldts": "npm:tldts@^6.0.14", "tseep": "npm:tseep@^1.2.1", "type-fest": "npm:type-fest@^4.3.0", diff --git a/deno.lock b/deno.lock index ccb3974a..f7fef00f 100644 --- a/deno.lock +++ b/deno.lock @@ -36,6 +36,7 @@ "jsr:@nostrify/nostrify@~0.22.4": "0.22.4", "jsr:@nostrify/nostrify@~0.22.5": "0.22.5", "jsr:@nostrify/policies@0.33": "0.33.0", + "jsr:@nostrify/policies@0.33.1": "0.33.1", "jsr:@nostrify/policies@0.34": "0.34.0", "jsr:@nostrify/policies@0.35": "0.35.0", "jsr:@nostrify/policies@0.36": "0.36.0", @@ -80,7 +81,6 @@ "jsr:@std/streams@0.223": "0.223.0", "npm:@electric-sql/pglite@~0.2.8": "0.2.8", "npm:@isaacs/ttlcache@^1.4.1": "1.4.1", - "npm:@jcayzac/image-information@1.1.1": "1.1.1", "npm:@noble/hashes@^1.4.0": "1.4.0", "npm:@noble/secp256k1@2": "2.1.0", "npm:@scure/base@^1.1.6": "1.1.6", @@ -118,6 +118,7 @@ "npm:png-to-ico@^2.1.8": "2.1.8", "npm:postgres@3.4.4": "3.4.4", "npm:prom-client@^15.1.2": "15.1.2", + "npm:sharp@~0.33.5": "0.33.5", "npm:tldts@^6.0.14": "6.1.18", "npm:tseep@^1.2.1": "1.2.1", "npm:type-fest@^4.3.0": "4.18.2", @@ -657,15 +658,99 @@ "@electric-sql/pglite@0.2.8": { "integrity": "sha512-0wSmQu22euBRzR5ghqyIHnBH4MfwlkL5WstOrrA3KOsjEWEglvoL/gH92JajEUA6Ufei/+qbkB2hVloC/K/RxQ==" }, + "@emnapi/runtime@1.3.1": { + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "dependencies": [ + "tslib" + ] + }, + "@img/sharp-darwin-arm64@0.33.5": { + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "dependencies": [ + "@img/sharp-libvips-darwin-arm64" + ] + }, + "@img/sharp-darwin-x64@0.33.5": { + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "dependencies": [ + "@img/sharp-libvips-darwin-x64" + ] + }, + "@img/sharp-libvips-darwin-arm64@1.0.4": { + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==" + }, + "@img/sharp-libvips-darwin-x64@1.0.4": { + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==" + }, + "@img/sharp-libvips-linux-arm64@1.0.4": { + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==" + }, + "@img/sharp-libvips-linux-arm@1.0.5": { + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==" + }, + "@img/sharp-libvips-linux-s390x@1.0.4": { + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==" + }, + "@img/sharp-libvips-linux-x64@1.0.4": { + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==" + }, + "@img/sharp-libvips-linuxmusl-arm64@1.0.4": { + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==" + }, + "@img/sharp-libvips-linuxmusl-x64@1.0.4": { + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==" + }, + "@img/sharp-linux-arm64@0.33.5": { + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "dependencies": [ + "@img/sharp-libvips-linux-arm64" + ] + }, + "@img/sharp-linux-arm@0.33.5": { + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "dependencies": [ + "@img/sharp-libvips-linux-arm" + ] + }, + "@img/sharp-linux-s390x@0.33.5": { + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "dependencies": [ + "@img/sharp-libvips-linux-s390x" + ] + }, + "@img/sharp-linux-x64@0.33.5": { + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "dependencies": [ + "@img/sharp-libvips-linux-x64" + ] + }, + "@img/sharp-linuxmusl-arm64@0.33.5": { + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "dependencies": [ + "@img/sharp-libvips-linuxmusl-arm64" + ] + }, + "@img/sharp-linuxmusl-x64@0.33.5": { + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "dependencies": [ + "@img/sharp-libvips-linuxmusl-x64" + ] + }, + "@img/sharp-wasm32@0.33.5": { + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "dependencies": [ + "@emnapi/runtime" + ] + }, + "@img/sharp-win32-ia32@0.33.5": { + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==" + }, + "@img/sharp-win32-x64@0.33.5": { + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==" + }, "@isaacs/ttlcache@1.4.1": { "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==" }, - "@jcayzac/image-information@1.1.1": { - "integrity": "sha512-WXM5RTu3tTuAPXPx4ytbywjqTxnjBUWM1sY+7A9UPqPwQbM9V3jkY/rzo1OJ6VYSpogFdK4cyc+o9FZFZLW+nw==", - "dependencies": [ - "image-size" - ] - }, "@noble/ciphers@0.5.3": { "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==" }, @@ -802,6 +887,29 @@ "string-width" ] }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name" + ] + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string@1.9.1": { + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": [ + "color-name", + "simple-swizzle" + ] + }, + "color@4.2.3": { + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": [ + "color-convert", + "color-string" + ] + }, "colorette@2.0.20": { "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" }, @@ -865,6 +973,9 @@ "delayed-stream@1.0.0": { "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "detect-libc@2.0.3": { + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" + }, "dom-serializer@2.0.0": { "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dependencies": [ @@ -1007,6 +1118,9 @@ "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "is-arrayish@0.3.2": { + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "is-fullwidth-code-point@4.0.0": { "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==" }, @@ -1308,6 +1422,36 @@ "xmlchars" ] }, + "semver@7.6.3": { + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + }, + "sharp@0.33.5": { + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dependencies": [ + "@img/sharp-darwin-arm64", + "@img/sharp-darwin-x64", + "@img/sharp-libvips-darwin-arm64", + "@img/sharp-libvips-darwin-x64", + "@img/sharp-libvips-linux-arm", + "@img/sharp-libvips-linux-arm64", + "@img/sharp-libvips-linux-s390x", + "@img/sharp-libvips-linux-x64", + "@img/sharp-libvips-linuxmusl-arm64", + "@img/sharp-libvips-linuxmusl-x64", + "@img/sharp-linux-arm", + "@img/sharp-linux-arm64", + "@img/sharp-linux-s390x", + "@img/sharp-linux-x64", + "@img/sharp-linuxmusl-arm64", + "@img/sharp-linuxmusl-x64", + "@img/sharp-wasm32", + "@img/sharp-win32-ia32", + "@img/sharp-win32-x64", + "color", + "detect-libc", + "semver" + ] + }, "shebang-command@2.0.0": { "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dependencies": [ @@ -1323,6 +1467,12 @@ "signal-exit@4.1.0": { "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" }, + "simple-swizzle@0.2.2": { + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": [ + "is-arrayish" + ] + }, "slice-ansi@5.0.0": { "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dependencies": [ @@ -1411,6 +1561,9 @@ "tseep@1.2.1": { "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==" }, + "tslib@2.6.2": { + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "type-fest@3.13.1": { "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==" }, @@ -2130,7 +2283,6 @@ "jsr:@std/streams@0.223", "npm:@electric-sql/pglite@~0.2.8", "npm:@isaacs/ttlcache@^1.4.1", - "npm:@jcayzac/image-information@1.1.1", "npm:@noble/secp256k1@2", "npm:@scure/base@^1.1.6", "npm:blurhash@2.0.5", @@ -2140,6 +2292,7 @@ "npm:entities@^4.5.0", "npm:fast-stable-stringify@1", "npm:formdata-helper@0.3", + "npm:get-pixels@3.3.3", "npm:hono-rate-limiter@0.3", "npm:iso-639-1@2.1.15", "npm:isomorphic-dompurify@^2.16.0", @@ -2156,6 +2309,7 @@ "npm:path-to-regexp@^7.1.0", "npm:png-to-ico@^2.1.8", "npm:prom-client@^15.1.2", + "npm:sharp@~0.33.5", "npm:tldts@^6.0.14", "npm:tseep@^1.2.1", "npm:type-fest@^4.3.0", diff --git a/src/uploaders/IPFSUploader.ts b/src/uploaders/IPFSUploader.ts index 75f082e3..1065ea0c 100644 --- a/src/uploaders/IPFSUploader.ts +++ b/src/uploaders/IPFSUploader.ts @@ -1,11 +1,11 @@ import { NUploader } from '@nostrify/nostrify'; import { z } from 'zod'; -import { probe } from '@jcayzac/image-information'; +import sharp from 'sharp'; import { Stickynotes } from '@soapbox/stickynotes'; import { encode } from 'blurhash'; import { encodeHex } from '@std/encoding/hex'; -const console = new Stickynotes('ditto:ipfs:uploader'); +const console = new Stickynotes('ditto:uploader:ipfs'); export interface IPFSUploaderOpts { baseUrl: string; @@ -25,6 +25,9 @@ function toByteArray(f: File): Promise { reader.readAsArrayBuffer(f); }); } +type Nip94Metadata = + & Record<'url' | 'm', string> + & Partial>; /** * IPFS uploader. It expects an IPFS node up and running. @@ -55,29 +58,44 @@ export class IPFSUploader implements NUploader { }); const { Hash: cid } = IPFSUploader.schema().parse(await response.json()); - const tags: [['url', string], ...string[][]] = [ - ['url', new URL(`/ipfs/${cid}`, this.baseUrl).toString()], - ['m', file.type], - ['cid', cid], - ['size', file.size.toString()], - ]; + const tags: Nip94Metadata = { + url: new URL(`/ipfs/${cid}`, this.baseUrl).toString(), + m: file.type, + cid, + size: file.size.toString(), + }; try { const buffer = await toByteArray(file); const hash = await crypto.subtle.digest('SHA-256', buffer).then(encodeHex); - tags.push(['x', hash], ['ox', hash]); - const metadata = probe(buffer); - if (metadata) { - // sane default from https://github.com/woltapp/blurhash readme - const blurhash = encode(new Uint8ClampedArray(buffer), metadata.width, metadata.height, 4, 4); - tags.push(['blurhash', blurhash]); - tags.push(['dim', `${metadata.width}x${metadata.height}`]); + tags.x = tags.ox = hash; + const img = sharp(buffer); + const metadata = await img.metadata(); + + if (metadata.width && metadata.height) { + tags.dim = `${metadata.width}x${metadata.height}`; + const pixels = await img + .raw() + .ensureAlpha() + .toBuffer({ resolveWithObject: true }) + .then((buf) => { + return new Uint8ClampedArray(buf.data); + }); + tags.blurhash = encode( + pixels, + metadata.width, + metadata.height, + // sane default from https://github.com/woltapp/blurhash readme + 4, + 4, + ); } } catch (e) { console.error(`Error parsing ipfs metadata: ${e}`); } - return tags; + console.debug(tags); + return Object.entries(tags) as [['url', string], ...string[][]]; } async delete(cid: string, opts?: { signal?: AbortSignal }): Promise {