definitely not done yet

This commit is contained in:
split / May 2024-05-01 03:23:12 -07:00
parent 5c3a324c64
commit 60b0308e31
7 changed files with 100 additions and 27 deletions

View file

@ -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
) { ) {

View file

@ -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({})
} }
) )

View file

@ -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"))
}
} }
/** /**

View file

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

View file

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

View file

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

View file

@ -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)"
}">` }">`
) )