diff --git a/bot/src/bot/commands/warn.ts b/bot/src/bot/commands/warn.ts index baf2479..e75d174 100644 --- a/bot/src/bot/commands/warn.ts +++ b/bot/src/bot/commands/warn.ts @@ -1,11 +1,13 @@ import SimpleCommand from "../../struct/commands/SimpleCommand"; -import { isModerator, NO_MANAGER_MSG, parseUserOrId, storeInfraction } from "../util"; +import { dedupeArray, embed, EmbedColor, isModerator, NO_MANAGER_MSG, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../util"; import Infraction from "../../struct/antispam/Infraction"; import { ulid } from "ulid"; import InfractionType from "../../struct/antispam/InfractionType"; import { fetchUsername, logModAction } from "../modules/mod_logs"; import MessageCommandContext from "../../struct/MessageCommandContext"; import CommandCategory from "../../struct/commands/CommandCategory"; +import { SendableEmbed } from "revolt-api"; +import { User } from "@janderedev/revolt.js"; export default { name: 'warn', @@ -15,36 +17,114 @@ export default { category: CommandCategory.Moderation, run: async (message: MessageCommandContext, args: string[]) => { if (!await isModerator(message)) return message.reply(NO_MANAGER_MSG); - let user = await parseUserOrId(args.shift() ?? ''); - if (!user) return message.reply('I can\'t find that user.'); - if ((user as any)?.bot != null) return message.reply('You cannot warn bots.'); - let reason: string = args.join(' ') + const userInput = args.shift(); + if (!userInput) return message.reply({ embeds: [ + embed( + `Please specify one or more users by replying to their message while running this command or ` + + `by specifying a comma-separated list of usernames.`, + 'No target user specified', + EmbedColor.SoftError, + ), + ] }); + + let reason = args.join(' ') ?.replace(new RegExp('`', 'g'), '\'') - ?.replace(new RegExp('\n', 'g'), ' ') - || 'No reason provided.'; - if (reason.length > 200) return message.reply('Warn reason may not be longer than 200 characters.'); + ?.replace(new RegExp('\n', 'g'), ' '); - let infraction = { - _id: ulid(), - createdBy: message.author_id, - user: user._id, - reason: reason, - server: message.serverContext._id, - type: InfractionType.Manual, - date: Date.now(), - } as Infraction; + if (reason.length > 200) return message.reply({ + embeds: [ embed('Warn reason may not be longer than 200 characters.', null, EmbedColor.SoftError) ] + }); - let { userWarnCount } = await storeInfraction(infraction); + const embeds: SendableEmbed[] = []; + const handledUsers: string[] = []; + const targetUsers: User|{ _id: string }[] = []; - await Promise.all([ - message.reply(`### User warned` - + `${message.serverContext._id != message.channel?.server_id ? ` in **${message.serverContext.name}**` : ''}.\n` - + `This is ${userWarnCount == 1 ? '**the first warn**' : `warn number **${userWarnCount}**`}` - + ` for ${await fetchUsername(user._id)}.\n` - + `**Infraction ID:** \`${infraction._id}\`\n` - + `**Reason:** \`${infraction.reason}\``), - logModAction('warn', message.serverContext, message.member!, user._id, reason, infraction._id, `This is warn number ${userWarnCount} for this user.`), - ]); + const targetInput = dedupeArray( + // Replied messages + (await Promise.allSettled( + (message.reply_ids ?? []).map(msg => message.channel?.fetchMessage(msg)) + )) + .filter(m => m.status == 'fulfilled').map(m => (m as any).value.author_id), + // Provided users + userInput.split(','), + ); + + for (const userStr of targetInput) { + try { + let user = await parseUserOrId(userStr); + if (!user) { + if (message.reply_ids?.length && userStr == userInput) { + reason = reason ? `${userInput} ${reason}` : userInput; + } + else { + embeds.push(embed(`I can't resolve \`${sanitizeMessageContent(userStr).trim()}\` to a user.`, null, '#ff785d')); + } + continue; + } + + // Silently ignore duplicates + if (handledUsers.includes(user._id)) continue; + handledUsers.push(user._id); + + if ((user as any)?.bot != null) return await message.reply({ embeds: [ + embed('You cannot warn bots.', null, EmbedColor.SoftError) + ]}); + + targetUsers.push(user); + } catch(e) { + console.error(e); + embeds.push(embed( + `Failed to warn target \`${sanitizeMessageContent(userStr).trim()}\`: ${e}`, + 'Failed to warn: An error has occurred', + EmbedColor.Error, + )); + } + } + + for (const user of targetUsers) { + let infraction = { + _id: ulid(), + createdBy: message.author_id, + user: user._id, + reason: reason || 'No reason provided', + server: message.serverContext._id, + type: InfractionType.Manual, + date: Date.now(), + } as Infraction; + + let { userWarnCount } = await storeInfraction(infraction); + await logModAction( + 'warn', + message.serverContext, + message.member!, + user._id, + reason || 'No reason provided', + infraction._id, + `This is warn number ${userWarnCount} for this user.` + ); + + embeds.push({ + title: `User warned`, + icon_url: user instanceof User ? user.generateAvatarURL() : undefined, + colour: EmbedColor.Success, + description: `This is ${userWarnCount == 1 ? '**the first warn**' : `warn number **${userWarnCount}**`}` + + ` for ${await fetchUsername(user._id)}.\n` + + `**Infraction ID:** \`${infraction._id}\`\n` + + `**Reason:** \`${infraction.reason}\`` + }); + } + + let firstMsg = true; + while (embeds.length > 0) { + const targetEmbeds = embeds.splice(0, 10); + + if (firstMsg) { + await message.reply({ embeds: targetEmbeds, content: 'Operation completed.' }, false); + } else { + await message.channel?.sendMessage({ embeds: targetEmbeds }); + } + firstMsg = false; + } } } as SimpleCommand; diff --git a/bot/src/bot/util.ts b/bot/src/bot/util.ts index 69547c0..28ff56d 100644 --- a/bot/src/bot/util.ts +++ b/bot/src/bot/util.ts @@ -14,6 +14,7 @@ import { Channel } from "@janderedev/revolt.js/dist/maps/Channels"; import { Permission } from "@janderedev/revolt.js/dist/permissions/definitions"; import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { isSudo } from "./commands/botadm"; +import { SendableEmbed } from "revolt-api"; const NO_MANAGER_MSG = '🔒 Missing permission'; const ULID_REGEX = /^[0-9A-HJ-KM-NP-TV-Z]{26}$/i; @@ -318,6 +319,33 @@ function sanitizeMessageContent(msg: string): string { return str; } +enum EmbedColor { + Error = "#ff450c", + SoftError = "#ff785d", + Warning = "#ffda55", + Success = "#23ff91", +} + +function embed(content: string, title?: string|null, color?: string|EmbedColor): SendableEmbed { + return { + description: content, + title: title, + colour: color, + } +} + +function dedupeArray(...arrays: T[][]): T[] { + const found: T[] = []; + + for (const array of arrays) { + for (const item of array) { + if (!found.includes(item)) found.push(item); + } + } + + return found; +} + export { getAutumnURL, hasPerm, @@ -333,6 +361,9 @@ export { uploadFile, sanitizeMessageContent, sendLogMessage, + embed, + dedupeArray, + EmbedColor, NO_MANAGER_MSG, ULID_REGEX, USER_MENTION_REGEX,