From c89be75e5b9f240c8eba9fa180cad7ebc6301d76 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 16:22:24 -0500 Subject: [PATCH 1/3] Add a nostr.build uploader --- fixtures/nostrbuild-gif.json | 34 ++++++++++++++++++++++++++++++++++ fixtures/nostrbuild-mp3.json | 29 +++++++++++++++++++++++++++++ src/config.ts | 4 ++++ src/schemas/nostrbuild.ts | 18 ++++++++++++++++++ src/upload.ts | 6 +++++- src/uploaders/config.ts | 3 +++ src/uploaders/nostrbuild.ts | 33 +++++++++++++++++++++++++++++++++ src/uploaders/types.ts | 2 ++ 8 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 fixtures/nostrbuild-gif.json create mode 100644 fixtures/nostrbuild-mp3.json create mode 100644 src/schemas/nostrbuild.ts create mode 100644 src/uploaders/nostrbuild.ts diff --git a/fixtures/nostrbuild-gif.json b/fixtures/nostrbuild-gif.json new file mode 100644 index 0000000..49a969a --- /dev/null +++ b/fixtures/nostrbuild-gif.json @@ -0,0 +1,34 @@ +{ + "status": "success", + "message": "Upload successful.", + "data": [ + { + "input_name": "APIv2", + "name": "e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "sha256": "0a71f1c9dd982079bc52e96403368209cbf9507c5f6956134686f56e684b6377", + "original_sha256": "e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3", + "type": "picture", + "mime": "image/gif", + "size": 1796276, + "blurhash": "LGH-S^Vwm]x]04kX-qR-R]SL5FxZ", + "dimensions": { + "width": 360, + "height": 216 + }, + "dimensionsString": "360x216", + "url": "https://image.nostr.build/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "thumbnail": "https://image.nostr.build/thumb/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "responsive": { + "240p": "https://image.nostr.build/resp/240p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "360p": "https://image.nostr.build/resp/360p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "480p": "https://image.nostr.build/resp/480p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "720p": "https://image.nostr.build/resp/720p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif", + "1080p": "https://image.nostr.build/resp/1080p/e5f6e0e380536780efa774e8d3c8a5a040e3f9f99dbb48910b261c32872ee3a3.gif" + }, + "metadata": { + "date:create": "2024-05-18T02:11:39+00:00", + "date:modify": "2024-05-18T02:11:39+00:00" + } + } + ] +} \ No newline at end of file diff --git a/fixtures/nostrbuild-mp3.json b/fixtures/nostrbuild-mp3.json new file mode 100644 index 0000000..42a60b4 --- /dev/null +++ b/fixtures/nostrbuild-mp3.json @@ -0,0 +1,29 @@ +{ + "status": "success", + "message": "Upload successful.", + "data": [ + { + "id": 0, + "input_name": "APIv2", + "name": "f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "url": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "thumbnail": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "responsive": { + "240p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "360p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "480p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "720p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3", + "1080p": "https://media.nostr.build/av/f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725.mp3" + }, + "blurhash": "", + "sha256": "f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725", + "original_sha256": "f94665e6877741feb3fa3031342f95ae2ee00caae1cc651ce31ed6d524e05725", + "type": "video", + "mime": "audio/mpeg", + "size": 1519616, + "metadata": [], + "dimensions": [], + "dimensionsString": "0x0" + } + ] +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 6fe62b9..f3b472e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -136,6 +136,10 @@ class Conf { return Deno.env.get('IPFS_API_URL') || 'http://localhost:5001'; }, }; + /** nostr.build API endpoint when the `nostrbuild` uploader is used. */ + static get nostrbuildEndpoint(): string { + return Deno.env.get('NOSTRBUILD_ENDPOINT') || 'https://nostr.build/api/v2/upload/files'; + } /** Module to upload files with. */ static get uploader() { return Deno.env.get('DITTO_UPLOADER'); diff --git a/src/schemas/nostrbuild.ts b/src/schemas/nostrbuild.ts new file mode 100644 index 0000000..c9fd680 --- /dev/null +++ b/src/schemas/nostrbuild.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +export const nostrbuildFileSchema = z.object({ + name: z.string(), + url: z.string().url(), + thumbnail: z.string(), + blurhash: z.string(), + sha256: z.string(), + mime: z.string(), + dimensions: z.object({ + width: z.number(), + height: z.number(), + }), +}); + +export const nostrbuildSchema = z.object({ + data: nostrbuildFileSchema.array().min(1), +}); diff --git a/src/upload.ts b/src/upload.ts index 40184f0..5b43f39 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -16,7 +16,7 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro throw new Error('File size is too large.'); } - const { url, sha256, cid } = await uploader.upload(file, { signal }); + const { url, sha256, cid, blurhash } = await uploader.upload(file, { signal }); const data: string[][] = [ ['url', url], @@ -32,6 +32,10 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro data.push(['cid', cid]); } + if (blurhash) { + data.push(['blurhash', blurhash]); + } + if (description) { data.push(['alt', description]); } diff --git a/src/uploaders/config.ts b/src/uploaders/config.ts index 5b4c7af..8ce22b6 100644 --- a/src/uploaders/config.ts +++ b/src/uploaders/config.ts @@ -2,6 +2,7 @@ import { Conf } from '@/config.ts'; import { ipfsUploader } from '@/uploaders/ipfs.ts'; import { localUploader } from '@/uploaders/local.ts'; +import { nostrbuildUploader } from '@/uploaders/nostrbuild.ts'; import { s3Uploader } from '@/uploaders/s3.ts'; import type { Uploader } from './types.ts'; @@ -25,6 +26,8 @@ function uploader() { return ipfsUploader; case 'local': return localUploader; + case 'nostrbuild': + return nostrbuildUploader; default: throw new Error('No `DITTO_UPLOADER` configured. Uploads are disabled.'); } diff --git a/src/uploaders/nostrbuild.ts b/src/uploaders/nostrbuild.ts new file mode 100644 index 0000000..d9eed24 --- /dev/null +++ b/src/uploaders/nostrbuild.ts @@ -0,0 +1,33 @@ +import { Conf } from '@/config.ts'; +import { nostrbuildSchema } from '@/schemas/nostrbuild.ts'; + +import type { Uploader } from './types.ts'; + +/** nostr.build uploader. */ +export const nostrbuildUploader: Uploader = { + async upload(file) { + const formData = new FormData(); + formData.append('fileToUpload', file); + + const response = await fetch(Conf.nostrbuildEndpoint, { + method: 'POST', + body: formData, + }); + + const json = await response.json(); + console.log(JSON.stringify(json)); + + const [data] = nostrbuildSchema.parse(json).data; + + return { + id: data.url, + sha256: data.sha256, + url: data.url, + blurhash: data.blurhash, + }; + }, + // deno-lint-ignore require-await + async delete(): Promise { + return; + }, +}; diff --git a/src/uploaders/types.ts b/src/uploaders/types.ts index c514ad1..ac5bf05 100644 --- a/src/uploaders/types.ts +++ b/src/uploaders/types.ts @@ -14,6 +14,8 @@ interface UploadResult { url: string; /** SHA-256 hash of the file. */ sha256?: string; + /** Blurhash of the file. */ + blurhash?: string; /** IPFS CID of the file. */ cid?: string; } From ce49c500ae2fe62f6a83607055803aa18034c714 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 16:47:47 -0500 Subject: [PATCH 2/3] renderStatus: fix duplicated attachments --- src/views/mastodon/statuses.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index c674e16..c707ebb 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -76,13 +76,11 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< const cw = event.tags.find(isCWTag); const subject = event.tags.find((tag) => tag[0] === 'subject'); - const mediaLinks = getMediaLinks(links); - const imeta: string[][][] = event.tags .filter(([name]) => name === 'imeta') .map(([_, ...entries]) => entries.map((entry) => entry.split(' '))); - const media = [...mediaLinks, ...imeta]; + const media = imeta.length ? imeta : getMediaLinks(links); return { id: event.id, From 353111051a79a37aa3b7be1c44b4c54ffbeb9904 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 18 May 2024 16:53:17 -0500 Subject: [PATCH 3/3] Use dimensions from nostr.build --- src/schemas/nostrbuild.ts | 3 ++- src/upload.ts | 6 +++++- src/uploaders/nostrbuild.ts | 4 ++-- src/uploaders/types.ts | 4 ++++ src/views/mastodon/attachments.ts | 14 ++++++++++++++ 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/schemas/nostrbuild.ts b/src/schemas/nostrbuild.ts index c9fd680..db9f607 100644 --- a/src/schemas/nostrbuild.ts +++ b/src/schemas/nostrbuild.ts @@ -6,11 +6,12 @@ export const nostrbuildFileSchema = z.object({ thumbnail: z.string(), blurhash: z.string(), sha256: z.string(), + original_sha256: z.string(), mime: z.string(), dimensions: z.object({ width: z.number(), height: z.number(), - }), + }).optional().catch(undefined), }); export const nostrbuildSchema = z.object({ diff --git a/src/upload.ts b/src/upload.ts index 5b43f39..1da5a7d 100644 --- a/src/upload.ts +++ b/src/upload.ts @@ -16,7 +16,7 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro throw new Error('File size is too large.'); } - const { url, sha256, cid, blurhash } = await uploader.upload(file, { signal }); + const { url, sha256, cid, blurhash, width, height } = await uploader.upload(file, { signal }); const data: string[][] = [ ['url', url], @@ -24,6 +24,10 @@ async function uploadFile(file: File, meta: FileMeta, signal?: AbortSignal): Pro ['size', size.toString()], ]; + if (typeof width === 'number' && typeof height === 'number') { + data.push(['dim', `${width}x${height}`]); + } + if (sha256) { data.push(['x', sha256]); } diff --git a/src/uploaders/nostrbuild.ts b/src/uploaders/nostrbuild.ts index d9eed24..d9d0865 100644 --- a/src/uploaders/nostrbuild.ts +++ b/src/uploaders/nostrbuild.ts @@ -15,8 +15,6 @@ export const nostrbuildUploader: Uploader = { }); const json = await response.json(); - console.log(JSON.stringify(json)); - const [data] = nostrbuildSchema.parse(json).data; return { @@ -24,6 +22,8 @@ export const nostrbuildUploader: Uploader = { sha256: data.sha256, url: data.url, blurhash: data.blurhash, + width: data.dimensions?.width, + height: data.dimensions?.height, }; }, // deno-lint-ignore require-await diff --git a/src/uploaders/types.ts b/src/uploaders/types.ts index ac5bf05..e423028 100644 --- a/src/uploaders/types.ts +++ b/src/uploaders/types.ts @@ -18,6 +18,10 @@ interface UploadResult { blurhash?: string; /** IPFS CID of the file. */ cid?: string; + /** Width of the file, if applicable. */ + width?: number; + /** Height of the file, if applicable. */ + height?: number; } export type { Uploader }; diff --git a/src/views/mastodon/attachments.ts b/src/views/mastodon/attachments.ts index 273b460..0b1b8eb 100644 --- a/src/views/mastodon/attachments.ts +++ b/src/views/mastodon/attachments.ts @@ -6,10 +6,23 @@ function renderAttachment(media: { id?: string; data: string[][] }) { const url = tags.find(([name]) => name === 'url')?.[1]; const alt = tags.find(([name]) => name === 'alt')?.[1]; const cid = tags.find(([name]) => name === 'cid')?.[1]; + const dim = tags.find(([name]) => name === 'dim')?.[1]; const blurhash = tags.find(([name]) => name === 'blurhash')?.[1]; if (!url) return; + const [width, height] = dim?.split('x').map(Number) ?? [null, null]; + + const meta = (typeof width === 'number' && typeof height === 'number') + ? { + original: { + width, + height, + aspect: width / height, + }, + } + : undefined; + return { id: id ?? url, type: getAttachmentType(m ?? ''), @@ -18,6 +31,7 @@ function renderAttachment(media: { id?: string; data: string[][] }) { remote_url: null, description: alt ?? '', blurhash: blurhash || null, + meta, cid: cid, }; }