From 2b7ec5c629d37e09643252ed71619d69b3b5c3f1 Mon Sep 17 00:00:00 2001 From: Declan Chidlow Date: Thu, 16 Jul 2026 12:41:05 +0800 Subject: [PATCH] Significant performance improvements --- .../api/src/routes/dash/server-antispam.ts | 30 ++----------------- packages/api/src/routes/dash/server.ts | 14 ++++++++- packages/api/src/routes/dash/servers.ts | 16 +++++++++- packages/bot/src/bot/modules/api/servers.ts | 14 +++++++-- packages/bot/src/bot/modules/fetch_all.ts | 4 ++- packages/bot/src/bot/util.ts | 20 +++++++++++-- packages/web/static/js/index.js | 26 ++++++++++------ packages/web/static/layout.html | 2 -- 8 files changed, 80 insertions(+), 46 deletions(-) diff --git a/packages/api/src/routes/dash/server-antispam.ts b/packages/api/src/routes/dash/server-antispam.ts index eaed784..9ba5b51 100644 --- a/packages/api/src/routes/dash/server-antispam.ts +++ b/packages/api/src/routes/dash/server-antispam.ts @@ -1,7 +1,6 @@ import { app, db } from "../.."; import type { Request, Response } from "express"; -import { badRequest, ensureObjectStructure, isAuthenticated, requireAuth, unauthorized } from "../../utils"; -import { botReq } from "../internal/ws"; +import { badRequest, ensureObjectStructure, isAuthenticated, requireAuth } from "../../utils"; import type { Collection } from "mongodb"; import { ulid } from "ulid"; @@ -28,21 +27,12 @@ app.get("/dash/server/:server/antispam", requireAuth({ permission: 2 }), async ( const { server } = req.params; if (!server || typeof server != "string") return badRequest(res); - const response = await botReq("getUserServerDetails", { user, server }); - if (!response.success) { - return res.status(response.statusCode ?? 500).send({ error: response.error }); - } - - if (!response["server"]) return res.status(404).send({ error: "Server not found" }); - - const permissionLevel: 0 | 1 | 2 | 3 = response["perms"]; - if (permissionLevel < 1) return unauthorized(res, `Only moderators and bot managers may view this.`); - const serverConfig = await (await serversCollection()).findOne({ id: server }); + if (!serverConfig) return res.status(404).send({ error: "Server not found" }); const result = { antispam: - (serverConfig?.["automodSettings"]?.spam as AntispamRule[] | undefined)?.map( + (serverConfig["automodSettings"]?.spam as AntispamRule[] | undefined)?.map( (r) => ({ action: r.action, @@ -100,13 +90,6 @@ app.post("/dash/server/:server/antispam", requireAuth({ permission: 2 }), async const { server } = req.params; if (!server || typeof server != "string") return badRequest(res); - const response = await botReq("getUserServerDetails", { user, server }); - if (!response.success) { - return res.status(response.statusCode ?? 500).send({ error: response.error }); - } - - if (!response["server"]) return res.status(404).send({ error: "Server not found" }); - let rule: any; try { rule = ensureObjectStructure( @@ -151,13 +134,6 @@ app.delete("/dash/server/:server/antispam/:ruleid", requireAuth({ permission: 2 const { server, ruleid } = req.params; if (!server || typeof server != "string" || !ruleid || typeof ruleid != "string") return badRequest(res); - const response = await botReq("getUserServerDetails", { user, server }); - if (!response.success) { - return res.status(response.statusCode ?? 500).send({ error: response.error }); - } - - if (!response["server"]) return res.status(404).send({ error: "Server not found" }); - let result; try { result = await ( diff --git a/packages/api/src/routes/dash/server.ts b/packages/api/src/routes/dash/server.ts index bbca7cf..23dd5de 100644 --- a/packages/api/src/routes/dash/server.ts +++ b/packages/api/src/routes/dash/server.ts @@ -2,6 +2,7 @@ import { app, db } from "../.."; import type { Request, Response } from "express"; import { badRequest, getPermissionLevel, isAuthenticated, requireAuth, unauthorized } from "../../utils"; import { botReq } from "../internal/ws"; +import { redis } from "../../db"; type User = { id: string; username?: string; avatarURL?: string }; type Channel = { id: string; name: string; icon?: string; type: "VOICE" | "TEXT"; nsfw: boolean }; @@ -33,6 +34,13 @@ app.get("/dash/server/:server", requireAuth({ permission: 0 }), async (req: Requ const { server } = req.params; if (!server || typeof server != "string") return badRequest(res); + const cacheKey = `dash:server:${server}:${user}`; + + const cached = await redis.get(cacheKey); + if (cached) { + return res.send(JSON.parse(cached)); + } + const response = await botReq("getUserServerDetails", { user, server }); if (!response.success) { return res.status(response.statusCode ?? 500).send({ error: response.error }); @@ -41,7 +49,11 @@ app.get("/dash/server/:server", requireAuth({ permission: 0 }), async (req: Requ if (!response["server"]) return res.status(404).send({ error: "Not found" }); const s: ServerDetails = response["server"]; - res.send({ server: s }); + const body = { server: s }; + + redis.set(cacheKey, JSON.stringify(body), { EX: 15 }).catch(() => {}); + + res.send(body); }); app.put("/dash/server/:server/:option", async (req: Request, res: Response) => { diff --git a/packages/api/src/routes/dash/servers.ts b/packages/api/src/routes/dash/servers.ts index 341e26c..b0699be 100644 --- a/packages/api/src/routes/dash/servers.ts +++ b/packages/api/src/routes/dash/servers.ts @@ -2,6 +2,7 @@ import { app } from "../.."; import type { Request, Response } from "express"; import { isAuthenticated, requireAuth } from "../../utils"; import { botReq } from "../internal/ws"; +import { redis } from "../../db"; type Server = { id: string; @@ -15,10 +16,19 @@ type Server = { roleCount: number; }; +const CACHE_TTL_SECONDS = 15; + app.get("/dash/servers", requireAuth({ requireLogin: true }), async (req: Request, res: Response) => { const user = await isAuthenticated(req, res, true); if (!user) return; + const cacheKey = `dash:servers:${user}`; + + const cached = await redis.get(cacheKey); + if (cached) { + return res.send(JSON.parse(cached)); + } + const response = await botReq("getUserServers", { user }); if (!response.success) { return res.status(response.statusCode ?? 500).send({ error: response.error }); @@ -27,5 +37,9 @@ app.get("/dash/servers", requireAuth({ requireLogin: true }), async (req: Reques if (!response["servers"]) return res.status(404).send({ error: "Not found" }); const servers: Server[] = response["servers"]; - res.send({ servers }); + const body = { servers }; + + redis.set(cacheKey, JSON.stringify(body), { EX: CACHE_TTL_SECONDS }).catch(() => {}); + + res.send(body); }); diff --git a/packages/bot/src/bot/modules/api/servers.ts b/packages/bot/src/bot/modules/api/servers.ts index 675e1a8..94ca28b 100644 --- a/packages/bot/src/bot/modules/api/servers.ts +++ b/packages/bot/src/bot/modules/api/servers.ts @@ -1,4 +1,6 @@ -import { getMutualServers, getPermissionLevel, parseUser } from "../../util"; +import { getMutualServers, getPermissionLevelFromMember, parseUser } from "../../util"; +import { client, dbs } from "../../.."; +import ServerConfig from "automod-lib/dist/types/ServerConfig"; import type { WSResponse } from "../api_communication"; import { wsEvents } from "../api_communication"; @@ -26,6 +28,10 @@ wsEvents.on("req:getUserServers", async (data: ReqData, cb: (data: WSResponse) = roleCount: number; }; + const serverIds = mutuals.map((s) => s.id); + const configs = await dbs.SERVERS.find({ id: { $in: serverIds } }).toArray(); + const configMap = new Map(configs.map((c) => [c.id, c])); + const promises: Promise[] = []; for (const server of mutuals) { @@ -33,7 +39,11 @@ wsEvents.on("req:getUserServers", async (data: ReqData, cb: (data: WSResponse) = new Promise(async (resolve, reject) => { try { if (!server) return reject("Server not found"); - const perms = await getPermissionLevel(user, server); + const member = client.serverMembers.getByKey({ server: server.id, user: user.id }); + const config = configMap.get(server.id); + const perms = member + ? getPermissionLevelFromMember(member, server, config) + : 0; resolve({ id: server.id, perms, diff --git a/packages/bot/src/bot/modules/fetch_all.ts b/packages/bot/src/bot/modules/fetch_all.ts index ddc22e5..a62aa23 100644 --- a/packages/bot/src/bot/modules/fetch_all.ts +++ b/packages/bot/src/bot/modules/fetch_all.ts @@ -3,7 +3,9 @@ import { client } from "../.."; // Fetch all known users on bot startup. const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -const RATE_LIMIT_DELAY = 3000; +// Stoat API catch-all bucket allows 20 requests per 10 seconds (2 req/s). +// A 500ms delay keeps us at 2 req/s which is well within the limit while warming the cache ~6x faster. +const RATE_LIMIT_DELAY = 500; (async () => { if (!client.user) await new Promise((r) => client.once("ready", () => r())); diff --git a/packages/bot/src/bot/util.ts b/packages/bot/src/bot/util.ts index d544176..9ed3dbb 100644 --- a/packages/bot/src/bot/util.ts +++ b/packages/bot/src/bot/util.ts @@ -87,18 +87,30 @@ async function getPermissionLevel(member: ServerMember | User, server: Server): serverMember = member; } + const config = await dbs.SERVERS.findOne({ id: server.id }); + return getPermissionLevelFromMember(serverMember, server, config); +} + +function getPermissionLevelFromMember(serverMember: ServerMember, server: Server, config?: ServerConfig | null): 0 | 1 | 2 | 3 { if (isSudo(serverMember.user!)) return 3; if (server.ownerId === serverMember.id.user) return 3; if (serverMember.hasPermission(server, "ManageServer")) return 2; - const config = await dbs.SERVERS.findOne({ id: server.id }); - if (config?.botManagers?.includes(serverMember.id.user)) return 2; if (config?.moderators?.includes(serverMember.id.user) || serverMember.hasPermission(server, "BanMembers")) return 1; return 0; } +async function resolvePermissionLevelConfig(server: Server): Promise { + return await dbs.SERVERS.findOne({ id: server.id }); +} + +async function getPermissionLevelWithConfig(serverMember: ServerMember, server: Server, config?: ServerConfig | null): Promise<0 | 1 | 2 | 3> { + if (!config) config = await resolvePermissionLevelConfig(server); + return getPermissionLevelFromMember(serverMember, server, config); +} + function getPermissionBasedOnRole(member: ServerMember): 0 | 1 | 2 | 3 { if (member.server && member.server.ownerId === member.id.user) return 3; if (member.hasPermission(member.server!, "ManageServer")) return 2; @@ -282,7 +294,7 @@ async function getMutualServers(user: User) { if (fetched) return server; return null; }, - 100, + 20, ) ).filter((s): s is Server => s !== null); } @@ -531,6 +543,8 @@ export { isModerator, isBotManager, getPermissionLevel, + getPermissionLevelFromMember, + getPermissionLevelWithConfig, getPermissionBasedOnRole, parseUser, parseUserOrId, diff --git a/packages/web/static/js/index.js b/packages/web/static/js/index.js index 797956a..7bc8e10 100644 --- a/packages/web/static/js/index.js +++ b/packages/web/static/js/index.js @@ -4,10 +4,13 @@ if (isLoggedIn()) { (async () => { const loadingEl = document.querySelector(".loading"); if (loadingEl) { - loadingEl.insertAdjacentHTML("beforebegin", `
+ loadingEl.insertAdjacentHTML( + "beforebegin", + `

AutoMod Dashboard

Manage AutoMod in your servers.

-
`); +
`, + ); } try { const data = await request("GET", "/dash/servers"); @@ -47,17 +50,22 @@ if (isLoggedIn()) { try { const stats = await request("GET", "/stats"); if (stats.botId) botMention = "<@" + stats.botId + ">"; - } catch (_) { /* use default */ } + } catch (_) { + /* use default */ + } - main.innerHTML += ` -

AutoMod Dashboard

+ main.innerHTML += `
+

AutoMod Dashboard

+

The AutoMod dashboard allows you to manage AutoMod in your servers.

+
-
-
+
+
-

Enter your Stoat user ID to begin. You'll get a code to confirm with the bot using ${botMention} web login <code>.

`; + +

Don't know how to get your User ID? Run ${botMention} info in a server with AutoMod and it'll respond with your User ID.

`; let loginNonce, loginUser, loginCode; @@ -85,7 +93,7 @@ if (isLoggedIn()) { function renderConfirmStep(code) { main.innerHTML = `

Confirm Login

-

Your login code:

${escHtml(code)}
+

Your login code is ${escHtml(code)}.

Run this command in a direct message or server channel with AutoMod present:

${botMention} web login ${escHtml(code)}

After running the command, click below to finish.

diff --git a/packages/web/static/layout.html b/packages/web/static/layout.html index bec9143..adc24ad 100644 --- a/packages/web/static/layout.html +++ b/packages/web/static/layout.html @@ -5,8 +5,6 @@ {{title}} | AutoMod - - -- 2.51.2