diff --git a/packages/bot/src/bot/commands/moderation/infractions.ts b/packages/bot/src/bot/commands/moderation/infractions.ts index 6b84e0e..14a52e3 100644 --- a/packages/bot/src/bot/commands/moderation/infractions.ts +++ b/packages/bot/src/bot/commands/moderation/infractions.ts @@ -178,14 +178,16 @@ const getTopInfractionsMessage = async (userInfractions: Map { - try { - if (!message.member) return; - if (!message.member.hasPermission(message.member.server!, "ManageRole") && !(await isModerator(message))) return message.reply(NO_MANAGER_MSG); - - const action = args.shift()?.toLowerCase(); - - const normalizeEmoji = (emoji: string) => { - return emoji.replace(/^:([A-Z0-9]+):$/i, "$1").replace(/[\uFE0F\uE0E2]/g, ""); - }; - - if (action === "reaction") { - const subAction = args.shift()?.toLowerCase(); - - if (subAction === "add") { - const messageId = args.shift()?.trim(); - const emojiRaw = args.shift()?.trim(); - const roleArg = args.shift()?.trim(); - - if (!messageId || !emojiRaw || !roleArg) { - return message.reply("Usage: `/role reaction add `"); - } - - const roleIdMatch = roleArg.match(/^<%([A-Z0-9]+)>$/i); - const roleId = roleIdMatch ? roleIdMatch[1] : roleArg; - - const server = message.channel?.server; - if (!server || !server.roles || !server.roles.get(roleId)) { - return message.reply(`Role "${roleArg}" does not exist in this server.`); - } - - const emoji = normalizeEmoji(emojiRaw); +const normalizeEmoji = (emoji: string) => { + return emoji.replace(/^:([A-Z0-9]+):$/i, "$1").replace(/[\uFE0F\uE0E2]/g, ""); +}; - const isCustomEmoji = /^[A-Z0-9]{26}$/i.test(emoji); - const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }); - const graphemeCount = [...segmenter.segment(emoji)].length; - - if (!isCustomEmoji && graphemeCount > 1) { - return message.reply("Please provide exactly **one** valid emoji."); - } - - const channel = message.channel; - if (!channel) { - return message.reply("This command must be used in a channel."); - } - - try { - const targetMsg = await channel.fetchMessage(messageId); - await targetMsg.react(emoji); - - await dbs.REACTION_ROLES.insertOne({ - server: server.id, - messageId: messageId, - emoji: emoji, - roleId: roleId, - }); - - const displayEmoji = isCustomEmoji ? `:${emoji}:` : emoji; - return message.reply(`Reaction role added! Reacting to message \`${messageId}\` with ${displayEmoji} will now grant the role.`); - } catch (e) { - console.error("Could not add initial reaction:", e); - return message.reply(`Failed to add reaction role. Ensure the message ID is correct and the emoji is valid.`); - } - } - - if (subAction === "rm" || subAction === "remove") { - const messageId = args.shift()?.trim(); - const emojiRaw = args.shift()?.trim(); - - if (!messageId || !emojiRaw) { - return message.reply("Usage: `/role reaction rm `"); - } +export default { + name: "role", + aliases: ["roles"], + description: "Add and remove roles from a member, or manage reaction roles.", + documentation: "/moderation/role", + category: CommandCategory.Moderation, + run: async (message: MessageCommandContext, args: string[]) => { + try { + if (!message.member) return; + if (!message.member.hasPermission(message.member.server!, "ManageRole") && !(await isModerator(message))) return message.reply(NO_MANAGER_MSG); - const emoji = normalizeEmoji(emojiRaw); + const action = args.shift()?.toLowerCase(); - const result = await dbs.REACTION_ROLES.deleteOne({ messageId, emoji }); - if (result.deletedCount === 0) { - return message.reply("No reaction role found for that message and emoji combination."); - } + // Reaction Subcommand + if (action === "reaction") { + const subAction = args.shift()?.toLowerCase(); + if (subAction === "add") { + const messageId = args.shift()?.trim(); + const emojiRaw = args.shift()?.trim(); + const roleArg = args.shift()?.trim(); + if (!messageId || !emojiRaw || !roleArg) { + return message.reply("Usage: `/role reaction add `"); + } + const roleIdMatch = roleArg.match(/^<%([A-Z0-9]+)>$/i); + const roleId = roleIdMatch ? roleIdMatch[1] : roleArg; + const server = message.channel?.server; + if (!server || !server.roles || !server.roles.get(roleId)) { + return message.reply(`Role "${roleArg}" does not exist in this server.`); + } + const emoji = normalizeEmoji(emojiRaw); + const isCustomEmoji = /^[A-Z0-9]{26}$/i.test(emoji); + const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }); + const graphemeCount = [...segmenter.segment(emoji)].length; + if (!isCustomEmoji && graphemeCount > 1) { + return message.reply("Please provide exactly **one** valid emoji."); + } + const channel = message.channel; + if (!channel) { + return message.reply("This command must be used in a channel."); + } + try { + const targetMsg = await channel.fetchMessage(messageId); + await targetMsg.react(emoji); + await dbs.REACTION_ROLES.insertOne({ + server: server.id, + messageId: messageId, + emoji: emoji, + roleId: roleId, + }); + const displayEmoji = isCustomEmoji ? `:${emoji}:` : emoji; + return message.reply(`Reaction role added! Reacting to message \`${messageId}\` with ${displayEmoji} will now grant the role.`); + } catch (e) { + console.error("Could not add initial reaction:", e); + return message.reply(`Failed to add reaction role. Ensure the message ID is correct and the emoji is valid.`); + } + } + if (subAction === "rm" || subAction === "remove") { + const messageId = args.shift()?.trim(); + const emojiRaw = args.shift()?.trim(); + if (!messageId || !emojiRaw) { + return message.reply("Usage: `/role reaction rm `"); + } + const emoji = normalizeEmoji(emojiRaw); + const result = await dbs.REACTION_ROLES.deleteOne({ messageId, emoji }); + if (result.deletedCount === 0) { + return message.reply("No reaction role found for that message and emoji combination."); + } + const channel = message.channel; + if (channel) { + try { + const targetMsg = await channel.fetchMessage(messageId); + await targetMsg.unreact(emoji); + } catch (e) { + console.error("Could not remove bot reaction:", e); + } + } + return message.reply("Reaction role removed successfully."); + } + return message.reply("Invalid reaction action. Use `add` or `rm`."); + } - const channel = message.channel; - if (channel) { - try { - const targetMsg = await channel.fetchMessage(messageId); - await targetMsg.unreact(emoji); - } catch (e) { - console.error("Could not remove bot reaction:", e); - } - } + // Add/Remove + if (!action || (action !== "add" && action !== "rm" && action !== "remove")) { + return message.reply("Invalid action. Use `/role add @user role`, `/role remove @user role`, or `/role reaction add/remove ...`."); + } - return message.reply("Reaction role removed successfully."); - } + // Support for multi users + const userInput = !message.replyIds?.length ? args.shift() || "" : undefined; + if (!userInput && !message.replyIds?.length) { + return message.reply({ + embeds: [ + embed( + `Please specify one or more users by replying to their messages or by providing a comma-separated list of usernames/IDs.`, + "No target user specified", + EmbedColor.SoftError, + ), + ], + }); + } - return message.reply("Invalid reaction action. Use `add` or `rm`."); - } + const roleArg = args.shift(); + if (!roleArg) return message.reply("No role specified."); + const roleIdMatch = roleArg.match(/^<%([A-Z0-9]+)>$/i); + const roleId = roleIdMatch ? roleIdMatch[1] : roleArg; + const server = message.channel?.server; + if (!server || !server.roles || !server.roles.get(roleId)) { + return message.reply(`Role "${roleArg}" does not exist in this server.`); + } - if (!action || (action !== "add" && action !== "rm" && action !== "remove")) { - return message.reply("Invalid action. Use `/role add @user role`, `/role remove @user role`, or `/role reaction add/remove ...`."); - } + const embeds: SendableEmbed[] = []; + const handledUsers: string[] = []; + const targetMembers: Array<{ id: string; currentRoles: string[] }> = []; - const targetStr = args.shift(); - if (!targetStr) return message.reply("No target user specified."); - const targetUser = await parseUser(targetStr); - if (!targetUser) return message.reply("Couldn't find the specified user."); - const target = await message.channel?.server?.fetchMember(targetUser); - if (!target) return message.reply("The target is not part of this server."); + // Build user lists + const targetInput = dedupeArray( + message.replyIds?.length + ? (await Promise.allSettled(message.replyIds.map((msg) => message.channel?.fetchMessage(msg)))) + .filter((m) => m.status == "fulfilled") + .map((m) => (m as any).value.authorId) + : userInput!.split(","), + ); - const roleArg = args.shift(); - if (!roleArg) return message.reply("No role specified."); + for (const userStr of targetInput) { + try { + let user = await parseUserOrId(userStr); + if (!user) { + embeds.push( + embed( + `I can't resolve \`${sanitizeMessageContent(userStr).trim()}\` to a user.`, + null, + EmbedColor.SoftError, + ), + ); + continue; + } + if (handledUsers.includes(user.id)) continue; + handledUsers.push(user.id); - const roleIdMatch = roleArg.match(/^<%([A-Z0-9]+)>$/i); - const roleId = roleIdMatch ? roleIdMatch[1] : roleArg; + const target = await server.fetchMember(user.id); + if (!target) { + embeds.push(embed(`<@${user.id}> is not a member of this server.`, null, EmbedColor.SoftError)); + continue; + } + targetMembers.push({ id: user.id, currentRoles: target.roles || [] }); + } catch (e) { + console.error(e); + embeds.push( + embed( + `Failed to resolve target \`${sanitizeMessageContent(userStr).trim()}\`: ${e}`, + "Failed to resolve user", + EmbedColor.Error, + ), + ); + } + } - const server = message.channel?.server; - if (!server || !server.roles || !server.roles.get(roleId)) { - return message.reply(`Role "${roleArg}" does not exist in this server.`); - } + if (targetMembers.length === 0) { + if (embeds.length > 0) { + let firstMsg = true; + const embedsToSend = [...embeds]; + while (embedsToSend.length > 0) { + const targetEmbeds = embedsToSend.splice(0, 10); + if (firstMsg) { + await message.reply({ embeds: targetEmbeds, content: "Operation completed with errors." }, false); + } else { + await message.channel?.sendMessage({ embeds: targetEmbeds }); + } + firstMsg = false; + } + return; + } + return await message.reply({ + embeds: [embed("No valid server members were specified to manage roles for.", null, EmbedColor.SoftError)], + }); + } - const currentRoles = target.roles || []; + // Add/Remove for each users + for (const member of targetMembers) { + try { + const currentRoles = member.currentRoles; + if (action === "add") { + if (currentRoles.includes(roleId)) { + embeds.push(embed(`<@${member.id}> already has the role <@&${roleId}>.`, null, EmbedColor.Warning)); + continue; + } + const newRoles = [...currentRoles, roleId]; + await client.api.patch( + `/servers/${server.id}/members/${member.id}` as "/servers/{server}/members/{target}", + { roles: newRoles } as any + ); + embeds.push({ + title: `Role added`, + colour: EmbedColor.Success, + description: `Role <@&${roleId}> has been added to <@${member.id}>.`, + }); + } else { // action === "rm" or "remove" + if (!currentRoles.includes(roleId)) { + embeds.push(embed(`<@${member.id}> does not have the role <@&${roleId}>.`, null, EmbedColor.Warning)); + continue; + } + const newRoles = currentRoles.filter((role) => role !== roleId); + await client.api.patch( + `/servers/${server.id}/members/${member.id}` as "/servers/{server}/members/{target}", + { roles: newRoles } as any + ); + embeds.push({ + title: `Role removed`, + colour: EmbedColor.Success, + description: `Role <@&${roleId}> has been removed from <@${member.id}>.`, + }); + } + } catch (error: any) { + console.error("Role operation error for user", member.id, error); + embeds.push( + embed( + `Failed to ${action} role <@&${roleId}> for <@${member.id}>: ${error.message || error}`, + "Operation failed", + EmbedColor.Error, + ), + ); + } + } - if (action === "add") { - if (currentRoles.includes(roleId)) { - return message.reply(`User \`@${targetUser.username}\` already has the role \`${roleId}\`.`); - } - try { - await target.edit({ roles: [...currentRoles, roleId] }); - await message.reply(`Role \`${roleId}\` has been added to \`@${targetUser.username}\`.`); - } catch (error) { - console.error("Role add error:", error); - return message.reply(`Failed to add role: ${error}`); - } - } else { - if (!currentRoles.includes(roleId)) { - return message.reply(`User \`@${targetUser.username}\` doesn't have the role \`${roleId}\`.`); - } - try { - await target.edit({ roles: currentRoles.filter((role) => role !== roleId) }); - await message.reply(`Role \`${roleId}\` has been removed from \`@${targetUser.username}\`.`); - } catch (error) { - console.error("Role remove error:", error); - return message.reply(`Failed to remove role: ${error}`); - } - } - } catch (e) { - console.error("" + e); - message.reply("Something went wrong: " + e); - } - }, + // Send all results + let firstMsg = true; + const embedsToSend = [...embeds]; + while (embedsToSend.length > 0) { + const targetEmbeds = embedsToSend.splice(0, 10); + if (firstMsg) { + await message.reply( + { + embeds: targetEmbeds, + content: `Operation completed.`, + }, + false, + ); + } else { + await message.channel?.sendMessage({ embeds: targetEmbeds }); + } + firstMsg = false; + } + } catch (e) { + console.error("" + e); + message.reply({ + embeds: [embed("Something went wrong: " + e, "Command Error", EmbedColor.Error)], + }); + } + }, } as SimpleCommand; diff --git a/packages/bot/src/bot/commands/moderation/timeout.ts b/packages/bot/src/bot/commands/moderation/timeout.ts index 5f53950..a4bdc5c 100644 --- a/packages/bot/src/bot/commands/moderation/timeout.ts +++ b/packages/bot/src/bot/commands/moderation/timeout.ts @@ -2,81 +2,284 @@ import { client } from "../../.."; import CommandCategory from "../../../struct/commands/CommandCategory"; import SimpleCommand from "../../../struct/commands/SimpleCommand"; import MessageCommandContext from "../../../struct/MessageCommandContext"; -import { isModerator, NO_MANAGER_MSG, parseUser } from "../../util"; +import { dedupeArray, embed, EmbedColor, isModerator, NO_MANAGER_MSG, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util"; +import Infraction from "automod-lib/dist/types/antispam/Infraction"; +import InfractionType from "automod-lib/dist/types/antispam/InfractionType"; +import { fetchUsername, logModAction } from "../../modules/mod_logs"; +import { ulid } from "ulid"; +import type { SendableEmbed } from "stoat-api"; +import { User } from "stoat.js"; function parseTimeInput(input: string) { - if (!/([0-9]{1,3}[smhdwy])+/g.test(input)) return null; - - let pieces = input.match(/([0-9]{1,3}[smhdwy])/g) ?? []; - let res = 0; - - // Being able to specify the same letter multiple times - // (e.g. 1s1s) and having their values stack is a feature - for (const piece of pieces) { - let [num, letter] = [Number(piece.slice(0, piece.length - 1)), piece.slice(piece.length - 1)]; - let multiplier = 0; - - switch (letter) { - case "s": - multiplier = 1000; - break; - case "m": - multiplier = 1000 * 60; - break; - case "h": - multiplier = 1000 * 60 * 60; - break; - case "d": - multiplier = 1000 * 60 * 60 * 24; - break; - case "w": - multiplier = 1000 * 60 * 60 * 24 * 7; - break; - case "y": - multiplier = 1000 * 60 * 60 * 24 * 365; - break; - } - - res += num * multiplier; - } - - return res; + if (!/([0-9]{1,3}[smhdwy])+/g.test(input)) return null; + + let pieces = input.match(/([0-9]{1,3}[smhdwy])/g) ?? []; + let res = 0; + + // Being able to specify the same letter multiple times + // (e.g. 1s1s) and having their values stack is a feature + for (const piece of pieces) { + let [num, letter] = [Number(piece.slice(0, piece.length - 1)), piece.slice(piece.length - 1)]; + let multiplier = 0; + + switch (letter) { + case "s": + multiplier = 1000; + break; + case "m": + multiplier = 1000 * 60; + break; + case "h": + multiplier = 1000 * 60 * 60; + break; + case "d": + multiplier = 1000 * 60 * 60 * 24; + break; + case "w": + multiplier = 1000 * 60 * 60 * 24 * 7; + break; + case "y": + multiplier = 1000 * 60 * 60 * 24 * 365; + break; + } + + res += num * multiplier; + } + + return res; } export default { - name: "timeout", - aliases: ["mute", "silence"], - description: "Sets a timeout on a user, making them unable to send messages for a given duration.", - documentation: "/moderation/timeout", - category: CommandCategory.Moderation, - run: async (message: MessageCommandContext, args: string[]) => { - try { - if (!(await isModerator(message))) return await message.reply(NO_MANAGER_MSG); - - const target = await parseUser(args[0] ?? ""); - if (!target) return await message.reply("No user provided or provided user is not valid"); - - const duration = parseTimeInput(args[1] ?? ""); - if (!duration) { - await client.api.patch( - `/servers/${message.serverContext.id}/members/${target.id}` as "/servers/{server}/members/{target}", - { - timeout: new Date(0).toISOString(), - } as any, - ); - await message.reply(`Timeout cleared on @${target.username}`); - } else { - await client.api.patch( - `/servers/${message.serverContext.id}/members/${target.id}` as "/servers/{server}/members/{target}", - { - timeout: new Date(Date.now() + duration).toISOString(), - } as any, - ); - await message.reply(`Successfully timed out @${target.username}`); + name: "timeout", + aliases: ["mute", "silence"], + description: "Sets a timeout on a user, making them unable to send messages for a given duration.", + documentation: "/moderation/timeout", + category: CommandCategory.Moderation, + run: async (message: MessageCommandContext, args: string[]) => { + try { + if (!(await isModerator(message))) return await message.reply(NO_MANAGER_MSG); + + // Multi Users + const userInput = !message.replyIds?.length ? args.shift() || "" : undefined; + if (!userInput && !message.replyIds?.length) { + return message.reply({ + embeds: [ + embed( + `Please specify one or more users by replying to their messages or by providing a comma-separated list of usernames/IDs.`, + "No target user specified", + EmbedColor.SoftError, + ), + ], + }); + } + + // Time + let durationInput = args.shift(); + const duration = durationInput ? parseTimeInput(durationInput) : null; + // Reason + let reason = args.join(" ")?.replace(new RegExp("`", "g"), "'")?.replace(new RegExp("\n", "g"), " ") || "No reason provided"; + + if (reason.length > 500) + return message.reply({ + embeds: [embed("Timeout reason may not exceed 500 characters.", null, EmbedColor.SoftError)], + }); + + const embeds: SendableEmbed[] = []; + const handledUsers: string[] = []; + const targetUsers: (User | { id: string })[] = []; + + // Build user lists + const targetInput = dedupeArray( + message.replyIds?.length + ? (await Promise.allSettled(message.replyIds.map((msg) => message.channel?.fetchMessage(msg)))) + .filter((m) => m.status == "fulfilled") + .map((m) => (m as any).value.authorId) + : userInput!.split(","), + ); + + for (const userStr of targetInput) { + try { + let user = await parseUserOrId(userStr); + if (!user) { + embeds.push( + embed( + `I can't resolve \`${sanitizeMessageContent(userStr).trim()}\` to a user.`, + null, + EmbedColor.SoftError, + ), + ); + continue; + } + + if (handledUsers.includes(user.id)) continue; + handledUsers.push(user.id); + + // Check + if (user.id == message.authorId) { + embeds.push(embed("You cannot timeout yourself.", null, EmbedColor.Warning)); + continue; + } + if (user.id == client.user!.id) { + embeds.push(embed("You cannot timeout the bot.", null, EmbedColor.Warning)); + continue; + } + + targetUsers.push(user); + } catch (e) { + console.error(e); + embeds.push( + embed( + `Failed to resolve target \`${sanitizeMessageContent(userStr).trim()}\`: ${e}`, + "Failed to resolve user", + EmbedColor.Error, + ), + ); + } } - } catch (e) { - console.error("" + e); - message.reply("Something went wrong: " + e); - } - }, + + if (targetUsers.length === 0) { + if (embeds.length > 0) { + let firstMsg = true; + const embedsToSend = [...embeds]; + while (embedsToSend.length > 0) { + const targetEmbeds = embedsToSend.splice(0, 10); + if (firstMsg) { + await message.reply({ embeds: targetEmbeds, content: "Operation completed with errors." }, false); + } else { + await message.channel?.sendMessage({ embeds: targetEmbeds }); + } + firstMsg = false; + } + return; + } + return await message.reply({ + embeds: [embed("No valid users were specified to timeout.", null, EmbedColor.SoftError)], + }); + } + + // Timeout for each users + for (const user of targetUsers) { + try { + const infractionId = ulid(); + + if (duration === null) { + // Timeout Clear + await client.api.patch( + `/servers/${message.serverContext.id}/members/${user.id}` as "/servers/{server}/members/{target}", + { + timeout: new Date(0).toISOString(), + } as any, + ); + + // Log + await logModAction( + "timeout", + message.serverContext, + message.member!, + user.id, + reason, + infractionId, + "Timeout cleared." + ); + + embeds.push({ + title: `Timeout cleared`, + colour: EmbedColor.Success, + description: `Timeout cleared for <@${user.id}> (\`${user.id}\`)`, + }); + } else { + // Create Record + const infraction: Infraction = { + _id: infractionId, + createdBy: message.authorId!, + date: Date.now(), + reason: reason || "No reason provided", + server: message.serverContext.id, + type: InfractionType.Manual, + user: user.id, + actionType: "timeout", // Mark as timeout + }; + + // Store Record + const { userWarnCount } = await storeInfraction(infraction); + + // Timeout Set + await client.api.patch( + `/servers/${message.serverContext.id}/members/${user.id}` as "/servers/{server}/members/{target}", + { + timeout: new Date(Date.now() + duration).toISOString(), + } as any, + ); + + // MS --> READABLE + const durationMs = duration; + const days = Math.floor(durationMs / (1000 * 60 * 60 * 24)); + const hours = Math.floor((durationMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((durationMs % (1000 * 60)) / 1000); + let durationStr = ""; + if (days > 0) durationStr += `${days}d `; + if (hours > 0) durationStr += `${hours}h `; + if (minutes > 0) durationStr += `${minutes}m `; + if (seconds > 0 || durationStr === "") durationStr += `${seconds}s`; + + // Log + await logModAction( + "timeout", + message.serverContext, + message.member!, + user.id, + reason + (durationInput ? ` (${durationInput})` : ""), + infractionId, + `Timeout duration: **${durationStr.trim()}**` + ); + + embeds.push({ + title: `User timed out`, + colour: EmbedColor.Success, + description: + `This is ${userWarnCount == 1 ? "**the first infraction**" : `infraction number **${userWarnCount}**`} for ${await fetchUsername(user.id)}.\n` + + `**Timeout duration:** ${durationStr.trim()}\n` + + `**User ID:** \`${user.id}\`\n` + + `**Infraction ID:** \`${infractionId}\`\n` + + `**Reason:** \`${infraction.reason}\``, + }); + } + } catch (e: any) { + console.error("" + e); + embeds.push( + embed( + `Failed to timeout <@${user.id}>: ${e.message || e}`, + "Failed to timeout user", + EmbedColor.Error, + ), + ); + } + } + + // Send all results + let firstMsg = true; + const embedsToSend = [...embeds]; + while (embedsToSend.length > 0) { + const targetEmbeds = embedsToSend.splice(0, 10); + if (firstMsg) { + await message.reply( + { + embeds: targetEmbeds, + content: `Operation completed. ${duration === null ? "Timeouts cleared." : "Timeouts set."}`, + }, + false, + ); + } else { + await message.channel?.sendMessage({ embeds: targetEmbeds }); + } + firstMsg = false; + } + } catch (e) { + console.error("" + e); + message.reply({ + embeds: [embed("Something went wrong: " + e, "Command Error", EmbedColor.Error)], + }); + } + }, } as SimpleCommand; diff --git a/packages/bot/src/bot/modules/mod_logs.ts b/packages/bot/src/bot/modules/mod_logs.ts index bce10af..71549b6 100644 --- a/packages/bot/src/bot/modules/mod_logs.ts +++ b/packages/bot/src/bot/modules/mod_logs.ts @@ -126,37 +126,66 @@ client.on("messageDeleteBulk", async (messages) => { }); async function logModAction( - type: "warn" | "kick" | "ban" | "votekick", - server: Server, - mod: ServerMember, - target: string, - reason: string | null, - infractionID: string, - extraText?: string, + // Added Timeout + type: "warn" | "kick" | "ban" | "votekick" | "timeout", + server: Server, + mod: ServerMember, + target: string, + reason: string | null, + infractionID: string, + extraText?: string, ): Promise { - try { - let config = await dbs.SERVERS.findOne({ id: server.id }); - - if (config?.logs?.modAction) { - let aType = type == "ban" ? "banned" : type + "ed"; - let embedColor = "#0576ff"; - if (type == "kick") embedColor = "#ff861d"; - if (type == "ban") embedColor = "#ff2f05"; - - sendLogMessage(config.logs.modAction, { - title: `User ${aType}`, - description: - `\`@${mod.user?.username}\` **${aType}** \`` + - `${await fetchUsername(target)}\`${type == "warn" ? "." : ` from ${server.name}.`}\n` + - `**Reason**: \`${reason ? reason : "No reason provided."}\`\n` + - `**Warn ID**: \`${infractionID}\`\n` + - (extraText ?? ""), - color: embedColor, - }); - } - } catch (e) { - console.error(e); - } + try { + let config = await dbs.SERVERS.findOne({ id: server.id }); + + if (config?.logs?.modAction) { + let aType: string; + let embedColor: string; + + switch (type) { + case "warn": + aType = "warned"; + embedColor = "#0576ff"; // Blue + break; + case "kick": + aType = "kicked"; + embedColor = "#ff861d"; // Orange + break; + case "ban": + aType = "banned"; + embedColor = "#ff2f05"; // Red + break; + case "votekick": + aType = "votekicked"; + embedColor = "#ffaa00"; // VOTEKICKED??? + break; + case "timeout": + // Description for timeout + aType = "timed out"; + embedColor = "#9c42f5"; // Purple + break; + default: + aType = "acted upon"; + embedColor = "#cccccc"; + } + + // Generate sending texts, and generate extra text for time out + let description = `\`@${mod.user?.username}\` **${aType}** \`` + + `${await fetchUsername(target)}\`${["warn", "timeout"].includes(type) ? "." : ` from ${server.name}.`}\n` + + `**Reason**: \`${reason ? reason : "No reason provided."}\`\n` + + `**Infraction ID**: \`${infractionID}\`\n` + // ID + (extraText ? extraText : ""); + + // Send + sendLogMessage(config.logs.modAction, { + title: `User ${aType}`, // Such as “User timed out” + description: description, + color: embedColor, + }); + } + } catch (e) { + console.error(e); + } } let fetchUsername = async (id: string, fallbackText?: string) => { diff --git a/packages/lib/src/types/antispam/Infraction.ts b/packages/lib/src/types/antispam/Infraction.ts index 54f6f14..031c671 100644 --- a/packages/lib/src/types/antispam/Infraction.ts +++ b/packages/lib/src/types/antispam/Infraction.ts @@ -3,7 +3,7 @@ import InfractionType from "./InfractionType"; class Infraction { _id: string; type: InfractionType; - actionType?: "kick" | "ban"; + actionType?: "kick" | "ban" | "timeout"; // Added timeout user: string; createdBy: string | null; server: string;