mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-24 22:56:26 -08:00
definitely not done yet
This commit is contained in:
parent
5c3a324c64
commit
60b0308e31
|
@ -7,7 +7,7 @@ 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(
|
export function convertSnowflakeToDate(
|
||||||
snowflake: string | number,
|
snowflake: string | number,
|
||||||
epoch = DISCORD_EPOCH
|
epoch = DISCORD_EPOCH
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -2,12 +2,14 @@ import crypto from "crypto"
|
||||||
import * as auth from "./auth.js";
|
import * as auth from "./auth.js";
|
||||||
import { readFile, writeFile } from "fs/promises"
|
import { readFile, writeFile } from "fs/promises"
|
||||||
import { FileVisibility } from "./files.js";
|
import { FileVisibility } from "./files.js";
|
||||||
|
import { AccountSchemas } from "./schemas/index.js";
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
// this is probably horrible
|
// this is probably horrible
|
||||||
// but i don't even care anymore
|
// but i don't even care anymore
|
||||||
|
|
||||||
export let Accounts: Account[] = []
|
export let Accounts: Account[] = []
|
||||||
|
/*
|
||||||
export interface Account {
|
export interface Account {
|
||||||
id : string
|
id : string
|
||||||
username : string
|
username : string
|
||||||
|
@ -25,7 +27,9 @@ export interface Account {
|
||||||
color? : string
|
color? : string
|
||||||
largeImage? : boolean
|
largeImage? : boolean
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
export type Account = z.infer<typeof AccountSchemas.Account>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Create a new account.
|
* @description Create a new account.
|
||||||
|
@ -45,7 +49,8 @@ export async function create(username:string,pwd:string,admin:boolean=false):Pro
|
||||||
password: password.hash(pwd),
|
password: password.hash(pwd),
|
||||||
files: [],
|
files: [],
|
||||||
admin: admin,
|
admin: admin,
|
||||||
defaultFileVisibility: "public"
|
defaultFileVisibility: "public",
|
||||||
|
settings: AccountSchemas.Settings.User.parse({})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { readFile, writeFile } from "node:fs/promises"
|
||||||
import { Readable, Writable } from "node:stream"
|
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, convertSnowflakeToDate } 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 config, { Configuration } from "./config.js"
|
||||||
import "dotenv/config"
|
import "dotenv/config"
|
||||||
|
@ -624,28 +624,37 @@ export default class Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Update a file from monofile 1.2 to allow for range requests with Content-Length to that file.
|
* @description Update a file from monofile 1.x to 2.x
|
||||||
* @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 attachments: APIAttachment[] = []
|
||||||
|
|
||||||
for (let message of target_file.messageids) {
|
for (let message of target_file.messageids) {
|
||||||
let attachments = (await this.api.fetchMessage(message)).attachments
|
let attachments = (await this.api.fetchMessage(message)).attachments
|
||||||
for (let attachment of attachments) {
|
for (let attachment of attachments) {
|
||||||
attachment_sizes.push(attachment.size)
|
attachments.push(attachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target_file.sizeInBytes)
|
if (!target_file.sizeInBytes)
|
||||||
target_file.sizeInBytes = attachment_sizes.reduce(
|
target_file.sizeInBytes = attachments.reduce(
|
||||||
(a, b) => a + b,
|
(a, b) => a + b.size,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!target_file.chunkSize) target_file.chunkSize = attachment_sizes[0]
|
if (!target_file.chunkSize) target_file.chunkSize = attachments[0].size
|
||||||
|
|
||||||
|
if (!target_file.lastModified) target_file.lastModified = convertSnowflakeToDate(target_file.messageids[target_file.messageids.length-1]).getTime()
|
||||||
|
|
||||||
|
// this feels like needlessly heavy
|
||||||
|
// we should probably just do this in an actual readFile idk
|
||||||
|
if (!target_file.md5) {
|
||||||
|
let hash = crypto.createHash("md5");
|
||||||
|
(await this.readFileStream(uploadId)).pipe(hash).once("end", () => target_file.md5 = hash.digest("hex"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {z} from "zod"
|
import {z} from "zod"
|
||||||
import { FileVisibility } from "./files.js"
|
import { FileId, FileVisibility } from "./files.js"
|
||||||
|
|
||||||
export const StringPassword = z.string().min(8,"password must be at least 8 characters")
|
export const StringPassword = z.string().min(8,"password must be at least 8 characters")
|
||||||
export const Password =
|
export const Password =
|
||||||
|
@ -8,7 +8,52 @@ export const Password =
|
||||||
salt: z.string()
|
salt: z.string()
|
||||||
})
|
})
|
||||||
export const Username =
|
export const Username =
|
||||||
z.string().min(3, "username too short").max(20, "username too long").regex(/[A-Za-z0-9_\-\.]+/, "username contains invalid characters")
|
z.string()
|
||||||
|
.min(3, "username too short")
|
||||||
|
.max(20, "username too long")
|
||||||
|
.regex(/^[A-Za-z0-9_\-\.]+$/, "username contains invalid characters")
|
||||||
|
|
||||||
|
export namespace Settings {
|
||||||
|
export const Theme = z.discriminatedUnion("theme", [
|
||||||
|
z.object({
|
||||||
|
theme: z.literal("catppuccin"),
|
||||||
|
variant: z.enum(["latte","frappe","macchiato","mocha","adaptive"]),
|
||||||
|
accent: z.enum([
|
||||||
|
"rosewater",
|
||||||
|
"flamingo",
|
||||||
|
"pink",
|
||||||
|
"mauve",
|
||||||
|
"red",
|
||||||
|
"maroon",
|
||||||
|
"peach",
|
||||||
|
"yellow",
|
||||||
|
"green",
|
||||||
|
"teal",
|
||||||
|
"sky",
|
||||||
|
"sapphire",
|
||||||
|
"blue",
|
||||||
|
"lavender"
|
||||||
|
])
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
theme: z.literal("custom"),
|
||||||
|
id: FileId
|
||||||
|
})
|
||||||
|
])
|
||||||
|
export const BarSide = z.enum(["top","left","bottom","right"])
|
||||||
|
export const Interface = z.object({
|
||||||
|
theme: Theme.default({theme: "catppuccin", variant: "adaptive", accent: "sky"}),
|
||||||
|
barSide: BarSide.default("left")
|
||||||
|
})
|
||||||
|
export const Links = z.object({
|
||||||
|
color: z.string().toLowerCase().length(6).regex(/^[a-f0-9]+$/,"illegal characters").optional(),
|
||||||
|
largeImage: z.boolean().default(false)
|
||||||
|
})
|
||||||
|
export const User = z.object({
|
||||||
|
interface: Interface.default({}), links: Links.default({})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const Account =
|
export const Account =
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
@ -17,5 +62,7 @@ export const Account =
|
||||||
password: Password,
|
password: Password,
|
||||||
files: z.array(z.string()),
|
files: z.array(z.string()),
|
||||||
admin: z.boolean(),
|
admin: z.boolean(),
|
||||||
defaultFileVisibility: FileVisibility
|
defaultFileVisibility: FileVisibility,
|
||||||
|
|
||||||
|
settings: Settings.User
|
||||||
})
|
})
|
|
@ -2,7 +2,7 @@ import {z} from "zod"
|
||||||
import config from "../config.js"
|
import config from "../config.js"
|
||||||
|
|
||||||
export const FileId = z.string()
|
export const FileId = z.string()
|
||||||
.regex(/[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/,"file ID uses invalid characters")
|
.regex(/^[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+$/,"file ID uses invalid characters")
|
||||||
.max(config.maxUploadIdLength,"file ID too long")
|
.max(config.maxUploadIdLength,"file ID too long")
|
||||||
.min(1, "you... *need* a file ID")
|
.min(1, "you... *need* a file ID")
|
||||||
export const FileVisibility = z.enum(["public", "anonymous", "private"])
|
export const FileVisibility = z.enum(["public", "anonymous", "private"])
|
||||||
|
|
|
@ -81,7 +81,7 @@ const validators: {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
schema: AccountSchemas.Account.shape.email.optional(),
|
schema: AccountSchemas.Account.shape.email.nullable(),
|
||||||
validator: (actor, target, params, ctx) => {
|
validator: (actor, target, params, ctx) => {
|
||||||
if (
|
if (
|
||||||
!params.currentPassword || // actor on purpose here to allow admins
|
!params.currentPassword || // actor on purpose here to allow admins
|
||||||
|
@ -191,6 +191,23 @@ const validators: {
|
||||||
else return [400, "cannot demote an admin"]
|
else return [400, "cannot demote an admin"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
schema: AccountSchemas.Settings.User.partial(),
|
||||||
|
validator: (actor, target, params) => {
|
||||||
|
let base = AccountSchemas.Settings.User.default({}).parse(target.settings)
|
||||||
|
|
||||||
|
let visit = (bse: Record<string, any>, nw: Record<string, any>) => {
|
||||||
|
for (let [key,value] of Object.entries(nw)) {
|
||||||
|
if (typeof value == "object") visit(bse[key], value)
|
||||||
|
else bse[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(base, params.settings)
|
||||||
|
|
||||||
|
return AccountSchemas.Settings.User.parse(base) // so that toLowerCase is called again... yeah that's it
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
router.use(getAccount)
|
router.use(getAccount)
|
||||||
|
@ -246,8 +263,9 @@ export default function (files: Files) {
|
||||||
login(ctx, account)
|
login(ctx, account)
|
||||||
return ctx.text("logged in")
|
return ctx.text("logged in")
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
return ServeError(ctx, 500, "internal server error")
|
console.error(e)
|
||||||
|
return ServeError(ctx, 500, e instanceof z.ZodError ? issuesToMessage(e.issues) : "internal server error")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -354,11 +372,5 @@ export default function (files: Files) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get("/:user/css", async (ctx) => {
|
|
||||||
let acc = ctx.get("account")
|
|
||||||
if (acc?.customCSS) return ctx.redirect(`/file/${acc.customCSS}`)
|
|
||||||
else return ctx.text("")
|
|
||||||
})
|
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,18 +75,18 @@ export default function (files: Files) {
|
||||||
<meta property="og:video:height" content="720">`
|
<meta property="og:video:height" content="720">`
|
||||||
: "")
|
: "")
|
||||||
: "") +
|
: "") +
|
||||||
(fileOwner?.embed?.largeImage &&
|
(fileOwner?.settings?.links?.largeImage &&
|
||||||
file.visibility != "anonymous" &&
|
file.visibility != "anonymous" &&
|
||||||
file.mime.startsWith("image/")
|
file.mime.startsWith("image/")
|
||||||
? `<meta name="twitter:card" content="summary_large_image">`
|
? `<meta name="twitter:card" content="summary_large_image">`
|
||||||
: "") +
|
: "") +
|
||||||
`\n<meta name="theme-color" content="${
|
`\n<meta name="theme-color" content="${
|
||||||
fileOwner?.embed?.color &&
|
fileOwner?.settings?.links.color &&
|
||||||
file.visibility != "anonymous" &&
|
file.visibility != "anonymous" &&
|
||||||
(ctx.req.header("user-agent") || "").includes(
|
(ctx.req.header("user-agent") || "").includes(
|
||||||
"Discordbot"
|
"Discordbot"
|
||||||
)
|
)
|
||||||
? `#${fileOwner.embed.color}`
|
? `#${fileOwner?.settings?.links.color}`
|
||||||
: "rgb(30, 33, 36)"
|
: "rgb(30, 33, 36)"
|
||||||
}">`
|
}">`
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue