Prettier u9

This commit is contained in:
Rory&
2025-12-17 09:25:41 +01:00
parent 45ee84aca9
commit 5a3c1a3f53
9 changed files with 124 additions and 330 deletions
@@ -17,14 +17,7 @@
*/
import { route } from "@spacebar/api";
import {
ApiError,
ConnectedAccount,
ConnectionStore,
DiscordApiErrors,
FieldErrors,
RefreshableConnection,
} from "@spacebar/util";
import { ApiError, ConnectedAccount, ConnectionStore, DiscordApiErrors, FieldErrors, RefreshableConnection } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router({ mergeParams: true });
@@ -62,33 +55,17 @@ router.get("/", route({}), async (req: Request, res: Response) => {
external_id: connection_id,
user_id: req.user_id,
},
select: [
"external_id",
"type",
"name",
"verified",
"visibility",
"show_activity",
"revoked",
"token_data",
"friend_sync",
"integrations",
],
select: ["external_id", "type", "name", "verified", "visibility", "show_activity", "revoked", "token_data", "friend_sync", "integrations"],
});
if (!connectedAccount) throw DiscordApiErrors.UNKNOWN_CONNECTION;
if (connectedAccount.revoked) throw DiscordApiErrors.CONNECTION_REVOKED;
if (!connectedAccount.token_data)
throw new ApiError("No token data", 0, 400);
if (!connectedAccount.token_data) throw new ApiError("No token data", 0, 400);
let access_token = connectedAccount.token_data.access_token;
const { expires_at, expires_in, fetched_at } = connectedAccount.token_data;
if (
(expires_at && expires_at < Date.now()) ||
(expires_in && fetched_at + expires_in * 1000 < Date.now())
) {
if (!(connection instanceof RefreshableConnection))
throw new ApiError("Access token expired", 0, 400);
if ((expires_at && expires_at < Date.now()) || (expires_in && fetched_at + expires_in * 1000 < Date.now())) {
if (!(connection instanceof RefreshableConnection)) throw new ApiError("Access token expired", 0, 400);
const tokenData = await connection.refresh(connectedAccount);
access_token = tokenData.access_token;
}
@@ -19,62 +19,48 @@
import { route } from "@spacebar/api";
import { ConnectedAccount, DiscordApiErrors, emitEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";
import { ConnectionUpdateSchema } from "@spacebar/schemas"
import { ConnectionUpdateSchema } from "@spacebar/schemas";
const router = Router({ mergeParams: true });
// TODO: connection update schema
router.patch(
"/",
route({ requestBody: "ConnectionUpdateSchema" }),
async (req: Request, res: Response) => {
const { connection_name, connection_id } = req.params;
const body = req.body as ConnectionUpdateSchema;
router.patch("/", route({ requestBody: "ConnectionUpdateSchema" }), async (req: Request, res: Response) => {
const { connection_name, connection_id } = req.params;
const body = req.body as ConnectionUpdateSchema;
const connection = await ConnectedAccount.findOne({
where: {
user_id: req.user_id,
external_id: connection_id,
type: connection_name,
},
select: [
"external_id",
"type",
"name",
"verified",
"visibility",
"show_activity",
"revoked",
"friend_sync",
"integrations",
],
});
const connection = await ConnectedAccount.findOne({
where: {
user_id: req.user_id,
external_id: connection_id,
type: connection_name,
},
select: ["external_id", "type", "name", "verified", "visibility", "show_activity", "revoked", "friend_sync", "integrations"],
});
if (!connection) return DiscordApiErrors.UNKNOWN_CONNECTION;
// TODO: do we need to do anything if the connection is revoked?
if (!connection) return DiscordApiErrors.UNKNOWN_CONNECTION;
// TODO: do we need to do anything if the connection is revoked?
if (typeof body.visibility === "boolean")
//@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number?
body.visibility = body.visibility ? 1 : 0;
if (typeof body.show_activity === "boolean")
//@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number?
body.show_activity = body.show_activity ? 1 : 0;
if (typeof body.metadata_visibility === "boolean")
//@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number?
body.metadata_visibility = body.metadata_visibility ? 1 : 0;
if (typeof body.visibility === "boolean")
//@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number?
body.visibility = body.visibility ? 1 : 0;
if (typeof body.show_activity === "boolean")
//@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number?
body.show_activity = body.show_activity ? 1 : 0;
if (typeof body.metadata_visibility === "boolean")
//@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number?
body.metadata_visibility = body.metadata_visibility ? 1 : 0;
connection.assign(req.body);
connection.assign(req.body);
await ConnectedAccount.update(
{
user_id: req.user_id,
external_id: connection_id,
type: connection_name,
},
connection,
);
res.json(connection.toJSON());
},
);
await ConnectedAccount.update(
{
user_id: req.user_id,
external_id: connection_id,
type: connection_name,
},
connection,
);
res.json(connection.toJSON());
});
router.delete("/", route({}), async (req: Request, res: Response) => {
const { connection_name, connection_id } = req.params;
+11 -41
View File
@@ -16,26 +16,15 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
DiscordApiErrors,
EVENT,
FieldErrors,
PermissionResolvable,
Permissions,
RightResolvable,
Rights,
SpacebarApiErrors,
getPermission,
getRights,
} from "@spacebar/util";
import { DiscordApiErrors, EVENT, FieldErrors, PermissionResolvable, Permissions, RightResolvable, Rights, SpacebarApiErrors, getPermission, getRights } from "@spacebar/util";
import { AnyValidateFunction } from "ajv/dist/core";
import { NextFunction, Request, Response } from "express";
import { ajv } from "@spacebar/schemas"
import { ajv } from "@spacebar/schemas";
const ignoredRequestSchemas = [
// skip validation for settings proto JSON updates - TODO: figure out if this even possible to fix?
"SettingsProtoUpdateJsonSchema"
]
"SettingsProtoUpdateJsonSchema",
];
declare global {
// TODO: fix this
@@ -94,27 +83,18 @@ export function route(opts: RouteOptions) {
throw e;
}
if (!validate)
throw new Error(`Body schema ${opts.requestBody} not found`);
if (!validate) throw new Error(`Body schema ${opts.requestBody} not found`);
}
return async (req: Request, res: Response, next: NextFunction) => {
if (opts.permission) {
req.permission = await getPermission(
req.user_id,
req.params.guild_id,
req.params.channel_id,
);
req.permission = await getPermission(req.user_id, req.params.guild_id, req.params.channel_id);
const requiredPerms = Array.isArray(opts.permission)
? opts.permission
: [opts.permission];
const requiredPerms = Array.isArray(opts.permission) ? opts.permission : [opts.permission];
requiredPerms.forEach((perm) => {
// bitfield comparison: check if user lacks certain permission
if (!req.permission!.has(new Permissions(perm))) {
throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(
perm as string,
);
throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(perm as string);
}
});
}
@@ -124,20 +104,14 @@ export function route(opts: RouteOptions) {
req.rights = await getRights(req.user_id);
if (!req.rights || !req.rights.has(required)) {
throw SpacebarApiErrors.MISSING_RIGHTS.withParams(
opts.right as string,
);
throw SpacebarApiErrors.MISSING_RIGHTS.withParams(opts.right as string);
}
}
if (validate && !ignoredRequestSchemas.includes(opts.requestBody!)) {
const valid = validate(req.body);
if (!valid) {
const fields: Record<
string,
{ code?: string; message: string }
> = {};
const fields: Record<string, { code?: string; message: string }> = {};
validate.errors?.forEach(
(x) =>
(fields[x.instancePath.slice(1)] = {
@@ -145,11 +119,7 @@ export function route(opts: RouteOptions) {
message: x.message || "",
}),
);
if (process.env.LOG_VALIDATION_ERRORS)
console.log(
`[VALIDATION ERROR] ${req.method} ${req.originalUrl} - SCHEMA='${opts.requestBody}' -`,
validate?.errors,
);
if (process.env.LOG_VALIDATION_ERRORS) console.log(`[VALIDATION ERROR] ${req.method} ${req.originalUrl} - SCHEMA='${opts.requestBody}' -`, validate?.errors);
throw FieldErrors(fields, validate.errors!);
}
}
+21 -85
View File
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Config }from "@spacebar/util";
import { Config } from "@spacebar/util";
import { Embed, EmbedImage, EmbedType } from "@spacebar/schemas";
import * as cheerio from "cheerio";
import crypto from "crypto";
@@ -26,18 +26,13 @@ import probe from "probe-image-size";
export const DEFAULT_FETCH_OPTIONS: RequestInit = {
redirect: "follow",
headers: {
"user-agent":
"Mozilla/5.0 (compatible; Spacebar/1.0; +https://github.com/spacebarchat/server)",
"user-agent": "Mozilla/5.0 (compatible; Spacebar/1.0; +https://github.com/spacebarchat/server)",
},
// size: 1024 * 1024 * 5, // grabbed from config later
method: "GET",
};
const makeEmbedImage = (
url: string | undefined,
width: number | undefined,
height: number | undefined,
): Required<EmbedImage> | undefined => {
const makeEmbedImage = (url: string | undefined, width: number | undefined, height: number | undefined): Required<EmbedImage> | undefined => {
if (!url || !width || !height) return undefined;
return {
url,
@@ -49,13 +44,8 @@ const makeEmbedImage = (
let hasWarnedAboutImagor = false;
export const getProxyUrl = (
url: URL,
width: number,
height: number,
): string => {
const { resizeWidthMax, resizeHeightMax, imagorServerUrl } =
Config.get().cdn;
export const getProxyUrl = (url: URL, width: number, height: number): string => {
const { resizeWidthMax, resizeHeightMax, imagorServerUrl } = Config.get().cdn;
const secret = Config.get().security.requestSignature;
width = Math.min(width || 500, resizeWidthMax || width);
height = Math.min(height || 500, resizeHeightMax || width);
@@ -64,24 +54,14 @@ export const getProxyUrl = (
if (imagorServerUrl) {
const path = `${width}x${height}/${url.host}${url.pathname}`;
const hash = crypto
.createHmac("sha1", secret)
.update(path)
.digest("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_");
const hash = crypto.createHmac("sha1", secret).update(path).digest("base64").replace(/\+/g, "-").replace(/\//g, "_");
return `${imagorServerUrl}/${hash}/${path}`;
}
if (!hasWarnedAboutImagor) {
hasWarnedAboutImagor = true;
console.log(
"[Embeds]",
yellow(
"Imagor has not been set up correctly. https://docs.spacebar.chat/setup/server/configuration/imagor/",
),
);
console.log("[Embeds]", yellow("Imagor has not been set up correctly. https://docs.spacebar.chat/setup/server/configuration/imagor/"));
}
return url.toString();
@@ -161,11 +141,7 @@ const genericImageHandler = async (url: URL): Promise<Embed | null> => {
const response = await doFetch(url);
if (!response) return null;
const metas = getMetaDescriptions(await response.text());
image = makeEmbedImage(
metas.image || metas.image_fallback,
metas.width,
metas.height,
);
image = makeEmbedImage(metas.image || metas.image_fallback, metas.width, metas.height);
}
if (!image) return null;
@@ -186,8 +162,7 @@ export const EmbedHandlers: {
...DEFAULT_FETCH_OPTIONS,
method: "HEAD",
});
if (type.headers.get("content-type")?.indexOf("image") !== -1)
return await genericImageHandler(url);
if (type.headers.get("content-type")?.indexOf("image") !== -1) return await genericImageHandler(url);
const response = await doFetch(url);
if (!response) return null;
@@ -300,9 +275,7 @@ export const EmbedHandlers: {
const text = json.data.text;
const created_at = new Date(json.data.created_at);
const metrics = json.data.public_metrics;
const media = json.includes.media?.filter(
(x: { type: string }) => x.type == "photo",
);
const media = json.includes.media?.filter((x: { type: string }) => x.type == "photo");
const embed: Embed = {
type: EmbedType.rich,
@@ -311,11 +284,7 @@ export const EmbedHandlers: {
author: {
url: `https://twitter.com/${author.username}`,
name: `${author.name} (@${author.username})`,
proxy_icon_url: getProxyUrl(
new URL(author.profile_image_url),
400,
400,
),
proxy_icon_url: getProxyUrl(new URL(author.profile_image_url), 400, 400),
icon_url: author.profile_image_url,
},
timestamp: created_at,
@@ -334,15 +303,8 @@ export const EmbedHandlers: {
color: 1942002,
footer: {
text: "Twitter",
proxy_icon_url: getProxyUrl(
new URL(
"https://abs.twimg.com/icons/apple-touch-icon-192x192.png",
),
192,
192,
),
icon_url:
"https://abs.twimg.com/icons/apple-touch-icon-192x192.png",
proxy_icon_url: getProxyUrl(new URL("https://abs.twimg.com/icons/apple-touch-icon-192x192.png"), 192, 192),
icon_url: "https://abs.twimg.com/icons/apple-touch-icon-192x192.png",
},
// Discord doesn't send this?
// provider: {
@@ -356,11 +318,7 @@ export const EmbedHandlers: {
width: media[0].width,
height: media[0].height,
url: media[0].url,
proxy_url: getProxyUrl(
new URL(media[0].url),
media[0].width,
media[0].height,
),
proxy_url: getProxyUrl(new URL(media[0].url), media[0].width, media[0].height),
};
media.shift();
}
@@ -413,11 +371,7 @@ export const EmbedHandlers: {
type: EmbedType.image,
title: metas.title,
description: metas.description,
image: makeEmbedImage(
metas.image || metas.image_fallback,
metas.width,
metas.height,
),
image: makeEmbedImage(metas.image || metas.image_fallback, metas.width, metas.height),
provider: {
url: "https://pixiv.net",
name: "Pixiv",
@@ -429,17 +383,9 @@ export const EmbedHandlers: {
const response = await doFetch(url);
if (!response) return null;
const metas = getMetaDescriptions(await response.text());
const numReviews = metas.$("#review_summary_num_reviews").val() as
| string
| undefined;
const price = metas
.$(".game_purchase_price.price")
.data("price-final") as number | undefined;
const releaseDate = metas
.$(".release_date")
.find("div.date")
.text()
.trim();
const numReviews = metas.$("#review_summary_num_reviews").val() as string | undefined;
const price = metas.$(".game_purchase_price.price").data("price-final") as number | undefined;
const releaseDate = metas.$(".release_date").find("div.date").text().trim();
const isReleased = new Date(releaseDate) < new Date();
const fields: Embed["fields"] = [];
@@ -477,9 +423,7 @@ export const EmbedHandlers: {
width: 460,
height: 215,
url: metas.image,
proxy_url: metas.image
? getProxyUrl(new URL(metas.image), 460, 215)
: undefined,
proxy_url: metas.image ? getProxyUrl(new URL(metas.image), 460, 215) : undefined,
},
provider: {
url: "https://store.steampowered.com",
@@ -510,19 +454,11 @@ export const EmbedHandlers: {
const metas = getMetaDescriptions(await response.text());
return {
video: makeEmbedImage(
metas.youtube_embed,
metas.width,
metas.height,
),
video: makeEmbedImage(metas.youtube_embed, metas.width, metas.height),
url: url.href,
type: metas.youtube_embed ? EmbedType.video : EmbedType.link,
title: metas.title,
thumbnail: makeEmbedImage(
metas.image || metas.image_fallback,
metas.width,
metas.height,
),
thumbnail: makeEmbedImage(metas.image || metas.image_fallback, metas.width, metas.height),
provider: {
url: "https://www.youtube.com",
name: "YouTube",
+3 -9
View File
@@ -35,8 +35,7 @@ const reSYMBOLS = /[A-Za-z0-9]/g;
* Returns: 0 > pw > 1
*/
export function checkPassword(password: string): number {
const { minLength, minNumbers, minUpperCase, minSymbols } =
Config.get().register.password;
const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password;
let strength = 0;
// checks for total password len
@@ -60,10 +59,7 @@ export function checkPassword(password: string): number {
}
// checks if password only consists of numbers or only consists of chars
if (
password.length == password.match(reNUMBER)?.length ||
password.length === password.match(reUPPERCASELETTER)?.length
) {
if (password.length == password.match(reNUMBER)?.length || password.length === password.match(reUPPERCASELETTER)?.length) {
strength = 0;
}
@@ -76,8 +72,6 @@ export function checkPassword(password: string): number {
const entropies = Object.values(entropyMap);
entropies.map((x) => x / entropyMap.length);
strength +=
entropies.reduceRight((a: number, x: number) => a - x * Math.log2(x)) /
Math.log2(password.length);
strength += entropies.reduceRight((a: number, x: number) => a - x * Math.log2(x)) / Math.log2(password.length);
return strength;
}
+21 -37
View File
@@ -30,50 +30,35 @@ import { multer } from "../util/multer";
// TODO: delete old icons
const ANIMATED_MIME_TYPES = ["image/apng", "image/gif", "image/gifv"];
const STATIC_MIME_TYPES = [
"image/png",
"image/jpeg",
"image/webp",
"image/svg+xml",
"image/svg",
];
const STATIC_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/svg+xml", "image/svg"];
const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES];
const router = Router({ mergeParams: true });
router.post(
"/:user_id",
multer.single("file"),
async (req: Request, res: Response) => {
if (req.headers.signature !== Config.get().security.requestSignature)
throw new HTTPError("Invalid request signature");
if (!req.file) throw new HTTPError("Missing file");
const { buffer, size } = req.file;
const { user_id } = req.params;
router.post("/:user_id", multer.single("file"), async (req: Request, res: Response) => {
if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
if (!req.file) throw new HTTPError("Missing file");
const { buffer, size } = req.file;
const { user_id } = req.params;
let hash = crypto
.createHash("md5")
.update(Snowflake.generate())
.digest("hex");
let hash = crypto.createHash("md5").update(Snowflake.generate()).digest("hex");
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED_MIME_TYPES.includes(type.mime))
throw new HTTPError("Invalid file type");
if (ANIMATED_MIME_TYPES.includes(type.mime)) hash = `a_${hash}`; // animated icons have a_ infront of the hash
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED_MIME_TYPES.includes(type.mime)) throw new HTTPError("Invalid file type");
if (ANIMATED_MIME_TYPES.includes(type.mime)) hash = `a_${hash}`; // animated icons have a_ infront of the hash
const path = `avatars/${user_id}/${hash}`;
const endpoint = Config.get().cdn.endpointPublic;
const path = `avatars/${user_id}/${hash}`;
const endpoint = Config.get().cdn.endpointPublic;
await storage.set(path, buffer);
await storage.set(path, buffer);
return res.json({
id: hash,
content_type: type.mime,
size,
url: `${endpoint}${req.baseUrl}/${user_id}/${hash}`,
});
},
);
return res.json({
id: hash,
content_type: type.mime,
size,
url: `${endpoint}${req.baseUrl}/${user_id}/${hash}`,
});
});
router.get("/:user_id", async (req: Request, res: Response) => {
let { user_id } = req.params;
@@ -109,8 +94,7 @@ export const getAvatar = async (req: Request, res: Response) => {
router.get("/:user_id/:hash", getAvatar);
router.delete("/:user_id/:id", async (req: Request, res: Response) => {
if (req.headers.signature !== Config.get().security.requestSignature)
throw new HTTPError("Invalid request signature");
if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
const { user_id, id } = req.params;
const path = `avatars/${user_id}/${id}`;
+2 -18
View File
@@ -63,15 +63,7 @@ router.get("/avatars/:id", async (req: Request, res: Response) => {
id = id.split(".")[0]; // remove .file extension
const hash = defaultAvatarHashMap.get(id);
if (!hash) throw new HTTPError("not found", 404);
const path = join(
__dirname,
"..",
"..",
"..",
"assets",
"public",
`${hash}.png`,
);
const path = join(__dirname, "..", "..", "..", "assets", "public", `${hash}.png`);
const file = await getFile(path);
if (!file) throw new HTTPError("not found", 404);
@@ -88,15 +80,7 @@ router.get("/group-avatars/:id", async (req: Request, res: Response) => {
id = id.split(".")[0]; // remove .file extension
const hash = defaultGroupDMAvatarHashMap.get(id);
if (!hash) throw new HTTPError("not found", 404);
const path = join(
__dirname,
"..",
"..",
"..",
"assets",
"public",
`${hash}.png`,
);
const path = join(__dirname, "..", "..", "..", "assets", "public", `${hash}.png`);
const file = await getFile(path);
if (!file) throw new HTTPError("not found", 404);
+5 -17
View File
@@ -30,32 +30,21 @@ import { fileTypeFromBuffer } from "file-type";
// TODO: delete old icons
const ANIMATED_MIME_TYPES = ["image/apng", "image/gif", "image/gifv"];
const STATIC_MIME_TYPES = [
"image/png",
"image/jpeg",
"image/webp",
"image/svg+xml",
"image/svg",
];
const STATIC_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/svg+xml", "image/svg"];
const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES];
const router = Router({ mergeParams: true });
router.post("/", multer.single("file"), async (req: Request, res: Response) => {
if (req.headers.signature !== Config.get().security.requestSignature)
throw new HTTPError("Invalid request signature");
if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
if (!req.file) throw new HTTPError("Missing file");
const { buffer, size } = req.file;
const { guild_id, user_id } = req.params;
let hash = crypto
.createHash("md5")
.update(Snowflake.generate())
.digest("hex");
let hash = crypto.createHash("md5").update(Snowflake.generate()).digest("hex");
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED_MIME_TYPES.includes(type.mime))
throw new HTTPError("Invalid file type");
if (!type || !ALLOWED_MIME_TYPES.includes(type.mime)) throw new HTTPError("Invalid file type");
if (ANIMATED_MIME_TYPES.includes(type.mime)) hash = `a_${hash}`; // animated icons have a_ infront of the hash
const path = `guilds/${guild_id}/users/${user_id}/avatars/${hash}`;
@@ -104,8 +93,7 @@ router.get("/:hash", async (req: Request, res: Response) => {
});
router.delete("/:id", async (req: Request, res: Response) => {
if (req.headers.signature !== Config.get().security.requestSignature)
throw new HTTPError("Invalid request signature");
if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
const { guild_id, user_id, id } = req.params;
const path = `guilds/${guild_id}/users/${user_id}/avatars/${id}`;
+22 -47
View File
@@ -30,49 +30,34 @@ import { multer } from "../util/multer";
// TODO: generate different sizes of icon
// TODO: generate different image types of icon
const STATIC_MIME_TYPES = [
"image/png",
"image/jpeg",
"image/webp",
"image/svg+xml",
"image/svg",
];
const STATIC_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/svg+xml", "image/svg"];
const ALLOWED_MIME_TYPES = [...STATIC_MIME_TYPES];
const router = Router({ mergeParams: true });
router.post(
"/:role_id",
multer.single("file"),
async (req: Request, res: Response) => {
if (req.headers.signature !== Config.get().security.requestSignature)
throw new HTTPError("Invalid request signature");
if (!req.file) throw new HTTPError("Missing file");
const { buffer, size } = req.file;
const { role_id } = req.params;
router.post("/:role_id", multer.single("file"), async (req: Request, res: Response) => {
if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
if (!req.file) throw new HTTPError("Missing file");
const { buffer, size } = req.file;
const { role_id } = req.params;
const hash = crypto
.createHash("md5")
.update(Snowflake.generate())
.digest("hex");
const hash = crypto.createHash("md5").update(Snowflake.generate()).digest("hex");
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED_MIME_TYPES.includes(type.mime))
throw new HTTPError("Invalid file type");
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED_MIME_TYPES.includes(type.mime)) throw new HTTPError("Invalid file type");
const path = `role-icons/${role_id}/${hash}.png`;
const endpoint = Config.get().cdn.endpointPublic;
const path = `role-icons/${role_id}/${hash}.png`;
const endpoint = Config.get().cdn.endpointPublic;
await storage.set(path, buffer);
await storage.set(path, buffer);
return res.json({
id: hash,
content_type: type.mime,
size,
url: `${endpoint}${req.baseUrl}/${role_id}/${hash}`,
});
},
);
return res.json({
id: hash,
content_type: type.mime,
size,
url: `${endpoint}${req.baseUrl}/${role_id}/${hash}`,
});
});
router.get("/:role_id", async (req: Request, res: Response) => {
const { role_id } = req.params;
@@ -96,19 +81,10 @@ router.get("/:role_id/:hash", async (req: Request, res: Response) => {
const role_icon_hash = hash.split(".")[0];
let file: Buffer | null = null;
const extensions_to_try = [
requested_extension,
"png",
"jpg",
"jpeg",
"webp",
"svg",
];
const extensions_to_try = [requested_extension, "png", "jpg", "jpeg", "webp", "svg"];
for (let i = 0; i < extensions_to_try.length; i++) {
file = await storage.get(
`role-icons/${role_id}/${role_icon_hash}.${extensions_to_try[i]}`,
);
file = await storage.get(`role-icons/${role_id}/${role_icon_hash}.${extensions_to_try[i]}`);
if (file) break;
}
@@ -122,8 +98,7 @@ router.get("/:role_id/:hash", async (req: Request, res: Response) => {
});
router.delete("/:role_id/:id", async (req: Request, res: Response) => {
if (req.headers.signature !== Config.get().security.requestSignature)
throw new HTTPError("Invalid request signature");
if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
const { role_id, id } = req.params;
const path = `role-icons/${role_id}/${id}`;