From fb01ce57400130c93751a11573eb444c0ba103eb Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Mon, 24 Jun 2024 20:48:04 +0700 Subject: [PATCH] feat(bots/discord/commands): add `purge` and `role-preset` commands --- bots/discord/src/commands/moderation/purge.ts | 73 ++++++++++++++++ .../src/commands/moderation/role-preset.ts | 84 +++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 bots/discord/src/commands/moderation/purge.ts create mode 100644 bots/discord/src/commands/moderation/role-preset.ts diff --git a/bots/discord/src/commands/moderation/purge.ts b/bots/discord/src/commands/moderation/purge.ts new file mode 100644 index 0000000..b1eb0e4 --- /dev/null +++ b/bots/discord/src/commands/moderation/purge.ts @@ -0,0 +1,73 @@ +import { EmbedBuilder, GuildChannel, SlashCommandBuilder } from 'discord.js' + +import CommandError, { CommandErrorType } from '$/classes/CommandError' +import { config } from '$/context' +import { applyCommonEmbedStyles } from '$/utils/discord/embeds' + +import type { Command } from '..' + +export default { + data: new SlashCommandBuilder() + .setName('purge') + .setDescription('Purge messages from a channel') + .addIntegerOption(option => + option.setName('amount').setDescription('The amount of messages to remove').setMaxValue(100).setMinValue(1), + ) + .addUserOption(option => + option.setName('user').setDescription('The user to remove messages from (needs `until`)'), + ) + .addStringOption(option => + option.setName('until').setDescription('The message ID to remove messages until (overrides `amount`)'), + ) + .toJSON(), + + memberRequirements: { + roles: config.moderation?.roles ?? [], + }, + + global: false, + + async execute({ logger }, interaction) { + const amount = interaction.options.getInteger('amount') + const user = interaction.options.getUser('user') + const until = interaction.options.getString('until') + + if (!amount && !until) + throw new CommandError(CommandErrorType.MissingArgument, 'Either `amount` or `until` must be provided.') + + const channel = interaction.channel! + if (!(channel.isTextBased() && channel instanceof GuildChannel)) + throw new CommandError(CommandErrorType.InvalidChannel, 'The supplied channel is not a text channel.') + + const embed = applyCommonEmbedStyles( + new EmbedBuilder({ + title: 'Purging messages', + description: 'Accumulating messages...', + }), + true, + true, + true, + ) + + const msgsPromise = channel.messages.fetch(until ? { after: until } : { limit: amount! }) + const reply = await interaction.reply({ embeds: [embed] }).then(it => it.fetch()) + + const messages = ( + user ? (await msgsPromise).filter(msg => msg.author.id === user.id) : await msgsPromise + ).filter(msg => msg.id !== reply.id) + + await channel.bulkDelete(messages, true) + + logger.info( + `Moderator ${interaction.user.tag} (${interaction.user.id}) purged ${messages.size} messages in #${channel.name} (${channel.id})`, + ) + await reply.edit({ + embeds: [ + embed.setTitle('Purged messages').setDescription(null).addFields({ + name: 'Deleted messages', + value: messages.size.toString(), + }), + ], + }) + }, +} satisfies Command diff --git a/bots/discord/src/commands/moderation/role-preset.ts b/bots/discord/src/commands/moderation/role-preset.ts new file mode 100644 index 0000000..db7b7cd --- /dev/null +++ b/bots/discord/src/commands/moderation/role-preset.ts @@ -0,0 +1,84 @@ +import { SlashCommandBuilder } from 'discord.js' + +import CommandError, { CommandErrorType } from '$/classes/CommandError' +import { sendPresetReplyAndLogs } from '$/utils/discord/moderation' +import { applyRolePreset, removeRolePreset } from '$/utils/discord/rolePresets' +import { parseDuration } from '$/utils/duration' +import type { Command } from '..' + +export default { + data: new SlashCommandBuilder() + .setName('role-preset') + .setDescription('Manage role presets for a member') + .addStringOption(option => + option + .setName('action') + .setRequired(true) + .setDescription('The action to perform') + .addChoices([ + { name: 'apply', value: 'apply' }, + { name: 'remove', value: 'remove' }, + ]), + ) + .addUserOption(option => option.setName('member').setRequired(true).setDescription('The member to manage')) + .addStringOption(option => + option.setName('preset').setRequired(true).setDescription('The preset to apply or remove'), + ) + .addStringOption(option => + option.setName('duration').setDescription('The duration to apply the preset for (only for apply action)'), + ) + .toJSON(), + + memberRequirements: { + roles: ['955220417969262612', '973886585294704640'], + }, + + global: false, + + async execute({ logger }, interaction) { + const action = interaction.options.getString('action', true) as 'apply' | 'remove' + const user = interaction.options.getUser('member', true) + const preset = interaction.options.getString('preset', true) + const duration = interaction.options.getString('duration') + + let expires: number | null | undefined = undefined + const moderator = await interaction.guild!.members.fetch(interaction.user.id) + const member = await interaction.guild!.members.fetch(user.id) + if (!member) + throw new CommandError( + CommandErrorType.InvalidUser, + 'The provided member is not in the server or does not exist.', + ) + + if (member.manageable) + throw new CommandError(CommandErrorType.Generic, 'This user cannot be managed by the bot.') + + if (action === 'apply') { + const durationMs = duration ? parseDuration(duration) : null + if (Number.isInteger(durationMs) && durationMs! < 1) + throw new CommandError( + CommandErrorType.InvalidDuration, + 'The duration must be at least 1 millisecond long.', + ) + + if (moderator.roles.highest.comparePositionTo(member.roles.highest) <= 0) + throw new CommandError( + CommandErrorType.InvalidUser, + 'You cannot apply a role preset to a user with a role equal to or higher than yours.', + ) + + expires = durationMs ? Date.now() + durationMs : null + await applyRolePreset(member, preset, expires) + logger.info( + `Moderator ${interaction.user.tag} (${interaction.user.id}) applied role preset ${preset} to ${user.id} until ${expires}`, + ) + } else if (action === 'remove') { + await removeRolePreset(member, preset) + logger.info( + `Moderator ${interaction.user.tag} (${interaction.user.id}) removed role preset ${preset} from ${user.id}`, + ) + } + + await sendPresetReplyAndLogs(action, interaction, user, preset, expires) + }, +} satisfies Command