From 21329458e517813303d2be4ab0496d5a3370908a Mon Sep 17 00:00:00 2001 From: cirroskais Date: Sun, 28 Apr 2024 03:33:06 -0400 Subject: [PATCH 01/10] start unify config, create dockerfile --- .dockerignore | 12 ++ .env.example | 23 +++ Dockerfile | 25 +++ src/server/index.ts | 40 ++-- src/server/lib/config.ts | 32 +++ src/server/lib/files.ts | 412 +++++++++++++++++++++++---------------- src/server/lib/mail.ts | 53 ++--- 7 files changed, 390 insertions(+), 207 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 src/server/lib/config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b76e09f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.vscode +.gitignore +.prettierrc +config.json +LICENSE +README.md +node_modules +.env +.data +out +dist +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..546c7c7 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +PORT= +REQUEST_TIMEOUT= +TRUST_PROXY= +FORCE_SSL= + +DISCORD__TOKEN= + +MAX__DISCORD_FILES= +MAX__DISCORD_FILE_SIZE= +MAX__UPLOAD_ID_LENGTH= + +TARGET__GUILD= +TARGET__CHANNEL= + +ACCOUNTS__REGISTRATION_ENABLED= +ACCOUNTS__REQUIRED_FOR_UPLOAD= + +MAIL__HOST= +MAIL__PORT= +MAIL__SECURE= +MAIL__SEND_FROM= +MAIL__USER= +MAIL__PASS= \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..57009fa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:21-alpine AS base +WORKDIR /usr/src/app + +FROM base AS install +RUN mkdir -p /tmp/dev +COPY package.json package-lock.json /tmp/dev/ +RUN cd /tmp/dev && npm install + +RUN mkdir -p /tmp/prod +COPY package.json package-lock.json /tmp/prod/ +RUN cd /tmp/prod && npm install --omit=dev + +FROM base AS build +COPY --from=install /tmp/dev/node_modules node_modules +COPY . . + +RUN npm run build + +FROM base AS app +COPY --from=install /tmp/prod/node_modules node_modules +COPY --from=build out ./ +COPY package.json . + +EXPOSE 3000 +ENTRYPOINT [ "node", "./index.js" ] \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 74c2cff..fe894bd 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -7,10 +7,10 @@ import Files from "./lib/files.js" import { getAccount } from "./lib/middleware.js" import APIRouter from "./routes/api.js" import preview from "./routes/api/web/preview.js" -import {fileURLToPath} from "url" -import {dirname} from "path" -import pkg from "../../package.json" assert {type:"json"} -import config from "../../config.json" assert {type:"json"} +import { fileURLToPath } from "url" +import { dirname } from "path" +import pkg from "../../package.json" assert { type: "json" } +import config from "../../config.json" assert { type: "json" } const app = new Hono() @@ -36,10 +36,8 @@ app.get( // haha... app.on(["MOLLER"], "*", async (ctx) => { - ctx.header("Content-Type", "image/webp") - return ctx.body( await readFile("./assets/moller.png") ) - + return ctx.body(await readFile("./assets/moller.png")) }) //app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]})) @@ -64,9 +62,11 @@ if (config.forceSSL) { app.get("/server", (ctx) => ctx.json({ - ...config, version: pkg.version, files: Object.keys(files.files).length, + maxDiscordFiles: config.maxDiscordFiles, + maxDiscordFileSize: config.maxDiscordFileSize, + accounts: config.accounts, }) ) @@ -87,28 +87,30 @@ apiRouter.loadAPIMethods().then(() => { console.log("API OK!") // moved here to ensure it's matched last - app.get("/:fileId", async (ctx) => + app.get("/:fileId", async (ctx) => app.fetch( new Request( - (new URL( - `/api/v1/file/${ctx.req.param("fileId")}`, ctx.req.raw.url)).href, - ctx.req.raw - ), + new URL( + `/api/v1/file/${ctx.req.param("fileId")}`, + ctx.req.raw.url + ).href, + ctx.req.raw + ), ctx.env ) ) - // listen on 3000 or MONOFILE_PORT + // listen on 3000 or PORT // moved here to prevent a crash if someone manages to access monofile before api routes are mounted - + serve( { fetch: app.fetch, - port: Number(process.env.MONOFILE_PORT || 3000), + port: Number(process.env.PORT || 3000), serverOptions: { //@ts-ignore - requestTimeout: config.requestTimeout - } + requestTimeout: config.requestTimeout, + }, }, (info) => { console.log("Web OK!", info.port, info.address) @@ -133,4 +135,4 @@ app.get("/", async (ctx) => file serving */ -export default app \ No newline at end of file +export default app diff --git a/src/server/lib/config.ts b/src/server/lib/config.ts new file mode 100644 index 0000000..22f33b2 --- /dev/null +++ b/src/server/lib/config.ts @@ -0,0 +1,32 @@ +import "dotenv/config" + +export default { + port: Number(process.env.PORT), + requestTimeout: Number(process.env.REQUEST_TIMEOUT), + trustProxy: process.env.TRUST_PROXY === "true", + forceSSL: process.env.FORCE_SSL === "true", + discordToken: process.env.DISCORD__TOKEN, + maxDiscordFiles: Number(process.env.MAX__DISCORD_FILES), + maxDiscordFileSize: Number(process.env.MAX__DISCORD_FILES), + maxUploadIdLength: Number(process.env.MAX__UPLOAD_ID_LENGTH), + targetGuild: process.env.TARGET__GUILD, + targetChannel: process.env.TARGET__CHANNEL, + accounts: { + registrationEnabled: + process.env.ACCOUNTS__REGISTRATION_ENABLED === "true", + requiredForUpload: process.env.ACCOUNTS__REQUIRED_FOR_UPLOAD === "true", + }, + + mail: { + transport: { + host: process.env.MAIL__HOST, + port: Number(process.env.MAIL__PORT), + secure: process.env.MAIL__SECURE === "true", + }, + send: { + from: process.env.MAIL__SEND_FROM, + }, + user: process.env.MAIL__USER, + pass: process.env.MAIL__PASS, + }, +} diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index 8edd2ac..33f309e 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -3,7 +3,8 @@ import { Readable, Writable } from "node:stream" import crypto from "node:crypto" import { files } from "./accounts.js" import { Client as API } from "./DiscordAPI/index.js" -import type {APIAttachment} from "discord-api-types/v10" +import type { APIAttachment } from "discord-api-types/v10" +import config from "./config.js" import "dotenv/config" import * as Accounts from "./accounts.js" @@ -32,10 +33,12 @@ export function generateFileId(length: number = 5) { /** * @description Assert multiple conditions... this exists out of pure laziness - * @param conditions + * @param conditions */ -function multiAssert(conditions: Map) { +function multiAssert( + conditions: Map +) { for (let [cond, err] of conditions.entries()) { if (cond) return err } @@ -80,18 +83,15 @@ export interface StatusCodeError { } export class WebError extends Error { - readonly statusCode: number = 500 - + constructor(status: number, message: string) { super(message) this.statusCode = status } - } export class ReadStream extends Readable { - files: Files pointer: FilePointer @@ -100,52 +100,60 @@ export class ReadStream extends Readable { position: number = 0 ranges: { - useRanges: boolean, - byteStart: number, + useRanges: boolean + byteStart: number byteEnd: number - scan_msg_begin: number, - scan_msg_end: number, - scan_files_begin: number, + scan_msg_begin: number + scan_msg_end: number + scan_files_begin: number scan_files_end: number } id: number = Math.random() aborter?: AbortController - constructor(files: Files, pointer: FilePointer, range?: {start: number, end: number}) { + constructor( + files: Files, + pointer: FilePointer, + range?: { start: number; end: number } + ) { super() console.log(this.id, range) this.files = files this.pointer = pointer - let useRanges = - Boolean(range && pointer.chunkSize && pointer.sizeInBytes) - + let useRanges = Boolean( + range && pointer.chunkSize && pointer.sizeInBytes + ) + this.ranges = { useRanges, scan_msg_begin: 0, scan_msg_end: pointer.messageids.length - 1, - scan_files_begin: - useRanges + scan_files_begin: useRanges ? Math.floor(range!.start / pointer.chunkSize!) : 0, - scan_files_end: - useRanges + scan_files_end: useRanges ? Math.ceil(range!.end / pointer.chunkSize!) - 1 : -1, byteStart: range?.start || 0, - byteEnd: range?.end || 0 + byteEnd: range?.end || 0, } if (useRanges) - this.ranges.scan_msg_begin = Math.floor(this.ranges.scan_files_begin / 10), - this.ranges.scan_msg_end = Math.ceil(this.ranges.scan_files_end / 10), - this.msgIdx = this.ranges.scan_msg_begin - + (this.ranges.scan_msg_begin = Math.floor( + this.ranges.scan_files_begin / 10 + )), + (this.ranges.scan_msg_end = Math.ceil( + this.ranges.scan_files_end / 10 + )), + (this.msgIdx = this.ranges.scan_msg_begin) + console.log(this.ranges) } - async _read() {/* + async _read() { + /* console.log("Calling for more data") if (this.busy) return this.busy = true @@ -160,24 +168,32 @@ export class ReadStream extends Readable { this.pushData() } - async _destroy(error: Error | null, callback: (error?: Error | null | undefined) => void): Promise { - if (this.aborter) - this.aborter.abort() + async _destroy( + error: Error | null, + callback: (error?: Error | null | undefined) => void + ): Promise { + if (this.aborter) this.aborter.abort() callback() } async getNextAttachment() { // return first in our attachment buffer - let ret = this.attachmentBuffer.splice(0,1)[0] + let ret = this.attachmentBuffer.splice(0, 1)[0] if (ret) return ret - console.log(this.id, this.msgIdx, this.ranges.scan_msg_end, this.pointer.messageids[this.msgIdx]) + console.log( + this.id, + this.msgIdx, + this.ranges.scan_msg_end, + this.pointer.messageids[this.msgIdx] + ) // oh, there's none left. let's fetch a new message, then. if ( - !this.pointer.messageids[this.msgIdx] - || this.msgIdx > this.ranges.scan_msg_end - ) return null + !this.pointer.messageids[this.msgIdx] || + this.msgIdx > this.ranges.scan_msg_end + ) + return null let msg = await this.files.api .fetchMessage(this.pointer.messageids[this.msgIdx]) @@ -190,95 +206,113 @@ export class ReadStream extends Readable { let attach = msg.attachments console.log(attach) - this.attachmentBuffer = this.ranges.useRanges ? attach.slice( - this.msgIdx == this.ranges.scan_msg_begin - ? this.ranges.scan_files_begin - this.ranges.scan_msg_begin * 10 - : 0, - this.msgIdx == this.ranges.scan_msg_end - ? this.ranges.scan_files_end - this.ranges.scan_msg_end * 10 + 1 - : attach.length - ) : attach + this.attachmentBuffer = this.ranges.useRanges + ? attach.slice( + this.msgIdx == this.ranges.scan_msg_begin + ? this.ranges.scan_files_begin - + this.ranges.scan_msg_begin * 10 + : 0, + this.msgIdx == this.ranges.scan_msg_end + ? this.ranges.scan_files_end - + this.ranges.scan_msg_end * 10 + + 1 + : attach.length + ) + : attach console.log(this.attachmentBuffer) } this.msgIdx++ - return this.attachmentBuffer.splice(0,1)[0] + return this.attachmentBuffer.splice(0, 1)[0] } async getPusherForWebStream(webStream: ReadableStream) { const reader = await webStream.getReader() let pushing = false // acts as a debounce just in case - // (words of a girl paranoid from writing readfilestream) + // (words of a girl paranoid from writing readfilestream) let pushToStream = this.push.bind(this) let stream = this - - return function() { + + return function () { if (pushing) return pushing = true - return reader.read().catch(e => { - // Probably means an AbortError; whatever it is we'll need to abort - if (webStream.locked) reader.releaseLock() - webStream.cancel().catch(e => undefined) - if (!stream.destroyed) stream.destroy() - return e - }).then(result => { - if (result instanceof Error || !result) return result - - let pushed - if (!result.done) { - pushing = false - pushed = pushToStream(result.value) - } - return {readyForMore: pushed || false, streamDone: result.done } - }) + return reader + .read() + .catch((e) => { + // Probably means an AbortError; whatever it is we'll need to abort + if (webStream.locked) reader.releaseLock() + webStream.cancel().catch((e) => undefined) + if (!stream.destroyed) stream.destroy() + return e + }) + .then((result) => { + if (result instanceof Error || !result) return result + + let pushed + if (!result.done) { + pushing = false + pushed = pushToStream(result.value) + } + return { + readyForMore: pushed || false, + streamDone: result.done, + } + }) } } async getNextChunk() { let scanning_chunk = await this.getNextAttachment() - console.log(this.id, "Next chunk requested; got attachment", scanning_chunk) + console.log( + this.id, + "Next chunk requested; got attachment", + scanning_chunk + ) if (!scanning_chunk) return null - let { - byteStart, byteEnd, scan_files_begin, scan_files_end - } = this.ranges + let { byteStart, byteEnd, scan_files_begin, scan_files_end } = + this.ranges - let headers: HeadersInit = - this.ranges.useRanges - ? { - Range: `bytes=${ - this.position == 0 - ? byteStart - scan_files_begin * this.pointer.chunkSize! - : "0" - }-${ - this.attachmentBuffer.length == 0 && this.msgIdx == scan_files_end - ? byteEnd - scan_files_end * this.pointer.chunkSize! - : "" - }`, - } - : {} + let headers: HeadersInit = this.ranges.useRanges + ? { + Range: `bytes=${ + this.position == 0 + ? byteStart - + scan_files_begin * this.pointer.chunkSize! + : "0" + }-${ + this.attachmentBuffer.length == 0 && + this.msgIdx == scan_files_end + ? byteEnd - scan_files_end * this.pointer.chunkSize! + : "" + }`, + } + : {} this.aborter = new AbortController() - let response = await fetch(scanning_chunk.url, {headers, signal: this.aborter.signal}) - .catch((e: Error) => { - console.error(e) - return {body: e} - }) + let response = await fetch(scanning_chunk.url, { + headers, + signal: this.aborter.signal, + }).catch((e: Error) => { + console.error(e) + return { body: e } + }) this.position++ - + return response.body } - currentPusher?: (() => Promise<{readyForMore: boolean, streamDone: boolean } | void> | undefined) + currentPusher?: () => + | Promise<{ readyForMore: boolean; streamDone: boolean } | void> + | undefined busy: boolean = false async pushData(): Promise { - // uh oh, we don't have a currentPusher // let's make one then if (!this.currentPusher) { @@ -292,7 +326,8 @@ export class ReadStream extends Readable { // or the stream has ended. // let's destroy the stream console.log(this.id, "Ending", next) - if (next) this.destroy(next); else this.push(null) + if (next) this.destroy(next) + else this.push(null) return } } @@ -304,12 +339,10 @@ export class ReadStream extends Readable { this.currentPusher = undefined return this.pushData() } else return result?.readyForMore - } } export class UploadStream extends Writable { - uploadId?: string name?: string mime?: string @@ -331,7 +364,11 @@ export class UploadStream extends Writable { async _write(data: Buffer, encoding: string, callback: () => void) { console.log("Write to stream attempted") - if (this.filled + data.byteLength > (this.files.config.maxDiscordFileSize*this.files.config.maxDiscordFiles)) + if ( + this.filled + data.byteLength > + this.files.config.maxDiscordFileSize * + this.files.config.maxDiscordFiles + ) return this.destroy(new WebError(413, "maximum file size exceeded")) this.hash.update(data) @@ -343,21 +380,30 @@ export class UploadStream extends Writable { while (position < data.byteLength) { let capture = Math.min( - ((this.files.config.maxDiscordFileSize*10) - (this.filled % (this.files.config.maxDiscordFileSize*10))), - data.byteLength-position + this.files.config.maxDiscordFileSize * 10 - + (this.filled % (this.files.config.maxDiscordFileSize * 10)), + data.byteLength - position + ) + console.log( + `Capturing ${capture} bytes for megachunk, ${data.subarray(position, position + capture).byteLength}` ) - console.log(`Capturing ${capture} bytes for megachunk, ${data.subarray(position, position + capture).byteLength}`) if (!this.current) await this.getNextStream() if (!this.current) { - this.destroy(new Error("getNextStream called during debounce")); return + this.destroy(new Error("getNextStream called during debounce")) + return } - readyForMore = this.current.push( data.subarray(position, position+capture) ) - console.log(`pushed ${data.byteLength} byte chunk`); - position += capture, this.filled += capture + readyForMore = this.current.push( + data.subarray(position, position + capture) + ) + console.log(`pushed ${data.byteLength} byte chunk`) + ;(position += capture), (this.filled += capture) // message is full, so tell the next run to get a new message - if (this.filled % (this.files.config.maxDiscordFileSize*10) == 0) { + if ( + this.filled % (this.files.config.maxDiscordFileSize * 10) == + 0 + ) { this.current!.push(null) this.current = undefined } @@ -369,24 +415,27 @@ export class UploadStream extends Writable { async _final(callback: (error?: Error | null | undefined) => void) { if (this.current) { - this.current.push(null); + this.current.push(null) // i probably dnt need this but whateverrr :3 - await new Promise((res,rej) => this.once("debounceReleased", res)) + await new Promise((res, rej) => this.once("debounceReleased", res)) } callback() } aborted: boolean = false - async _destroy(error: Error | null, callback: (err?: Error|null) => void) { + async _destroy( + error: Error | null, + callback: (err?: Error | null) => void + ) { this.error = error || undefined await this.abort() callback(error) } - /** + /** * @description Cancel & unlock the file. When destroy() is called with a non-WebError, this is automatically called - */ + */ async abort() { if (this.aborted) return this.aborted = true @@ -406,8 +455,13 @@ export class UploadStream extends Writable { async commit() { if (this.errored) throw this.error if (!this.writableFinished) { - let err = Error("attempted to commit file when the stream was still unfinished") - if (!this.destroyed) {this.destroy(err)}; throw err + let err = Error( + "attempted to commit file when the stream was still unfinished" + ) + if (!this.destroyed) { + this.destroy(err) + } + throw err } // Perform checks @@ -421,7 +475,7 @@ export class UploadStream extends Writable { } if (!this.uploadId) this.setUploadId(generateFileId()) - + let ogf = this.files.files[this.uploadId!] this.files.files[this.uploadId!] = { @@ -430,19 +484,18 @@ export class UploadStream extends Writable { messageids: this.messages, owner: this.owner, sizeInBytes: this.filled, - visibility: ogf ? ogf.visibility - : ( - this.owner - ? Accounts.getFromId(this.owner)?.defaultFileVisibility - : undefined - ), + visibility: ogf + ? ogf.visibility + : this.owner + ? Accounts.getFromId(this.owner)?.defaultFileVisibility + : undefined, // so that json.stringify doesnt include tag:undefined - ...((ogf||{}).tag ? {tag:ogf.tag} : {}), + ...((ogf || {}).tag ? { tag: ogf.tag } : {}), chunkSize: this.files.config.maxDiscordFileSize, md5: this.hash.digest("hex"), - lastModified: Date.now() + lastModified: Date.now(), } delete this.files.locks[this.uploadId!] @@ -456,52 +509,72 @@ export class UploadStream extends Writable { setName(name: string) { if (this.name) - return this.destroy( new WebError(400, "duplicate attempt to set filename") ) + return this.destroy( + new WebError(400, "duplicate attempt to set filename") + ) if (name.length > 512) - return this.destroy( new WebError(400, "filename can be a maximum of 512 characters") ) - - this.name = name; + return this.destroy( + new WebError(400, "filename can be a maximum of 512 characters") + ) + + this.name = name return this } - setType(type: string) { + setType(type: string) { if (this.mime) - return this.destroy( new WebError(400, "duplicate attempt to set mime type") ) + return this.destroy( + new WebError(400, "duplicate attempt to set mime type") + ) if (type.length > 256) - return this.destroy( new WebError(400, "mime type can be a maximum of 256 characters") ) - - this.mime = type; + return this.destroy( + new WebError( + 400, + "mime type can be a maximum of 256 characters" + ) + ) + + this.mime = type return this } setUploadId(id: string) { if (this.uploadId) - return this.destroy( new WebError(400, "duplicate attempt to set upload ID") ) - if (!id || id.match(id_check_regex)?.[0] != id - || id.length > this.files.config.maxUploadIdLength) - return this.destroy( new WebError(400, "invalid file ID") ) + return this.destroy( + new WebError(400, "duplicate attempt to set upload ID") + ) + if ( + !id || + id.match(id_check_regex)?.[0] != id || + id.length > this.files.config.maxUploadIdLength + ) + return this.destroy(new WebError(400, "invalid file ID")) if (this.files.files[id] && this.files.files[id].owner != this.owner) - return this.destroy( new WebError(403, "you don't own this file") ) + return this.destroy(new WebError(403, "you don't own this file")) if (this.files.locks[id]) - return this.destroy( new WebError(409, "a file with this ID is already being uploaded") ) + return this.destroy( + new WebError( + 409, + "a file with this ID is already being uploaded" + ) + ) this.files.locks[id] = true this.uploadId = id return this - } + } // merged StreamBuffer helper - + filled: number = 0 current?: Readable messages: string[] = [] - - private newmessage_debounce : boolean = true - - private async startMessage(): Promise { + private newmessage_debounce: boolean = true + + private async startMessage(): Promise { if (!this.newmessage_debounce) return this.newmessage_debounce = false @@ -510,24 +583,28 @@ export class UploadStream extends Writable { let stream = new Readable({ read() { // this is stupid but it should work - console.log("Read called; calling on server to execute callback") + console.log( + "Read called; calling on server to execute callback" + ) wrt.emit("exec-callback") - } + }, }) stream.pause() - + console.log(`Starting a message`) - this.files.api.send(stream).then(message => { - this.messages.push(message.id) - console.log(`Sent: ${message.id}`) - this.newmessage_debounce = true - this.emit("debounceReleased") - }).catch(e => { - if (!this.errored) this.destroy(e) - }) + this.files.api + .send(stream) + .then((message) => { + this.messages.push(message.id) + console.log(`Sent: ${message.id}`) + this.newmessage_debounce = true + this.emit("debounceReleased") + }) + .catch((e) => { + if (!this.errored) this.destroy(e) + }) return stream - } private async getNextStream() { @@ -536,12 +613,14 @@ export class UploadStream extends Writable { if (this.current) return this.current else if (this.newmessage_debounce) { // startmessage.... idk - this.current = await this.startMessage(); + this.current = await this.startMessage() return this.current } else { return new Promise((resolve, reject) => { console.log("Waiting for debounce to be released...") - this.once("debounceReleased", async () => resolve(await this.getNextStream())) + this.once("debounceReleased", async () => + resolve(await this.getNextStream()) + ) }) } } @@ -553,13 +632,13 @@ export default class Files { files: { [key: string]: FilePointer } = {} data_directory: string = `${process.cwd()}/.data` - locks: Record = {} // I'll, like, do something more proper later + locks: Record = {} // I'll, like, do something more proper later constructor(config: Configuration) { this.config = config - this.api = new API(process.env.TOKEN!, config) + this.api = new API(config.discordToken!, config) - readFile(this.data_directory+ "/files.json") + readFile(this.data_directory + "/files.json") .then((buf) => { this.files = JSON.parse(buf.toString() || "{}") }) @@ -574,7 +653,7 @@ export default class Files { /** * @description Saves file database - * + * */ async write(): Promise { await writeFile( @@ -592,7 +671,7 @@ export default class Files { * @param uploadId Target file's ID */ - async update( uploadId: string ) { + async update(uploadId: string) { let target_file = this.files[uploadId] let attachment_sizes = [] @@ -604,12 +683,12 @@ export default class Files { } if (!target_file.sizeInBytes) - target_file.sizeInBytes = attachment_sizes.reduce((a, b) => a + b, 0) - - if (!target_file.chunkSize) - target_file.chunkSize = attachment_sizes[0] + target_file.sizeInBytes = attachment_sizes.reduce( + (a, b) => a + b, + 0 + ) - + if (!target_file.chunkSize) target_file.chunkSize = attachment_sizes[0] } /** @@ -624,7 +703,8 @@ export default class Files { ): Promise { if (this.files[uploadId]) { let file = this.files[uploadId] - if (!file.sizeInBytes || !file.chunkSize) await this.update(uploadId) + if (!file.sizeInBytes || !file.chunkSize) + await this.update(uploadId) return new ReadStream(this, file, range) } else { throw { status: 404, message: "not found" } @@ -648,9 +728,9 @@ export default class Files { } delete this.files[uploadId] - if (!noWrite) this.write().catch((err) => { - throw err - }) + if (!noWrite) + this.write().catch((err) => { + throw err + }) } - } diff --git a/src/server/lib/mail.ts b/src/server/lib/mail.ts index afab792..a10d74a 100644 --- a/src/server/lib/mail.ts +++ b/src/server/lib/mail.ts @@ -1,14 +1,14 @@ import { createTransport } from "nodemailer" import "dotenv/config" -import config from "../../../config.json" assert {type:"json"} +import config from "../../../config.json" assert { type: "json" } import { generateFileId } from "./files.js" let mailConfig = config.mail, transport = createTransport({ ...mailConfig.transport, auth: { - user: process.env.MAIL_USER, - pass: process.env.MAIL_PASS, + user: process.env.MAIL__USER, + pass: process.env.MAIL__PASS, }, }) @@ -37,25 +37,30 @@ export function sendMail(to: string, subject: string, content: string) { } export namespace CodeMgr { + export const Intents = ["verifyEmail", "recoverAccount"] as const - export const Intents = [ - "verifyEmail", - "recoverAccount" - ] as const + export type Intent = (typeof Intents)[number] - export type Intent = typeof Intents[number] - - export function isIntent(intent: string): intent is Intent { return intent in Intents } + export function isIntent(intent: string): intent is Intent { + return intent in Intents + } export let codes = Object.fromEntries( - Intents.map(e => [ - e, - {byId: new Map(), byUser: new Map()} - ])) as Record, byUser: Map }> + Intents.map((e) => [ + e, + { + byId: new Map(), + byUser: new Map(), + }, + ]) + ) as Record< + Intent, + { byId: Map; byUser: Map } + > // this is stupid whyd i write this - export class Code { + export class Code { readonly id: string = generateFileId(12) readonly for: string @@ -65,25 +70,30 @@ export namespace CodeMgr { readonly data: any - constructor(intent: Intent, forUser: string, data?: any, time: number = 15*60*1000) { - this.for = forUser; + constructor( + intent: Intent, + forUser: string, + data?: any, + time: number = 15 * 60 * 1000 + ) { + this.for = forUser this.intent = intent this.expiryClear = setTimeout(this.terminate.bind(this), time) this.data = data - codes[intent].byId.set(this.id, this); + codes[intent].byId.set(this.id, this) let byUser = codes[intent].byUser.get(this.for) if (!byUser) { byUser = [] - codes[intent].byUser.set(this.for, byUser); + codes[intent].byUser.set(this.for, byUser) } byUser.push(this) } terminate() { - codes[this.intent].byId.delete(this.id); + codes[this.intent].byId.delete(this.id) let bu = codes[this.intent].byUser.get(this.id)! bu.splice(bu.indexOf(this), 1) clearTimeout(this.expiryClear) @@ -93,5 +103,4 @@ export namespace CodeMgr { return forUser === this.for } } - -} \ No newline at end of file +} From 9fba6b15e81a18b104357b652915a8693d230b3a Mon Sep 17 00:00:00 2001 From: cirroskais Date: Sun, 28 Apr 2024 03:41:38 -0400 Subject: [PATCH 02/10] mail.ts uses unified config --- src/server/lib/mail.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/server/lib/mail.ts b/src/server/lib/mail.ts index a10d74a..ee5899f 100644 --- a/src/server/lib/mail.ts +++ b/src/server/lib/mail.ts @@ -1,16 +1,18 @@ import { createTransport } from "nodemailer" -import "dotenv/config" -import config from "../../../config.json" assert { type: "json" } +import config from "./config.js" import { generateFileId } from "./files.js" -let mailConfig = config.mail, - transport = createTransport({ - ...mailConfig.transport, - auth: { - user: process.env.MAIL__USER, - pass: process.env.MAIL__PASS, - }, - }) +const { mail } = config +const transport = createTransport({ + host: mail.transport.host, + port: mail.transport.port, + secure: mail.transport.secure, + from: mail.send.from, + auth: { + user: mail.user, + pass: mail.pass, + }, +}) /** * @description Sends an email @@ -23,7 +25,6 @@ export function sendMail(to: string, subject: string, content: string) { return transport.sendMail({ to, subject, - from: mailConfig.send.from, html: `monofile accounts
Gain control of your uploads.

${content .replaceAll( "", From c214a06c673189febedfbb8aaa5719c43d202a9f Mon Sep 17 00:00:00 2001 From: cirroskais Date: Sun, 28 Apr 2024 04:04:52 -0400 Subject: [PATCH 03/10] more unifying work --- src/server/cli.ts | 64 +++--- src/server/index.ts | 4 +- src/server/lib/DiscordAPI/index.ts | 246 +++++++++++--------- src/server/lib/config.ts | 42 +++- src/server/lib/files.ts | 18 +- src/server/routes/api/v0/authRoutes.ts | 14 +- src/server/routes/api/v1/account.ts | 299 +++++++++++++++---------- src/svelte/elem/stores.ts | 62 +++-- 8 files changed, 440 insertions(+), 309 deletions(-) diff --git a/src/server/cli.ts b/src/server/cli.ts index d2bcf58..f6ce73c 100644 --- a/src/server/cli.ts +++ b/src/server/cli.ts @@ -4,8 +4,8 @@ import Files from "./lib/files.js" import { program } from "commander" import { basename } from "path" import { Writable } from "node:stream" +import config from "./lib/config.js" import pkg from "../../package.json" assert { type: "json" } -import config from "../../config.json" assert { type: "json" } import { fileURLToPath } from "url" import { dirname } from "path" @@ -23,65 +23,61 @@ program .description("Quickly run monofile to execute a query or so") .version(pkg.version) -program.command("list") +program + .command("list") .alias("ls") .description("List files in the database") .action(() => { - Object.keys(files.files).forEach(e => console.log(e)) + Object.keys(files.files).forEach((e) => console.log(e)) }) - -program.command("download") +program + .command("download") .alias("dl") .description("Download a file from the database") .argument("", "ID of the file you'd like to download") - .option("-o, --output ", 'Folder or filename to output to') + .option("-o, --output ", "Folder or filename to output to") .action(async (id, options) => { - - await (new Promise(resolve => setTimeout(() => resolve(), 1000))) + await new Promise((resolve) => setTimeout(() => resolve(), 1000)) let fp = files.files[id] - if (!fp) - throw `file ${id} not found` - - let out = options.output as string || `./` + if (!fp) throw `file ${id} not found` + + let out = (options.output as string) || `./` if (fs.existsSync(out) && (await stat(out)).isDirectory()) out = `${out.replace(/\/+$/, "")}/${fp.filename}` let filestream = await files.readFileStream(id) - let prog=0 - filestream.on("data", dt => { - prog+=dt.byteLength - console.log(`Downloading ${fp.filename}: ${Math.floor(prog/(fp.sizeInBytes??0)*10000)/100}% (${Math.floor(prog/(1024*1024))}MiB/${Math.floor((fp.sizeInBytes??0)/(1024*1024))}MiB)`) + let prog = 0 + filestream.on("data", (dt) => { + prog += dt.byteLength + console.log( + `Downloading ${fp.filename}: ${Math.floor((prog / (fp.sizeInBytes ?? 0)) * 10000) / 100}% (${Math.floor(prog / (1024 * 1024))}MiB/${Math.floor((fp.sizeInBytes ?? 0) / (1024 * 1024))}MiB)` + ) }) - filestream.pipe( - fs.createWriteStream(out) - ) + filestream.pipe(fs.createWriteStream(out)) }) - -program.command("upload") +program + .command("upload") .alias("up") .description("Upload a file to the instance") .argument("", "Path to the file you'd like to upload") - .option("-id, --fileid ", 'Custom file ID to use') + .option("-id, --fileid ", "Custom file ID to use") .action(async (file, options) => { - - await (new Promise(resolve => setTimeout(() => resolve(), 1000))) + await new Promise((resolve) => setTimeout(() => resolve(), 1000)) if (!(fs.existsSync(file) && (await stat(file)).isFile())) throw `${file} is not a file` - + let writable = files.createWriteStream() - writable - .setName(basename(file)) - ?.setType("application/octet-stream") - + writable.setName(basename(file))?.setType("application/octet-stream") + if (options.id) writable.setUploadId(options.id) if (!(writable instanceof Writable)) @@ -90,7 +86,7 @@ program.command("upload") console.log(`started: ${file}`) writable.on("drain", () => { - console.log("Drained"); + console.log("Drained") }) writable.on("finish", async () => { @@ -108,11 +104,9 @@ program.command("upload") writable.on("close", () => { console.log("Closed.") - }); + }) - ;(await fs.createReadStream(file)).pipe( - writable - ) + ;(await fs.createReadStream(file)).pipe(writable) }) -program.parse() \ No newline at end of file +program.parse() diff --git a/src/server/index.ts b/src/server/index.ts index fe894bd..9cf2e34 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -10,7 +10,7 @@ import preview from "./routes/api/web/preview.js" import { fileURLToPath } from "url" import { dirname } from "path" import pkg from "../../package.json" assert { type: "json" } -import config from "../../config.json" assert { type: "json" } +import config, { ClientConfiguration } from "./lib/config.js" const app = new Hono() @@ -67,7 +67,7 @@ app.get("/server", (ctx) => maxDiscordFiles: config.maxDiscordFiles, maxDiscordFileSize: config.maxDiscordFileSize, accounts: config.accounts, - }) + } as ClientConfiguration) ) // funcs diff --git a/src/server/lib/DiscordAPI/index.ts b/src/server/lib/DiscordAPI/index.ts index 437ad8d..0d3decb 100644 --- a/src/server/lib/DiscordAPI/index.ts +++ b/src/server/lib/DiscordAPI/index.ts @@ -2,12 +2,15 @@ import { REST } from "./DiscordRequests.js" import type { APIMessage } from "discord-api-types/v10" import FormData from "form-data" import { Transform, type Readable } from "node:stream" -import { Configuration } from "../files.js" +import type { Configuration } from "../config.js" const EXPIRE_AFTER = 20 * 60 * 1000 const DISCORD_EPOCH = 1420070400000 // Converts a snowflake ID string into a JS Date object using the provided epoch (in ms), or Discord's epoch if not provided -function convertSnowflakeToDate(snowflake: string|number, epoch = DISCORD_EPOCH) { +function convertSnowflakeToDate( + snowflake: string | number, + epoch = DISCORD_EPOCH +) { // Convert snowflake to BigInt to extract timestamp bits // https://discord.com/developers/docs/reference#snowflakes const milliseconds = BigInt(snowflake) >> 22n @@ -15,133 +18,164 @@ function convertSnowflakeToDate(snowflake: string|number, epoch = DISCORD_EPOCH) } interface MessageCacheObject { - expire: number, - object: string + expire: number + object: string } export class Client { - private readonly token : string - private readonly rest : REST - private readonly targetChannel : string - private readonly config : Configuration - private messageCache : Map = new Map() + private readonly token: string + private readonly rest: REST + private readonly targetChannel: string + private readonly config: Configuration + private messageCache: Map = new Map() - constructor(token: string, config: Configuration) { - this.token = token - this.rest = new REST(token) - this.targetChannel = config.targetChannel - this.config = config - } + constructor(token: string, config: Configuration) { + this.token = token + this.rest = new REST(token) + this.targetChannel = config.targetChannel + this.config = config + } - async fetchMessage(id: string, cache: boolean = true) { - if (cache && this.messageCache.has(id)) { - let cachedMessage = this.messageCache.get(id)! - if (cachedMessage.expire >= Date.now()) { - return JSON.parse(cachedMessage.object) as APIMessage - } - } + async fetchMessage(id: string, cache: boolean = true) { + if (cache && this.messageCache.has(id)) { + let cachedMessage = this.messageCache.get(id)! + if (cachedMessage.expire >= Date.now()) { + return JSON.parse(cachedMessage.object) as APIMessage + } + } - let message = await (this.rest.fetch(`/channels/${this.targetChannel}/messages/${id}`).then(res=>res.json()) as Promise) + let message = await (this.rest + .fetch(`/channels/${this.targetChannel}/messages/${id}`) + .then((res) => res.json()) as Promise) - this.messageCache.set(id, { object: JSON.stringify(message) /* clone object so that removing ids from the array doesn't. yeah */, expire: EXPIRE_AFTER + Date.now() }) - return message - } + this.messageCache.set(id, { + object: JSON.stringify( + message + ) /* clone object so that removing ids from the array doesn't. yeah */, + expire: EXPIRE_AFTER + Date.now(), + }) + return message + } - async deleteMessage(id: string) { - await this.rest.fetch(`/channels/${this.targetChannel}/messages/${id}`, {method: "DELETE"}) - this.messageCache.delete(id) - } + async deleteMessage(id: string) { + await this.rest.fetch( + `/channels/${this.targetChannel}/messages/${id}`, + { method: "DELETE" } + ) + this.messageCache.delete(id) + } - // https://discord.com/developers/docs/resources/channel#bulk-delete-messages + // https://discord.com/developers/docs/resources/channel#bulk-delete-messages // "This endpoint will not delete messages older than 2 weeks" so we need to check each id async deleteMessages(ids: string[]) { - - // Remove bulk deletable messages + // Remove bulk deletable messages - let bulkDeletable = ids.filter(e => Date.now()-convertSnowflakeToDate(e).valueOf() < 2 * 7 * 24 * 60 * 60 * 1000) - await this.rest.fetch(`/channels/${this.targetChannel}/messages/bulk-delete`, { - method: "POST", - body: JSON.stringify({messages: bulkDeletable}) - }) + let bulkDeletable = ids.filter( + (e) => + Date.now() - convertSnowflakeToDate(e).valueOf() < + 2 * 7 * 24 * 60 * 60 * 1000 + ) + await this.rest.fetch( + `/channels/${this.targetChannel}/messages/bulk-delete`, + { + method: "POST", + body: JSON.stringify({ messages: bulkDeletable }), + } + ) bulkDeletable.forEach(Map.prototype.delete.bind(this.messageCache)) - // everything else, we can do manually... - // there's probably a better way to do this @Jack5079 - // fix for me if possible - await Promise.all(ids.map(async e => { - if (Date.now()-convertSnowflakeToDate(e).valueOf() >= 2 * 7 * 24 * 60 * 60 * 1000) { - return await this.deleteMessage(e) - } - }).filter(Boolean)) // filter based on whether or not it's undefined - + // everything else, we can do manually... + // there's probably a better way to do this @Jack5079 + // fix for me if possible + await Promise.all( + ids + .map(async (e) => { + if ( + Date.now() - convertSnowflakeToDate(e).valueOf() >= + 2 * 7 * 24 * 60 * 60 * 1000 + ) { + return await this.deleteMessage(e) + } + }) + .filter(Boolean) + ) // filter based on whether or not it's undefined } - - async send(stream: Readable) { - - let bytes_sent = 0 - let file_number = 0 - let boundary = "-".repeat(20) + Math.random().toString().slice(2) - let pushBoundary = (stream: Readable) => - stream.push(`${(file_number++) == 0 ? "" : "\r\n"}--${boundary}\r\nContent-Disposition: form-data; name="files[${file_number}]"; filename="${Math.random().toString().slice(2)}\r\nContent-Type: application/octet-stream\r\n\r\n`) - let boundPush = (stream: Readable, chunk: Buffer) => { - let position = 0 - console.log(`Chunk length ${chunk.byteLength}`) + async send(stream: Readable) { + let bytes_sent = 0 + let file_number = 0 + let boundary = "-".repeat(20) + Math.random().toString().slice(2) - while (position < chunk.byteLength) { - if ((bytes_sent % this.config.maxDiscordFileSize) == 0) { - console.log("Progress is 0. Pushing boundary") - pushBoundary(stream) - } + let pushBoundary = (stream: Readable) => + stream.push( + `${file_number++ == 0 ? "" : "\r\n"}--${boundary}\r\nContent-Disposition: form-data; name="files[${file_number}]"; filename="${Math.random().toString().slice(2)}\r\nContent-Type: application/octet-stream\r\n\r\n` + ) + let boundPush = (stream: Readable, chunk: Buffer) => { + let position = 0 + console.log(`Chunk length ${chunk.byteLength}`) - let capture = Math.min( - (this.config.maxDiscordFileSize - (bytes_sent % this.config.maxDiscordFileSize)), - chunk.byteLength-position - ) - console.log(`Capturing ${capture} bytes, ${chunk.subarray(position, position+capture).byteLength}`) - stream.push( chunk.subarray(position, position + capture) ) - position += capture, bytes_sent += capture + while (position < chunk.byteLength) { + if (bytes_sent % this.config.maxDiscordFileSize == 0) { + console.log("Progress is 0. Pushing boundary") + pushBoundary(stream) + } - console.log("Chunk progress:", bytes_sent % this.config.maxDiscordFileSize, "B") - } + let capture = Math.min( + this.config.maxDiscordFileSize - + (bytes_sent % this.config.maxDiscordFileSize), + chunk.byteLength - position + ) + console.log( + `Capturing ${capture} bytes, ${chunk.subarray(position, position + capture).byteLength}` + ) + stream.push(chunk.subarray(position, position + capture)) + ;(position += capture), (bytes_sent += capture) - - } + console.log( + "Chunk progress:", + bytes_sent % this.config.maxDiscordFileSize, + "B" + ) + } + } - let transformed = new Transform({ - transform(chunk, encoding, callback) { - boundPush(this, chunk) - callback() - }, - flush(callback) { - this.push(`\r\n--${boundary}--`) - callback() - } - }) + let transformed = new Transform({ + transform(chunk, encoding, callback) { + boundPush(this, chunk) + callback() + }, + flush(callback) { + this.push(`\r\n--${boundary}--`) + callback() + }, + }) - let controller = new AbortController() - stream.on("error", _ => controller.abort()) + let controller = new AbortController() + stream.on("error", (_) => controller.abort()) - //pushBoundary(transformed) - stream.pipe(transformed) + //pushBoundary(transformed) + stream.pipe(transformed) - let returned = await this.rest.fetch(`/channels/${this.targetChannel}/messages`, { - method: "POST", - body: transformed, - headers: { - "Content-Type": `multipart/form-data; boundary=${boundary}` - }, - signal: controller.signal - }) + let returned = await this.rest.fetch( + `/channels/${this.targetChannel}/messages`, + { + method: "POST", + body: transformed, + headers: { + "Content-Type": `multipart/form-data; boundary=${boundary}`, + }, + signal: controller.signal, + } + ) + if (!returned.ok) { + throw new Error( + `[Message creation] ${returned.status} ${returned.statusText}` + ) + } - if (!returned.ok) { - throw new Error(`[Message creation] ${returned.status} ${returned.statusText}`) - } - - let response = (await returned.json() as APIMessage) - console.log(JSON.stringify(response, null, 4)) - return response - - } -} \ No newline at end of file + let response = (await returned.json()) as APIMessage + console.log(JSON.stringify(response, null, 4)) + return response + } +} diff --git a/src/server/lib/config.ts b/src/server/lib/config.ts index 22f33b2..61e830b 100644 --- a/src/server/lib/config.ts +++ b/src/server/lib/config.ts @@ -1,5 +1,45 @@ import "dotenv/config" +export interface Configuration { + port: number + requestTimeout: number + trustProxy: boolean + forceSSL: boolean + discordToken: string + maxDiscordFiles: number + maxDiscordFileSize: number + maxUploadIdLength: number + targetGuild: string + targetChannel: string + accounts: { + registrationEnabled: boolean + requiredForUpload: boolean + } + mail: { + transport: { + host: string + port: number + secure: boolean + } + send: { + from: string + } + user: string + pass: string + } +} + +export interface ClientConfiguration { + version: string + files: number + maxDiscordFiles: number + maxDiscordFileSize: number + accounts: { + registrationEnabled: boolean + requiredForUpload: boolean + } +} + export default { port: Number(process.env.PORT), requestTimeout: Number(process.env.REQUEST_TIMEOUT), @@ -29,4 +69,4 @@ export default { user: process.env.MAIL__USER, pass: process.env.MAIL__PASS, }, -} +} as Configuration diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index 33f309e..0145e78 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -4,7 +4,7 @@ import crypto from "node:crypto" import { files } from "./accounts.js" import { Client as API } from "./DiscordAPI/index.js" import type { APIAttachment } from "discord-api-types/v10" -import config from "./config.js" +import config, { Configuration } from "./config.js" import "dotenv/config" import * as Accounts from "./accounts.js" @@ -47,22 +47,6 @@ function multiAssert( export type FileUploadSettings = Partial> & Pick & { uploadId?: string } -export interface Configuration { - maxDiscordFiles: number - maxDiscordFileSize: number - targetChannel: string - requestTimeout: number - maxUploadIdLength: number - - accounts: { - registrationEnabled: boolean - requiredForUpload: boolean - } - - trustProxy: boolean - forceSSL: boolean -} - export interface FilePointer { filename: string mime: string diff --git a/src/server/routes/api/v0/authRoutes.ts b/src/server/routes/api/v0/authRoutes.ts index 7b9cbff..0f9fc6a 100644 --- a/src/server/routes/api/v0/authRoutes.ts +++ b/src/server/routes/api/v0/authRoutes.ts @@ -10,7 +10,7 @@ import { requiresPermissions, } from "../../../lib/middleware.js" import { accountRatelimit } from "../../../lib/ratelimit.js" - +import config from "../../../lib/config.js" import ServeError from "../../../lib/errors.js" import Files, { FileVisibility, @@ -26,7 +26,6 @@ export let authRoutes = new Hono<{ } }>() -import config from "../../../../../config.json" assert {type:"json"} authRoutes.all("*", getAccount) export default function (files: Files) { @@ -419,10 +418,13 @@ export default function (files: Files) { pwReset.set(acc.id, { code, - expiry: setTimeout(() => { - pwReset.delete(acc?.id || "") - prcIdx.delete(pResetCode?.code || "") - }, 15 * 60 * 1000), + expiry: setTimeout( + () => { + pwReset.delete(acc?.id || "") + prcIdx.delete(pResetCode?.code || "") + }, + 15 * 60 * 1000 + ), requestedAt: Date.now(), }) diff --git a/src/server/routes/api/v1/account.ts b/src/server/routes/api/v1/account.ts index 6c031b4..ab6a41d 100644 --- a/src/server/routes/api/v1/account.ts +++ b/src/server/routes/api/v1/account.ts @@ -1,6 +1,5 @@ // Modules - import { type Context, Hono } from "hono" import { getCookie, setCookie } from "hono/cookie" @@ -20,54 +19,83 @@ import { import ServeError from "../../../lib/errors.js" import { CodeMgr, sendMail } from "../../../lib/mail.js" -import Configuration from "../../../../../config.json" assert {type:"json"} +import Configuration from "../../../lib/config.js" const router = new Hono<{ Variables: { - account: Accounts.Account, + account: Accounts.Account target: Accounts.Account } }>() -type UserUpdateParameters = Partial & { password: string, currentPassword?: string }> +type UserUpdateParameters = Partial< + Omit & { + password: string + currentPassword?: string + } +> type Message = [200 | 400 | 401 | 403 | 429 | 501, string] // there's probably a less stupid way to do this than `K in keyof Pick` // @Jack5079 make typings better if possible -type Validator, ValueNotNull extends boolean> = +type Validator< + T extends keyof Partial, + ValueNotNull extends boolean, +> = /** * @param actor The account performing this action * @param target The target account for this action * @param params Changes being patched in by the user */ - (actor: Accounts.Account, target: Accounts.Account, params: UserUpdateParameters & (ValueNotNull extends true ? { - [K in keyof Pick]-? : UserUpdateParameters[K] - } : {}), ctx: Context) => Accounts.Account[T] | Message + ( + actor: Accounts.Account, + target: Accounts.Account, + params: UserUpdateParameters & + (ValueNotNull extends true + ? { + [K in keyof Pick< + UserUpdateParameters, + T + >]-?: UserUpdateParameters[K] + } + : {}), + ctx: Context + ) => Accounts.Account[T] | Message // this type is so stupid stg -type ValidatorWithSettings> = { - acceptsNull: true, - validator: Validator -} | { - acceptsNull?: false, - validator: Validator -} +type ValidatorWithSettings> = + | { + acceptsNull: true + validator: Validator + } + | { + acceptsNull?: false + validator: Validator + } const validators: { - [T in keyof Partial]: - Validator | ValidatorWithSettings + [T in keyof Partial]: + | Validator + | ValidatorWithSettings } = { defaultFileVisibility(actor, target, params) { - if (["public", "private", "anonymous"].includes(params.defaultFileVisibility)) + if ( + ["public", "private", "anonymous"].includes( + params.defaultFileVisibility + ) + ) return params.defaultFileVisibility else return [400, "invalid file visibility"] }, email: { acceptsNull: true, validator: (actor, target, params, ctx) => { - if (!params.currentPassword // actor on purpose here to allow admins - || (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))) + if ( + !params.currentPassword || // actor on purpose here to allow admins + (params.currentPassword && + Accounts.password.check(actor.id, params.currentPassword)) + ) return [401, "current password incorrect"] if (!params.email) { @@ -81,13 +109,17 @@ const validators: { return undefined } - if (typeof params.email !== "string") return [400, "email must be string"] - if (actor.admin) - return params.email + if (typeof params.email !== "string") + return [400, "email must be string"] + if (actor.admin) return params.email // send verification email - if ((CodeMgr.codes.verifyEmail.byUser.get(target.id)?.length || 0) >= 2) return [429, "you have too many active codes"] + if ( + (CodeMgr.codes.verifyEmail.byUser.get(target.id)?.length || + 0) >= 2 + ) + return [429, "you have too many active codes"] let code = new CodeMgr.Code("verifyEmail", target.id, params.email) @@ -108,81 +140,97 @@ const validators: { ) return [200, "please check your inbox"] - } + }, }, password(actor, target, params) { if ( - !params.currentPassword // actor on purpose here to allow admins - || (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)) - ) return [401, "current password incorrect"] + !params.currentPassword || // actor on purpose here to allow admins + (params.currentPassword && + Accounts.password.check(actor.id, params.currentPassword)) + ) + return [401, "current password incorrect"] - if ( - typeof params.password != "string" - || params.password.length < 8 - ) return [400, "password must be 8 characters or longer"] + if (typeof params.password != "string" || params.password.length < 8) + return [400, "password must be 8 characters or longer"] if (target.email) { sendMail( target.email, `Your login details have been updated`, - `Hello there! Your password on your account, ${target.username}, has been updated` - + `${actor != target ? ` by ${actor.username}` : ""}. ` - + `Please update your saved login details accordingly.` + `Hello there! Your password on your account, ${target.username}, has been updated` + + `${actor != target ? ` by ${actor.username}` : ""}. ` + + `Please update your saved login details accordingly.` ).catch() } return Accounts.password.hash(params.password) - }, username(actor, target, params) { - if (!params.currentPassword // actor on purpose here to allow admins - || (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))) + if ( + !params.currentPassword || // actor on purpose here to allow admins + (params.currentPassword && + Accounts.password.check(actor.id, params.currentPassword)) + ) return [401, "current password incorrect"] if ( - typeof params.username != "string" - || params.username.length < 3 - || params.username.length > 20 - ) return [400, "username must be between 3 and 20 characters in length"] + typeof params.username != "string" || + params.username.length < 3 || + params.username.length > 20 + ) + return [ + 400, + "username must be between 3 and 20 characters in length", + ] if (Accounts.getFromUsername(params.username)) return [400, "account with this username already exists"] - if ((params.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != params.username) + if ( + (params.username.match(/[A-Za-z0-9_\-\.]+/) || [])[0] != + params.username + ) return [400, "username has invalid characters"] if (target.email) { sendMail( target.email, `Your login details have been updated`, - `Hello there! Your username on your account, ${target.username}, has been updated` - + `${actor != target ? ` by ${actor.username}` : ""} to ${params.username}. ` - + `Please update your saved login details accordingly.` + `Hello there! Your username on your account, ${target.username}, has been updated` + + `${actor != target ? ` by ${actor.username}` : ""} to ${params.username}. ` + + `Please update your saved login details accordingly.` ).catch() } return params.username - }, customCSS: { acceptsNull: true, validator: (actor, target, params) => { if ( !params.customCSS || - (params.customCSS.match(id_check_regex)?.[0] == params.customCSS && + (params.customCSS.match(id_check_regex)?.[0] == + params.customCSS && params.customCSS.length <= Configuration.maxUploadIdLength) - ) return params.customCSS + ) + return params.customCSS else return [400, "bad file id"] - } + }, }, embed(actor, target, params) { - if (typeof params.embed !== "object") return [400, "must use an object for embed"] + if (typeof params.embed !== "object") + return [400, "must use an object for embed"] if (params.embed.color === undefined) { params.embed.color = target.embed?.color - } else if (!((params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] == - params.embed.color.toLowerCase() && - params.embed.color.length == 6) || params.embed.color == null)) return [400, "bad embed color"] - + } else if ( + !( + (params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] == + params.embed.color.toLowerCase() && + params.embed.color.length == 6) || + params.embed.color == null + ) + ) + return [400, "bad embed color"] if (params.embed.largeImage === undefined) { params.embed.largeImage = target.embed?.largeImage @@ -194,23 +242,19 @@ const validators: { if (actor.admin && !target.admin) return params.admin else if (!actor.admin) return [400, "cannot promote yourself"] else return [400, "cannot demote an admin"] - } + }, } router.use(getAccount) router.all("/:user", async (ctx, next) => { - let acc = - ctx.req.param("user") == "me" - ? ctx.get("account") - : ( - ctx.req.param("user").startsWith("@") - ? Accounts.getFromUsername(ctx.req.param("user").slice(1)) - : Accounts.getFromId(ctx.req.param("user")) - ) - if ( - acc != ctx.get("account") - && !ctx.get("account")?.admin - ) return ServeError(ctx, 403, "you cannot manage this user") + let acc = + ctx.req.param("user") == "me" + ? ctx.get("account") + : ctx.req.param("user").startsWith("@") + ? Accounts.getFromUsername(ctx.req.param("user").slice(1)) + : Accounts.getFromId(ctx.req.param("user")) + if (acc != ctx.get("account") && !ctx.get("account")?.admin) + return ServeError(ctx, 403, "you cannot manage this user") if (!acc) return ServeError(ctx, 404, "account does not exist") ctx.set("target", acc) @@ -219,14 +263,15 @@ router.all("/:user", async (ctx, next) => { }) function isMessage(object: any): object is Message { - return Array.isArray(object) - && object.length == 2 - && typeof object[0] == "number" - && typeof object[1] == "string" + return ( + Array.isArray(object) && + object.length == 2 && + typeof object[0] == "number" && + typeof object[1] == "string" + ) } export default function (files: Files) { - router.post("/", async (ctx) => { const body = await ctx.req.json() if (!Configuration.accounts.registrationEnabled) { @@ -282,39 +327,60 @@ export default function (files: Files) { requiresAccount, requiresPermissions("manage"), async (ctx) => { - const body = await ctx.req.json() as UserUpdateParameters + const body = (await ctx.req.json()) as UserUpdateParameters const actor = ctx.get("account")! const target = ctx.get("target")! - if (Array.isArray(body)) - return ServeError(ctx, 400, "invalid body") + if (Array.isArray(body)) return ServeError(ctx, 400, "invalid body") - let results: ([keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]|Message)[] = - (Object.entries(body) - .filter(e => e[0] !== "currentPassword") as [keyof Accounts.Account, UserUpdateParameters[keyof Accounts.Account]][]) - .map(([x, v]) => { - if (!validators[x]) - return [400, `the ${x} parameter cannot be set or is not a valid parameter`] as Message + let results: ( + | [ + keyof Accounts.Account, + Accounts.Account[keyof Accounts.Account], + ] + | Message + )[] = ( + Object.entries(body).filter( + (e) => e[0] !== "currentPassword" + ) as [ + keyof Accounts.Account, + UserUpdateParameters[keyof Accounts.Account], + ][] + ).map(([x, v]) => { + if (!validators[x]) + return [ + 400, + `the ${x} parameter cannot be set or is not a valid parameter`, + ] as Message - let validator = - (typeof validators[x] == "object" - ? validators[x] - : { - validator: validators[x] as Validator, - acceptsNull: false - }) as ValidatorWithSettings + let validator = ( + typeof validators[x] == "object" + ? validators[x] + : { + validator: validators[x] as Validator< + typeof x, + false + >, + acceptsNull: false, + } + ) as ValidatorWithSettings - if (!validator.acceptsNull && !v) - return [400, `the ${x} validator does not accept null values`] as Message + if (!validator.acceptsNull && !v) + return [ + 400, + `the ${x} validator does not accept null values`, + ] as Message - return [ - x, - validator.validator(actor, target, body as any, ctx) - ] as [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]] - }) + return [ + x, + validator.validator(actor, target, body as any, ctx), + ] as [ + keyof Accounts.Account, + Accounts.Account[keyof Accounts.Account], + ] + }) let allMsgs = results.map((v) => { - if (isMessage(v)) - return v + if (isMessage(v)) return v target[v[0]] = v[1] as never // lol return [200, "OK"] as Message }) @@ -322,7 +388,9 @@ export default function (files: Files) { await Accounts.save() if (allMsgs.length == 1) - return ctx.text(...allMsgs[0]!.reverse() as [Message[1], Message[0]]) // im sorry + return ctx.text( + ...(allMsgs[0]!.reverse() as [Message[1], Message[0]]) + ) // im sorry else return ctx.json(allMsgs) } ) @@ -330,11 +398,9 @@ export default function (files: Files) { router.delete("/:user", requiresAccount, noAPIAccess, async (ctx) => { let acc = ctx.get("target") - auth.AuthTokens.filter((e) => e.account == acc?.id).forEach( - (token) => { - auth.invalidate(token.token) - } - ) + auth.AuthTokens.filter((e) => e.account == acc?.id).forEach((token) => { + auth.invalidate(token.token) + }) await Accounts.deleteAccount(acc.id) @@ -342,20 +408,18 @@ export default function (files: Files) { await sendMail( acc.email, "Notice of account deletion", - `Your account, ${ - acc.username - }, has been removed. Thank you for using monofile.` + `Your account, ${acc.username}, has been removed. Thank you for using monofile.` ).catch() return ctx.text("OK") } - + return ctx.text("account deleted") }) router.get("/:user", requiresAccount, async (ctx) => { let acc = ctx.get("target") let sessionToken = auth.tokenFor(ctx)! - + return ctx.json({ ...acc, password: undefined, @@ -364,19 +428,18 @@ export default function (files: Files) { auth.getPermissions(sessionToken)?.includes("email") ? acc.email : undefined, - activeSessions: auth.AuthTokens.filter( - (e) => - e.type != "App" && - e.account == acc.id && - (e.expire > Date.now() || !e.expire) - ).length, + activeSessions: auth.AuthTokens.filter( + (e) => + e.type != "App" && + e.account == acc.id && + (e.expire > Date.now() || !e.expire) + ).length, }) }) router.get("/css", async (ctx) => { - let acc = ctx.get('account') - if (acc?.customCSS) - return ctx.redirect(`/file/${acc.customCSS}`) + let acc = ctx.get("account") + if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`) else return ctx.text("") }) diff --git a/src/svelte/elem/stores.ts b/src/svelte/elem/stores.ts index 7eb9589..8fd4785 100644 --- a/src/svelte/elem/stores.ts +++ b/src/svelte/elem/stores.ts @@ -2,39 +2,53 @@ import { writable } from "svelte/store" //import type Pulldown from "./pulldowns/Pulldown.svelte" import type { SvelteComponent } from "svelte" import type { Account } from "../../server/lib/accounts" -import type cfg from "../../../config.json" +import type { ClientConfiguration } from "../../server/lib/config" import type { FilePointer } from "../../server/lib/files" export let refreshNeeded = writable(false) export let pulldownManager = writable() -export let account = writable() -export let serverStats = writable() -export let files = writable<(FilePointer & {id:string})[]>([]) +export let account = writable< + (Account & { sessionCount: number; sessionExpires: number }) | undefined +>() +export let serverStats = writable() +export let files = writable<(FilePointer & { id: string })[]>([]) -export let fetchAccountData = function() { - fetch("/auth/me").then(async (response) => { - if (response.status == 200) { - account.set(await response.json()) - } else { - account.set(undefined) - } - }).catch((err) => { console.error(err) }) +export let fetchAccountData = function () { + fetch("/auth/me") + .then(async (response) => { + if (response.status == 200) { + account.set(await response.json()) + } else { + account.set(undefined) + } + }) + .catch((err) => { + console.error(err) + }) } -export let fetchFilePointers = function() { - fetch("/files/list", { cache: "no-cache" }).then(async (response) => { - if (response.status == 200) { - files.set(await response.json()) - } else { - files.set([]) - } - }).catch((err) => { console.error(err) }) +export let fetchFilePointers = function () { + fetch("/files/list", { cache: "no-cache" }) + .then(async (response) => { + if (response.status == 200) { + files.set(await response.json()) + } else { + files.set([]) + } + }) + .catch((err) => { + console.error(err) + }) } export let refresh_stats = () => { - fetch("/server").then(async (data) => { - serverStats.set(await data.json()) - }).catch((err) => { console.error(err) }) + fetch("/server") + .then(async (data) => { + serverStats.set(await data.json()) + }) + .catch((err) => { + console.error(err) + }) } -fetchAccountData() \ No newline at end of file +fetchAccountData() From f7f8b390e4f74e9322b4bfca01ae222c961390fc Mon Sep 17 00:00:00 2001 From: cirroskais Date: Sun, 28 Apr 2024 04:07:10 -0400 Subject: [PATCH 04/10] typo --- src/server/lib/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/lib/config.ts b/src/server/lib/config.ts index 61e830b..e88a7d8 100644 --- a/src/server/lib/config.ts +++ b/src/server/lib/config.ts @@ -47,7 +47,7 @@ export default { forceSSL: process.env.FORCE_SSL === "true", discordToken: process.env.DISCORD__TOKEN, maxDiscordFiles: Number(process.env.MAX__DISCORD_FILES), - maxDiscordFileSize: Number(process.env.MAX__DISCORD_FILES), + maxDiscordFileSize: Number(process.env.MAX__DISCORD_FILE_SIZE), maxUploadIdLength: Number(process.env.MAX__UPLOAD_ID_LENGTH), targetGuild: process.env.TARGET__GUILD, targetChannel: process.env.TARGET__CHANNEL, From 4264ee41f4950096660aac1945ddbfe42aa05a20 Mon Sep 17 00:00:00 2001 From: cirroskais Date: Sun, 28 Apr 2024 04:29:25 -0400 Subject: [PATCH 05/10] make image actually buildable --- .dockerignore | 1 - Dockerfile | 5 +++-- tsconfig.json | 18 ++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.dockerignore b/.dockerignore index b76e09f..95e452b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,6 @@ .vscode .gitignore .prettierrc -config.json LICENSE README.md node_modules diff --git a/Dockerfile b/Dockerfile index 57009fa..f408d03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,8 +18,9 @@ RUN npm run build FROM base AS app COPY --from=install /tmp/prod/node_modules node_modules -COPY --from=build out ./ +COPY --from=build /usr/src/app/out out +COPY --from=build /usr/src/app/dist dist COPY package.json . EXPOSE 3000 -ENTRYPOINT [ "node", "./index.js" ] \ No newline at end of file +ENTRYPOINT [ "node", "./out/server/index.js" ] \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 38121b9..70c8f58 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,10 @@ { - "compilerOptions": { - "rootDir": ".", - "outDir": ".", - "resolveJsonModule": true, - "composite": true, - "skipLibCheck": true - }, - "files": [ - "package.json", "config.json" - ] + "compilerOptions": { + "rootDir": ".", + "outDir": ".", + "resolveJsonModule": true, + "composite": true, + "skipLibCheck": true + }, + "files": ["package.json"] } From e6ca2c37076eceef1e9d6dd508cbdf602f119624 Mon Sep 17 00:00:00 2001 From: cirroskais Date: Sun, 28 Apr 2024 05:05:09 -0400 Subject: [PATCH 06/10] make api routes mountable --- src/server/routes/api.ts | 31 +++++++++++------------------- src/server/routes/api/apis.ts | 9 +++++++++ src/server/routes/api/v0/api.json | 2 +- src/server/routes/api/v1/api.json | 2 +- src/server/routes/api/web/api.json | 7 ++----- 5 files changed, 24 insertions(+), 27 deletions(-) create mode 100644 src/server/routes/api/apis.ts diff --git a/src/server/routes/api.ts b/src/server/routes/api.ts index a7a44a7..eec22f5 100644 --- a/src/server/routes/api.ts +++ b/src/server/routes/api.ts @@ -1,8 +1,8 @@ import { Hono } from "hono" -import { readFile, readdir } from "fs/promises" import Files from "../lib/files.js" -import {fileURLToPath} from "url" -import {dirname} from "path" +import { fileURLToPath } from "url" +import { dirname } from "path" +import apis from "./api/apis.js" const APIDirectory = dirname(fileURLToPath(import.meta.url)) + "/api" @@ -39,10 +39,12 @@ class APIVersion { async load() { for (let _mount of this.definition.mount) { - let mount = resolveMount(_mount); + let mount = resolveMount(_mount) // no idea if there's a better way to do this but this is all i can think of - let { default: route } = await import(`${this.apiPath}/${mount.file}.js`) as { default: (files: Files, apiRoot: Hono) => Hono } - + let { default: route } = (await import( + `${this.apiPath}/${mount.file}.js` + )) as { default: (files: Files, apiRoot: Hono) => Hono } + this.root.route(mount.to, route(this.files, this.apiRoot)) } } @@ -67,23 +69,12 @@ export default class APIRouter { let def = new APIVersion(definition, this.files, this.root) await def.load() - this.root.route( - definition.baseURL, - def.root - ) + this.root.route(definition.baseURL, def.root) } async loadAPIMethods() { - let files = await readdir(APIDirectory) - for (let version of files) { - let def = JSON.parse( - ( - await readFile( - `${process.cwd()}/src/server/routes/api/${version}/api.json` - ) - ).toString() - ) as APIDefinition - await this.mount(def) + for (let api of apis) { + await this.mount(api as APIDefinition) } } } diff --git a/src/server/routes/api/apis.ts b/src/server/routes/api/apis.ts new file mode 100644 index 0000000..571c156 --- /dev/null +++ b/src/server/routes/api/apis.ts @@ -0,0 +1,9 @@ +// EXTREME BANDAID SOLUTION +// +// SHOULD BE FIXED IN SVELTEKIT REWRITE + +import web from "./web/api.json" assert { type: "json" } +import v0 from "./v0/api.json" assert { type: "json" } +import v1 from "./v1/api.json" assert { type: "json" } + +export default [web, v0, v1] diff --git a/src/server/routes/api/v0/api.json b/src/server/routes/api/v0/api.json index ad0bffb..2a35e07 100644 --- a/src/server/routes/api/v0/api.json +++ b/src/server/routes/api/v0/api.json @@ -7,4 +7,4 @@ { "file": "authRoutes", "to": "/auth" }, { "file": "fileApiRoutes", "to": "/files" } ] -} \ No newline at end of file +} diff --git a/src/server/routes/api/v1/api.json b/src/server/routes/api/v1/api.json index 7e5affe..52553b5 100644 --- a/src/server/routes/api/v1/api.json +++ b/src/server/routes/api/v1/api.json @@ -13,4 +13,4 @@ "to": "/file" } ] -} \ No newline at end of file +} diff --git a/src/server/routes/api/web/api.json b/src/server/routes/api/web/api.json index a20237d..a8e17c3 100644 --- a/src/server/routes/api/web/api.json +++ b/src/server/routes/api/web/api.json @@ -1,8 +1,5 @@ { "name": "web", "baseURL": "/", - "mount": [ - { "file": "preview", "to": "/download" }, - "go" - ] -} \ No newline at end of file + "mount": [{ "file": "preview", "to": "/download" }, "go"] +} From a25d268d191ec7a84bbea8cf8560e5254908fdb7 Mon Sep 17 00:00:00 2001 From: cirroskais Date: Sun, 28 Apr 2024 05:15:02 -0400 Subject: [PATCH 07/10] add docker-compose example --- docker-compose.dev.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docker-compose.dev.yml diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..9aa20dc --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,9 @@ +services: + monofile: + image: monofile + build: . + env_file: .env + volumes: + - ".data:/usr/src/app/.data" + ports: + - "3000:3000" From be5ec97450a7be03f76d6cd3ab4c494c74d1f434 Mon Sep 17 00:00:00 2001 From: cirroskais Date: Sun, 28 Apr 2024 05:26:43 -0400 Subject: [PATCH 08/10] copy over assets to image --- Dockerfile | 1 + docker-compose.dev.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index f408d03..fb466fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ COPY --from=install /tmp/prod/node_modules node_modules COPY --from=build /usr/src/app/out out COPY --from=build /usr/src/app/dist dist COPY package.json . +COPY assets assets EXPOSE 3000 ENTRYPOINT [ "node", "./out/server/index.js" ] \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 9aa20dc..2b533e3 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,5 +1,6 @@ services: monofile: + container_name: "monofile" image: monofile build: . env_file: .env From fadad4b852d46cb1ac4e00debb15b3cd05521694 Mon Sep 17 00:00:00 2001 From: cirro <34550332+cirroskais@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:08:57 +0000 Subject: [PATCH 09/10] Update .env.example Co-authored-by: Jack W. <29169102+Jack5079@users.noreply.github.com> --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 546c7c7..4ca9c33 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ REQUEST_TIMEOUT= TRUST_PROXY= FORCE_SSL= -DISCORD__TOKEN= +DISCORD_TOKEN= MAX__DISCORD_FILES= MAX__DISCORD_FILE_SIZE= From 144d2c60011057e4500d9c45d1d25ca647593a29 Mon Sep 17 00:00:00 2001 From: cirroskais Date: Mon, 29 Apr 2024 15:09:53 -0400 Subject: [PATCH 10/10] Fuck you! You, fuckin' dick. Always nay-saying! --- src/server/lib/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/lib/config.ts b/src/server/lib/config.ts index e88a7d8..0fda6d3 100644 --- a/src/server/lib/config.ts +++ b/src/server/lib/config.ts @@ -45,7 +45,7 @@ export default { requestTimeout: Number(process.env.REQUEST_TIMEOUT), trustProxy: process.env.TRUST_PROXY === "true", forceSSL: process.env.FORCE_SSL === "true", - discordToken: process.env.DISCORD__TOKEN, + discordToken: process.env.DISCORD_TOKEN, maxDiscordFiles: Number(process.env.MAX__DISCORD_FILES), maxDiscordFileSize: Number(process.env.MAX__DISCORD_FILE_SIZE), maxUploadIdLength: Number(process.env.MAX__UPLOAD_ID_LENGTH),