mirror of
https://github.com/mollersuite/monofile.git
synced 2024-11-21 13:36:25 -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 DISCORD_EPOCH = 1420070400000
|
||||
// Converts a snowflake ID string into a JS Date object using the provided epoch (in ms), or Discord's epoch if not provided
|
||||
function convertSnowflakeToDate(
|
||||
export function convertSnowflakeToDate(
|
||||
snowflake: string | number,
|
||||
epoch = DISCORD_EPOCH
|
||||
) {
|
||||
|
|
|
@ -2,12 +2,14 @@ import crypto from "crypto"
|
|||
import * as auth from "./auth.js";
|
||||
import { readFile, writeFile } from "fs/promises"
|
||||
import { FileVisibility } from "./files.js";
|
||||
import { AccountSchemas } from "./schemas/index.js";
|
||||
import { z } from "zod"
|
||||
|
||||
// this is probably horrible
|
||||
// but i don't even care anymore
|
||||
|
||||
export let Accounts: Account[] = []
|
||||
|
||||
/*
|
||||
export interface Account {
|
||||
id : string
|
||||
username : string
|
||||
|
@ -25,7 +27,9 @@ export interface Account {
|
|||
color? : string
|
||||
largeImage? : boolean
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
export type Account = z.infer<typeof AccountSchemas.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),
|
||||
files: [],
|
||||
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 crypto from "node:crypto"
|
||||
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 config, { Configuration } from "./config.js"
|
||||
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
|
||||
*/
|
||||
|
||||
async update(uploadId: string) {
|
||||
let target_file = this.files[uploadId]
|
||||
let attachment_sizes = []
|
||||
let attachments: APIAttachment[] = []
|
||||
|
||||
for (let message of target_file.messageids) {
|
||||
let attachments = (await this.api.fetchMessage(message)).attachments
|
||||
for (let attachment of attachments) {
|
||||
attachment_sizes.push(attachment.size)
|
||||
attachments.push(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
if (!target_file.sizeInBytes)
|
||||
target_file.sizeInBytes = attachment_sizes.reduce(
|
||||
(a, b) => a + b,
|
||||
target_file.sizeInBytes = attachments.reduce(
|
||||
(a, b) => a + b.size,
|
||||
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 { 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 Password =
|
||||
|
@ -8,7 +8,52 @@ export const Password =
|
|||
salt: z.string()
|
||||
})
|
||||
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 =
|
||||
z.object({
|
||||
id: z.string(),
|
||||
|
@ -17,5 +62,7 @@ export const Account =
|
|||
password: Password,
|
||||
files: z.array(z.string()),
|
||||
admin: z.boolean(),
|
||||
defaultFileVisibility: FileVisibility
|
||||
defaultFileVisibility: FileVisibility,
|
||||
|
||||
settings: Settings.User
|
||||
})
|
|
@ -2,7 +2,7 @@ import {z} from "zod"
|
|||
import config from "../config.js"
|
||||
|
||||
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")
|
||||
.min(1, "you... *need* a file ID")
|
||||
export const FileVisibility = z.enum(["public", "anonymous", "private"])
|
||||
|
|
|
@ -81,7 +81,7 @@ const validators: {
|
|||
}
|
||||
},
|
||||
email: {
|
||||
schema: AccountSchemas.Account.shape.email.optional(),
|
||||
schema: AccountSchemas.Account.shape.email.nullable(),
|
||||
validator: (actor, target, params, ctx) => {
|
||||
if (
|
||||
!params.currentPassword || // actor on purpose here to allow admins
|
||||
|
@ -191,6 +191,23 @@ const validators: {
|
|||
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)
|
||||
|
@ -246,8 +263,9 @@ export default function (files: Files) {
|
|||
login(ctx, account)
|
||||
return ctx.text("logged in")
|
||||
})
|
||||
.catch(() => {
|
||||
return ServeError(ctx, 500, "internal server error")
|
||||
.catch((e) => {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -75,18 +75,18 @@ export default function (files: Files) {
|
|||
<meta property="og:video:height" content="720">`
|
||||
: "")
|
||||
: "") +
|
||||
(fileOwner?.embed?.largeImage &&
|
||||
(fileOwner?.settings?.links?.largeImage &&
|
||||
file.visibility != "anonymous" &&
|
||||
file.mime.startsWith("image/")
|
||||
? `<meta name="twitter:card" content="summary_large_image">`
|
||||
: "") +
|
||||
`\n<meta name="theme-color" content="${
|
||||
fileOwner?.embed?.color &&
|
||||
fileOwner?.settings?.links.color &&
|
||||
file.visibility != "anonymous" &&
|
||||
(ctx.req.header("user-agent") || "").includes(
|
||||
"Discordbot"
|
||||
)
|
||||
? `#${fileOwner.embed.color}`
|
||||
? `#${fileOwner?.settings?.links.color}`
|
||||
: "rgb(30, 33, 36)"
|
||||
}">`
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue