mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-24 22:56:26 -08:00
Merge pull request #62 from cirroskais/api-v1
unify configuration (use environment variables) and dockerize
This commit is contained in:
commit
2112c75a7d
11
.dockerignore
Normal file
11
.dockerignore
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.vscode
|
||||||
|
.gitignore
|
||||||
|
.prettierrc
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.data
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
tsconfig.tsbuildinfo
|
23
.env.example
Normal file
23
.env.example
Normal file
|
@ -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=
|
27
Dockerfile
Normal file
27
Dockerfile
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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 /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" ]
|
10
docker-compose.dev.yml
Normal file
10
docker-compose.dev.yml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
services:
|
||||||
|
monofile:
|
||||||
|
container_name: "monofile"
|
||||||
|
image: monofile
|
||||||
|
build: .
|
||||||
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ".data:/usr/src/app/.data"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
|
@ -4,8 +4,8 @@ import Files from "./lib/files.js"
|
||||||
import { program } from "commander"
|
import { program } from "commander"
|
||||||
import { basename } from "path"
|
import { basename } from "path"
|
||||||
import { Writable } from "node:stream"
|
import { Writable } from "node:stream"
|
||||||
|
import config from "./lib/config.js"
|
||||||
import pkg from "../../package.json" assert { type: "json" }
|
import pkg from "../../package.json" assert { type: "json" }
|
||||||
import config from "../../config.json" assert { type: "json" }
|
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
import { dirname } from "path"
|
import { dirname } from "path"
|
||||||
|
|
||||||
|
@ -23,64 +23,60 @@ program
|
||||||
.description("Quickly run monofile to execute a query or so")
|
.description("Quickly run monofile to execute a query or so")
|
||||||
.version(pkg.version)
|
.version(pkg.version)
|
||||||
|
|
||||||
program.command("list")
|
program
|
||||||
|
.command("list")
|
||||||
.alias("ls")
|
.alias("ls")
|
||||||
.description("List files in the database")
|
.description("List files in the database")
|
||||||
.action(() => {
|
.action(() => {
|
||||||
Object.keys(files.files).forEach(e => console.log(e))
|
Object.keys(files.files).forEach((e) => console.log(e))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
program
|
||||||
program.command("download")
|
.command("download")
|
||||||
.alias("dl")
|
.alias("dl")
|
||||||
.description("Download a file from the database")
|
.description("Download a file from the database")
|
||||||
.argument("<id>", "ID of the file you'd like to download")
|
.argument("<id>", "ID of the file you'd like to download")
|
||||||
.option("-o, --output <path>", 'Folder or filename to output to')
|
.option("-o, --output <path>", "Folder or filename to output to")
|
||||||
.action(async (id, options) => {
|
.action(async (id, options) => {
|
||||||
|
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000))
|
||||||
await (new Promise<void>(resolve => setTimeout(() => resolve(), 1000)))
|
|
||||||
|
|
||||||
let fp = files.files[id]
|
let fp = files.files[id]
|
||||||
|
|
||||||
if (!fp)
|
if (!fp) throw `file ${id} not found`
|
||||||
throw `file ${id} not found`
|
|
||||||
|
|
||||||
let out = options.output as string || `./`
|
let out = (options.output as string) || `./`
|
||||||
|
|
||||||
if (fs.existsSync(out) && (await stat(out)).isDirectory())
|
if (fs.existsSync(out) && (await stat(out)).isDirectory())
|
||||||
out = `${out.replace(/\/+$/, "")}/${fp.filename}`
|
out = `${out.replace(/\/+$/, "")}/${fp.filename}`
|
||||||
|
|
||||||
let filestream = await files.readFileStream(id)
|
let filestream = await files.readFileStream(id)
|
||||||
|
|
||||||
let prog=0
|
let prog = 0
|
||||||
filestream.on("data", dt => {
|
filestream.on("data", (dt) => {
|
||||||
prog+=dt.byteLength
|
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)`)
|
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")
|
.alias("up")
|
||||||
.description("Upload a file to the instance")
|
.description("Upload a file to the instance")
|
||||||
.argument("<file>", "Path to the file you'd like to upload")
|
.argument("<file>", "Path to the file you'd like to upload")
|
||||||
.option("-id, --fileid <id>", 'Custom file ID to use')
|
.option("-id, --fileid <id>", "Custom file ID to use")
|
||||||
.action(async (file, options) => {
|
.action(async (file, options) => {
|
||||||
|
await new Promise<void>((resolve) => setTimeout(() => resolve(), 1000))
|
||||||
await (new Promise<void>(resolve => setTimeout(() => resolve(), 1000)))
|
|
||||||
|
|
||||||
if (!(fs.existsSync(file) && (await stat(file)).isFile()))
|
if (!(fs.existsSync(file) && (await stat(file)).isFile()))
|
||||||
throw `${file} is not a file`
|
throw `${file} is not a file`
|
||||||
|
|
||||||
let writable = files.createWriteStream()
|
let writable = files.createWriteStream()
|
||||||
|
|
||||||
writable
|
writable.setName(basename(file))?.setType("application/octet-stream")
|
||||||
.setName(basename(file))
|
|
||||||
?.setType("application/octet-stream")
|
|
||||||
|
|
||||||
if (options.id) writable.setUploadId(options.id)
|
if (options.id) writable.setUploadId(options.id)
|
||||||
|
|
||||||
|
@ -90,7 +86,7 @@ program.command("upload")
|
||||||
console.log(`started: ${file}`)
|
console.log(`started: ${file}`)
|
||||||
|
|
||||||
writable.on("drain", () => {
|
writable.on("drain", () => {
|
||||||
console.log("Drained");
|
console.log("Drained")
|
||||||
})
|
})
|
||||||
|
|
||||||
writable.on("finish", async () => {
|
writable.on("finish", async () => {
|
||||||
|
@ -108,11 +104,9 @@ program.command("upload")
|
||||||
|
|
||||||
writable.on("close", () => {
|
writable.on("close", () => {
|
||||||
console.log("Closed.")
|
console.log("Closed.")
|
||||||
});
|
})
|
||||||
|
|
||||||
;(await fs.createReadStream(file)).pipe(
|
;(await fs.createReadStream(file)).pipe(writable)
|
||||||
writable
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
program.parse()
|
program.parse()
|
|
@ -7,10 +7,10 @@ import Files from "./lib/files.js"
|
||||||
import { getAccount } from "./lib/middleware.js"
|
import { getAccount } from "./lib/middleware.js"
|
||||||
import APIRouter from "./routes/api.js"
|
import APIRouter from "./routes/api.js"
|
||||||
import preview from "./routes/api/web/preview.js"
|
import preview from "./routes/api/web/preview.js"
|
||||||
import {fileURLToPath} from "url"
|
import { fileURLToPath } from "url"
|
||||||
import {dirname} from "path"
|
import { dirname } from "path"
|
||||||
import pkg from "../../package.json" assert {type:"json"}
|
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()
|
const app = new Hono()
|
||||||
|
|
||||||
|
@ -36,10 +36,8 @@ app.get(
|
||||||
// haha...
|
// haha...
|
||||||
|
|
||||||
app.on(["MOLLER"], "*", async (ctx) => {
|
app.on(["MOLLER"], "*", async (ctx) => {
|
||||||
|
|
||||||
ctx.header("Content-Type", "image/webp")
|
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"]}))
|
//app.use(bodyParser.text({limit:(config.maxDiscordFileSize*config.maxDiscordFiles)+1048576,type:["application/json","text/plain"]}))
|
||||||
|
@ -64,10 +62,12 @@ if (config.forceSSL) {
|
||||||
|
|
||||||
app.get("/server", (ctx) =>
|
app.get("/server", (ctx) =>
|
||||||
ctx.json({
|
ctx.json({
|
||||||
...config,
|
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
files: Object.keys(files.files).length,
|
files: Object.keys(files.files).length,
|
||||||
})
|
maxDiscordFiles: config.maxDiscordFiles,
|
||||||
|
maxDiscordFileSize: config.maxDiscordFileSize,
|
||||||
|
accounts: config.accounts,
|
||||||
|
} as ClientConfiguration)
|
||||||
)
|
)
|
||||||
|
|
||||||
// funcs
|
// funcs
|
||||||
|
@ -90,25 +90,27 @@ apiRouter.loadAPIMethods().then(() => {
|
||||||
app.get("/:fileId", async (ctx) =>
|
app.get("/:fileId", async (ctx) =>
|
||||||
app.fetch(
|
app.fetch(
|
||||||
new Request(
|
new Request(
|
||||||
(new URL(
|
new URL(
|
||||||
`/api/v1/file/${ctx.req.param("fileId")}`, ctx.req.raw.url)).href,
|
`/api/v1/file/${ctx.req.param("fileId")}`,
|
||||||
|
ctx.req.raw.url
|
||||||
|
).href,
|
||||||
ctx.req.raw
|
ctx.req.raw
|
||||||
),
|
),
|
||||||
ctx.env
|
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
|
// moved here to prevent a crash if someone manages to access monofile before api routes are mounted
|
||||||
|
|
||||||
serve(
|
serve(
|
||||||
{
|
{
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
port: Number(process.env.MONOFILE_PORT || 3000),
|
port: Number(process.env.PORT || 3000),
|
||||||
serverOptions: {
|
serverOptions: {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
requestTimeout: config.requestTimeout
|
requestTimeout: config.requestTimeout,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
(info) => {
|
(info) => {
|
||||||
console.log("Web OK!", info.port, info.address)
|
console.log("Web OK!", info.port, info.address)
|
||||||
|
|
|
@ -2,12 +2,15 @@ import { REST } from "./DiscordRequests.js"
|
||||||
import type { APIMessage } from "discord-api-types/v10"
|
import type { APIMessage } from "discord-api-types/v10"
|
||||||
import FormData from "form-data"
|
import FormData from "form-data"
|
||||||
import { Transform, type Readable } from "node:stream"
|
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 EXPIRE_AFTER = 20 * 60 * 1000
|
||||||
const DISCORD_EPOCH = 1420070400000
|
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
|
// 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
|
// Convert snowflake to BigInt to extract timestamp bits
|
||||||
// https://discord.com/developers/docs/reference#snowflakes
|
// https://discord.com/developers/docs/reference#snowflakes
|
||||||
const milliseconds = BigInt(snowflake) >> 22n
|
const milliseconds = BigInt(snowflake) >> 22n
|
||||||
|
@ -15,16 +18,16 @@ function convertSnowflakeToDate(snowflake: string|number, epoch = DISCORD_EPOCH)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageCacheObject {
|
interface MessageCacheObject {
|
||||||
expire: number,
|
expire: number
|
||||||
object: string
|
object: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
private readonly token : string
|
private readonly token: string
|
||||||
private readonly rest : REST
|
private readonly rest: REST
|
||||||
private readonly targetChannel : string
|
private readonly targetChannel: string
|
||||||
private readonly config : Configuration
|
private readonly config: Configuration
|
||||||
private messageCache : Map<string, MessageCacheObject> = new Map()
|
private messageCache: Map<string, MessageCacheObject> = new Map()
|
||||||
|
|
||||||
constructor(token: string, config: Configuration) {
|
constructor(token: string, config: Configuration) {
|
||||||
this.token = token
|
this.token = token
|
||||||
|
@ -41,71 +44,99 @@ export class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = await (this.rest.fetch(`/channels/${this.targetChannel}/messages/${id}`).then(res=>res.json()) as Promise<APIMessage>)
|
let message = await (this.rest
|
||||||
|
.fetch(`/channels/${this.targetChannel}/messages/${id}`)
|
||||||
|
.then((res) => res.json()) as Promise<APIMessage>)
|
||||||
|
|
||||||
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() })
|
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
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteMessage(id: string) {
|
async deleteMessage(id: string) {
|
||||||
await this.rest.fetch(`/channels/${this.targetChannel}/messages/${id}`, {method: "DELETE"})
|
await this.rest.fetch(
|
||||||
|
`/channels/${this.targetChannel}/messages/${id}`,
|
||||||
|
{ method: "DELETE" }
|
||||||
|
)
|
||||||
this.messageCache.delete(id)
|
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
|
// "This endpoint will not delete messages older than 2 weeks" so we need to check each id
|
||||||
async deleteMessages(ids: string[]) {
|
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)
|
let bulkDeletable = ids.filter(
|
||||||
await this.rest.fetch(`/channels/${this.targetChannel}/messages/bulk-delete`, {
|
(e) =>
|
||||||
|
Date.now() - convertSnowflakeToDate(e).valueOf() <
|
||||||
|
2 * 7 * 24 * 60 * 60 * 1000
|
||||||
|
)
|
||||||
|
await this.rest.fetch(
|
||||||
|
`/channels/${this.targetChannel}/messages/bulk-delete`,
|
||||||
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({messages: bulkDeletable})
|
body: JSON.stringify({ messages: bulkDeletable }),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
bulkDeletable.forEach(Map.prototype.delete.bind(this.messageCache))
|
bulkDeletable.forEach(Map.prototype.delete.bind(this.messageCache))
|
||||||
|
|
||||||
// everything else, we can do manually...
|
// everything else, we can do manually...
|
||||||
// there's probably a better way to do this @Jack5079
|
// there's probably a better way to do this @Jack5079
|
||||||
// fix for me if possible
|
// fix for me if possible
|
||||||
await Promise.all(ids.map(async e => {
|
await Promise.all(
|
||||||
if (Date.now()-convertSnowflakeToDate(e).valueOf() >= 2 * 7 * 24 * 60 * 60 * 1000) {
|
ids
|
||||||
|
.map(async (e) => {
|
||||||
|
if (
|
||||||
|
Date.now() - convertSnowflakeToDate(e).valueOf() >=
|
||||||
|
2 * 7 * 24 * 60 * 60 * 1000
|
||||||
|
) {
|
||||||
return await this.deleteMessage(e)
|
return await this.deleteMessage(e)
|
||||||
}
|
}
|
||||||
}).filter(Boolean)) // filter based on whether or not it's undefined
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
) // filter based on whether or not it's undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async send(stream: Readable) {
|
async send(stream: Readable) {
|
||||||
|
|
||||||
let bytes_sent = 0
|
let bytes_sent = 0
|
||||||
let file_number = 0
|
let file_number = 0
|
||||||
let boundary = "-".repeat(20) + Math.random().toString().slice(2)
|
let boundary = "-".repeat(20) + Math.random().toString().slice(2)
|
||||||
|
|
||||||
let pushBoundary = (stream: Readable) =>
|
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`)
|
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 boundPush = (stream: Readable, chunk: Buffer) => {
|
||||||
let position = 0
|
let position = 0
|
||||||
console.log(`Chunk length ${chunk.byteLength}`)
|
console.log(`Chunk length ${chunk.byteLength}`)
|
||||||
|
|
||||||
while (position < chunk.byteLength) {
|
while (position < chunk.byteLength) {
|
||||||
if ((bytes_sent % this.config.maxDiscordFileSize) == 0) {
|
if (bytes_sent % this.config.maxDiscordFileSize == 0) {
|
||||||
console.log("Progress is 0. Pushing boundary")
|
console.log("Progress is 0. Pushing boundary")
|
||||||
pushBoundary(stream)
|
pushBoundary(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
let capture = Math.min(
|
let capture = Math.min(
|
||||||
(this.config.maxDiscordFileSize - (bytes_sent % this.config.maxDiscordFileSize)),
|
this.config.maxDiscordFileSize -
|
||||||
chunk.byteLength-position
|
(bytes_sent % this.config.maxDiscordFileSize),
|
||||||
|
chunk.byteLength - position
|
||||||
)
|
)
|
||||||
console.log(`Capturing ${capture} bytes, ${chunk.subarray(position, position+capture).byteLength}`)
|
console.log(
|
||||||
stream.push( chunk.subarray(position, position + capture) )
|
`Capturing ${capture} bytes, ${chunk.subarray(position, position + capture).byteLength}`
|
||||||
position += capture, bytes_sent += capture
|
)
|
||||||
|
stream.push(chunk.subarray(position, position + capture))
|
||||||
|
;(position += capture), (bytes_sent += capture)
|
||||||
|
|
||||||
console.log("Chunk progress:", bytes_sent % this.config.maxDiscordFileSize, "B")
|
console.log(
|
||||||
|
"Chunk progress:",
|
||||||
|
bytes_sent % this.config.maxDiscordFileSize,
|
||||||
|
"B"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let transformed = new Transform({
|
let transformed = new Transform({
|
||||||
|
@ -116,32 +147,35 @@ export class Client {
|
||||||
flush(callback) {
|
flush(callback) {
|
||||||
this.push(`\r\n--${boundary}--`)
|
this.push(`\r\n--${boundary}--`)
|
||||||
callback()
|
callback()
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let controller = new AbortController()
|
let controller = new AbortController()
|
||||||
stream.on("error", _ => controller.abort())
|
stream.on("error", (_) => controller.abort())
|
||||||
|
|
||||||
//pushBoundary(transformed)
|
//pushBoundary(transformed)
|
||||||
stream.pipe(transformed)
|
stream.pipe(transformed)
|
||||||
|
|
||||||
let returned = await this.rest.fetch(`/channels/${this.targetChannel}/messages`, {
|
let returned = await this.rest.fetch(
|
||||||
|
`/channels/${this.targetChannel}/messages`,
|
||||||
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: transformed,
|
body: transformed,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": `multipart/form-data; boundary=${boundary}`
|
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
||||||
},
|
},
|
||||||
signal: controller.signal
|
signal: controller.signal,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (!returned.ok) {
|
if (!returned.ok) {
|
||||||
throw new Error(`[Message creation] ${returned.status} ${returned.statusText}`)
|
throw new Error(
|
||||||
|
`[Message creation] ${returned.status} ${returned.statusText}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = (await returned.json() as APIMessage)
|
let response = (await returned.json()) as APIMessage
|
||||||
console.log(JSON.stringify(response, null, 4))
|
console.log(JSON.stringify(response, null, 4))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
72
src/server/lib/config.ts
Normal file
72
src/server/lib/config.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
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),
|
||||||
|
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_FILE_SIZE),
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
} as Configuration
|
|
@ -3,7 +3,8 @@ import { Readable, Writable } from "node:stream"
|
||||||
import crypto from "node:crypto"
|
import crypto from "node:crypto"
|
||||||
import { files } from "./accounts.js"
|
import { files } from "./accounts.js"
|
||||||
import { Client as API } from "./DiscordAPI/index.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, { Configuration } from "./config.js"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
|
|
||||||
import * as Accounts from "./accounts.js"
|
import * as Accounts from "./accounts.js"
|
||||||
|
@ -35,7 +36,9 @@ export function generateFileId(length: number = 5) {
|
||||||
* @param conditions
|
* @param conditions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function multiAssert(conditions: Map<boolean, { message: string, status: number }>) {
|
function multiAssert(
|
||||||
|
conditions: Map<boolean, { message: string; status: number }>
|
||||||
|
) {
|
||||||
for (let [cond, err] of conditions.entries()) {
|
for (let [cond, err] of conditions.entries()) {
|
||||||
if (cond) return err
|
if (cond) return err
|
||||||
}
|
}
|
||||||
|
@ -44,22 +47,6 @@ function multiAssert(conditions: Map<boolean, { message: string, status: number
|
||||||
export type FileUploadSettings = Partial<Pick<FilePointer, "mime" | "owner">> &
|
export type FileUploadSettings = Partial<Pick<FilePointer, "mime" | "owner">> &
|
||||||
Pick<FilePointer, "mime" | "filename"> & { uploadId?: string }
|
Pick<FilePointer, "mime" | "filename"> & { 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 {
|
export interface FilePointer {
|
||||||
filename: string
|
filename: string
|
||||||
mime: string
|
mime: string
|
||||||
|
@ -80,18 +67,15 @@ export interface StatusCodeError {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebError extends Error {
|
export class WebError extends Error {
|
||||||
|
|
||||||
readonly statusCode: number = 500
|
readonly statusCode: number = 500
|
||||||
|
|
||||||
constructor(status: number, message: string) {
|
constructor(status: number, message: string) {
|
||||||
super(message)
|
super(message)
|
||||||
this.statusCode = status
|
this.statusCode = status
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReadStream extends Readable {
|
export class ReadStream extends Readable {
|
||||||
|
|
||||||
files: Files
|
files: Files
|
||||||
pointer: FilePointer
|
pointer: FilePointer
|
||||||
|
|
||||||
|
@ -100,52 +84,60 @@ export class ReadStream extends Readable {
|
||||||
position: number = 0
|
position: number = 0
|
||||||
|
|
||||||
ranges: {
|
ranges: {
|
||||||
useRanges: boolean,
|
useRanges: boolean
|
||||||
byteStart: number,
|
byteStart: number
|
||||||
byteEnd: number
|
byteEnd: number
|
||||||
scan_msg_begin: number,
|
scan_msg_begin: number
|
||||||
scan_msg_end: number,
|
scan_msg_end: number
|
||||||
scan_files_begin: number,
|
scan_files_begin: number
|
||||||
scan_files_end: number
|
scan_files_end: number
|
||||||
}
|
}
|
||||||
|
|
||||||
id: number = Math.random()
|
id: number = Math.random()
|
||||||
aborter?: AbortController
|
aborter?: AbortController
|
||||||
|
|
||||||
constructor(files: Files, pointer: FilePointer, range?: {start: number, end: number}) {
|
constructor(
|
||||||
|
files: Files,
|
||||||
|
pointer: FilePointer,
|
||||||
|
range?: { start: number; end: number }
|
||||||
|
) {
|
||||||
super()
|
super()
|
||||||
console.log(this.id, range)
|
console.log(this.id, range)
|
||||||
this.files = files
|
this.files = files
|
||||||
this.pointer = pointer
|
this.pointer = pointer
|
||||||
|
|
||||||
let useRanges =
|
let useRanges = Boolean(
|
||||||
Boolean(range && pointer.chunkSize && pointer.sizeInBytes)
|
range && pointer.chunkSize && pointer.sizeInBytes
|
||||||
|
)
|
||||||
|
|
||||||
this.ranges = {
|
this.ranges = {
|
||||||
useRanges,
|
useRanges,
|
||||||
scan_msg_begin: 0,
|
scan_msg_begin: 0,
|
||||||
scan_msg_end: pointer.messageids.length - 1,
|
scan_msg_end: pointer.messageids.length - 1,
|
||||||
scan_files_begin:
|
scan_files_begin: useRanges
|
||||||
useRanges
|
|
||||||
? Math.floor(range!.start / pointer.chunkSize!)
|
? Math.floor(range!.start / pointer.chunkSize!)
|
||||||
: 0,
|
: 0,
|
||||||
scan_files_end:
|
scan_files_end: useRanges
|
||||||
useRanges
|
|
||||||
? Math.ceil(range!.end / pointer.chunkSize!) - 1
|
? Math.ceil(range!.end / pointer.chunkSize!) - 1
|
||||||
: -1,
|
: -1,
|
||||||
byteStart: range?.start || 0,
|
byteStart: range?.start || 0,
|
||||||
byteEnd: range?.end || 0
|
byteEnd: range?.end || 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useRanges)
|
if (useRanges)
|
||||||
this.ranges.scan_msg_begin = Math.floor(this.ranges.scan_files_begin / 10),
|
(this.ranges.scan_msg_begin = Math.floor(
|
||||||
this.ranges.scan_msg_end = Math.ceil(this.ranges.scan_files_end / 10),
|
this.ranges.scan_files_begin / 10
|
||||||
this.msgIdx = this.ranges.scan_msg_begin
|
)),
|
||||||
|
(this.ranges.scan_msg_end = Math.ceil(
|
||||||
|
this.ranges.scan_files_end / 10
|
||||||
|
)),
|
||||||
|
(this.msgIdx = this.ranges.scan_msg_begin)
|
||||||
|
|
||||||
console.log(this.ranges)
|
console.log(this.ranges)
|
||||||
}
|
}
|
||||||
|
|
||||||
async _read() {/*
|
async _read() {
|
||||||
|
/*
|
||||||
console.log("Calling for more data")
|
console.log("Calling for more data")
|
||||||
if (this.busy) return
|
if (this.busy) return
|
||||||
this.busy = true
|
this.busy = true
|
||||||
|
@ -160,24 +152,32 @@ export class ReadStream extends Readable {
|
||||||
this.pushData()
|
this.pushData()
|
||||||
}
|
}
|
||||||
|
|
||||||
async _destroy(error: Error | null, callback: (error?: Error | null | undefined) => void): Promise<void> {
|
async _destroy(
|
||||||
if (this.aborter)
|
error: Error | null,
|
||||||
this.aborter.abort()
|
callback: (error?: Error | null | undefined) => void
|
||||||
|
): Promise<void> {
|
||||||
|
if (this.aborter) this.aborter.abort()
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextAttachment() {
|
async getNextAttachment() {
|
||||||
// return first in our attachment buffer
|
// 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
|
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.
|
// oh, there's none left. let's fetch a new message, then.
|
||||||
if (
|
if (
|
||||||
!this.pointer.messageids[this.msgIdx]
|
!this.pointer.messageids[this.msgIdx] ||
|
||||||
|| this.msgIdx > this.ranges.scan_msg_end
|
this.msgIdx > this.ranges.scan_msg_end
|
||||||
) return null
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
let msg = await this.files.api
|
let msg = await this.files.api
|
||||||
.fetchMessage(this.pointer.messageids[this.msgIdx])
|
.fetchMessage(this.pointer.messageids[this.msgIdx])
|
||||||
|
@ -190,20 +190,25 @@ export class ReadStream extends Readable {
|
||||||
let attach = msg.attachments
|
let attach = msg.attachments
|
||||||
console.log(attach)
|
console.log(attach)
|
||||||
|
|
||||||
this.attachmentBuffer = this.ranges.useRanges ? attach.slice(
|
this.attachmentBuffer = this.ranges.useRanges
|
||||||
|
? attach.slice(
|
||||||
this.msgIdx == this.ranges.scan_msg_begin
|
this.msgIdx == this.ranges.scan_msg_begin
|
||||||
? this.ranges.scan_files_begin - this.ranges.scan_msg_begin * 10
|
? this.ranges.scan_files_begin -
|
||||||
|
this.ranges.scan_msg_begin * 10
|
||||||
: 0,
|
: 0,
|
||||||
this.msgIdx == this.ranges.scan_msg_end
|
this.msgIdx == this.ranges.scan_msg_end
|
||||||
? this.ranges.scan_files_end - this.ranges.scan_msg_end * 10 + 1
|
? this.ranges.scan_files_end -
|
||||||
|
this.ranges.scan_msg_end * 10 +
|
||||||
|
1
|
||||||
: attach.length
|
: attach.length
|
||||||
) : attach
|
)
|
||||||
|
: attach
|
||||||
|
|
||||||
console.log(this.attachmentBuffer)
|
console.log(this.attachmentBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.msgIdx++
|
this.msgIdx++
|
||||||
return this.attachmentBuffer.splice(0,1)[0]
|
return this.attachmentBuffer.splice(0, 1)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPusherForWebStream(webStream: ReadableStream) {
|
async getPusherForWebStream(webStream: ReadableStream) {
|
||||||
|
@ -214,17 +219,20 @@ export class ReadStream extends Readable {
|
||||||
let pushToStream = this.push.bind(this)
|
let pushToStream = this.push.bind(this)
|
||||||
let stream = this
|
let stream = this
|
||||||
|
|
||||||
return function() {
|
return function () {
|
||||||
if (pushing) return
|
if (pushing) return
|
||||||
pushing = true
|
pushing = true
|
||||||
|
|
||||||
return reader.read().catch(e => {
|
return reader
|
||||||
|
.read()
|
||||||
|
.catch((e) => {
|
||||||
// Probably means an AbortError; whatever it is we'll need to abort
|
// Probably means an AbortError; whatever it is we'll need to abort
|
||||||
if (webStream.locked) reader.releaseLock()
|
if (webStream.locked) reader.releaseLock()
|
||||||
webStream.cancel().catch(e => undefined)
|
webStream.cancel().catch((e) => undefined)
|
||||||
if (!stream.destroyed) stream.destroy()
|
if (!stream.destroyed) stream.destroy()
|
||||||
return e
|
return e
|
||||||
}).then(result => {
|
})
|
||||||
|
.then((result) => {
|
||||||
if (result instanceof Error || !result) return result
|
if (result instanceof Error || !result) return result
|
||||||
|
|
||||||
let pushed
|
let pushed
|
||||||
|
@ -232,29 +240,36 @@ export class ReadStream extends Readable {
|
||||||
pushing = false
|
pushing = false
|
||||||
pushed = pushToStream(result.value)
|
pushed = pushToStream(result.value)
|
||||||
}
|
}
|
||||||
return {readyForMore: pushed || false, streamDone: result.done }
|
return {
|
||||||
|
readyForMore: pushed || false,
|
||||||
|
streamDone: result.done,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextChunk() {
|
async getNextChunk() {
|
||||||
let scanning_chunk = await this.getNextAttachment()
|
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
|
if (!scanning_chunk) return null
|
||||||
|
|
||||||
let {
|
let { byteStart, byteEnd, scan_files_begin, scan_files_end } =
|
||||||
byteStart, byteEnd, scan_files_begin, scan_files_end
|
this.ranges
|
||||||
} = this.ranges
|
|
||||||
|
|
||||||
let headers: HeadersInit =
|
let headers: HeadersInit = this.ranges.useRanges
|
||||||
this.ranges.useRanges
|
|
||||||
? {
|
? {
|
||||||
Range: `bytes=${
|
Range: `bytes=${
|
||||||
this.position == 0
|
this.position == 0
|
||||||
? byteStart - scan_files_begin * this.pointer.chunkSize!
|
? byteStart -
|
||||||
|
scan_files_begin * this.pointer.chunkSize!
|
||||||
: "0"
|
: "0"
|
||||||
}-${
|
}-${
|
||||||
this.attachmentBuffer.length == 0 && this.msgIdx == scan_files_end
|
this.attachmentBuffer.length == 0 &&
|
||||||
|
this.msgIdx == scan_files_end
|
||||||
? byteEnd - scan_files_end * this.pointer.chunkSize!
|
? byteEnd - scan_files_end * this.pointer.chunkSize!
|
||||||
: ""
|
: ""
|
||||||
}`,
|
}`,
|
||||||
|
@ -263,10 +278,12 @@ export class ReadStream extends Readable {
|
||||||
|
|
||||||
this.aborter = new AbortController()
|
this.aborter = new AbortController()
|
||||||
|
|
||||||
let response = await fetch(scanning_chunk.url, {headers, signal: this.aborter.signal})
|
let response = await fetch(scanning_chunk.url, {
|
||||||
.catch((e: Error) => {
|
headers,
|
||||||
|
signal: this.aborter.signal,
|
||||||
|
}).catch((e: Error) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
return {body: e}
|
return { body: e }
|
||||||
})
|
})
|
||||||
|
|
||||||
this.position++
|
this.position++
|
||||||
|
@ -274,11 +291,12 @@ export class ReadStream extends Readable {
|
||||||
return response.body
|
return response.body
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPusher?: (() => Promise<{readyForMore: boolean, streamDone: boolean } | void> | undefined)
|
currentPusher?: () =>
|
||||||
|
| Promise<{ readyForMore: boolean; streamDone: boolean } | void>
|
||||||
|
| undefined
|
||||||
busy: boolean = false
|
busy: boolean = false
|
||||||
|
|
||||||
async pushData(): Promise<boolean | undefined> {
|
async pushData(): Promise<boolean | undefined> {
|
||||||
|
|
||||||
// uh oh, we don't have a currentPusher
|
// uh oh, we don't have a currentPusher
|
||||||
// let's make one then
|
// let's make one then
|
||||||
if (!this.currentPusher) {
|
if (!this.currentPusher) {
|
||||||
|
@ -292,7 +310,8 @@ export class ReadStream extends Readable {
|
||||||
// or the stream has ended.
|
// or the stream has ended.
|
||||||
// let's destroy the stream
|
// let's destroy the stream
|
||||||
console.log(this.id, "Ending", next)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -304,12 +323,10 @@ export class ReadStream extends Readable {
|
||||||
this.currentPusher = undefined
|
this.currentPusher = undefined
|
||||||
return this.pushData()
|
return this.pushData()
|
||||||
} else return result?.readyForMore
|
} else return result?.readyForMore
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UploadStream extends Writable {
|
export class UploadStream extends Writable {
|
||||||
|
|
||||||
uploadId?: string
|
uploadId?: string
|
||||||
name?: string
|
name?: string
|
||||||
mime?: string
|
mime?: string
|
||||||
|
@ -331,7 +348,11 @@ export class UploadStream extends Writable {
|
||||||
|
|
||||||
async _write(data: Buffer, encoding: string, callback: () => void) {
|
async _write(data: Buffer, encoding: string, callback: () => void) {
|
||||||
console.log("Write to stream attempted")
|
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"))
|
return this.destroy(new WebError(413, "maximum file size exceeded"))
|
||||||
|
|
||||||
this.hash.update(data)
|
this.hash.update(data)
|
||||||
|
@ -343,21 +364,30 @@ export class UploadStream extends Writable {
|
||||||
|
|
||||||
while (position < data.byteLength) {
|
while (position < data.byteLength) {
|
||||||
let capture = Math.min(
|
let capture = Math.min(
|
||||||
((this.files.config.maxDiscordFileSize*10) - (this.filled % (this.files.config.maxDiscordFileSize*10))),
|
this.files.config.maxDiscordFileSize * 10 -
|
||||||
data.byteLength-position
|
(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) await this.getNextStream()
|
||||||
if (!this.current) {
|
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) )
|
readyForMore = this.current.push(
|
||||||
console.log(`pushed ${data.byteLength} byte chunk`);
|
data.subarray(position, position + capture)
|
||||||
position += capture, this.filled += 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
|
// 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!.push(null)
|
||||||
this.current = undefined
|
this.current = undefined
|
||||||
}
|
}
|
||||||
|
@ -369,16 +399,19 @@ export class UploadStream extends Writable {
|
||||||
|
|
||||||
async _final(callback: (error?: Error | null | undefined) => void) {
|
async _final(callback: (error?: Error | null | undefined) => void) {
|
||||||
if (this.current) {
|
if (this.current) {
|
||||||
this.current.push(null);
|
this.current.push(null)
|
||||||
// i probably dnt need this but whateverrr :3
|
// 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()
|
callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
aborted: boolean = false
|
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
|
this.error = error || undefined
|
||||||
await this.abort()
|
await this.abort()
|
||||||
callback(error)
|
callback(error)
|
||||||
|
@ -406,8 +439,13 @@ export class UploadStream extends Writable {
|
||||||
async commit() {
|
async commit() {
|
||||||
if (this.errored) throw this.error
|
if (this.errored) throw this.error
|
||||||
if (!this.writableFinished) {
|
if (!this.writableFinished) {
|
||||||
let err = Error("attempted to commit file when the stream was still unfinished")
|
let err = Error(
|
||||||
if (!this.destroyed) {this.destroy(err)}; throw err
|
"attempted to commit file when the stream was still unfinished"
|
||||||
|
)
|
||||||
|
if (!this.destroyed) {
|
||||||
|
this.destroy(err)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform checks
|
// Perform checks
|
||||||
|
@ -430,19 +468,18 @@ export class UploadStream extends Writable {
|
||||||
messageids: this.messages,
|
messageids: this.messages,
|
||||||
owner: this.owner,
|
owner: this.owner,
|
||||||
sizeInBytes: this.filled,
|
sizeInBytes: this.filled,
|
||||||
visibility: ogf ? ogf.visibility
|
visibility: ogf
|
||||||
: (
|
? ogf.visibility
|
||||||
this.owner
|
: this.owner
|
||||||
? Accounts.getFromId(this.owner)?.defaultFileVisibility
|
? Accounts.getFromId(this.owner)?.defaultFileVisibility
|
||||||
: undefined
|
: undefined,
|
||||||
),
|
|
||||||
// so that json.stringify doesnt include tag:undefined
|
// so that json.stringify doesnt include tag:undefined
|
||||||
...((ogf||{}).tag ? {tag:ogf.tag} : {}),
|
...((ogf || {}).tag ? { tag: ogf.tag } : {}),
|
||||||
|
|
||||||
chunkSize: this.files.config.maxDiscordFileSize,
|
chunkSize: this.files.config.maxDiscordFileSize,
|
||||||
|
|
||||||
md5: this.hash.digest("hex"),
|
md5: this.hash.digest("hex"),
|
||||||
lastModified: Date.now()
|
lastModified: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.files.locks[this.uploadId!]
|
delete this.files.locks[this.uploadId!]
|
||||||
|
@ -456,36 +493,57 @@ export class UploadStream extends Writable {
|
||||||
|
|
||||||
setName(name: string) {
|
setName(name: string) {
|
||||||
if (this.name)
|
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)
|
if (name.length > 512)
|
||||||
return this.destroy( new WebError(400, "filename can be a maximum of 512 characters") )
|
return this.destroy(
|
||||||
|
new WebError(400, "filename can be a maximum of 512 characters")
|
||||||
|
)
|
||||||
|
|
||||||
this.name = name;
|
this.name = name
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
setType(type: string) {
|
setType(type: string) {
|
||||||
if (this.mime)
|
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)
|
if (type.length > 256)
|
||||||
return this.destroy( new WebError(400, "mime type can be a maximum of 256 characters") )
|
return this.destroy(
|
||||||
|
new WebError(
|
||||||
|
400,
|
||||||
|
"mime type can be a maximum of 256 characters"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
this.mime = type;
|
this.mime = type
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadId(id: string) {
|
setUploadId(id: string) {
|
||||||
if (this.uploadId)
|
if (this.uploadId)
|
||||||
return this.destroy( new WebError(400, "duplicate attempt to set upload ID") )
|
return this.destroy(
|
||||||
if (!id || id.match(id_check_regex)?.[0] != id
|
new WebError(400, "duplicate attempt to set upload ID")
|
||||||
|| id.length > this.files.config.maxUploadIdLength)
|
)
|
||||||
return this.destroy( new WebError(400, "invalid file 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)
|
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])
|
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.files.locks[id] = true
|
||||||
this.uploadId = id
|
this.uploadId = id
|
||||||
|
@ -498,10 +556,9 @@ export class UploadStream extends Writable {
|
||||||
current?: Readable
|
current?: Readable
|
||||||
messages: string[] = []
|
messages: string[] = []
|
||||||
|
|
||||||
private newmessage_debounce : boolean = true
|
private newmessage_debounce: boolean = true
|
||||||
|
|
||||||
private async startMessage(): Promise<Readable | undefined> {
|
private async startMessage(): Promise<Readable | undefined> {
|
||||||
|
|
||||||
if (!this.newmessage_debounce) return
|
if (!this.newmessage_debounce) return
|
||||||
this.newmessage_debounce = false
|
this.newmessage_debounce = false
|
||||||
|
|
||||||
|
@ -510,24 +567,28 @@ export class UploadStream extends Writable {
|
||||||
let stream = new Readable({
|
let stream = new Readable({
|
||||||
read() {
|
read() {
|
||||||
// this is stupid but it should work
|
// 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")
|
wrt.emit("exec-callback")
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
stream.pause()
|
stream.pause()
|
||||||
|
|
||||||
console.log(`Starting a message`)
|
console.log(`Starting a message`)
|
||||||
this.files.api.send(stream).then(message => {
|
this.files.api
|
||||||
|
.send(stream)
|
||||||
|
.then((message) => {
|
||||||
this.messages.push(message.id)
|
this.messages.push(message.id)
|
||||||
console.log(`Sent: ${message.id}`)
|
console.log(`Sent: ${message.id}`)
|
||||||
this.newmessage_debounce = true
|
this.newmessage_debounce = true
|
||||||
this.emit("debounceReleased")
|
this.emit("debounceReleased")
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch((e) => {
|
||||||
if (!this.errored) this.destroy(e)
|
if (!this.errored) this.destroy(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
return stream
|
return stream
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getNextStream() {
|
private async getNextStream() {
|
||||||
|
@ -536,12 +597,14 @@ export class UploadStream extends Writable {
|
||||||
if (this.current) return this.current
|
if (this.current) return this.current
|
||||||
else if (this.newmessage_debounce) {
|
else if (this.newmessage_debounce) {
|
||||||
// startmessage.... idk
|
// startmessage.... idk
|
||||||
this.current = await this.startMessage();
|
this.current = await this.startMessage()
|
||||||
return this.current
|
return this.current
|
||||||
} else {
|
} else {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.log("Waiting for debounce to be released...")
|
console.log("Waiting for debounce to be released...")
|
||||||
this.once("debounceReleased", async () => resolve(await this.getNextStream()))
|
this.once("debounceReleased", async () =>
|
||||||
|
resolve(await this.getNextStream())
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -557,9 +620,9 @@ export default class Files {
|
||||||
|
|
||||||
constructor(config: Configuration) {
|
constructor(config: Configuration) {
|
||||||
this.config = config
|
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) => {
|
.then((buf) => {
|
||||||
this.files = JSON.parse(buf.toString() || "{}")
|
this.files = JSON.parse(buf.toString() || "{}")
|
||||||
})
|
})
|
||||||
|
@ -592,7 +655,7 @@ export default class Files {
|
||||||
* @param uploadId Target file's ID
|
* @param uploadId Target file's ID
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async update( uploadId: string ) {
|
async update(uploadId: string) {
|
||||||
let target_file = this.files[uploadId]
|
let target_file = this.files[uploadId]
|
||||||
let attachment_sizes = []
|
let attachment_sizes = []
|
||||||
|
|
||||||
|
@ -604,12 +667,12 @@ export default class Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target_file.sizeInBytes)
|
if (!target_file.sizeInBytes)
|
||||||
target_file.sizeInBytes = attachment_sizes.reduce((a, b) => a + b, 0)
|
target_file.sizeInBytes = attachment_sizes.reduce(
|
||||||
|
(a, b) => a + b,
|
||||||
if (!target_file.chunkSize)
|
0
|
||||||
target_file.chunkSize = attachment_sizes[0]
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if (!target_file.chunkSize) target_file.chunkSize = attachment_sizes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -624,7 +687,8 @@ export default class Files {
|
||||||
): Promise<ReadStream> {
|
): Promise<ReadStream> {
|
||||||
if (this.files[uploadId]) {
|
if (this.files[uploadId]) {
|
||||||
let file = 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)
|
return new ReadStream(this, file, range)
|
||||||
} else {
|
} else {
|
||||||
throw { status: 404, message: "not found" }
|
throw { status: 404, message: "not found" }
|
||||||
|
@ -648,9 +712,9 @@ export default class Files {
|
||||||
}
|
}
|
||||||
delete this.files[uploadId]
|
delete this.files[uploadId]
|
||||||
|
|
||||||
if (!noWrite) this.write().catch((err) => {
|
if (!noWrite)
|
||||||
|
this.write().catch((err) => {
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { createTransport } from "nodemailer"
|
import { createTransport } from "nodemailer"
|
||||||
import "dotenv/config"
|
import config from "./config.js"
|
||||||
import config from "../../../config.json" assert {type:"json"}
|
|
||||||
import { generateFileId } from "./files.js"
|
import { generateFileId } from "./files.js"
|
||||||
|
|
||||||
let mailConfig = config.mail,
|
const { mail } = config
|
||||||
transport = createTransport({
|
const transport = createTransport({
|
||||||
...mailConfig.transport,
|
host: mail.transport.host,
|
||||||
|
port: mail.transport.port,
|
||||||
|
secure: mail.transport.secure,
|
||||||
|
from: mail.send.from,
|
||||||
auth: {
|
auth: {
|
||||||
user: process.env.MAIL_USER,
|
user: mail.user,
|
||||||
pass: process.env.MAIL_PASS,
|
pass: mail.pass,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Sends an email
|
* @description Sends an email
|
||||||
|
@ -23,7 +25,6 @@ export function sendMail(to: string, subject: string, content: string) {
|
||||||
return transport.sendMail({
|
return transport.sendMail({
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
from: mailConfig.send.from,
|
|
||||||
html: `<span style="font-size:x-large;font-weight:600;">monofile <span style="opacity:0.5">accounts</span></span><br><span style="opacity:0.5">Gain control of your uploads.</span><hr><br>${content
|
html: `<span style="font-size:x-large;font-weight:600;">monofile <span style="opacity:0.5">accounts</span></span><br><span style="opacity:0.5">Gain control of your uploads.</span><hr><br>${content
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
"<span username>",
|
"<span username>",
|
||||||
|
@ -37,21 +38,26 @@ export function sendMail(to: string, subject: string, content: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace CodeMgr {
|
export namespace CodeMgr {
|
||||||
|
export const Intents = ["verifyEmail", "recoverAccount"] as const
|
||||||
|
|
||||||
export const Intents = [
|
export type Intent = (typeof Intents)[number]
|
||||||
"verifyEmail",
|
|
||||||
"recoverAccount"
|
|
||||||
] as const
|
|
||||||
|
|
||||||
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(
|
export let codes = Object.fromEntries(
|
||||||
Intents.map(e => [
|
Intents.map((e) => [
|
||||||
e,
|
e,
|
||||||
{byId: new Map<string, Code>(), byUser: new Map<string, Code[]>()}
|
{
|
||||||
])) as Record<Intent, { byId: Map<string, Code>, byUser: Map<string, Code[]> }>
|
byId: new Map<string, Code>(),
|
||||||
|
byUser: new Map<string, Code[]>(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
) as Record<
|
||||||
|
Intent,
|
||||||
|
{ byId: Map<string, Code>; byUser: Map<string, Code[]> }
|
||||||
|
>
|
||||||
|
|
||||||
// this is stupid whyd i write this
|
// this is stupid whyd i write this
|
||||||
|
|
||||||
|
@ -65,25 +71,30 @@ export namespace CodeMgr {
|
||||||
|
|
||||||
readonly data: any
|
readonly data: any
|
||||||
|
|
||||||
constructor(intent: Intent, forUser: string, data?: any, time: number = 15*60*1000) {
|
constructor(
|
||||||
this.for = forUser;
|
intent: Intent,
|
||||||
|
forUser: string,
|
||||||
|
data?: any,
|
||||||
|
time: number = 15 * 60 * 1000
|
||||||
|
) {
|
||||||
|
this.for = forUser
|
||||||
this.intent = intent
|
this.intent = intent
|
||||||
this.expiryClear = setTimeout(this.terminate.bind(this), time)
|
this.expiryClear = setTimeout(this.terminate.bind(this), time)
|
||||||
this.data = data
|
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)
|
let byUser = codes[intent].byUser.get(this.for)
|
||||||
if (!byUser) {
|
if (!byUser) {
|
||||||
byUser = []
|
byUser = []
|
||||||
codes[intent].byUser.set(this.for, byUser);
|
codes[intent].byUser.set(this.for, byUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
byUser.push(this)
|
byUser.push(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
terminate() {
|
terminate() {
|
||||||
codes[this.intent].byId.delete(this.id);
|
codes[this.intent].byId.delete(this.id)
|
||||||
let bu = codes[this.intent].byUser.get(this.id)!
|
let bu = codes[this.intent].byUser.get(this.id)!
|
||||||
bu.splice(bu.indexOf(this), 1)
|
bu.splice(bu.indexOf(this), 1)
|
||||||
clearTimeout(this.expiryClear)
|
clearTimeout(this.expiryClear)
|
||||||
|
@ -93,5 +104,4 @@ export namespace CodeMgr {
|
||||||
return forUser === this.for
|
return forUser === this.for
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { readFile, readdir } from "fs/promises"
|
|
||||||
import Files from "../lib/files.js"
|
import Files from "../lib/files.js"
|
||||||
import {fileURLToPath} from "url"
|
import { fileURLToPath } from "url"
|
||||||
import {dirname} from "path"
|
import { dirname } from "path"
|
||||||
|
import apis from "./api/apis.js"
|
||||||
|
|
||||||
const APIDirectory = dirname(fileURLToPath(import.meta.url)) + "/api"
|
const APIDirectory = dirname(fileURLToPath(import.meta.url)) + "/api"
|
||||||
|
|
||||||
|
@ -39,9 +39,11 @@ class APIVersion {
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
for (let _mount of this.definition.mount) {
|
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
|
// 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))
|
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)
|
let def = new APIVersion(definition, this.files, this.root)
|
||||||
await def.load()
|
await def.load()
|
||||||
|
|
||||||
this.root.route(
|
this.root.route(definition.baseURL, def.root)
|
||||||
definition.baseURL,
|
|
||||||
def.root
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAPIMethods() {
|
async loadAPIMethods() {
|
||||||
let files = await readdir(APIDirectory)
|
for (let api of apis) {
|
||||||
for (let version of files) {
|
await this.mount(api as APIDefinition)
|
||||||
let def = JSON.parse(
|
|
||||||
(
|
|
||||||
await readFile(
|
|
||||||
`${process.cwd()}/src/server/routes/api/${version}/api.json`
|
|
||||||
)
|
|
||||||
).toString()
|
|
||||||
) as APIDefinition
|
|
||||||
await this.mount(def)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
9
src/server/routes/api/apis.ts
Normal file
9
src/server/routes/api/apis.ts
Normal file
|
@ -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]
|
|
@ -10,7 +10,7 @@ import {
|
||||||
requiresPermissions,
|
requiresPermissions,
|
||||||
} from "../../../lib/middleware.js"
|
} from "../../../lib/middleware.js"
|
||||||
import { accountRatelimit } from "../../../lib/ratelimit.js"
|
import { accountRatelimit } from "../../../lib/ratelimit.js"
|
||||||
|
import config from "../../../lib/config.js"
|
||||||
import ServeError from "../../../lib/errors.js"
|
import ServeError from "../../../lib/errors.js"
|
||||||
import Files, {
|
import Files, {
|
||||||
FileVisibility,
|
FileVisibility,
|
||||||
|
@ -26,7 +26,6 @@ export let authRoutes = new Hono<{
|
||||||
}
|
}
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
import config from "../../../../../config.json" assert {type:"json"}
|
|
||||||
authRoutes.all("*", getAccount)
|
authRoutes.all("*", getAccount)
|
||||||
|
|
||||||
export default function (files: Files) {
|
export default function (files: Files) {
|
||||||
|
@ -417,10 +416,13 @@ export default function (files: Files) {
|
||||||
|
|
||||||
pwReset.set(acc.id, {
|
pwReset.set(acc.id, {
|
||||||
code,
|
code,
|
||||||
expiry: setTimeout(() => {
|
expiry: setTimeout(
|
||||||
|
() => {
|
||||||
pwReset.delete(acc?.id || "")
|
pwReset.delete(acc?.id || "")
|
||||||
prcIdx.delete(pResetCode?.code || "")
|
prcIdx.delete(pResetCode?.code || "")
|
||||||
}, 15 * 60 * 1000),
|
},
|
||||||
|
15 * 60 * 1000
|
||||||
|
),
|
||||||
requestedAt: Date.now(),
|
requestedAt: Date.now(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// Modules
|
// Modules
|
||||||
|
|
||||||
|
|
||||||
import { type Context, Hono } from "hono"
|
import { type Context, Hono } from "hono"
|
||||||
import { getCookie, setCookie } from "hono/cookie"
|
import { getCookie, setCookie } from "hono/cookie"
|
||||||
|
|
||||||
|
@ -20,54 +19,83 @@ import {
|
||||||
import ServeError from "../../../lib/errors.js"
|
import ServeError from "../../../lib/errors.js"
|
||||||
import { CodeMgr, sendMail } from "../../../lib/mail.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<{
|
const router = new Hono<{
|
||||||
Variables: {
|
Variables: {
|
||||||
account: Accounts.Account,
|
account: Accounts.Account
|
||||||
target: Accounts.Account
|
target: Accounts.Account
|
||||||
}
|
}
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
type UserUpdateParameters = Partial<Omit<Accounts.Account, "password"> & { password: string, currentPassword?: string }>
|
type UserUpdateParameters = Partial<
|
||||||
|
Omit<Accounts.Account, "password"> & {
|
||||||
|
password: string
|
||||||
|
currentPassword?: string
|
||||||
|
}
|
||||||
|
>
|
||||||
type Message = [200 | 400 | 401 | 403 | 429 | 501, 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<UserUpdateParameters, T>`
|
// there's probably a less stupid way to do this than `K in keyof Pick<UserUpdateParameters, T>`
|
||||||
// @Jack5079 make typings better if possible
|
// @Jack5079 make typings better if possible
|
||||||
|
|
||||||
type Validator<T extends keyof Partial<Accounts.Account>, ValueNotNull extends boolean> =
|
type Validator<
|
||||||
|
T extends keyof Partial<Accounts.Account>,
|
||||||
|
ValueNotNull extends boolean,
|
||||||
|
> =
|
||||||
/**
|
/**
|
||||||
* @param actor The account performing this action
|
* @param actor The account performing this action
|
||||||
* @param target The target account for this action
|
* @param target The target account for this action
|
||||||
* @param params Changes being patched in by the user
|
* @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, T>]-? : UserUpdateParameters[K]
|
actor: Accounts.Account,
|
||||||
} : {}), ctx: Context) => Accounts.Account[T] | Message
|
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
|
// this type is so stupid stg
|
||||||
type ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> = {
|
type ValidatorWithSettings<T extends keyof Partial<Accounts.Account>> =
|
||||||
acceptsNull: true,
|
| {
|
||||||
|
acceptsNull: true
|
||||||
validator: Validator<T, false>
|
validator: Validator<T, false>
|
||||||
} | {
|
}
|
||||||
acceptsNull?: false,
|
| {
|
||||||
|
acceptsNull?: false
|
||||||
validator: Validator<T, true>
|
validator: Validator<T, true>
|
||||||
}
|
}
|
||||||
|
|
||||||
const validators: {
|
const validators: {
|
||||||
[T in keyof Partial<Accounts.Account>]:
|
[T in keyof Partial<Accounts.Account>]:
|
||||||
Validator<T, true> | ValidatorWithSettings<T>
|
| Validator<T, true>
|
||||||
|
| ValidatorWithSettings<T>
|
||||||
} = {
|
} = {
|
||||||
defaultFileVisibility(actor, target, params) {
|
defaultFileVisibility(actor, target, params) {
|
||||||
if (["public", "private", "anonymous"].includes(params.defaultFileVisibility))
|
if (
|
||||||
|
["public", "private", "anonymous"].includes(
|
||||||
|
params.defaultFileVisibility
|
||||||
|
)
|
||||||
|
)
|
||||||
return params.defaultFileVisibility
|
return params.defaultFileVisibility
|
||||||
else return [400, "invalid file visibility"]
|
else return [400, "invalid file visibility"]
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
acceptsNull: true,
|
acceptsNull: true,
|
||||||
validator: (actor, target, params, ctx) => {
|
validator: (actor, target, params, ctx) => {
|
||||||
if (!params.currentPassword // actor on purpose here to allow admins
|
if (
|
||||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
|
!params.currentPassword || // actor on purpose here to allow admins
|
||||||
|
(params.currentPassword &&
|
||||||
|
Accounts.password.check(actor.id, params.currentPassword))
|
||||||
|
)
|
||||||
return [401, "current password incorrect"]
|
return [401, "current password incorrect"]
|
||||||
|
|
||||||
if (!params.email) {
|
if (!params.email) {
|
||||||
|
@ -81,13 +109,17 @@ const validators: {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof params.email !== "string") return [400, "email must be string"]
|
if (typeof params.email !== "string")
|
||||||
if (actor.admin)
|
return [400, "email must be string"]
|
||||||
return params.email
|
if (actor.admin) return params.email
|
||||||
|
|
||||||
// send verification 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)
|
let code = new CodeMgr.Code("verifyEmail", target.id, params.email)
|
||||||
|
|
||||||
|
@ -108,81 +140,97 @@ const validators: {
|
||||||
)
|
)
|
||||||
|
|
||||||
return [200, "please check your inbox"]
|
return [200, "please check your inbox"]
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
password(actor, target, params) {
|
password(actor, target, params) {
|
||||||
if (
|
if (
|
||||||
!params.currentPassword // actor on purpose here to allow admins
|
!params.currentPassword || // actor on purpose here to allow admins
|
||||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword))
|
(params.currentPassword &&
|
||||||
) return [401, "current password incorrect"]
|
Accounts.password.check(actor.id, params.currentPassword))
|
||||||
|
)
|
||||||
|
return [401, "current password incorrect"]
|
||||||
|
|
||||||
if (
|
if (typeof params.password != "string" || params.password.length < 8)
|
||||||
typeof params.password != "string"
|
return [400, "password must be 8 characters or longer"]
|
||||||
|| params.password.length < 8
|
|
||||||
) return [400, "password must be 8 characters or longer"]
|
|
||||||
|
|
||||||
if (target.email) {
|
if (target.email) {
|
||||||
sendMail(
|
sendMail(
|
||||||
target.email,
|
target.email,
|
||||||
`Your login details have been updated`,
|
`Your login details have been updated`,
|
||||||
`<b>Hello there!</b> Your password on your account, <span username>${target.username}</span>, has been updated`
|
`<b>Hello there!</b> Your password on your account, <span username>${target.username}</span>, has been updated` +
|
||||||
+ `${actor != target ? ` by <span username>${actor.username}</span>` : ""}. `
|
`${actor != target ? ` by <span username>${actor.username}</span>` : ""}. ` +
|
||||||
+ `Please update your saved login details accordingly.`
|
`Please update your saved login details accordingly.`
|
||||||
).catch()
|
).catch()
|
||||||
}
|
}
|
||||||
|
|
||||||
return Accounts.password.hash(params.password)
|
return Accounts.password.hash(params.password)
|
||||||
|
|
||||||
},
|
},
|
||||||
username(actor, target, params) {
|
username(actor, target, params) {
|
||||||
if (!params.currentPassword // actor on purpose here to allow admins
|
if (
|
||||||
|| (params.currentPassword && Accounts.password.check(actor.id, params.currentPassword)))
|
!params.currentPassword || // actor on purpose here to allow admins
|
||||||
|
(params.currentPassword &&
|
||||||
|
Accounts.password.check(actor.id, params.currentPassword))
|
||||||
|
)
|
||||||
return [401, "current password incorrect"]
|
return [401, "current password incorrect"]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof params.username != "string"
|
typeof params.username != "string" ||
|
||||||
|| params.username.length < 3
|
params.username.length < 3 ||
|
||||||
|| params.username.length > 20
|
params.username.length > 20
|
||||||
) return [400, "username must be between 3 and 20 characters in length"]
|
)
|
||||||
|
return [
|
||||||
|
400,
|
||||||
|
"username must be between 3 and 20 characters in length",
|
||||||
|
]
|
||||||
|
|
||||||
if (Accounts.getFromUsername(params.username))
|
if (Accounts.getFromUsername(params.username))
|
||||||
return [400, "account with this username already exists"]
|
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"]
|
return [400, "username has invalid characters"]
|
||||||
|
|
||||||
if (target.email) {
|
if (target.email) {
|
||||||
sendMail(
|
sendMail(
|
||||||
target.email,
|
target.email,
|
||||||
`Your login details have been updated`,
|
`Your login details have been updated`,
|
||||||
`<b>Hello there!</b> Your username on your account, <span username>${target.username}</span>, has been updated`
|
`<b>Hello there!</b> Your username on your account, <span username>${target.username}</span>, has been updated` +
|
||||||
+ `${actor != target ? ` by <span username>${actor.username}</span>` : ""} to <span username>${params.username}</span>. `
|
`${actor != target ? ` by <span username>${actor.username}</span>` : ""} to <span username>${params.username}</span>. ` +
|
||||||
+ `Please update your saved login details accordingly.`
|
`Please update your saved login details accordingly.`
|
||||||
).catch()
|
).catch()
|
||||||
}
|
}
|
||||||
|
|
||||||
return params.username
|
return params.username
|
||||||
|
|
||||||
},
|
},
|
||||||
customCSS: {
|
customCSS: {
|
||||||
acceptsNull: true,
|
acceptsNull: true,
|
||||||
validator: (actor, target, params) => {
|
validator: (actor, target, params) => {
|
||||||
if (
|
if (
|
||||||
!params.customCSS ||
|
!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)
|
params.customCSS.length <= Configuration.maxUploadIdLength)
|
||||||
) return params.customCSS
|
)
|
||||||
|
return params.customCSS
|
||||||
else return [400, "bad file id"]
|
else return [400, "bad file id"]
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
embed(actor, target, params) {
|
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) {
|
if (params.embed.color === undefined) {
|
||||||
params.embed.color = target.embed?.color
|
params.embed.color = target.embed?.color
|
||||||
} else if (!((params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] ==
|
} else if (
|
||||||
|
!(
|
||||||
|
(params.embed.color.toLowerCase().match(/[a-f0-9]+/)?.[0] ==
|
||||||
params.embed.color.toLowerCase() &&
|
params.embed.color.toLowerCase() &&
|
||||||
params.embed.color.length == 6) || params.embed.color == null)) return [400, "bad embed color"]
|
params.embed.color.length == 6) ||
|
||||||
|
params.embed.color == null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return [400, "bad embed color"]
|
||||||
|
|
||||||
if (params.embed.largeImage === undefined) {
|
if (params.embed.largeImage === undefined) {
|
||||||
params.embed.largeImage = target.embed?.largeImage
|
params.embed.largeImage = target.embed?.largeImage
|
||||||
|
@ -194,7 +242,7 @@ const validators: {
|
||||||
if (actor.admin && !target.admin) return params.admin
|
if (actor.admin && !target.admin) return params.admin
|
||||||
else if (!actor.admin) return [400, "cannot promote yourself"]
|
else if (!actor.admin) return [400, "cannot promote yourself"]
|
||||||
else return [400, "cannot demote an admin"]
|
else return [400, "cannot demote an admin"]
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
router.use(getAccount)
|
router.use(getAccount)
|
||||||
|
@ -202,15 +250,11 @@ router.all("/:user", async (ctx, next) => {
|
||||||
let acc =
|
let acc =
|
||||||
ctx.req.param("user") == "me"
|
ctx.req.param("user") == "me"
|
||||||
? ctx.get("account")
|
? ctx.get("account")
|
||||||
: (
|
: ctx.req.param("user").startsWith("@")
|
||||||
ctx.req.param("user").startsWith("@")
|
|
||||||
? Accounts.getFromUsername(ctx.req.param("user").slice(1))
|
? Accounts.getFromUsername(ctx.req.param("user").slice(1))
|
||||||
: Accounts.getFromId(ctx.req.param("user"))
|
: Accounts.getFromId(ctx.req.param("user"))
|
||||||
)
|
if (acc != ctx.get("account") && !ctx.get("account")?.admin)
|
||||||
if (
|
return ServeError(ctx, 403, "you cannot manage this user")
|
||||||
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")
|
if (!acc) return ServeError(ctx, 404, "account does not exist")
|
||||||
|
|
||||||
ctx.set("target", acc)
|
ctx.set("target", acc)
|
||||||
|
@ -219,14 +263,15 @@ router.all("/:user", async (ctx, next) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function isMessage(object: any): object is Message {
|
function isMessage(object: any): object is Message {
|
||||||
return Array.isArray(object)
|
return (
|
||||||
&& object.length == 2
|
Array.isArray(object) &&
|
||||||
&& typeof object[0] == "number"
|
object.length == 2 &&
|
||||||
&& typeof object[1] == "string"
|
typeof object[0] == "number" &&
|
||||||
|
typeof object[1] == "string"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (files: Files) {
|
export default function (files: Files) {
|
||||||
|
|
||||||
router.post("/", async (ctx) => {
|
router.post("/", async (ctx) => {
|
||||||
const body = await ctx.req.json()
|
const body = await ctx.req.json()
|
||||||
if (!Configuration.accounts.registrationEnabled) {
|
if (!Configuration.accounts.registrationEnabled) {
|
||||||
|
@ -282,39 +327,60 @@ export default function (files: Files) {
|
||||||
requiresAccount,
|
requiresAccount,
|
||||||
requiresPermissions("manage"),
|
requiresPermissions("manage"),
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const body = await ctx.req.json() as UserUpdateParameters
|
const body = (await ctx.req.json()) as UserUpdateParameters
|
||||||
const actor = ctx.get("account")!
|
const actor = ctx.get("account")!
|
||||||
const target = ctx.get("target")!
|
const target = ctx.get("target")!
|
||||||
if (Array.isArray(body))
|
if (Array.isArray(body)) return ServeError(ctx, 400, "invalid body")
|
||||||
return ServeError(ctx, 400, "invalid body")
|
|
||||||
|
|
||||||
let results: ([keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]|Message)[] =
|
let results: (
|
||||||
(Object.entries(body)
|
| [
|
||||||
.filter(e => e[0] !== "currentPassword") as [keyof Accounts.Account, UserUpdateParameters[keyof Accounts.Account]][])
|
keyof Accounts.Account,
|
||||||
.map(([x, v]) => {
|
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])
|
if (!validators[x])
|
||||||
return [400, `the ${x} parameter cannot be set or is not a valid parameter`] as Message
|
return [
|
||||||
|
400,
|
||||||
|
`the ${x} parameter cannot be set or is not a valid parameter`,
|
||||||
|
] as Message
|
||||||
|
|
||||||
let validator =
|
let validator = (
|
||||||
(typeof validators[x] == "object"
|
typeof validators[x] == "object"
|
||||||
? validators[x]
|
? validators[x]
|
||||||
: {
|
: {
|
||||||
validator: validators[x] as Validator<typeof x, false>,
|
validator: validators[x] as Validator<
|
||||||
acceptsNull: false
|
typeof x,
|
||||||
}) as ValidatorWithSettings<typeof x>
|
false
|
||||||
|
>,
|
||||||
|
acceptsNull: false,
|
||||||
|
}
|
||||||
|
) as ValidatorWithSettings<typeof x>
|
||||||
|
|
||||||
if (!validator.acceptsNull && !v)
|
if (!validator.acceptsNull && !v)
|
||||||
return [400, `the ${x} validator does not accept null values`] as Message
|
return [
|
||||||
|
400,
|
||||||
|
`the ${x} validator does not accept null values`,
|
||||||
|
] as Message
|
||||||
|
|
||||||
return [
|
return [
|
||||||
x,
|
x,
|
||||||
validator.validator(actor, target, body as any, ctx)
|
validator.validator(actor, target, body as any, ctx),
|
||||||
] as [keyof Accounts.Account, Accounts.Account[keyof Accounts.Account]]
|
] as [
|
||||||
|
keyof Accounts.Account,
|
||||||
|
Accounts.Account[keyof Accounts.Account],
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
let allMsgs = results.map((v) => {
|
let allMsgs = results.map((v) => {
|
||||||
if (isMessage(v))
|
if (isMessage(v)) return v
|
||||||
return v
|
|
||||||
target[v[0]] = v[1] as never // lol
|
target[v[0]] = v[1] as never // lol
|
||||||
return [200, "OK"] as Message
|
return [200, "OK"] as Message
|
||||||
})
|
})
|
||||||
|
@ -322,7 +388,9 @@ export default function (files: Files) {
|
||||||
await Accounts.save()
|
await Accounts.save()
|
||||||
|
|
||||||
if (allMsgs.length == 1)
|
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)
|
else return ctx.json(allMsgs)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -330,11 +398,9 @@ export default function (files: Files) {
|
||||||
router.delete("/:user", requiresAccount, noAPIAccess, async (ctx) => {
|
router.delete("/:user", requiresAccount, noAPIAccess, async (ctx) => {
|
||||||
let acc = ctx.get("target")
|
let acc = ctx.get("target")
|
||||||
|
|
||||||
auth.AuthTokens.filter((e) => e.account == acc?.id).forEach(
|
auth.AuthTokens.filter((e) => e.account == acc?.id).forEach((token) => {
|
||||||
(token) => {
|
|
||||||
auth.invalidate(token.token)
|
auth.invalidate(token.token)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
await Accounts.deleteAccount(acc.id)
|
await Accounts.deleteAccount(acc.id)
|
||||||
|
|
||||||
|
@ -342,9 +408,7 @@ export default function (files: Files) {
|
||||||
await sendMail(
|
await sendMail(
|
||||||
acc.email,
|
acc.email,
|
||||||
"Notice of account deletion",
|
"Notice of account deletion",
|
||||||
`Your account, <span username>${
|
`Your account, <span username>${acc.username}</span>, has been removed. Thank you for using monofile.`
|
||||||
acc.username
|
|
||||||
}</span>, has been removed. Thank you for using monofile.`
|
|
||||||
).catch()
|
).catch()
|
||||||
return ctx.text("OK")
|
return ctx.text("OK")
|
||||||
}
|
}
|
||||||
|
@ -374,9 +438,8 @@ export default function (files: Files) {
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get("/css", async (ctx) => {
|
router.get("/css", async (ctx) => {
|
||||||
let acc = ctx.get('account')
|
let acc = ctx.get("account")
|
||||||
if (acc?.customCSS)
|
if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`)
|
||||||
return ctx.redirect(`/file/${acc.customCSS}`)
|
|
||||||
else return ctx.text("")
|
else return ctx.text("")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"baseURL": "/",
|
"baseURL": "/",
|
||||||
"mount": [
|
"mount": [{ "file": "preview", "to": "/download" }, "go"]
|
||||||
{ "file": "preview", "to": "/download" },
|
|
||||||
"go"
|
|
||||||
]
|
|
||||||
}
|
}
|
|
@ -2,39 +2,53 @@ import { writable } from "svelte/store"
|
||||||
//import type Pulldown from "./pulldowns/Pulldown.svelte"
|
//import type Pulldown from "./pulldowns/Pulldown.svelte"
|
||||||
import type { SvelteComponent } from "svelte"
|
import type { SvelteComponent } from "svelte"
|
||||||
import type { Account } from "../../server/lib/accounts"
|
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"
|
import type { FilePointer } from "../../server/lib/files"
|
||||||
|
|
||||||
export let refreshNeeded = writable(false)
|
export let refreshNeeded = writable(false)
|
||||||
export let pulldownManager = writable<SvelteComponent>()
|
export let pulldownManager = writable<SvelteComponent>()
|
||||||
export let account = writable<Account & {sessionCount: number, sessionExpires: number}|undefined>()
|
export let account = writable<
|
||||||
export let serverStats = writable<typeof cfg & {version: string, files: number} | undefined>()
|
(Account & { sessionCount: number; sessionExpires: number }) | undefined
|
||||||
export let files = writable<(FilePointer & {id:string})[]>([])
|
>()
|
||||||
|
export let serverStats = writable<ClientConfiguration | undefined>()
|
||||||
|
export let files = writable<(FilePointer & { id: string })[]>([])
|
||||||
|
|
||||||
export let fetchAccountData = function() {
|
export let fetchAccountData = function () {
|
||||||
fetch("/auth/me").then(async (response) => {
|
fetch("/auth/me")
|
||||||
|
.then(async (response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
account.set(await response.json())
|
account.set(await response.json())
|
||||||
} else {
|
} else {
|
||||||
account.set(undefined)
|
account.set(undefined)
|
||||||
}
|
}
|
||||||
}).catch((err) => { console.error(err) })
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export let fetchFilePointers = function() {
|
export let fetchFilePointers = function () {
|
||||||
fetch("/files/list", { cache: "no-cache" }).then(async (response) => {
|
fetch("/files/list", { cache: "no-cache" })
|
||||||
|
.then(async (response) => {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
files.set(await response.json())
|
files.set(await response.json())
|
||||||
} else {
|
} else {
|
||||||
files.set([])
|
files.set([])
|
||||||
}
|
}
|
||||||
}).catch((err) => { console.error(err) })
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export let refresh_stats = () => {
|
export let refresh_stats = () => {
|
||||||
fetch("/server").then(async (data) => {
|
fetch("/server")
|
||||||
|
.then(async (data) => {
|
||||||
serverStats.set(await data.json())
|
serverStats.set(await data.json())
|
||||||
}).catch((err) => { console.error(err) })
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAccountData()
|
fetchAccountData()
|
|
@ -6,7 +6,5 @@
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"files": [
|
"files": ["package.json"]
|
||||||
"package.json", "config.json"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue