Merge branch 'nostrbuild' into 'main'

Nostr.build uploader

See merge request soapbox-pub/ditto!275
This commit is contained in:
Alex Gleason 2024-05-18 21:58:12 +00:00
commit cc487f898e
10 changed files with 152 additions and 4 deletions

View File

@ -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"
}
}
]
}

View File

@ -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"
}
]
}

View File

@ -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');

19
src/schemas/nostrbuild.ts Normal file
View File

@ -0,0 +1,19 @@
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(),
original_sha256: z.string(),
mime: z.string(),
dimensions: z.object({
width: z.number(),
height: z.number(),
}).optional().catch(undefined),
});
export const nostrbuildSchema = z.object({
data: nostrbuildFileSchema.array().min(1),
});

View File

@ -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, 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]);
}
@ -32,6 +36,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]);
}

View File

@ -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.');
}

View File

@ -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();
const [data] = nostrbuildSchema.parse(json).data;
return {
id: data.url,
sha256: data.sha256,
url: data.url,
blurhash: data.blurhash,
width: data.dimensions?.width,
height: data.dimensions?.height,
};
},
// deno-lint-ignore require-await
async delete(): Promise<void> {
return;
},
};

View File

@ -14,8 +14,14 @@ 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;
/** Width of the file, if applicable. */
width?: number;
/** Height of the file, if applicable. */
height?: number;
}
export type { Uploader };

View File

@ -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,
};
}

View File

@ -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,