From e41f1761263fc6a7bd15d17674d7f8edf5f08b4d Mon Sep 17 00:00:00 2001 From: florian <> Date: Mon, 29 Apr 2024 19:38:59 +0200 Subject: [PATCH] feat: add params --- Dockerfile | 9 ++-- development.mjs | 8 ++++ src/env.ts | 4 +- src/helpers/blossom.ts | 99 ++++++++++++++++++++++-------------------- src/helpers/dvm.ts | 4 +- src/index.ts | 52 +++++++++++++++++----- 6 files changed, 112 insertions(+), 64 deletions(-) create mode 100644 development.mjs diff --git a/Dockerfile b/Dockerfile index 6080ae7..817f4e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ -FROM node:20-alpine +FROM node:21 + +RUN apt-get update -y && apt-get install -y ffmpeg WORKDIR /app COPY . /app/ - -RUN yarn install -RUN yarn build +RUN npm install +RUN npm run build ENTRYPOINT [ "node", "build/index.js" ] diff --git a/development.mjs b/development.mjs new file mode 100644 index 0000000..e3c8268 --- /dev/null +++ b/development.mjs @@ -0,0 +1,8 @@ +import shell from "shelljs"; +import "dotenv/config.js"; + +shell.exec("./node_modules/.bin/tsc"); +shell.exec("./node_modules/.bin/tsc --watch", { silent: true, async: true }); +shell.exec("./node_modules/.bin/nodemon --watch build -d 1 build/index.js", { + async: true, +}); diff --git a/src/env.ts b/src/env.ts index b0f2f77..493d1c4 100644 --- a/src/env.ts +++ b/src/env.ts @@ -12,6 +12,8 @@ const LNBITS_ADMIN_KEY = process.env.LNBITS_ADMIN_KEY; const NOSTR_RELAYS = process.env.NOSTR_RELAYS?.split(",") ?? []; if (NOSTR_RELAYS.length === 0) throw new Error("Missing NOSTR_RELAYS"); -export { NOSTR_PRIVATE_KEY, LNBITS_URL, LNBITS_ADMIN_KEY, NOSTR_RELAYS }; +const BLOSSOM_UPLOAD_SERVER = process.env.BLOSSOM_UPLOAD_SERVER || "https://media-server.slidestr.net"; + +export { NOSTR_PRIVATE_KEY, LNBITS_URL, LNBITS_ADMIN_KEY, NOSTR_RELAYS, BLOSSOM_UPLOAD_SERVER }; diff --git a/src/helpers/blossom.ts b/src/helpers/blossom.ts index dd9b7fd..3ba31d8 100644 --- a/src/helpers/blossom.ts +++ b/src/helpers/blossom.ts @@ -6,58 +6,63 @@ import { getFileSizeSync } from "./filesystem.js"; import { createReadStream } from "fs"; import axios from "axios"; import debug from "debug"; +import { randomUUID } from "crypto"; const logger = debug("dvm:blossom"); type BlobDescriptor = { - created: number; - type?: string; - sha256: string; - size: number; - url: string; - }; - + created: number; + type?: string; + sha256: string; + size: number; + url: string; +}; const signer: Signer = async (event: EventTemplate) => { - return new Promise((resolve, reject) => { - try { - const verifiedEvent = finalizeEvent(event, NOSTR_PRIVATE_KEY); - resolve(verifiedEvent); - } catch (error) { - reject(error); - } - }); - }; - -export async function uploadFile(filePath: string, server: string): Promise { + return new Promise((resolve, reject) => { try { - const oneHour = () => dayjs().unix() + 60 * 60; - const authEvent = await signer({ - created_at: dayjs().unix(), - kind: 24242, - content: "Upload thumbail", - tags: [ - ["t", "upload"], - // ["name", ], unknown - ["size", String(getFileSizeSync(filePath))], - ["expiration", String(oneHour)], - ], - }); - - // Create a read stream for the thumbnail file - const thumbnailStream = createReadStream(filePath); - - // Upload thumbnail stream using axios - const blob = await axios.put(`${server}/upload`, thumbnailStream, { - headers: { - "Content-Type": "image/jpeg", // Adjust content type as needed <--- TODO adjust for png - authorization: "Nostr " + btoa(JSON.stringify(authEvent)), - }, - }); - - logger(`File ${filePath} uploaded successfully.`); - return blob.data; - } catch (error: any) { - throw new Error(`Failed to upload thumbnail ${filePath}: ${error.message}`); + const verifiedEvent = finalizeEvent(event, NOSTR_PRIVATE_KEY); + resolve(verifiedEvent); + } catch (error) { + reject(error); } - } \ No newline at end of file + }); +}; + +export async function createDvmBlossemAuthToken() { + const tenMinutes = () => dayjs().unix() + 10 * 60; + const authEvent = await signer({ + created_at: dayjs().unix(), + kind: 24242, + content: "Upload thumbail", + tags: [ + ["t", "upload"], + ["name", randomUUID() ], // make sure the auth events are unique + ["expiration", String(tenMinutes)], + ], + }); + + return btoa(JSON.stringify(authEvent)); +} + +export async function uploadFile(filePath: string, server: string, authToken?: string): Promise { + try { + const blossomAuthToken = authToken || await createDvmBlossemAuthToken(); + + // Create a read stream for the thumbnail file + const thumbnailStream = createReadStream(filePath); + + // Upload thumbnail stream using axios + const blob = await axios.put(`${server}/upload`, thumbnailStream, { + headers: { + "Content-Type": "image/jpeg", // Adjust content type as needed <--- TODO adjust for png + "Authorization": "Nostr " + blossomAuthToken, + }, + }); + + logger(`File ${filePath} uploaded successfully.`); + return blob.data; + } catch (error: any) { + throw new Error(`Failed to upload thumbnail ${filePath}: ${error.message}`); + } +} diff --git a/src/helpers/dvm.ts b/src/helpers/dvm.ts index 139302c..d596861 100644 --- a/src/helpers/dvm.ts +++ b/src/helpers/dvm.ts @@ -24,8 +24,8 @@ export function getInputParams(e: Event, k: string) { return e.tags.filter((t) => t[0] === "param" && t[1] === k).map((t) => t[2]); } -export function getInputParam(e: Event, k: string) { - const value = getInputParams(e, k)[0]; +export function getInputParam(e: Event, k: string, defaultValue?: string) { + const value = getInputParams(e, k)[0] || defaultValue; if (value === undefined) throw new Error(`Missing ${k} param`); return value; } diff --git a/src/index.ts b/src/index.ts index 69007ee..336736c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node import dayjs from "dayjs"; -import { NostrEvent, Subscription, Filter, finalizeEvent } from "nostr-tools"; -import { NOSTR_PRIVATE_KEY, NOSTR_RELAYS } from "./env.js"; -import { getInput, getInputTag, getOutputType, getRelays } from "./helpers/dvm.js"; +import { NostrEvent, Subscription, Filter, finalizeEvent, nip04 } from "nostr-tools"; +import { BLOSSOM_UPLOAD_SERVER, NOSTR_PRIVATE_KEY, NOSTR_RELAYS } from "./env.js"; +import { getInput, getInputParam, getInputParams, getInputTag, getOutputType, getRelays } from "./helpers/dvm.js"; import { unique } from "./helpers/array.js"; import { pool } from "./pool.js"; import { logger } from "./debug.js"; @@ -14,19 +14,51 @@ import { rmSync } from "fs"; type JobContext = { request: NostrEvent; url: string; + thumbnailCount: number; + imageFormat: "jpg" | "png"; + uploadServer: string; }; async function shouldAcceptJob(request: NostrEvent): Promise { + //const decryptedContent = await nip04.decrypt(NOSTR_PRIVATE_KEY, request.pubkey, request.content); + //encryptedTags = JSON.parse(decryptedContent); + const input = getInput(request); const output = getOutputType(request); - // const lang = getInputParam(request, "language"); + + const authTokens = unique(getInputParams(request, "authToken")); + const thumbnailCount = parseInt(getInputParam(request, "thumbnailCount", "3"), 10); + const imageFormat = getInputParam(request, "imageFormat", "jpg"); + const uploadServer = getInputParam(request, "uploadServer", BLOSSOM_UPLOAD_SERVER); // if (output !== "text/plain") throw new Error(`Unsupported output type ${output}`); + if (thumbnailCount < 1 || thumbnailCount > 10) { + throw new Error(`Thumbnail count has to be between 1 and 10`); + } + + // uniq auth token either 0 or len>=thumbnailCount + if (authTokens.length > 0 && authTokens.length <= thumbnailCount) { + throw new Error(`Not enough auth tokens ${authTokens.length} for ${thumbnailCount} thumbnail uploads.`); + } + + // TODO check that auth tokens are not expired + // TODO add sanity checks for URL + if ( + !uploadServer.match( + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/, + ) + ) { + throw new Error(`Upload server is not a valid url.`); + } + + if (imageFormat != "jpg" && imageFormat != "png") { + throw new Error(`Unsupported image format ${imageFormat}`); + } if (input.type === "url") { - return { url: input.value, request }; + return { url: input.value, request, thumbnailCount, imageFormat, uploadServer }; } else throw new Error(`Unknown input type ${input.type}`); } @@ -60,19 +92,19 @@ async function doWork(context: JobContext) { const startTime = dayjs().unix(); logger(`creating thumb for URL ${context.url}`); - const server = "https://media-server.slidestr.net"; // TODO add env variable for this const resultTags = await retreiveMetaData(context.url); - const thumbnailContent = await extractThumbnails(context.url, 3, 'jpg'); // TODO add DVM param for these + const thumbnailContent = await extractThumbnails(context.url, context.thumbnailCount, context.imageFormat); for (const tp of thumbnailContent.thumbnailPaths) { - const blob = await uploadFile(tp, server); - logger(`Uplaoaded thumbnail file: ${blob.url}`); + const blob = await uploadFile(tp, context.uploadServer); // todo use user specfic auth Tokens + logger(`Uploaaded thumbnail file: ${blob.url}`); resultTags.push(["thumb", blob.url]); resultTags.push(["x", blob.sha256]); } + //nip04.encrypt(NOSTR_PRIVATE_KEY, p, JSON.stringify()) const result = finalizeEvent( { kind: DVM_VIDEO_THUMB_RESULT_KIND, @@ -89,7 +121,7 @@ async function doWork(context: JobContext) { NOSTR_PRIVATE_KEY, ); - rmSync(thumbnailContent.tempDir, { recursive: true }); // TODO also remove this when an error occurs + rmSync(thumbnailContent.tempDir, { recursive: true }); // TODO also remove this when an error occurs const endTime = dayjs().unix();