From b0a2422437f0c7f816eb2eb0769ef3a0e2a07d61 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Sun, 27 Oct 2024 07:35:29 +0530 Subject: [PATCH 01/36] add more metadata to IPFSUploader --- deno.json | 2 ++ deno.lock | 28 +++++++++++++++++++++++++ src/uploaders/IPFSUploader.ts | 39 +++++++++++++++++++++++++++++++++-- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index 0de69b95..ef4ea803 100644 --- a/deno.json +++ b/deno.json @@ -41,6 +41,7 @@ "@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", @@ -59,6 +60,7 @@ "@std/json": "jsr:@std/json@^0.223.0", "@std/media-types": "jsr:@std/media-types@^0.224.1", "@std/streams": "jsr:@std/streams@^0.223.0", + "blurhash": "npm:blurhash@2.0.5", "comlink": "npm:comlink@^4.4.1", "comlink-async-generator": "npm:comlink-async-generator@^0.0.1", "commander": "npm:commander@12.1.0", diff --git a/deno.lock b/deno.lock index c0e4711c..50d01408 100644 --- a/deno.lock +++ b/deno.lock @@ -80,12 +80,14 @@ "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", "npm:@scure/bip32@^1.4.0": "1.4.0", "npm:@scure/bip39@^1.3.0": "1.3.0", "npm:@types/node@*": "18.16.19", + "npm:blurhash@2.0.5": "2.0.5", "npm:comlink-async-generator@*": "0.0.1", "npm:comlink-async-generator@^0.0.1": "0.0.1", "npm:comlink@^4.4.1": "4.4.1", @@ -658,6 +660,12 @@ "@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==" }, @@ -769,6 +777,9 @@ "bintrees@1.0.2": { "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" }, + "blurhash@2.0.5": { + "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==" + }, "braces@3.0.2": { "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dependencies": [ @@ -987,6 +998,15 @@ "safer-buffer" ] }, + "image-size@1.1.1": { + "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "dependencies": [ + "queue" + ] + }, + "inherits@2.0.4": { + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "is-fullwidth-code-point@4.0.0": { "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==" }, @@ -1260,6 +1280,12 @@ "punycode@2.3.1": { "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, + "queue@6.0.2": { + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dependencies": [ + "inherits" + ] + }, "restore-cursor@4.0.0": { "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "dependencies": [ @@ -2104,12 +2130,14 @@ "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:comlink-async-generator@^0.0.1", "npm:comlink@^4.4.1", "npm:commander@12.1.0", "npm:entities@^4.5.0", + "npm:fast-blurhash@1.1.4", "npm:fast-stable-stringify@1", "npm:formdata-helper@0.3", "npm:hono-rate-limiter@0.3", diff --git a/src/uploaders/IPFSUploader.ts b/src/uploaders/IPFSUploader.ts index 7bf5165b..75f082e3 100644 --- a/src/uploaders/IPFSUploader.ts +++ b/src/uploaders/IPFSUploader.ts @@ -1,5 +1,11 @@ import { NUploader } from '@nostrify/nostrify'; import { z } from 'zod'; +import { probe } from '@jcayzac/image-information'; +import { Stickynotes } from '@soapbox/stickynotes'; +import { encode } from 'blurhash'; +import { encodeHex } from '@std/encoding/hex'; + +const console = new Stickynotes('ditto:ipfs:uploader'); export interface IPFSUploaderOpts { baseUrl: string; @@ -7,6 +13,19 @@ export interface IPFSUploaderOpts { fetch?: typeof fetch; } +function toByteArray(f: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener('loadend', (m) => { + if (m?.target?.result instanceof ArrayBuffer) { + resolve(new Uint8Array(m.target.result)); + } else reject('Error loading file: readAsArrayBufferFailed'); + }); + reader.addEventListener('error', (e) => reject(e)); + reader.readAsArrayBuffer(f); + }); +} + /** * IPFS uploader. It expects an IPFS node up and running. * It will try to connect to `http://localhost:5001` by default, @@ -36,13 +55,29 @@ export class IPFSUploader implements NUploader { }); const { Hash: cid } = IPFSUploader.schema().parse(await response.json()); - - return [ + const tags: [['url', string], ...string[][]] = [ ['url', new URL(`/ipfs/${cid}`, this.baseUrl).toString()], ['m', file.type], ['cid', 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}`]); + } + } catch (e) { + console.error(`Error parsing ipfs metadata: ${e}`); + } + + return tags; } async delete(cid: string, opts?: { signal?: AbortSignal }): Promise { From de4b5f55d39f0ee322ad99ea252d6233c5e469d4 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Sun, 27 Oct 2024 07:41:26 +0530 Subject: [PATCH 02/36] update lockfile --- deno.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.lock b/deno.lock index 50d01408..ccb3974a 100644 --- a/deno.lock +++ b/deno.lock @@ -2133,11 +2133,11 @@ "npm:@jcayzac/image-information@1.1.1", "npm:@noble/secp256k1@2", "npm:@scure/base@^1.1.6", + "npm:blurhash@2.0.5", "npm:comlink-async-generator@^0.0.1", "npm:comlink@^4.4.1", "npm:commander@12.1.0", "npm:entities@^4.5.0", - "npm:fast-blurhash@1.1.4", "npm:fast-stable-stringify@1", "npm:formdata-helper@0.3", "npm:hono-rate-limiter@0.3", From f5f31ce98f86c9477f086853dcee29f59ddbf534 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Sun, 27 Oct 2024 09:11:29 +0530 Subject: [PATCH 03/36] fix image uploads, use sharp to get image metadata instead of image-information --- deno.json | 3 +- deno.lock | 170 ++++++++++++++++++++++++++++++++-- src/uploaders/IPFSUploader.ts | 50 ++++++---- 3 files changed, 198 insertions(+), 25 deletions(-) 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 { From ff8374103e8ec7adfa8bcc788eaeba49f41151b4 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Sun, 27 Oct 2024 10:00:47 +0530 Subject: [PATCH 04/36] remove unused dep --- deno.json | 1 - deno.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/deno.json b/deno.json index 43a33de8..6db0fceb 100644 --- a/deno.json +++ b/deno.json @@ -68,7 +68,6 @@ "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", diff --git a/deno.lock b/deno.lock index f7fef00f..14cc1086 100644 --- a/deno.lock +++ b/deno.lock @@ -2292,7 +2292,6 @@ "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", From 713260e110c472c0cafc0a69a4f8eca7c708e9a6 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Sun, 27 Oct 2024 10:16:00 +0530 Subject: [PATCH 05/36] split off nip94 metadata stuff into its own file, port changes to s3uploader --- src/interfaces/Nip94Metadata.ts | 31 +++++++++++++++++ src/uploaders/IPFSUploader.ts | 61 +++------------------------------ src/uploaders/S3Uploader.ts | 13 +++---- src/utils/image-metadata.ts | 54 +++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 62 deletions(-) create mode 100644 src/interfaces/Nip94Metadata.ts create mode 100644 src/utils/image-metadata.ts diff --git a/src/interfaces/Nip94Metadata.ts b/src/interfaces/Nip94Metadata.ts new file mode 100644 index 00000000..6069ac31 --- /dev/null +++ b/src/interfaces/Nip94Metadata.ts @@ -0,0 +1,31 @@ +/** + * Required fields of NIP-94 metadata for images. + * Contains the following fields: + * * `url` - required, the URL to of the file + * * `m` - required, the file mimetype. + */ +export type Nip94MetadataRequired = Record<'url' | 'm', string>; + +/** + * Optional fields of NIP-94 metadata for images. + * Contains the following fields: + * * `x` - sha-256 hash + * * `ox` - sha-256 hash + * * `dim` - image dimensions in ${w}x${h} format + * * `blurhash` - the blurhash for the image. useful for image previews etc + * * `cid` - the ipfs cid of the image. + */ +export type Nip94MetadataOptional = Partial>; + +/** + * NIP-94 metadata for images. + * Contains the following fields: + * * `url` - required, the URL to of the file + * * `m` - required, the file mimetype. + * * `x` - sha-256 hash + * * `ox` - sha-256 hash + * * `dim` - image dimensions in ${w}x${h} format + * * `blurhash` - the blurhash for the image. useful for image previews etc + * * `cid` - the ipfs cid of the image. + */ +export type Nip94Metadata = Nip94MetadataOptional & Nip94MetadataRequired; diff --git a/src/uploaders/IPFSUploader.ts b/src/uploaders/IPFSUploader.ts index 1065ea0c..07184016 100644 --- a/src/uploaders/IPFSUploader.ts +++ b/src/uploaders/IPFSUploader.ts @@ -1,11 +1,6 @@ import { NUploader } from '@nostrify/nostrify'; import { z } from 'zod'; -import sharp from 'sharp'; -import { Stickynotes } from '@soapbox/stickynotes'; -import { encode } from 'blurhash'; -import { encodeHex } from '@std/encoding/hex'; - -const console = new Stickynotes('ditto:uploader:ipfs'); +import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts'; export interface IPFSUploaderOpts { baseUrl: string; @@ -13,22 +8,6 @@ export interface IPFSUploaderOpts { fetch?: typeof fetch; } -function toByteArray(f: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener('loadend', (m) => { - if (m?.target?.result instanceof ArrayBuffer) { - resolve(new Uint8Array(m.target.result)); - } else reject('Error loading file: readAsArrayBufferFailed'); - }); - reader.addEventListener('error', (e) => reject(e)); - reader.readAsArrayBuffer(f); - }); -} -type Nip94Metadata = - & Record<'url' | 'm', string> - & Partial>; - /** * IPFS uploader. It expects an IPFS node up and running. * It will try to connect to `http://localhost:5001` by default, @@ -58,44 +37,14 @@ export class IPFSUploader implements NUploader { }); const { Hash: cid } = IPFSUploader.schema().parse(await response.json()); - const tags: Nip94Metadata = { + + return Object.entries({ 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.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}`); - } - - console.debug(tags); - return Object.entries(tags) as [['url', string], ...string[][]]; + ...await getOptionalNip94Metadata(file), + }) as [['url', string], ...string[][]]; } async delete(cid: string, opts?: { signal?: AbortSignal }): Promise { diff --git a/src/uploaders/S3Uploader.ts b/src/uploaders/S3Uploader.ts index b74796ab..2059077d 100644 --- a/src/uploaders/S3Uploader.ts +++ b/src/uploaders/S3Uploader.ts @@ -7,6 +7,7 @@ import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; import { Conf } from '@/config.ts'; +import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts'; export interface S3UploaderOpts { endPoint: string; @@ -45,12 +46,12 @@ export class S3Uploader implements NUploader { const path = (pathStyle && bucket) ? join(bucket, filename) : filename; const url = new URL(path, Conf.mediaDomain).toString(); - return [ - ['url', url], - ['m', file.type], - ['x', sha256], - ['size', file.size.toString()], - ]; + return Object.entries({ + url: url, + m: file.type, + size: file.size.toString(), + ...await getOptionalNip94Metadata(file), + }) as [['url', string], ...string[][]]; } async delete(objectName: string) { diff --git a/src/utils/image-metadata.ts b/src/utils/image-metadata.ts new file mode 100644 index 00000000..2d5e861f --- /dev/null +++ b/src/utils/image-metadata.ts @@ -0,0 +1,54 @@ +import sharp from 'sharp'; +import { encode } from 'blurhash'; +import { encodeHex } from '@std/encoding/hex'; +import type { Nip94MetadataOptional } from '@/interfaces/Nip94Metadata.ts'; +import { Stickynotes } from '@soapbox/stickynotes'; + +const console = new Stickynotes('ditto:uploaders'); + +function toByteArray(f: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener('loadend', (m) => { + if (m?.target?.result instanceof ArrayBuffer) { + resolve(new Uint8Array(m.target.result)); + } else reject('Error loading file: readAsArrayBufferFailed'); + }); + reader.addEventListener('error', (e) => reject(e)); + reader.readAsArrayBuffer(f); + }); +} + +export async function getOptionalNip94Metadata(f: File): Promise { + const tags: Nip94MetadataOptional = {}; + try { + const buffer = await toByteArray(f); + const hash = await crypto.subtle.digest('SHA-256', buffer).then(encodeHex); + 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; +} From 57725fa9ac540bedb6fd0d333966b879da11b9d4 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Sun, 27 Oct 2024 10:21:53 +0530 Subject: [PATCH 06/36] neatness --- src/uploaders/S3Uploader.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/uploaders/S3Uploader.ts b/src/uploaders/S3Uploader.ts index 2059077d..771db03f 100644 --- a/src/uploaders/S3Uploader.ts +++ b/src/uploaders/S3Uploader.ts @@ -44,10 +44,9 @@ export class S3Uploader implements NUploader { const { pathStyle, bucket } = Conf.s3; const path = (pathStyle && bucket) ? join(bucket, filename) : filename; - const url = new URL(path, Conf.mediaDomain).toString(); return Object.entries({ - url: url, + url: new URL(path, Conf.mediaDomain).toString(), m: file.type, size: file.size.toString(), ...await getOptionalNip94Metadata(file), From e95441f5b94988a32292645171e9b41cfb5a3823 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Wed, 30 Oct 2024 11:04:19 +0530 Subject: [PATCH 07/36] revert changes to IPFSUploader and S3Uploader --- src/uploaders/IPFSUploader.ts | 14 ++++++-------- src/uploaders/S3Uploader.ts | 15 ++++++++------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/uploaders/IPFSUploader.ts b/src/uploaders/IPFSUploader.ts index 07184016..7bf5165b 100644 --- a/src/uploaders/IPFSUploader.ts +++ b/src/uploaders/IPFSUploader.ts @@ -1,6 +1,5 @@ import { NUploader } from '@nostrify/nostrify'; import { z } from 'zod'; -import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts'; export interface IPFSUploaderOpts { baseUrl: string; @@ -38,13 +37,12 @@ export class IPFSUploader implements NUploader { const { Hash: cid } = IPFSUploader.schema().parse(await response.json()); - return Object.entries({ - url: new URL(`/ipfs/${cid}`, this.baseUrl).toString(), - m: file.type, - cid, - size: file.size.toString(), - ...await getOptionalNip94Metadata(file), - }) as [['url', string], ...string[][]]; + return [ + ['url', new URL(`/ipfs/${cid}`, this.baseUrl).toString()], + ['m', file.type], + ['cid', cid], + ['size', file.size.toString()], + ]; } async delete(cid: string, opts?: { signal?: AbortSignal }): Promise { diff --git a/src/uploaders/S3Uploader.ts b/src/uploaders/S3Uploader.ts index 771db03f..c784cdab 100644 --- a/src/uploaders/S3Uploader.ts +++ b/src/uploaders/S3Uploader.ts @@ -7,7 +7,6 @@ import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; import { Conf } from '@/config.ts'; -import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts'; export interface S3UploaderOpts { endPoint: string; @@ -45,12 +44,14 @@ export class S3Uploader implements NUploader { const path = (pathStyle && bucket) ? join(bucket, filename) : filename; - return Object.entries({ - url: new URL(path, Conf.mediaDomain).toString(), - m: file.type, - size: file.size.toString(), - ...await getOptionalNip94Metadata(file), - }) as [['url', string], ...string[][]]; + const url = new URL(path, Conf.mediaDomain).toString(); + + return [ + ['url', url], + ['m', file.type], + ['x', sha256], + ['size', file.size.toString()], + ]; } async delete(objectName: string) { From 38288aadb3a7bb1d6cb39f6757fd929cc82beac7 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Wed, 30 Oct 2024 11:06:48 +0530 Subject: [PATCH 08/36] move tag logic to upload.ts --- src/utils/image-metadata.ts | 2 +- src/utils/upload.ts | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/utils/image-metadata.ts b/src/utils/image-metadata.ts index 2d5e861f..75b2f6ba 100644 --- a/src/utils/image-metadata.ts +++ b/src/utils/image-metadata.ts @@ -6,7 +6,7 @@ import { Stickynotes } from '@soapbox/stickynotes'; const console = new Stickynotes('ditto:uploaders'); -function toByteArray(f: File): Promise { +export function toByteArray(f: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener('loadend', (m) => { diff --git a/src/utils/upload.ts b/src/utils/upload.ts index 81c88c86..cadac9bc 100644 --- a/src/utils/upload.ts +++ b/src/utils/upload.ts @@ -3,6 +3,9 @@ import { HTTPException } from '@hono/hono/http-exception'; import { AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; import { DittoUpload, dittoUploads } from '@/DittoUploads.ts'; +import { getOptionalNip94Metadata, toByteArray } from '@/utils/image-metadata.ts'; +import type { Nip94MetadataOptional } from '@/interfaces/Nip94Metadata.ts'; +import { encodeHex } from '@std/encoding/hex'; interface FileMeta { pubkey: string; @@ -30,12 +33,31 @@ export async function uploadFile( } const tags = await uploader.upload(file, { signal }); + const tagMap = tags.reduce((map, value) => map.set(value[0], value.slice(1)), new Map()); + const url = tags[0][1]; if (description) { tags.push(['alt', description]); } + let metadata: Nip94MetadataOptional | undefined; + if (!tagMap.has('dim')) { + // blurhash needs us to call sharp() anyway to decode the image data. + // all getOptionalNip94Metadata does is call these in sequence, plus + // one extra sha256 which is whatever (and actually does come in handy later.) + metadata ??= await getOptionalNip94Metadata(file); + tags.push(['dim', metadata.dim!]); + if (!tagMap.has('blurhash')) { + tags.push(['blurhash', metadata.blurhash!]); + } + } + if (!tagMap.has('x') || !tagMap.has('ox')) { + const hash = metadata?.x || await crypto.subtle.digest('SHA-256', await toByteArray(file)).then(encodeHex); + tags.push(['x', hash!]); + tags.push(['ox', hash!]); + } + const upload = { id: crypto.randomUUID(), url, From 03d93ff8100850655955b82131e23fbc8c4d55d4 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 1 Nov 2024 18:14:51 -0300 Subject: [PATCH 09/36] fix: 'Context' resolves to a type-only declaration and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled. --- src/controllers/api/fallback.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/fallback.ts b/src/controllers/api/fallback.ts index 9e170093..0e98ac79 100644 --- a/src/controllers/api/fallback.ts +++ b/src/controllers/api/fallback.ts @@ -1,4 +1,4 @@ -import { Context } from '@hono/hono'; +import { type Context } from '@hono/hono'; const emptyArrayController = (c: Context) => c.json([]); const notImplementedController = (c: Context) => Promise.resolve(c.json({ error: 'Not implemented' }, 404)); From c6ff4a9d2547b25c18afceff7f839283908df182 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 1 Nov 2024 23:07:51 -0300 Subject: [PATCH 10/36] feat: add sizesSchema --- src/schema.test.ts | 11 ++++++++++- src/schema.ts | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/schema.test.ts b/src/schema.test.ts index c6b577de..66809c53 100644 --- a/src/schema.test.ts +++ b/src/schema.test.ts @@ -1,6 +1,6 @@ import { assertEquals } from '@std/assert'; -import { percentageSchema } from '@/schema.ts'; +import { percentageSchema, sizesSchema } from '@/schema.ts'; Deno.test('Value is any percentage from 1 to 100', () => { assertEquals(percentageSchema.safeParse('latvia' as unknown).success, false); @@ -20,3 +20,12 @@ Deno.test('Value is any percentage from 1 to 100', () => { assertEquals(percentageSchema.safeParse('1e1').success, true); }); + +Deno.test('Size or sizes has correct format', () => { + assertEquals(sizesSchema.safeParse('orphan' as unknown).success, false); + assertEquals(sizesSchema.safeParse('0000x 20x20' as unknown).success, false); + assertEquals(sizesSchema.safeParse('0000x10 20X20 1x22' as unknown).success, false); + assertEquals(sizesSchema.safeParse('1000x10 20X20 1x22' as unknown).success, true); + assertEquals(sizesSchema.safeParse('3333X6666 1x22 f' as unknown).success, false); + assertEquals(sizesSchema.safeParse('11xxxxxxx0 20X20 1x22' as unknown).success, false); +}); diff --git a/src/schema.ts b/src/schema.ts index a9dd56e3..23e2938d 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -65,6 +65,9 @@ const localeSchema = z.string().transform((val, ctx) => { } }); +/** White-space separated list of sizes, each in the format x or with "X" in upper case. */ +const sizesSchema = z.string().regex(/^(?:[1-9]\d{0,3}[xX][1-9]\d{0,3})(?:\s+[1-9]\d{0,3}[xX][1-9]\d{0,3})*$/); + export { booleanParamSchema, decode64Schema, @@ -75,4 +78,5 @@ export { localeSchema, percentageSchema, safeUrlSchema, + sizesSchema, }; From 339b13c084051a9d1f4189285c2d1c4fd3c46fbe Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 1 Nov 2024 23:14:59 -0300 Subject: [PATCH 11/36] feat: create updateInstanceController for now only update: title, description, screenshots (user must provide the image URL) and thumbnail (user must provide the image URL) screenshots array is stored in the content of the kind 0 of the --- src/app.ts | 3 ++ src/controllers/api/ditto.ts | 55 ++++++++++++++++++++++++++++++++- src/controllers/api/instance.ts | 1 + src/controllers/manifest.ts | 1 + src/schemas/nostr.ts | 35 +++++++++++++++++++-- src/utils/instance.ts | 5 ++- 6 files changed, 96 insertions(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index fdcacf29..74e6ff72 100644 --- a/src/app.ts +++ b/src/app.ts @@ -49,6 +49,7 @@ import { nameRequestController, nameRequestsController, statusZapSplitsController, + updateInstanceController, updateZapSplitsController, } from '@/controllers/api/ditto.ts'; import { emptyArrayController, notImplementedController } from '@/controllers/api/fallback.ts'; @@ -303,6 +304,8 @@ app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAd app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); +app.put('/api/v1/admin/ditto/instance', requireRole('admin'), updateInstanceController); + app.post('/api/v1/ditto/names', requireSigner, nameRequestController); app.get('/api/v1/ditto/names', requireSigner, nameRequestsController); diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index b9be027f..6682522a 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -7,11 +7,13 @@ import { addTag } from '@/utils/tags.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { booleanParamSchema, percentageSchema } from '@/schema.ts'; import { Conf } from '@/config.ts'; -import { createEvent, paginated, parseBody } from '@/utils/api.ts'; +import { createEvent, paginated, parseBody, updateEvent } from '@/utils/api.ts'; +import { getInstanceMetadata } from '@/utils/instance.ts'; import { deleteTag } from '@/utils/tags.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts'; import { getAuthor } from '@/queries.ts'; +import { screenshotsSchema } from '@/schemas/nostr.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { renderNameRequest } from '@/views/ditto.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; @@ -287,3 +289,54 @@ export const statusZapSplitsController: AppController = async (c) => { return c.json(zapSplits, 200); }; + +const updateInstanceSchema = z.object({ + title: z.string().optional(), + description: z.string().optional(), + screenshots: screenshotsSchema.optional(), + thumbnail: z.object({ + url: z.string().url(), + blurhash: z.string().optional(), + versions: z.object({ + '@1x': z.string().url().optional(), + '@2x': z.string().url().optional(), + }).optional(), + }).optional(), +}).strict(); + +export const updateInstanceController: AppController = async (c) => { + const body = await parseBody(c.req.raw); + const result = updateInstanceSchema.safeParse(body); + const pubkey = Conf.pubkey; + + if (!result.success) { + return c.json(result.error, 422); + } + + await updateEvent( + { kinds: [0], authors: [pubkey], limit: 1 }, + async (_) => { + const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal); + const { + title, + description, + screenshots, + thumbnail, + } = result.data; + + meta.name = title ?? meta.name; + meta.about = description ?? meta.about; + meta.screenshots = screenshots ?? meta.screenshots; + meta.thumbnail = thumbnail ?? meta.thumbnail; + + return { + kind: 0, + content: JSON.stringify(meta), + tags: [], + }; + }, + c, + ); + + return c.json(204); +}; diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 78a72dd4..9f504cad 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -96,6 +96,7 @@ const instanceV2Controller: AppController = async (c) => { '@2x': meta.picture, }, }, + screenshots: meta.screenshots, languages: [ 'en', ], diff --git a/src/controllers/manifest.ts b/src/controllers/manifest.ts index 60e2a2ac..2e75de04 100644 --- a/src/controllers/manifest.ts +++ b/src/controllers/manifest.ts @@ -20,6 +20,7 @@ export const manifestController: AppController = async (c) => { scope: '/', short_name: meta.name, start_url: '/', + screenshots: meta.screenshots, }; return c.json(manifest, { diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index d8aa29a4..46f68a34 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -2,17 +2,48 @@ import { NSchema as n } from '@nostrify/nostrify'; import { getEventHash, verifyEvent } from 'nostr-tools'; import { z } from 'zod'; -import { safeUrlSchema } from '@/schema.ts'; +import { safeUrlSchema, sizesSchema } from '@/schema.ts'; /** Nostr event schema that also verifies the event's signature. */ const signedEventSchema = n.event() .refine((event) => event.id === getEventHash(event), 'Event ID does not match hash') .refine(verifyEvent, 'Event signature is invalid'); +/** + * Stored in the kind 0 content. + * https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots + */ +const screenshotsSchema = z.array(z.object({ + form_factor: z.enum(['narrow', 'wide']).optional(), + label: z.string().optional(), + platform: z.enum([ + 'android', + 'chromeos', + 'ipados', + 'ios', + 'kaios', + 'macos', + 'windows', + 'xbox', + 'chrome_web_store', + 'itunes', + 'microsoft-inbox', + 'microsoft-store', + 'play', + ]).optional(), + /** https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots#sizes */ + sizes: sizesSchema, + /** Absolute URL. */ + src: z.string().url(), + /** MIME type of the image. */ + type: z.string().optional(), +})); + /** Kind 0 content schema for the Ditto server admin user. */ const serverMetaSchema = n.metadata().and(z.object({ tagline: z.string().optional().catch(undefined), email: z.string().optional().catch(undefined), + screenshots: screenshotsSchema.optional(), })); /** NIP-11 Relay Information Document. */ @@ -32,4 +63,4 @@ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url() /** NIP-30 custom emoji tag. */ type EmojiTag = z.infer; -export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, serverMetaSchema, signedEventSchema }; +export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, screenshotsSchema, serverMetaSchema, signedEventSchema }; diff --git a/src/utils/instance.ts b/src/utils/instance.ts index f46541c2..c0b9c0d4 100644 --- a/src/utils/instance.ts +++ b/src/utils/instance.ts @@ -1,7 +1,8 @@ import { NostrEvent, NostrMetadata, NSchema as n, NStore } from '@nostrify/nostrify'; +import { z } from 'zod'; import { Conf } from '@/config.ts'; -import { serverMetaSchema } from '@/schemas/nostr.ts'; +import { screenshotsSchema, serverMetaSchema } from '@/schemas/nostr.ts'; /** Like NostrMetadata, but some fields are required and also contains some extra fields. */ export interface InstanceMetadata extends NostrMetadata { @@ -11,6 +12,7 @@ export interface InstanceMetadata extends NostrMetadata { picture: string; tagline: string; event?: NostrEvent; + screenshots: z.infer; } /** Get and parse instance metadata from the kind 0 of the admin user. */ @@ -34,5 +36,6 @@ export async function getInstanceMetadata(store: NStore, signal?: AbortSignal): email: meta.email ?? `postmaster@${Conf.url.host}`, picture: meta.picture ?? Conf.local('/images/thumbnail.png'), event, + screenshots: meta.screenshots ?? [], }; } From f0be2a1c0227b177c356b476dfaf0fa23703426c Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Fri, 1 Nov 2024 23:19:17 -0300 Subject: [PATCH 12/36] fix: set thumbnail in --- src/controllers/api/ditto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index 6682522a..2ffce057 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -327,7 +327,7 @@ export const updateInstanceController: AppController = async (c) => { meta.name = title ?? meta.name; meta.about = description ?? meta.about; meta.screenshots = screenshots ?? meta.screenshots; - meta.thumbnail = thumbnail ?? meta.thumbnail; + meta.picture = thumbnail?.url ?? meta.picture; return { kind: 0, From 34d604dded04ef5e803f43df44e7f0f87d23031e Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sun, 3 Nov 2024 20:27:52 -0300 Subject: [PATCH 13/36] refactor: sizesSchema --- src/schema.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/schema.ts b/src/schema.ts index 23e2938d..6147f562 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -66,7 +66,9 @@ const localeSchema = z.string().transform((val, ctx) => { }); /** White-space separated list of sizes, each in the format x or with "X" in upper case. */ -const sizesSchema = z.string().regex(/^(?:[1-9]\d{0,3}[xX][1-9]\d{0,3})(?:\s+[1-9]\d{0,3}[xX][1-9]\d{0,3})*$/); +const sizesSchema = z.string().refine((value) => + value.split(' ').every((v) => /^[1-9]\d{0,3}[xX][1-9]\d{0,3}$/.test(v)) +); export { booleanParamSchema, From cd7e6bbd43f1ded45e587992be75daee2d80c74a Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 4 Nov 2024 11:10:27 -0300 Subject: [PATCH 14/36] feat: create thumbnailSchema --- src/schemas/activitypub.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/schemas/activitypub.ts b/src/schemas/activitypub.ts index f14c994f..513457d9 100644 --- a/src/schemas/activitypub.ts +++ b/src/schemas/activitypub.ts @@ -154,6 +154,16 @@ const objectSchema = z.union([ ]), ); +/** https://docs.joinmastodon.org/entities/Instance/#thumbnail */ +const thumbnailSchema = z.object({ + url: z.string().url(), + blurhash: z.string().optional(), + versions: z.object({ + '@1x': z.string().url().optional(), + '@2x': z.string().url().optional(), + }).optional(), +}); + const createNoteSchema = z.object({ type: z.literal('Create'), id: apId, @@ -318,6 +328,7 @@ export type { Note, Object, Proxy, + thumbnailSchema, Update, Zap, }; From bac2c51bc19568c2bc2910f62fc459da24d29c4e Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 4 Nov 2024 11:12:16 -0300 Subject: [PATCH 15/36] refactor: use screenshot_ids and thumbnail_id in updateInstanceController --- src/controllers/api/ditto.ts | 71 +++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index 2ffce057..a6e65235 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -1,24 +1,28 @@ import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; +import { HTTPException } from '@hono/hono/http-exception'; import { z } from 'zod'; -import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; import { AppController } from '@/app.ts'; -import { addTag } from '@/utils/tags.ts'; -import { AdminSigner } from '@/signers/AdminSigner.ts'; -import { booleanParamSchema, percentageSchema } from '@/schema.ts'; import { Conf } from '@/config.ts'; +import { type DittoUpload, dittoUploads } from '@/DittoUploads.ts'; +import { addTag } from '@/utils/tags.ts'; +import { getAuthor } from '@/queries.ts'; import { createEvent, paginated, parseBody, updateEvent } from '@/utils/api.ts'; import { getInstanceMetadata } from '@/utils/instance.ts'; import { deleteTag } from '@/utils/tags.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts'; -import { getAuthor } from '@/queries.ts'; +import { AdminSigner } from '@/signers/AdminSigner.ts'; import { screenshotsSchema } from '@/schemas/nostr.ts'; +import { booleanParamSchema, percentageSchema } from '@/schema.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { renderNameRequest } from '@/views/ditto.ts'; +import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; +import { renderAttachment } from '@/views/mastodon/attachments.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; import { Storages } from '@/storages.ts'; import { updateListAdminEvent } from '@/utils/api.ts'; +import { thumbnailSchema } from '@/schemas/activitypub.ts'; const markerSchema = z.enum(['read', 'write']); @@ -293,15 +297,10 @@ export const statusZapSplitsController: AppController = async (c) => { const updateInstanceSchema = z.object({ title: z.string().optional(), description: z.string().optional(), - screenshots: screenshotsSchema.optional(), - thumbnail: z.object({ - url: z.string().url(), - blurhash: z.string().optional(), - versions: z.object({ - '@1x': z.string().url().optional(), - '@2x': z.string().url().optional(), - }).optional(), - }).optional(), + /** Mastodon doesn't have this field. */ + screenshot_ids: z.string().array().nullish(), + /** Mastodon doesn't have this field. */ + thumbnail_id: z.string().optional(), }).strict(); export const updateInstanceController: AppController = async (c) => { @@ -320,14 +319,50 @@ export const updateInstanceController: AppController = async (c) => { const { title, description, - screenshots, - thumbnail, + screenshot_ids, + thumbnail_id, } = result.data; + const thumbnailUrl: string | undefined = (() => { + if (!thumbnail_id) { + return undefined; + } + + const upload = dittoUploads.get(thumbnail_id); + + if (!upload) { + throw new HTTPException(422, { message: 'Uploaded attachment is no longer available.' }); + } + return upload.url; + })(); + + const screenshots: z.infer = (screenshot_ids ?? []).map((id) => { + const upload = dittoUploads.get(id); + + if (!upload) { + throw new HTTPException(422, { message: 'Uploaded attachment is no longer available.' }); + } + + const data = renderAttachment(upload); + + if (!data?.url || !data.meta?.original) { + throw new HTTPException(422, { message: 'Image must have an URL and size dimensions.' }); + } + + const screenshot = { + src: data.url, + label: data.description, + sizes: `${data?.meta?.original?.width}x${data?.meta?.original?.height}`, + type: data?.type, // FIX-ME, I BEG YOU: Returns just `image` instead of a valid MIME type + }; + + return screenshot; + }); + meta.name = title ?? meta.name; meta.about = description ?? meta.about; - meta.screenshots = screenshots ?? meta.screenshots; - meta.picture = thumbnail?.url ?? meta.picture; + meta.screenshots = screenshot_ids ? screenshots : meta.screenshots; + meta.picture = thumbnailUrl ?? meta.picture; return { kind: 0, From f92c382da9c5419b4624b5b7731d1a0302b7c6e4 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 4 Nov 2024 11:16:36 -0300 Subject: [PATCH 16/36] refactor: remove unused imports --- src/controllers/api/ditto.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index a6e65235..b43b11ed 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { type DittoUpload, dittoUploads } from '@/DittoUploads.ts'; +import { dittoUploads } from '@/DittoUploads.ts'; import { addTag } from '@/utils/tags.ts'; import { getAuthor } from '@/queries.ts'; import { createEvent, paginated, parseBody, updateEvent } from '@/utils/api.ts'; @@ -22,7 +22,6 @@ import { renderAttachment } from '@/views/mastodon/attachments.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; import { Storages } from '@/storages.ts'; import { updateListAdminEvent } from '@/utils/api.ts'; -import { thumbnailSchema } from '@/schemas/activitypub.ts'; const markerSchema = z.enum(['read', 'write']); From e3aed99605b474b06375a61b0a185db2af034fa1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 4 Nov 2024 17:17:41 -0600 Subject: [PATCH 17/36] Let users host arbitrary .well-known paths --- src/app.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index fdcacf29..cc508443 100644 --- a/src/app.ts +++ b/src/app.ts @@ -169,6 +169,11 @@ const app = new Hono({ strict: false }); const debug = Debug('ditto:http'); +/** User-provided files in the gitignored `public/` directory. */ +const publicFiles = serveStatic({ root: './public/' }); +/** Static files provided by the Ditto repo, checked into git. */ +const staticFiles = serveStatic({ root: './static/' }); + app.use('*', rateLimitMiddleware(300, Time.minutes(5))); app.use('/api/*', metricsMiddleware, paginationMiddleware, logger(debug)); @@ -362,13 +367,10 @@ app.get('/api/v1/conversations', emptyArrayController); app.get('/api/v1/lists', emptyArrayController); app.use('/api/*', notImplementedController); -app.use('/.well-known/*', notImplementedController); +app.use('/.well-known/*', publicFiles, notImplementedController); app.use('/nodeinfo/*', notImplementedController); app.use('/oauth/*', notImplementedController); -const publicFiles = serveStatic({ root: './public/' }); -const staticFiles = serveStatic({ root: './static/' }); - // Known frontend routes app.get('/:acct{@.*}', frontendController); app.get('/:acct{@.*}/*', frontendController); From ea24050a27b9c1b2239622ea5ff91b5690716cca Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 5 Nov 2024 09:02:27 -0300 Subject: [PATCH 18/36] refactor: change thumbnailSchema to schemas/mastodon.ts --- src/schemas/activitypub.ts | 11 ----------- src/schemas/mastodon.ts | 13 +++++++++++++ 2 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 src/schemas/mastodon.ts diff --git a/src/schemas/activitypub.ts b/src/schemas/activitypub.ts index 513457d9..f14c994f 100644 --- a/src/schemas/activitypub.ts +++ b/src/schemas/activitypub.ts @@ -154,16 +154,6 @@ const objectSchema = z.union([ ]), ); -/** https://docs.joinmastodon.org/entities/Instance/#thumbnail */ -const thumbnailSchema = z.object({ - url: z.string().url(), - blurhash: z.string().optional(), - versions: z.object({ - '@1x': z.string().url().optional(), - '@2x': z.string().url().optional(), - }).optional(), -}); - const createNoteSchema = z.object({ type: z.literal('Create'), id: apId, @@ -328,7 +318,6 @@ export type { Note, Object, Proxy, - thumbnailSchema, Update, Zap, }; diff --git a/src/schemas/mastodon.ts b/src/schemas/mastodon.ts new file mode 100644 index 00000000..bedd1aad --- /dev/null +++ b/src/schemas/mastodon.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +/** https://docs.joinmastodon.org/entities/Instance/#thumbnail */ +const thumbnailSchema = z.object({ + url: z.string().url(), + blurhash: z.string().optional(), + versions: z.object({ + '@1x': z.string().url().optional(), + '@2x': z.string().url().optional(), + }).optional(), +}); + +export { thumbnailSchema }; From af0211d65b7e42ec024ac487198d28b0c81cf871 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 5 Nov 2024 09:46:33 -0300 Subject: [PATCH 19/36] fix(updateInstanceController): remove 'event' field from meta --- src/controllers/api/ditto.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index b43b11ed..77bb32ba 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -362,6 +362,7 @@ export const updateInstanceController: AppController = async (c) => { meta.about = description ?? meta.about; meta.screenshots = screenshot_ids ? screenshots : meta.screenshots; meta.picture = thumbnailUrl ?? meta.picture; + delete meta.event; return { kind: 0, From 9907d7ffd8295809fe9a4d71d07e31ebd1450274 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Tue, 5 Nov 2024 14:36:48 -0300 Subject: [PATCH 20/36] fix(updateInstanceController): allow for setting the tagline --- src/controllers/api/ditto.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index 77bb32ba..5d51027e 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -297,6 +297,8 @@ const updateInstanceSchema = z.object({ title: z.string().optional(), description: z.string().optional(), /** Mastodon doesn't have this field. */ + short_description: z.string().optional(), + /** Mastodon doesn't have this field. */ screenshot_ids: z.string().array().nullish(), /** Mastodon doesn't have this field. */ thumbnail_id: z.string().optional(), @@ -318,6 +320,7 @@ export const updateInstanceController: AppController = async (c) => { const { title, description, + short_description, screenshot_ids, thumbnail_id, } = result.data; @@ -360,6 +363,7 @@ export const updateInstanceController: AppController = async (c) => { meta.name = title ?? meta.name; meta.about = description ?? meta.about; + meta.tagline = short_description ?? meta.tagline; meta.screenshots = screenshot_ids ? screenshots : meta.screenshots; meta.picture = thumbnailUrl ?? meta.picture; delete meta.event; From e96a9e4f3559d26fabe8e02009cfc114aff5a875 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Wed, 6 Nov 2024 12:12:27 +0530 Subject: [PATCH 21/36] replace toByteArray with Response(stream).bytes() --- src/utils/image-metadata.ts | 15 +-------------- src/utils/upload.ts | 5 +++-- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/utils/image-metadata.ts b/src/utils/image-metadata.ts index 75b2f6ba..4429cc7a 100644 --- a/src/utils/image-metadata.ts +++ b/src/utils/image-metadata.ts @@ -6,23 +6,10 @@ import { Stickynotes } from '@soapbox/stickynotes'; const console = new Stickynotes('ditto:uploaders'); -export function toByteArray(f: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener('loadend', (m) => { - if (m?.target?.result instanceof ArrayBuffer) { - resolve(new Uint8Array(m.target.result)); - } else reject('Error loading file: readAsArrayBufferFailed'); - }); - reader.addEventListener('error', (e) => reject(e)); - reader.readAsArrayBuffer(f); - }); -} - export async function getOptionalNip94Metadata(f: File): Promise { const tags: Nip94MetadataOptional = {}; try { - const buffer = await toByteArray(f); + const buffer = await new Response(f.stream()).bytes(); const hash = await crypto.subtle.digest('SHA-256', buffer).then(encodeHex); tags.x = tags.ox = hash; const img = sharp(buffer); diff --git a/src/utils/upload.ts b/src/utils/upload.ts index cadac9bc..9f4c90a4 100644 --- a/src/utils/upload.ts +++ b/src/utils/upload.ts @@ -3,7 +3,7 @@ import { HTTPException } from '@hono/hono/http-exception'; import { AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; import { DittoUpload, dittoUploads } from '@/DittoUploads.ts'; -import { getOptionalNip94Metadata, toByteArray } from '@/utils/image-metadata.ts'; +import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts'; import type { Nip94MetadataOptional } from '@/interfaces/Nip94Metadata.ts'; import { encodeHex } from '@std/encoding/hex'; @@ -53,7 +53,8 @@ export async function uploadFile( } } if (!tagMap.has('x') || !tagMap.has('ox')) { - const hash = metadata?.x || await crypto.subtle.digest('SHA-256', await toByteArray(file)).then(encodeHex); + const hash = metadata?.x || + await crypto.subtle.digest('SHA-256', await new Response(file.stream()).bytes()).then(encodeHex); tags.push(['x', hash!]); tags.push(['ox', hash!]); } From 721fe521426d4f58d55105bda93b0bed4d4d4c50 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 7 Nov 2024 11:09:20 -0600 Subject: [PATCH 22/36] Reorganize sharp/blurhash into upload.ts --- src/interfaces/Nip94Metadata.ts | 31 ---------------- src/uploaders/S3Uploader.ts | 1 - src/utils/image-metadata.ts | 41 --------------------- src/utils/upload.ts | 64 ++++++++++++++++++++++----------- 4 files changed, 44 insertions(+), 93 deletions(-) delete mode 100644 src/interfaces/Nip94Metadata.ts delete mode 100644 src/utils/image-metadata.ts diff --git a/src/interfaces/Nip94Metadata.ts b/src/interfaces/Nip94Metadata.ts deleted file mode 100644 index 6069ac31..00000000 --- a/src/interfaces/Nip94Metadata.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Required fields of NIP-94 metadata for images. - * Contains the following fields: - * * `url` - required, the URL to of the file - * * `m` - required, the file mimetype. - */ -export type Nip94MetadataRequired = Record<'url' | 'm', string>; - -/** - * Optional fields of NIP-94 metadata for images. - * Contains the following fields: - * * `x` - sha-256 hash - * * `ox` - sha-256 hash - * * `dim` - image dimensions in ${w}x${h} format - * * `blurhash` - the blurhash for the image. useful for image previews etc - * * `cid` - the ipfs cid of the image. - */ -export type Nip94MetadataOptional = Partial>; - -/** - * NIP-94 metadata for images. - * Contains the following fields: - * * `url` - required, the URL to of the file - * * `m` - required, the file mimetype. - * * `x` - sha-256 hash - * * `ox` - sha-256 hash - * * `dim` - image dimensions in ${w}x${h} format - * * `blurhash` - the blurhash for the image. useful for image previews etc - * * `cid` - the ipfs cid of the image. - */ -export type Nip94Metadata = Nip94MetadataOptional & Nip94MetadataRequired; diff --git a/src/uploaders/S3Uploader.ts b/src/uploaders/S3Uploader.ts index c784cdab..b74796ab 100644 --- a/src/uploaders/S3Uploader.ts +++ b/src/uploaders/S3Uploader.ts @@ -43,7 +43,6 @@ export class S3Uploader implements NUploader { const { pathStyle, bucket } = Conf.s3; const path = (pathStyle && bucket) ? join(bucket, filename) : filename; - const url = new URL(path, Conf.mediaDomain).toString(); return [ diff --git a/src/utils/image-metadata.ts b/src/utils/image-metadata.ts deleted file mode 100644 index 4429cc7a..00000000 --- a/src/utils/image-metadata.ts +++ /dev/null @@ -1,41 +0,0 @@ -import sharp from 'sharp'; -import { encode } from 'blurhash'; -import { encodeHex } from '@std/encoding/hex'; -import type { Nip94MetadataOptional } from '@/interfaces/Nip94Metadata.ts'; -import { Stickynotes } from '@soapbox/stickynotes'; - -const console = new Stickynotes('ditto:uploaders'); - -export async function getOptionalNip94Metadata(f: File): Promise { - const tags: Nip94MetadataOptional = {}; - try { - const buffer = await new Response(f.stream()).bytes(); - const hash = await crypto.subtle.digest('SHA-256', buffer).then(encodeHex); - 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; -} diff --git a/src/utils/upload.ts b/src/utils/upload.ts index 9f4c90a4..df0be70b 100644 --- a/src/utils/upload.ts +++ b/src/utils/upload.ts @@ -1,11 +1,15 @@ import { HTTPException } from '@hono/hono/http-exception'; +import { Stickynotes } from '@soapbox/stickynotes'; +import { crypto } from '@std/crypto'; +import { encodeHex } from '@std/encoding/hex'; +import { encode } from 'blurhash'; +import sharp from 'sharp'; import { AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; import { DittoUpload, dittoUploads } from '@/DittoUploads.ts'; -import { getOptionalNip94Metadata } from '@/utils/image-metadata.ts'; -import type { Nip94MetadataOptional } from '@/interfaces/Nip94Metadata.ts'; -import { encodeHex } from '@std/encoding/hex'; + +const console = new Stickynotes('ditto:uploader'); interface FileMeta { pubkey: string; @@ -33,30 +37,50 @@ export async function uploadFile( } const tags = await uploader.upload(file, { signal }); - const tagMap = tags.reduce((map, value) => map.set(value[0], value.slice(1)), new Map()); - const url = tags[0][1]; if (description) { tags.push(['alt', description]); } - let metadata: Nip94MetadataOptional | undefined; - if (!tagMap.has('dim')) { - // blurhash needs us to call sharp() anyway to decode the image data. - // all getOptionalNip94Metadata does is call these in sequence, plus - // one extra sha256 which is whatever (and actually does come in handy later.) - metadata ??= await getOptionalNip94Metadata(file); - tags.push(['dim', metadata.dim!]); - if (!tagMap.has('blurhash')) { - tags.push(['blurhash', metadata.blurhash!]); - } + const x = tags.find(([key]) => key === 'x')?.[1]; + const m = tags.find(([key]) => key === 'm')?.[1]; + const dim = tags.find(([key]) => key === 'dim')?.[1]; + const blurhash = tags.find(([key]) => key === 'blurhash')?.[1]; + + if (!x) { + const sha256 = encodeHex(await crypto.subtle.digest('SHA-256', file.stream())); + tags.push(['x', sha256]); } - if (!tagMap.has('x') || !tagMap.has('ox')) { - const hash = metadata?.x || - await crypto.subtle.digest('SHA-256', await new Response(file.stream()).bytes()).then(encodeHex); - tags.push(['x', hash!]); - tags.push(['ox', hash!]); + + if (!m) { + tags.push(['m', file.type]); + } + + if (!blurhash || !dim) { + try { + const bytes = await new Response(file.stream()).bytes(); + const img = sharp(bytes); + + const { width, height } = await img.metadata(); + + if (!dim && (width && height)) { + tags.push(['dim', `${width}x${height}`]); + } + + if (!blurhash && (width && height)) { + const pixels = await img + .raw() + .ensureAlpha() + .toBuffer({ resolveWithObject: false }) + .then((buffer) => new Uint8ClampedArray(buffer)); + + const blurhash = encode(pixels, width, height, 4, 4); + tags.push(['blurhash', blurhash]); + } + } catch (e) { + console.error(`Error parsing image metadata: ${e}`); + } } const upload = { From f987effc15f5874461011b7547c3f2d10b12becb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 7 Nov 2024 11:18:39 -0600 Subject: [PATCH 23/36] Make media analyze optional (disabled by default) --- src/config.ts | 7 +++++++ src/utils/upload.ts | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index fba65159..68175a46 100644 --- a/src/config.ts +++ b/src/config.ts @@ -201,6 +201,13 @@ class Conf { return value; } + /** + * Whether to analyze media metadata with [blurhash](https://www.npmjs.com/package/blurhash) and [sharp](https://www.npmjs.com/package/sharp). + * This is prone to security vulnerabilities, which is why it's not enabled by default. + */ + static get mediaAnalyze(): boolean { + return optionalBooleanSchema.parse(Deno.env.get('MEDIA_ANALYZE')) ?? false; + } /** Max upload size for files in number of bytes. Default 100MiB. */ static get maxUploadSize(): number { return Number(Deno.env.get('MAX_UPLOAD_SIZE') || 100 * 1024 * 1024); diff --git a/src/utils/upload.ts b/src/utils/upload.ts index df0be70b..fc5d7086 100644 --- a/src/utils/upload.ts +++ b/src/utils/upload.ts @@ -57,7 +57,9 @@ export async function uploadFile( tags.push(['m', file.type]); } - if (!blurhash || !dim) { + // If the uploader didn't already, try to get a blurhash and media dimensions. + // This requires `MEDIA_ANALYZE=true` to be configured because it comes with security tradeoffs. + if (Conf.mediaAnalyze && (!blurhash || !dim)) { try { const bytes = await new Response(file.stream()).bytes(); const img = sharp(bytes); From e1dc3d79a471921a449a729be4c83eabe0d05981 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 7 Nov 2024 11:32:34 -0600 Subject: [PATCH 24/36] Upgrade Deno to v2.0.5 --- .gitlab-ci.yml | 2 +- .tool-versions | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8093329a..f4cc22ec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: denoland/deno:2.0.3 +image: denoland/deno:2.0.5 default: interruptible: true diff --git a/.tool-versions b/.tool-versions index f818ab4c..25fdc0ac 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -deno 2.0.3 \ No newline at end of file +deno 2.0.5 \ No newline at end of file From 115641b3c2df63c5c1b8706ce1e78deb82628cea Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 7 Nov 2024 15:22:28 -0300 Subject: [PATCH 25/36] fix: add short_description to instanceV2Controller --- src/controllers/api/instance.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/api/instance.ts b/src/controllers/api/instance.ts index 9f504cad..3b2d99fb 100644 --- a/src/controllers/api/instance.ts +++ b/src/controllers/api/instance.ts @@ -97,6 +97,7 @@ const instanceV2Controller: AppController = async (c) => { }, }, screenshots: meta.screenshots, + short_description: meta.tagline, languages: [ 'en', ], From 6fae72b4cf2072ac32fa891f3e7e831cb4a96166 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 7 Nov 2024 13:10:15 -0600 Subject: [PATCH 26/36] upload: append file size if it hasn't been --- src/utils/upload.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/upload.ts b/src/utils/upload.ts index fc5d7086..ae86298c 100644 --- a/src/utils/upload.ts +++ b/src/utils/upload.ts @@ -46,6 +46,7 @@ export async function uploadFile( const x = tags.find(([key]) => key === 'x')?.[1]; const m = tags.find(([key]) => key === 'm')?.[1]; const dim = tags.find(([key]) => key === 'dim')?.[1]; + const size = tags.find(([key]) => key === 'size')?.[1]; const blurhash = tags.find(([key]) => key === 'blurhash')?.[1]; if (!x) { @@ -57,6 +58,10 @@ export async function uploadFile( tags.push(['m', file.type]); } + if (!size) { + tags.push(['size', file.size.toString()]); + } + // If the uploader didn't already, try to get a blurhash and media dimensions. // This requires `MEDIA_ANALYZE=true` to be configured because it comes with security tradeoffs. if (Conf.mediaAnalyze && (!blurhash || !dim)) { From 459adadd4cd8f8482de99ece8178f98d7d9ae98d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 7 Nov 2024 13:32:53 -0600 Subject: [PATCH 27/36] Deny reading .env --- deno.json | 30 +++++++++++++++--------------- src/workers/policy.ts | 19 +++++++++---------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/deno.json b/deno.json index 6db0fceb..c87a6835 100644 --- a/deno.json +++ b/deno.json @@ -1,26 +1,26 @@ { "version": "1.1.0", "tasks": { - "start": "deno run -A --env-file src/server.ts", - "dev": "deno run -A --env-file --watch src/server.ts", + "start": "deno run -A --env-file --deny-read=.env src/server.ts", + "dev": "deno run -A --env-file --deny-read=.env --watch src/server.ts", "hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts", - "db:export": "deno run -A --env-file scripts/db-export.ts", - "db:import": "deno run -A --env-file scripts/db-import.ts", - "db:migrate": "deno run -A --env-file scripts/db-migrate.ts", - "nostr:pull": "deno run -A --env-file scripts/nostr-pull.ts", - "debug": "deno run -A --env-file --inspect src/server.ts", - "test": "deno test -A --env-file=.env.test --junit-path=./deno-test.xml", + "db:export": "deno run -A --env-file --deny-read=.env scripts/db-export.ts", + "db:import": "deno run -A --env-file --deny-read=.env scripts/db-import.ts", + "db:migrate": "deno run -A --env-file --deny-read=.env scripts/db-migrate.ts", + "nostr:pull": "deno run -A --env-file --deny-read=.env scripts/nostr-pull.ts", + "debug": "deno run -A --env-file --deny-read=.env --inspect src/server.ts", + "test": "deno test -A --env-file --deny-read=.env=.env.test --junit-path=./deno-test.xml", "check": "deno check --allow-import src/server.ts", "nsec": "deno run scripts/nsec.ts", - "admin:event": "deno run -A --env-file scripts/admin-event.ts", - "admin:role": "deno run -A --env-file scripts/admin-role.ts", - "setup": "deno run -A --env-file scripts/setup.ts", - "setup:kind0": "deno run -A --env-file scripts/setup-kind0.ts", - "stats:recompute": "deno run -A --env-file scripts/stats-recompute.ts", + "admin:event": "deno run -A --env-file --deny-read=.env scripts/admin-event.ts", + "admin:role": "deno run -A --env-file --deny-read=.env scripts/admin-role.ts", + "setup": "deno run -A --env-file --deny-read=.env scripts/setup.ts", + "setup:kind0": "deno run -A --env-file --deny-read=.env scripts/setup-kind0.ts", + "stats:recompute": "deno run -A --env-file --deny-read=.env scripts/stats-recompute.ts", "soapbox": "curl -O https://dl.soapbox.pub/main/soapbox.zip && mkdir -p public && mv soapbox.zip public/ && cd public/ && unzip -o soapbox.zip && rm soapbox.zip", - "trends": "deno run -A --env-file scripts/trends.ts", + "trends": "deno run -A --env-file --deny-read=.env scripts/trends.ts", "clean:deps": "deno cache --reload src/app.ts", - "db:populate-search": "deno run -A --env-file scripts/db-populate-search.ts", + "db:populate-search": "deno run -A --env-file --deny-read=.env scripts/db-populate-search.ts", "vapid": "deno run scripts/vapid.ts" }, "unstable": [ diff --git a/src/workers/policy.ts b/src/workers/policy.ts index a396468f..4124feb9 100644 --- a/src/workers/policy.ts +++ b/src/workers/policy.ts @@ -21,16 +21,15 @@ class PolicyWorker implements NPolicy { { type: 'module', name: 'PolicyWorker', - // FIXME: Disabled until Deno 2.0 adds support for `import` permission here. - // https://github.com/denoland/deno/issues/26074 - // deno: { - // permissions: { - // read: [Conf.denoDir, Conf.policy, Conf.dataDir], - // write: [Conf.dataDir], - // net: 'inherit', - // env: false, - // }, - // }, + deno: { + permissions: { + read: [Conf.denoDir, Conf.policy, Conf.dataDir], + write: [Conf.dataDir], + net: 'inherit', + env: false, + import: true, + }, + }, }, ), ); From 3dafe2b6d15c1928464592bd8a3b8628387a83fe Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 7 Nov 2024 13:40:37 -0600 Subject: [PATCH 28/36] playbook: upgrade Deno --- ansible/playbook.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ansible/playbook.yml b/ansible/playbook.yml index a2d8226e..59c2c8ea 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -3,6 +3,13 @@ hosts: all become: true tasks: + - name: Update Deno + shell: + cmd: curl -fsSL https://deno.land/x/install/install.sh | sh + environment: + DENO_INSTALL: /usr/local + become_user: root + - name: Update Soapbox shell: cmd: deno task soapbox From 715908082f0bcecb3cf8033dac4dace763b5d4dc Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 7 Nov 2024 18:18:33 -0300 Subject: [PATCH 29/36] fix(updateInstanceController): stop using screenshot_ids and thumbnail_id --- src/controllers/api/ditto.ts | 65 +++++++++--------------------------- 1 file changed, 15 insertions(+), 50 deletions(-) diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index 5d51027e..b99bf777 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -294,14 +294,15 @@ export const statusZapSplitsController: AppController = async (c) => { }; const updateInstanceSchema = z.object({ - title: z.string().optional(), - description: z.string().optional(), + title: z.string(), + description: z.string(), + short_description: z.string(), /** Mastodon doesn't have this field. */ - short_description: z.string().optional(), - /** Mastodon doesn't have this field. */ - screenshot_ids: z.string().array().nullish(), - /** Mastodon doesn't have this field. */ - thumbnail_id: z.string().optional(), + screenshots: screenshotsSchema, + /** https://docs.joinmastodon.org/entities/Instance/#thumbnail-url */ + thumbnail: z.object({ + url: z.string().url(), + }), }).strict(); export const updateInstanceController: AppController = async (c) => { @@ -321,51 +322,15 @@ export const updateInstanceController: AppController = async (c) => { title, description, short_description, - screenshot_ids, - thumbnail_id, + screenshots, + thumbnail, } = result.data; - const thumbnailUrl: string | undefined = (() => { - if (!thumbnail_id) { - return undefined; - } - - const upload = dittoUploads.get(thumbnail_id); - - if (!upload) { - throw new HTTPException(422, { message: 'Uploaded attachment is no longer available.' }); - } - return upload.url; - })(); - - const screenshots: z.infer = (screenshot_ids ?? []).map((id) => { - const upload = dittoUploads.get(id); - - if (!upload) { - throw new HTTPException(422, { message: 'Uploaded attachment is no longer available.' }); - } - - const data = renderAttachment(upload); - - if (!data?.url || !data.meta?.original) { - throw new HTTPException(422, { message: 'Image must have an URL and size dimensions.' }); - } - - const screenshot = { - src: data.url, - label: data.description, - sizes: `${data?.meta?.original?.width}x${data?.meta?.original?.height}`, - type: data?.type, // FIX-ME, I BEG YOU: Returns just `image` instead of a valid MIME type - }; - - return screenshot; - }); - - meta.name = title ?? meta.name; - meta.about = description ?? meta.about; - meta.tagline = short_description ?? meta.tagline; - meta.screenshots = screenshot_ids ? screenshots : meta.screenshots; - meta.picture = thumbnailUrl ?? meta.picture; + meta.name = title; + meta.about = description; + meta.tagline = short_description; + meta.screenshots = screenshots; + meta.picture = thumbnail.url; delete meta.event; return { From 3a656e6f6196c641e9afcfcb70d8f25fbd596878 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 7 Nov 2024 18:31:28 -0300 Subject: [PATCH 30/36] fix: remove unused imports --- src/controllers/api/ditto.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/controllers/api/ditto.ts b/src/controllers/api/ditto.ts index b99bf777..f7138d9f 100644 --- a/src/controllers/api/ditto.ts +++ b/src/controllers/api/ditto.ts @@ -1,10 +1,8 @@ import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; -import { HTTPException } from '@hono/hono/http-exception'; import { z } from 'zod'; import { AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; -import { dittoUploads } from '@/DittoUploads.ts'; import { addTag } from '@/utils/tags.ts'; import { getAuthor } from '@/queries.ts'; import { createEvent, paginated, parseBody, updateEvent } from '@/utils/api.ts'; @@ -18,7 +16,6 @@ import { booleanParamSchema, percentageSchema } from '@/schema.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { renderNameRequest } from '@/views/ditto.ts'; import { accountFromPubkey } from '@/views/mastodon/accounts.ts'; -import { renderAttachment } from '@/views/mastodon/attachments.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts'; import { Storages } from '@/storages.ts'; import { updateListAdminEvent } from '@/utils/api.ts'; From 5c45517db463bcade3387e288b3ca02fb39402f8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 7 Nov 2024 17:01:41 -0600 Subject: [PATCH 31/36] deno task test: fix --deny-read flag --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index c87a6835..38e44ff4 100644 --- a/deno.json +++ b/deno.json @@ -9,7 +9,7 @@ "db:migrate": "deno run -A --env-file --deny-read=.env scripts/db-migrate.ts", "nostr:pull": "deno run -A --env-file --deny-read=.env scripts/nostr-pull.ts", "debug": "deno run -A --env-file --deny-read=.env --inspect src/server.ts", - "test": "deno test -A --env-file --deny-read=.env=.env.test --junit-path=./deno-test.xml", + "test": "deno test -A --env-file=.env.test --deny-read=.env --junit-path=./deno-test.xml", "check": "deno check --allow-import src/server.ts", "nsec": "deno run scripts/nsec.ts", "admin:event": "deno run -A --env-file --deny-read=.env scripts/admin-event.ts", From 56a2e622e8c372098a28dc3e75a39a4d41fec1a7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sun, 10 Nov 2024 10:37:05 -0600 Subject: [PATCH 32/36] Remove `deny-read` from setup script --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 38e44ff4..1425b4c4 100644 --- a/deno.json +++ b/deno.json @@ -14,7 +14,7 @@ "nsec": "deno run scripts/nsec.ts", "admin:event": "deno run -A --env-file --deny-read=.env scripts/admin-event.ts", "admin:role": "deno run -A --env-file --deny-read=.env scripts/admin-role.ts", - "setup": "deno run -A --env-file --deny-read=.env scripts/setup.ts", + "setup": "deno run -A --env-file scripts/setup.ts", "setup:kind0": "deno run -A --env-file --deny-read=.env scripts/setup-kind0.ts", "stats:recompute": "deno run -A --env-file --deny-read=.env scripts/stats-recompute.ts", "soapbox": "curl -O https://dl.soapbox.pub/main/soapbox.zip && mkdir -p public && mv soapbox.zip public/ && cd public/ && unzip -o soapbox.zip && rm soapbox.zip", From 0bba4393bab753dda9ee097243fbbcf97ff9c5a2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 11 Nov 2024 19:54:59 -0600 Subject: [PATCH 33/36] Remove unused ActivityPub schema --- src/schemas/activitypub.ts | 323 ------------------------------------- 1 file changed, 323 deletions(-) delete mode 100644 src/schemas/activitypub.ts diff --git a/src/schemas/activitypub.ts b/src/schemas/activitypub.ts deleted file mode 100644 index f14c994f..00000000 --- a/src/schemas/activitypub.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { z } from 'zod'; - -const apId = z.string().url(); -const recipients = z.array(z.string()).catch([]); -const published = () => z.string().datetime().catch(new Date().toISOString()); - -/** Validates individual items in an array, dropping any that aren't valid. */ -function filteredArray(schema: T) { - return z.any().array() - .transform((arr) => ( - arr.map((item) => { - const parsed = schema.safeParse(item); - return parsed.success ? parsed.data : undefined; - }).filter((item): item is z.infer => Boolean(item)) - )); -} - -const imageSchema = z.object({ - type: z.literal('Image').catch('Image'), - url: z.string().url(), -}); - -const attachmentSchema = z.object({ - type: z.literal('Document').catch('Document'), - mediaType: z.string().optional().catch(undefined), - url: z.string().url(), -}); - -const mentionSchema = z.object({ - type: z.literal('Mention'), - href: z.string().url(), - name: z.string().optional().catch(undefined), -}); - -const hashtagSchema = z.object({ - type: z.literal('Hashtag'), - href: z.string().url(), - name: z.string(), -}); - -const emojiSchema = z.object({ - type: z.literal('Emoji'), - icon: imageSchema, - name: z.string(), -}); - -const tagSchema = z.discriminatedUnion('type', [ - mentionSchema, - hashtagSchema, - emojiSchema, -]); - -const propertyValueSchema = z.object({ - type: z.literal('PropertyValue'), - name: z.string(), - value: z.string(), - verified_at: z.string().nullish(), -}); - -/** https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-fffd.md */ -const proxySchema = z.object({ - protocol: z.string().url(), - proxied: z.string(), - authoritative: z.boolean().optional().catch(undefined), -}); - -const personSchema = z.object({ - type: z.literal('Person'), - id: apId, - icon: imageSchema.optional().catch(undefined), - image: imageSchema.optional().catch(undefined), - name: z.string().catch(''), - preferredUsername: z.string(), - inbox: apId, - followers: apId.optional().catch(undefined), - following: apId.optional().catch(undefined), - outbox: apId.optional().catch(undefined), - summary: z.string().catch(''), - attachment: filteredArray(propertyValueSchema).catch([]), - tag: filteredArray(emojiSchema).catch([]), - endpoints: z.object({ - sharedInbox: apId.optional(), - }).optional().catch({}), - publicKey: z.object({ - id: apId, - owner: apId, - publicKeyPem: z.string(), - }).optional().catch(undefined), - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -const applicationSchema = personSchema.merge(z.object({ type: z.literal('Application') })); -const groupSchema = personSchema.merge(z.object({ type: z.literal('Group') })); -const organizationSchema = personSchema.merge(z.object({ type: z.literal('Organization') })); -const serviceSchema = personSchema.merge(z.object({ type: z.literal('Service') })); - -const actorSchema = z.discriminatedUnion('type', [ - personSchema, - applicationSchema, - groupSchema, - organizationSchema, - serviceSchema, -]); - -const noteSchema = z.object({ - type: z.literal('Note'), - id: apId, - to: recipients, - cc: recipients, - content: z.string(), - attachment: z.array(attachmentSchema).optional().catch(undefined), - tag: filteredArray(tagSchema).catch([]), - inReplyTo: apId.optional().catch(undefined), - attributedTo: apId, - published: published(), - sensitive: z.boolean().optional().catch(undefined), - summary: z.string().nullish().catch(undefined), - quoteUrl: apId.optional().catch(undefined), - source: z.object({ - content: z.string(), - mediaType: z.literal('text/markdown'), - }).optional().catch(undefined), - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -const flexibleNoteSchema = noteSchema.extend({ - quoteURL: apId.optional().catch(undefined), - quoteUri: apId.optional().catch(undefined), - _misskey_quote: apId.optional().catch(undefined), -}).transform((note) => { - const { quoteUrl, quoteUri, quoteURL, _misskey_quote, ...rest } = note; - return { - quoteUrl: quoteUrl || quoteUri || quoteURL || _misskey_quote, - ...rest, - }; -}); - -// https://github.com/colinhacks/zod/discussions/2100#discussioncomment-5109781 -const objectSchema = z.union([ - flexibleNoteSchema, - personSchema, - applicationSchema, - groupSchema, - organizationSchema, - serviceSchema, -]).pipe( - z.discriminatedUnion('type', [ - noteSchema, - personSchema, - applicationSchema, - groupSchema, - organizationSchema, - serviceSchema, - ]), -); - -const createNoteSchema = z.object({ - type: z.literal('Create'), - id: apId, - to: recipients, - cc: recipients, - actor: apId, - object: noteSchema, - published: published(), - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -const announceNoteSchema = z.object({ - type: z.literal('Announce'), - id: apId, - to: recipients, - cc: recipients, - actor: apId, - object: apId.or(noteSchema), - published: published(), - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -const followSchema = z.object({ - type: z.literal('Follow'), - id: apId, - to: recipients, - cc: recipients, - actor: apId, - object: apId, - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -const acceptSchema = z.object({ - type: z.literal('Accept'), - id: apId, - actor: apId, - to: recipients, - cc: recipients, - object: apId.or(followSchema), -}); - -const likeSchema = z.object({ - type: z.literal('Like'), - id: apId, - actor: apId, - object: apId, - to: recipients, - cc: recipients, - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -const emojiReactSchema = z.object({ - type: z.literal('EmojiReact'), - id: apId, - actor: apId, - object: apId, - content: z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v)), - to: recipients, - cc: recipients, - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -const deleteSchema = z.object({ - type: z.literal('Delete'), - id: apId, - actor: apId, - object: apId, - to: recipients, - cc: recipients, - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -const updateActorSchema = z.object({ - type: z.literal('Update'), - id: apId, - actor: apId, - to: recipients, - cc: recipients, - object: actorSchema, - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -/** - * A custom Zap activity type we made up, based on: - * https://github.com/nostr-protocol/nips/blob/master/57.md - */ -const zapSchema = z.object({ - type: z.literal('Zap'), - id: apId, - actor: apId, - object: apId, - to: recipients, - cc: recipients, - proxyOf: z.array(proxySchema).optional().catch(undefined), -}); - -const activitySchema = z.discriminatedUnion('type', [ - followSchema, - acceptSchema, - createNoteSchema, - announceNoteSchema, - updateActorSchema, - likeSchema, - emojiReactSchema, - deleteSchema, - zapSchema, -]).refine((activity) => { - const ids: string[] = [activity.id]; - - if (activity.type === 'Create') { - ids.push( - activity.object.id, - activity.object.attributedTo, - ); - } - - if (activity.type === 'Update') { - ids.push(activity.object.id); - } - - const { origin: actorOrigin } = new URL(activity.actor); - - // Object containment - return ids.every((id) => { - const { origin: idOrigin } = new URL(id); - return idOrigin === actorOrigin; - }); -}); - -type Activity = z.infer; -type CreateNote = z.infer; -type Announce = z.infer; -type Update = z.infer; -type Object = z.infer; -type Follow = z.infer; -type Accept = z.infer; -type Actor = z.infer; -type Note = z.infer; -type Mention = z.infer; -type Hashtag = z.infer; -type Emoji = z.infer; -type Like = z.infer; -type EmojiReact = z.infer; -type Delete = z.infer; -type Zap = z.infer; -type Proxy = z.infer; - -export { acceptSchema, activitySchema, actorSchema, emojiSchema, followSchema, imageSchema, noteSchema, objectSchema }; -export type { - Accept, - Activity, - Actor, - Announce, - CreateNote, - Delete, - Emoji, - EmojiReact, - Follow, - Hashtag, - Like, - Mention, - Note, - Object, - Proxy, - Update, - Zap, -}; From acee8784eebc479ec9d6f9f670f2e02e5dc4c427 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Wed, 13 Nov 2024 17:19:11 -0300 Subject: [PATCH 34/36] fix(screenshotsSchema): make sizes field optional --- src/schemas/nostr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schemas/nostr.ts b/src/schemas/nostr.ts index 46f68a34..4e8f917d 100644 --- a/src/schemas/nostr.ts +++ b/src/schemas/nostr.ts @@ -32,7 +32,7 @@ const screenshotsSchema = z.array(z.object({ 'play', ]).optional(), /** https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots#sizes */ - sizes: sizesSchema, + sizes: sizesSchema.optional(), /** Absolute URL. */ src: z.string().url(), /** MIME type of the image. */ From 674781daf1a8714c97053b158e70ed202409a234 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 13 Nov 2024 15:23:42 -0600 Subject: [PATCH 35/36] Upgrade safe-fetch --- deno.json | 2 +- deno.lock | 20 ++++++++++++++++++-- src/server.ts | 2 -- src/workers/fetch.worker.ts | 9 +++++---- src/workers/policy.worker.ts | 2 +- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/deno.json b/deno.json index 1425b4c4..d86b3f99 100644 --- a/deno.json +++ b/deno.json @@ -50,6 +50,7 @@ "@scure/base": "npm:@scure/base@^1.1.6", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-pglite": "jsr:@soapbox/kysely-pglite@^1.0.0", + "@soapbox/safe-fetch": "jsr:@soapbox/safe-fetch@^2.0.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", "@std/assert": "jsr:@std/assert@^0.225.1", "@std/cli": "jsr:@std/cli@^0.223.0", @@ -63,7 +64,6 @@ "comlink": "npm:comlink@^4.4.1", "comlink-async-generator": "npm:comlink-async-generator@^0.0.1", "commander": "npm:commander@12.1.0", - "deno-safe-fetch/load": "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts", "deno.json": "./deno.json", "entities": "npm:entities@^4.5.0", "fast-stable-stringify": "npm:fast-stable-stringify@^1.0.0", diff --git a/deno.lock b/deno.lock index 14cc1086..7e2b01b2 100644 --- a/deno.lock +++ b/deno.lock @@ -46,6 +46,7 @@ "jsr:@nostrify/types@0.35": "0.35.0", "jsr:@nostrify/types@~0.30.1": "0.30.1", "jsr:@soapbox/kysely-pglite@1": "1.0.0", + "jsr:@soapbox/safe-fetch@2": "2.0.0", "jsr:@soapbox/stickynotes@0.4": "0.4.0", "jsr:@std/assert@0.223": "0.223.0", "jsr:@std/assert@0.224": "0.224.0", @@ -120,6 +121,7 @@ "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:tldts@^6.1.61": "6.1.61", "npm:tseep@^1.2.1": "1.2.1", "npm:type-fest@^4.3.0": "4.18.2", "npm:unfurl.js@^6.4.0": "6.4.0", @@ -483,6 +485,12 @@ "npm:kysely@~0.27.4" ] }, + "@soapbox/safe-fetch@2.0.0": { + "integrity": "f451d686501c76a0faa058fe9d2073676282a8a42c3b93c59159eb9191f11b5f", + "dependencies": [ + "npm:tldts@^6.1.61" + ] + }, "@soapbox/stickynotes@0.4.0": { "integrity": "60bfe61ab3d7e04bf708273b1e2d391a59534bdf29e54160e98d7afd328ca1ec" }, @@ -1522,6 +1530,9 @@ "tldts-core@6.1.49": { "integrity": "sha512-ctRO/wzBasOCxAStJG/60Qe8/QpGmaVPsE8djdk0vioxN4uCOgKoveH71Qc2EOmVMIjVf0BjigI5p9ZDuLOygg==" }, + "tldts-core@6.1.61": { + "integrity": "sha512-In7VffkDWUPgwa+c9picLUxvb0RltVwTkSgMNFgvlGSWveCzGBemBqTsgJCL4EDFWZ6WH0fKTsot6yNhzy3ZzQ==" + }, "tldts@6.1.18": { "integrity": "sha512-F+6zjPFnFxZ0h6uGb8neQWwHQm8u3orZVFribsGq4eBgEVrzSkHxzWS2l6aKr19T1vXiOMFjqfff4fQt+WgJFg==", "dependencies": [ @@ -1534,6 +1545,12 @@ "tldts-core@6.1.49" ] }, + "tldts@6.1.61": { + "integrity": "sha512-rv8LUyez4Ygkopqn+M6OLItAOT9FF3REpPQDkdMx5ix8w4qkuE7Vo2o/vw1nxKQYmJDV8JpAMJQr1b+lTKf0FA==", + "dependencies": [ + "tldts-core@6.1.61" + ] + }, "to-regex-range@5.0.1": { "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dependencies": [ @@ -2181,8 +2198,6 @@ "https://deno.land/x/sentry@7.112.2/index.mjs": "04382d5c2f4e233ba389611db46f77943b2a7f6efbeaaf31193f6e586f4366ef", "https://esm.sh/kysely@0.17.1/dist/esm/index-nodeless.js": "9c23bfd307118e3ccd3a9f0ec1261fc3451fb5301aa34aa6f28e05156818755a", "https://esm.sh/v135/kysely@0.17.1/denonext/dist/esm/index-nodeless.js": "6f73bbf2d73bc7e96cdabf941c4ae8c12f58fd7b441031edec44c029aed9532b", - "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts": "3f74ab08cf97d4a3e6994cb79422e9b0069495e017416858121d5ff8ae04ac2a", - "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/mod.ts": "5f505cd265aefbcb687cde6f98c79344d3292ee1dd978e85e5ffa84a617c6682", "https://gitlab.com/soapbox-pub/kysely-deno-postgres/-/raw/b4725e74ad6ca359ba0e370b55dbb8bb845a8a83/deps.ts": "b3dbecae69c30a5f161323b8c8ebd91d9af1eceb98fafab3091c7281a4b64fed", "https://gitlab.com/soapbox-pub/kysely-deno-postgres/-/raw/b4725e74ad6ca359ba0e370b55dbb8bb845a8a83/mod.ts": "662438fd3909984bb8cbaf3fd44d2121e949d11301baf21d6c3f057ccf9887de", "https://gitlab.com/soapbox-pub/kysely-deno-postgres/-/raw/b4725e74ad6ca359ba0e370b55dbb8bb845a8a83/src/PostgreSQLDriver.ts": "ea5a523bceeed420858b744beeb95d48976cb2b0d3f519a68b65a8229036cf6a", @@ -2272,6 +2287,7 @@ "jsr:@nostrify/nostrify@0.36", "jsr:@nostrify/policies@0.35", "jsr:@soapbox/kysely-pglite@1", + "jsr:@soapbox/safe-fetch@2", "jsr:@soapbox/stickynotes@0.4", "jsr:@std/assert@~0.225.1", "jsr:@std/cli@0.223", diff --git a/src/server.ts b/src/server.ts index f7a33dc0..4825e99d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,3 @@ -import 'deno-safe-fetch/load'; - import '@/precheck.ts'; import '@/sentry.ts'; import '@/nostr-wasm.ts'; diff --git a/src/workers/fetch.worker.ts b/src/workers/fetch.worker.ts index e6f98455..87d0a6c3 100644 --- a/src/workers/fetch.worker.ts +++ b/src/workers/fetch.worker.ts @@ -1,12 +1,13 @@ /// -import Debug from '@soapbox/stickynotes/debug'; +import { safeFetch } from '@soapbox/safe-fetch'; +import { Stickynotes } from '@soapbox/stickynotes'; import * as Comlink from 'comlink'; import '@/workers/handlers/abortsignal.ts'; import '@/sentry.ts'; -const debug = Debug('ditto:fetch.worker'); +const console = new Stickynotes('ditto:fetch.worker'); export const FetchWorker = { async fetch( @@ -14,8 +15,8 @@ export const FetchWorker = { init: Omit, signal: AbortSignal | null | undefined, ): Promise<[BodyInit, ResponseInit]> { - debug(init.method, url); - const response = await fetch(url, { ...init, signal }); + console.debug(init.method, url); + const response = await safeFetch(url, { ...init, signal }); return [ await response.arrayBuffer(), { diff --git a/src/workers/policy.worker.ts b/src/workers/policy.worker.ts index c5b4129d..5e9d4d4a 100644 --- a/src/workers/policy.worker.ts +++ b/src/workers/policy.worker.ts @@ -1,4 +1,4 @@ -import 'deno-safe-fetch/load'; +import '@soapbox/safe-fetch/load'; import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/nostrify'; import { ReadOnlyPolicy } from '@nostrify/policies'; import * as Comlink from 'comlink'; From 515c70f0389954249a66a1cf143d2dcdb01da7ab Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 13 Nov 2024 15:34:41 -0600 Subject: [PATCH 36/36] Switch to httpbingo in tests so testing over https works --- src/workers/fetch.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/workers/fetch.test.ts b/src/workers/fetch.test.ts index d657d1f5..e4c698d4 100644 --- a/src/workers/fetch.test.ts +++ b/src/workers/fetch.test.ts @@ -5,9 +5,9 @@ import { fetchWorker } from '@/workers/fetch.ts'; Deno.test({ name: 'fetchWorker', async fn() { - const response = await fetchWorker('http://httpbin.org/get'); + const response = await fetchWorker('https://httpbingo.org/get'); const json = await response.json(); - assertEquals(json.headers.Host, 'httpbin.org'); + assertEquals(json.headers.Host, ['httpbingo.org']); }, sanitizeResources: false, }); @@ -19,7 +19,7 @@ Deno.test({ const signal = controller.signal; setTimeout(() => controller.abort(), 100); - assertRejects(() => fetchWorker('http://httpbin.org/delay/10', { signal })); + assertRejects(() => fetchWorker('https://httpbingo.org/delay/10', { signal })); await new Promise((resolve) => { signal.addEventListener('abort', () => resolve(), { once: true });