feat: add params

This commit is contained in:
florian 2024-04-29 19:38:59 +02:00
parent 39239f0d2f
commit e41f176126
6 changed files with 112 additions and 64 deletions

View File

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

8
development.mjs Normal file
View File

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

View File

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

View File

@ -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<BlobDescriptor> {
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<BlobDescriptor>(`${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);
}
}
});
};
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<BlobDescriptor> {
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<BlobDescriptor>(`${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}`);
}
}

View File

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

View File

@ -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<JobContext> {
//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();