diff --git a/packages/api/src/routes/dash/server-infractions.ts b/packages/api/src/routes/dash/server-infractions.ts index a2cae25..94fb792 100644 --- a/packages/api/src/routes/dash/server-infractions.ts +++ b/packages/api/src/routes/dash/server-infractions.ts @@ -4,6 +4,32 @@ import { badRequest, forbidden, isAuthenticated, getPermissionLevel, requireAuth import { botReq } from "../internal/ws"; import { ObjectId } from "mongodb"; +async function resolveUserInfo(userIds: string[]): Promise> { + const map: Record = {}; + if (userIds.length === 0) return map; + try { + const userRes = await botReq("getUsers", { users: [...new Set(userIds)] }); + if (userRes.success && userRes.users) { + Object.assign(map, userRes.users); + } + } catch { + /* fallback: show IDs only */ + } + return map; +} + +async function resolveBannedUsers(server: string): Promise> { + try { + const banRes = await botReq("getBannedUserIds", { server }); + if (banRes.success && banRes.bannedIds) { + return new Set(banRes.bannedIds); + } + } catch { + /* fallback: no banned users shown */ + } + return new Set(); +} + app.get("/dash/server/:server/infractions", requireAuth({ permission: 1 }), async (req: Request, res: Response) => { const user = await isAuthenticated(req, res, true); if (!user) return; @@ -96,44 +122,11 @@ app.get("/dash/server/:server/infractions", requireAuth({ permission: 1 }), asyn }; }); - const userIds = [...new Set(items.flatMap((i) => [i.user, i.createdBy].filter(Boolean)))]; - const userMapPromise: Promise> = (async () => { - const map: Record = {}; - if (userIds.length > 0) { - try { - const userRes = await botReq("getUsers", { users: userIds }); - if (userRes.success && userRes.users) { - Object.assign(map, userRes.users); - } - } catch { - /* fallback: show IDs only */ - } - } - return map; - })(); - - const bannedSetPromise: Promise> = (async () => { - const banIds = items.filter((i) => i.actionType === "ban").map((i) => i.user); - if (banIds.length > 0) { - try { - const banRes = await botReq("getBannedUserIds", { server }); - if (banRes.success && banRes.bannedIds) { - return new Set(banRes.bannedIds); - } - } catch { - /* fallback: no banned users shown */ - } - } - return new Set(); - })(); - - const [userMap, bannedSet] = await Promise.all([userMapPromise, bannedSetPromise]); - const enriched = items.map((i) => ({ ...i, - userName: userMap[i.user]?.username || null, - createdByName: i.createdBy ? userMap[i.createdBy]?.username || null : null, - isBanned: i.actionType === "ban" && bannedSet.has(i.user), + userName: null, + createdByName: null, + isBanned: false, })); res.send({ infractions: enriched, total, hasMore, stats: { total, warns, kicks, bans, timeouts } }); @@ -143,6 +136,36 @@ app.get("/dash/server/:server/infractions", requireAuth({ permission: 1 }), asyn } }); +app.post("/dash/server/:server/infractions/resolve", requireAuth({ permission: 1 }), async (req: Request, res: Response) => { + const user = await isAuthenticated(req, res, true); + if (!user) return; + + const { server } = req.params; + if (!server || typeof server !== "string") return badRequest(res); + + const { userIds } = req.body; + if (!Array.isArray(userIds) || !userIds.every((id: any) => typeof id === "string")) { + return badRequest(res, "userIds must be an array of strings"); + } + + try { + const [userMap, bannedSet] = await Promise.all([ + resolveUserInfo(userIds), + resolveBannedUsers(server), + ]); + + const users: Record = {}; + for (const id of userIds) { + users[id] = userMap[id] || null; + } + + res.send({ users, bannedIds: [...bannedSet] }); + } catch (e: any) { + console.error(e); + res.status(500).send({ error: e.message || "Internal server error" }); + } +}); + app.delete("/dash/server/:server/infractions/:id", async (req: Request, res: Response) => { const user = await isAuthenticated(req, res, true); if (!user) return unauthorized(res); diff --git a/packages/bot/src/bot/commands/moderation/infractions.ts b/packages/bot/src/bot/commands/moderation/infractions.ts index ddce12b..d80ce02 100644 --- a/packages/bot/src/bot/commands/moderation/infractions.ts +++ b/packages/bot/src/bot/commands/moderation/infractions.ts @@ -182,7 +182,7 @@ const getTopInfractionsMessage = async (userInfractions: Map sentMsg?.edit({ content: msg.replace("%emoji%", ":unlock:") }).catch(() => {}), 200); + const sentMsg = await message.reply(msg.replace("%emoji%", "🔒"), false); + setTimeout(() => sentMsg?.edit({ content: msg.replace("%emoji%", "🔓") }).catch(() => {}), 200); break; } @@ -50,8 +50,8 @@ export default { sudoOverrides[message.authorId!] = null; let msg = `## %emoji% Override disabled`; - const sentMsg = await message.reply(msg.replace("%emoji%", ":unlock:"), false); - setTimeout(() => sentMsg?.edit({ content: msg.replace("%emoji%", ":lock:") }).catch(() => {}), 200); + const sentMsg = await message.reply(msg.replace("%emoji%", "🔓"), false); + setTimeout(() => sentMsg?.edit({ content: msg.replace("%emoji%", "🔒") }).catch(() => {}), 200); break; } diff --git a/packages/bot/src/bot/modules/event_handler.ts b/packages/bot/src/bot/modules/event_handler.ts index 76eb860..59bc844 100644 --- a/packages/bot/src/bot/modules/event_handler.ts +++ b/packages/bot/src/bot/modules/event_handler.ts @@ -109,7 +109,7 @@ client.on("serverMemberJoin", (member) => { if (!channel) return console.debug("Cannot send hello message: No suitable channel found"); channel .sendMessage({ - content: `:wave: "Hi there!")`, + content: `👋 "Hi there!")`, embeds: [embed], }) .catch((e) => console.debug("Cannot send hello message: " + e)); diff --git a/packages/bot/src/bot/util.ts b/packages/bot/src/bot/util.ts index 2538236..1143459 100644 --- a/packages/bot/src/bot/util.ts +++ b/packages/bot/src/bot/util.ts @@ -74,7 +74,7 @@ async function checkSudoPermission(message: Message, announce = true): PromiseFilter - ${items.length ? renderInfractionTable(items, hasMore) : `

No infractions found.

`}`; + ${renderInfractionTable(items)}`; } else { const table = container.querySelector("table tbody"); - const loadMore = container.querySelector("#inf-load-more"); - if (loadMore) loadMore.remove(); if (table) { table.insertAdjacentHTML("beforeend", buildInfractionRows(items)); @@ -743,22 +743,24 @@ function renderInfractions(container, data, append = false) { } bindInfractionEvents(); + + if (!append) resolvedUserIds = new Set(); + resolveInfractionUsers(); } -function renderInfractionTable(items, hasMore) { +function renderInfractionTable(items) { return ` ${perms >= 2 ? `` : ""} ${perms >= 2 ? `` : ""} - ${buildInfractionRows(items)} + ${items.length ? buildInfractionRows(items) : ``}
DateUserTypeModeratorReason
No infractions found.
- ${hasMore ? `
` : ""}`; + `; } function buildInfractionRows(items) { @@ -767,13 +769,13 @@ function buildInfractionRows(items) { (i) => ` ${perms >= 2 ? `` : ""} ${new Date(i.date).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })} - ${i.userName ? `${escHtml(i.userName)}` : `${escHtml(i.user)}`} + ${i.userName ? `${escHtml(i.userName)}` : `${escHtml(i.user)}`} ${i.actionType} - ${i.createdByName ? `${escHtml(i.createdByName)}` : i.createdBy ? `${escHtml(i.createdBy)}` : "AutoMod"} + ${i.createdByName ? `${escHtml(i.createdByName)}` : i.createdBy ? `${escHtml(i.createdBy)}` : "AutoMod"} ${escHtml(i.reason || "No reason provided")} ${perms >= 2 ? `` : ""} - ${perms >= 2 && i.actionType === "ban" && i.isBanned ? `` : ""} + ${perms >= 2 && i.actionType === "ban" ? `` : ""} `, ) @@ -782,6 +784,58 @@ function buildInfractionRows(items) { let infObs = null; +async function resolveInfractionUsers() { + if (infractionsResolving) return; + const container = document.getElementById("tab-infractions"); + const userCells = container.querySelectorAll("[data-resolve-user]"); + const banButtons = container.querySelectorAll("[data-resolve-ban]"); + + const idsToResolve = new Set(); + for (const cell of userCells) { + const id = (cell as HTMLElement).dataset.resolveUser; + if (id && !resolvedUserIds.has(id)) idsToResolve.add(id); + } + for (const btn of banButtons) { + const id = (btn as HTMLElement).dataset.resolveBan; + if (id && !resolvedUserIds.has(id)) idsToResolve.add(id); + } + + if (idsToResolve.size === 0) return; + + infractionsResolving = true; + try { + const data = await request("POST", `/dash/server/${serverId}/infractions/resolve`, { + userIds: [...idsToResolve], + }); + + const users = data.users || {}; + const bannedIds = new Set(data.bannedIds || []); + + for (const cell of userCells) { + const id = (cell as HTMLElement).dataset.resolveUser; + if (!id || !users[id] || !users[id].username) continue; + cell.innerHTML = `${escHtml(users[id].username)}`; + cell.removeAttribute("data-resolve-user"); + resolvedUserIds.add(id); + } + + for (const btn of banButtons) { + const id = (btn as HTMLElement).dataset.resolveBan; + if (!id) continue; + if (bannedIds.has(id)) { + (btn as HTMLElement).hidden = false; + } else { + btn.remove(); + } + resolvedUserIds.add(id); + } + } catch { + /* names will remain as IDs */ + } finally { + infractionsResolving = false; + } +} + async function handleInfractionAction(e) { const delBtn = e.target.closest(".inf-delete"); const unbanBtn = e.target.closest(".inf-unban"); diff --git a/packages/web/static/styles/pages/server.css b/packages/web/static/styles/pages/server.css index 0f2c267..8500747 100644 --- a/packages/web/static/styles/pages/server.css +++ b/packages/web/static/styles/pages/server.css @@ -168,6 +168,13 @@ display: none; } +td.empty { + text-align: center; + padding: 2rem 1rem; + color: var(--grey); + font-style: italic; +} + .inf-filter-checks { display: flex; gap: 0.75rem;