diff --git a/deno.json b/deno.json index 946b4e0..5567cd2 100644 --- a/deno.json +++ b/deno.json @@ -22,7 +22,7 @@ "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@lambdalisue/async": "jsr:@lambdalisue/async@^2.1.1", "@noble/secp256k1": "npm:@noble/secp256k1@^2.0.0", - "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.21.1", + "@nostrify/nostrify": "jsr:@nostrify/nostrify@^0.22.0", "@sentry/deno": "https://deno.land/x/sentry@7.112.2/index.mjs", "@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.1.0", "@soapbox/stickynotes": "jsr:@soapbox/stickynotes@^0.4.0", diff --git a/src/app.ts b/src/app.ts index ddc9990..5300b48 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NostrSigner, NStore } from '@nostrify/nostrify'; +import { NostrEvent, NostrSigner, NStore, NUploader } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { type Context, Env as HonoEnv, type Handler, Hono, Input as HonoInput, type MiddlewareHandler } from 'hono'; import { cors, logger, serveStatic } from 'hono/middleware'; @@ -81,7 +81,6 @@ import { hostMetaController } from '@/controllers/well-known/host-meta.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; import { webfingerController } from '@/controllers/well-known/webfinger.ts'; -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts'; import { cacheMiddleware } from '@/middleware/cacheMiddleware.ts'; import { cspMiddleware } from '@/middleware/cspMiddleware.ts'; @@ -97,7 +96,7 @@ interface AppEnv extends HonoEnv { /** Signer to get the logged-in user's pubkey, relays, and to sign events, or `undefined` if the user isn't logged in. */ signer?: NostrSigner; /** Uploader for the user to upload files. */ - uploader?: DittoUploader; + uploader?: NUploader; /** NIP-98 signed event proving the pubkey is owned by the user. */ proof?: NostrEvent; /** Store */ diff --git a/src/config.ts b/src/config.ts index f3b472e..cc14998 100644 --- a/src/config.ts +++ b/src/config.ts @@ -140,6 +140,10 @@ class Conf { static get nostrbuildEndpoint(): string { return Deno.env.get('NOSTRBUILD_ENDPOINT') || 'https://nostr.build/api/v2/upload/files'; } + /** Default Blossom servers to use when the `blossom` uploader is set. */ + static get blossomServers(): string[] { + return Deno.env.get('BLOSSOM_SERVERS')?.split(',') || ['https://blossom.primal.net/']; + } /** Module to upload files with. */ static get uploader() { return Deno.env.get('DITTO_UPLOADER'); diff --git a/src/interfaces/DittoUploader.ts b/src/interfaces/DittoUploader.ts deleted file mode 100644 index 08cbf50..0000000 --- a/src/interfaces/DittoUploader.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface DittoUploader { - upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]>; -} diff --git a/src/middleware/uploaderMiddleware.ts b/src/middleware/uploaderMiddleware.ts index 8279a12..38e8ace 100644 --- a/src/middleware/uploaderMiddleware.ts +++ b/src/middleware/uploaderMiddleware.ts @@ -1,13 +1,16 @@ +import { BlossomUploader, NostrBuildUploader } from '@nostrify/nostrify/uploaders'; + import { AppMiddleware } from '@/app.ts'; import { Conf } from '@/config.ts'; import { DenoUploader } from '@/uploaders/DenoUploader.ts'; import { IPFSUploader } from '@/uploaders/IPFSUploader.ts'; -import { NostrBuildUploader } from '@/uploaders/NostrBuildUploader.ts'; import { S3Uploader } from '@/uploaders/S3Uploader.ts'; import { fetchWorker } from '@/workers/fetch.ts'; /** Set an uploader for the user. */ export const uploaderMiddleware: AppMiddleware = async (c, next) => { + const signer = c.get('signer'); + switch (Conf.uploader) { case 's3': c.set('uploader', new S3Uploader(Conf.s3)); @@ -19,7 +22,12 @@ export const uploaderMiddleware: AppMiddleware = async (c, next) => { c.set('uploader', new DenoUploader({ baseUrl: Conf.mediaDomain, dir: Conf.uploadsDir })); break; case 'nostrbuild': - c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, fetch: fetchWorker })); + c.set('uploader', new NostrBuildUploader({ endpoint: Conf.nostrbuildEndpoint, signer, fetch: fetchWorker })); + break; + case 'blossom': + if (signer) { + c.set('uploader', new BlossomUploader({ servers: Conf.blossomServers, signer, fetch: fetchWorker })); + } break; } diff --git a/src/uploaders/DenoUploader.ts b/src/uploaders/DenoUploader.ts index e2224ab..fd30d8c 100644 --- a/src/uploaders/DenoUploader.ts +++ b/src/uploaders/DenoUploader.ts @@ -1,18 +1,17 @@ import { join } from 'node:path'; +import { NUploader } from '@nostrify/nostrify'; import { crypto } from '@std/crypto'; import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; - export interface DenoUploaderOpts { baseUrl: string; dir: string; } /** Local Deno filesystem uploader. */ -export class DenoUploader implements DittoUploader { +export class DenoUploader implements NUploader { baseUrl: string; dir: string; diff --git a/src/uploaders/IPFSUploader.ts b/src/uploaders/IPFSUploader.ts index 9141e78..7bf5165 100644 --- a/src/uploaders/IPFSUploader.ts +++ b/src/uploaders/IPFSUploader.ts @@ -1,7 +1,6 @@ +import { NUploader } from '@nostrify/nostrify'; import { z } from 'zod'; -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; - export interface IPFSUploaderOpts { baseUrl: string; apiUrl?: string; @@ -13,7 +12,7 @@ export interface IPFSUploaderOpts { * It will try to connect to `http://localhost:5001` by default, * and upload the file using the REST API. */ -export class IPFSUploader implements DittoUploader { +export class IPFSUploader implements NUploader { private baseUrl: string; private apiUrl: string; private fetch: typeof fetch; diff --git a/src/uploaders/NostrBuildUploader.ts b/src/uploaders/NostrBuildUploader.ts deleted file mode 100644 index ff4a4f0..0000000 --- a/src/uploaders/NostrBuildUploader.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { z } from 'zod'; - -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; - -export interface NostrBuildUploaderOpts { - endpoint?: string; - fetch?: typeof fetch; -} - -/** Upload files to nostr.build or another compatible server. */ -export class NostrBuildUploader implements DittoUploader { - private endpoint: string; - private fetch: typeof fetch; - - constructor(opts: NostrBuildUploaderOpts) { - this.endpoint = opts.endpoint ?? 'https://nostr.build/api/v2/upload/files'; - this.fetch = opts.fetch ?? globalThis.fetch; - } - - async upload(file: File, opts?: { signal?: AbortSignal }): Promise<[['url', string], ...string[][]]> { - const formData = new FormData(); - formData.append('fileToUpload', file); - - const response = await this.fetch(this.endpoint, { - method: 'POST', - body: formData, - signal: opts?.signal, - }); - - const json = await response.json(); - const [data] = NostrBuildUploader.schema().parse(json).data; - - const tags: [['url', string], ...string[][]] = [ - ['url', data.url], - ['m', data.mime], - ['x', data.sha256], - ['ox', data.original_sha256], - ['size', data.size.toString()], - ]; - - if (data.dimensions) { - tags.push(['dim', `${data.dimensions.width}x${data.dimensions.height}`]); - } - - if (data.blurhash) { - tags.push(['blurhash', data.blurhash]); - } - - return tags; - } - - /** nostr.build API response schema. */ - private static schema() { - return z.object({ - data: z.object({ - url: z.string().url(), - blurhash: z.string().optional().catch(undefined), - sha256: z.string(), - original_sha256: z.string(), - mime: z.string(), - size: z.number(), - dimensions: z.object({ - width: z.number(), - height: z.number(), - }).optional().catch(undefined), - }).array().min(1), - }); - } -} diff --git a/src/uploaders/S3Uploader.ts b/src/uploaders/S3Uploader.ts index f210ce8..b74796a 100644 --- a/src/uploaders/S3Uploader.ts +++ b/src/uploaders/S3Uploader.ts @@ -1,12 +1,12 @@ import { join } from 'node:path'; import { S3Client } from '@bradenmacdonald/s3-lite-client'; +import { NUploader } from '@nostrify/nostrify'; import { crypto } from '@std/crypto'; import { encodeHex } from '@std/encoding/hex'; import { extensionsByType } from '@std/media-types'; import { Conf } from '@/config.ts'; -import { DittoUploader } from '@/interfaces/DittoUploader.ts'; export interface S3UploaderOpts { endPoint: string; @@ -21,7 +21,7 @@ export interface S3UploaderOpts { } /** S3-compatible uploader for AWS, Wasabi, DigitalOcean Spaces, and more. */ -export class S3Uploader implements DittoUploader { +export class S3Uploader implements NUploader { private client: S3Client; constructor(opts: S3UploaderOpts) {