From d988f5b2554f2c7ae05b79ef7f951c0d7bcd2c0d Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:58:47 -0400 Subject: [PATCH 1/8] enhancement: allow commands to take user IDs --- src/commands/ban.ts | 4 ++-- src/commands/kick.ts | 4 ++-- src/commands/warn.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 0dbebb2..0d76883 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -1,4 +1,4 @@ -import { EmbedBuilder, MessageMentions } from 'discord.js'; +import { EmbedBuilder } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; import { Ban } from '@/models/bans'; import { sendEventLogMessage, ordinal } from '@/util'; @@ -17,7 +17,7 @@ async function banHandler(interaction: ChatInputCommandInteraction): Promise match[1]))]; + const userIds = [...new Set(Array.from(users!.matchAll(new RegExp(/\d{17,18}/, 'g')), match => match[0]))]; const bansListEmbed = new EmbedBuilder(); bansListEmbed.setTitle('User Bans :thumbsdown:'); diff --git a/src/commands/kick.ts b/src/commands/kick.ts index 5d33d7e..9ec8dcd 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -1,4 +1,4 @@ -import { MessageMentions, EmbedBuilder } from 'discord.js'; +import { EmbedBuilder } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; import { Kick } from '@/models/kicks'; import { Ban } from '@/models/bans'; @@ -18,7 +18,7 @@ async function kickHandler(interaction: ChatInputCommandInteraction): Promise match[1]))]; + const userIds = [...new Set(Array.from(users.matchAll(new RegExp(/\d{17,18}/, 'g')), match => match[0]))]; const kicksListEmbed = new EmbedBuilder(); kicksListEmbed.setTitle('User Kicks :thumbsdown:'); diff --git a/src/commands/warn.ts b/src/commands/warn.ts index 9ac45ff..6a28ce5 100644 --- a/src/commands/warn.ts +++ b/src/commands/warn.ts @@ -1,4 +1,4 @@ -import { EmbedBuilder, MessageMentions } from 'discord.js'; +import { EmbedBuilder } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; import { Warning } from '@/models/warnings'; import { Kick } from '@/models/kicks'; @@ -18,7 +18,7 @@ async function warnHandler(interaction: ChatInputCommandInteraction): Promise match[1]))]; + const userIds = [...new Set(Array.from(users.matchAll(new RegExp(/\d{17,18}/, 'g')), match => match[0]))]; const warningListEmbed = new EmbedBuilder(); warningListEmbed.setTitle('User Warnings :thumbsdown:'); From c93636ee70381db2867c9ffabd1a57ab3bcbc6ac Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:22:27 -0400 Subject: [PATCH 2/8] chore: add global flag to regex --- src/commands/ban.ts | 2 +- src/commands/kick.ts | 2 +- src/commands/warn.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 0d76883..43cc37f 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -17,7 +17,7 @@ async function banHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + const userIds = [...new Set(Array.from(users!.matchAll(new RegExp(/\d{17,18}/g)), match => match[0]))]; const bansListEmbed = new EmbedBuilder(); bansListEmbed.setTitle('User Bans :thumbsdown:'); diff --git a/src/commands/kick.ts b/src/commands/kick.ts index 9ec8dcd..52bc25b 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -18,7 +18,7 @@ async function kickHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + const userIds = [...new Set(Array.from(users.matchAll(new RegExp(/\d{17,18}/g)), match => match[0]))]; const kicksListEmbed = new EmbedBuilder(); kicksListEmbed.setTitle('User Kicks :thumbsdown:'); diff --git a/src/commands/warn.ts b/src/commands/warn.ts index 6a28ce5..1ec22a4 100644 --- a/src/commands/warn.ts +++ b/src/commands/warn.ts @@ -18,7 +18,7 @@ async function warnHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + const userIds = [...new Set(Array.from(users.matchAll(new RegExp(/\d{17,18}/g)), match => match[0]))]; const warningListEmbed = new EmbedBuilder(); warningListEmbed.setTitle('User Warnings :thumbsdown:'); From 85511ea486a943f77f6a99167de20f3d1258f1b7 Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:21:27 -0400 Subject: [PATCH 3/8] enhancement: separate single and multi-user commands --- src/commands/ban.ts | 71 +++++++++++++++++++++++++++----------------- src/commands/kick.ts | 71 +++++++++++++++++++++++++++----------------- src/commands/warn.ts | 47 ++++++++++++++++++++++------- src/util.ts | 13 +++++++- 4 files changed, 137 insertions(+), 65 deletions(-) diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 43cc37f..2d5ed84 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -1,7 +1,7 @@ import { EmbedBuilder } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; import { Ban } from '@/models/bans'; -import { sendEventLogMessage, ordinal } from '@/util'; +import { banMessageDeleteChoices, sendEventLogMessage, ordinal } from '@/util'; import { untrustUser } from '@/leveling'; import { notifyUser } from '@/notifications'; import type { ChatInputCommandInteraction } from 'discord.js'; @@ -13,11 +13,20 @@ async function banHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + let userIds; + if (subcommand === 'user') { + const user = interaction.options.getUser('user', true); + userIds = [user.id]; + } else if (subcommand === 'multiuser') { + const users = interaction.options.getString('users', true); + userIds = [...new Set(Array.from(users.matchAll(new RegExp(/\d{17,18}/g)), match => match[0]))]; + } else { + throw new Error(`Unknown ban subcommand: ${subcommand}`); + } const bansListEmbed = new EmbedBuilder(); bansListEmbed.setTitle('User Bans :thumbsdown:'); @@ -164,30 +173,38 @@ const command = new SlashCommandBuilder() .setDefaultMemberPermissions('0') .setName('ban') .setDescription('Ban user(s)') - .addStringOption(option => { - return option.setName('users') - .setDescription('User(s) to ban') - .setRequired(true); - }) - .addStringOption(option => { - return option.setName('reason') - .setDescription('Reason for the ban') - .setRequired(true); - }) - .addNumberOption(option => { - return option.setName('delete_messages') - .setDescription('How much of their recent message history to delete') - .addChoices( - { name: 'Previous 30 Minutes', value: 30 * 60 }, - { name: 'Previous Hour', value: 60 * 60 }, - { name: 'Previous 3 Hours', value: 3 * 60 * 60 }, - { name: 'Previous 6 Hours', value: 6 * 60 * 60 }, - { name: 'Previous 12 Hours', value: 12 * 60 * 60 }, - { name: 'Previous Day', value: 24 * 60 * 60 }, - { name: 'Previous 3 Days', value: 3 * 24 * 60 * 60 }, - { name: 'Previous Week', value: 7 * 24 * 60 * 60 }, - ); - }); + .addSubcommand(subcommand => + subcommand.setName('user') + .setDescription('Ban a single user') + .addUserOption(option => + option.setName('user') + .setDescription('User to ban') + .setRequired(true)) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for the ban') + .setRequired(true)) + .addNumberOption(option => + option.setName('delete_messages') + .setDescription('How much of their recent message history to delete') + .addChoices(banMessageDeleteChoices)) + ) + .addSubcommand(subcommand => + subcommand.setName('multiuser') + .setDescription('Ban multiple users') + .addStringOption(option => + option.setName('users') + .setDescription('User(s) to ban') + .setRequired(true)) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for the ban') + .setRequired(true)) + .addNumberOption(option => + option.setName('delete_messages') + .setDescription('How much of their recent message history to delete') + .addChoices(banMessageDeleteChoices)) + ); export default { name: command.name, diff --git a/src/commands/kick.ts b/src/commands/kick.ts index 52bc25b..0f11de0 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -2,7 +2,7 @@ import { EmbedBuilder } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; import { Kick } from '@/models/kicks'; import { Ban } from '@/models/bans'; -import { ordinal, sendEventLogMessage } from '@/util'; +import { banMessageDeleteChoices, ordinal, sendEventLogMessage } from '@/util'; import { untrustUser } from '@/leveling'; import { notifyUser } from '@/notifications'; import type { ChatInputCommandInteraction } from 'discord.js'; @@ -14,11 +14,20 @@ async function kickHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + let userIds; + if (subcommand === 'user') { + const user = interaction.options.getUser('user', true); + userIds = [user.id]; + } else if (subcommand === 'multiuser') { + const users = interaction.options.getString('users', true); + userIds = [...new Set(Array.from(users.matchAll(new RegExp(/\d{17,18}/g)), match => match[0]))]; + } else { + throw new Error(`Unknown kick subcommand: ${subcommand}`); + } const kicksListEmbed = new EmbedBuilder(); kicksListEmbed.setTitle('User Kicks :thumbsdown:'); @@ -226,30 +235,38 @@ const command = new SlashCommandBuilder() .setDefaultMemberPermissions('0') .setName('kick') .setDescription('Kick user(s)') - .addStringOption(option => { - return option.setName('users') - .setDescription('User(s) to kick') - .setRequired(true); - }) - .addStringOption(option => { - return option.setName('reason') - .setDescription('Reason for the kick') - .setRequired(true); - }) - .addNumberOption(option => { - return option.setName('delete_messages') - .setDescription('How much of their recent message history to delete') - .addChoices( - { name: 'Previous 30 Minutes', value: 30 * 60 }, - { name: 'Previous Hour', value: 60 * 60 }, - { name: 'Previous 3 Hours', value: 3 * 60 * 60 }, - { name: 'Previous 6 Hours', value: 6 * 60 * 60 }, - { name: 'Previous 12 Hours', value: 12 * 60 * 60 }, - { name: 'Previous Day', value: 24 * 60 * 60 }, - { name: 'Previous 3 Days', value: 3 * 24 * 60 * 60 }, - { name: 'Previous Week', value: 7 * 24 * 60 * 60 }, - ); - }); + .addSubcommand(subcommand => + subcommand.setName('user') + .setDescription('Kick user') + .addUserOption(option => + option.setName('user') + .setDescription('User to kick') + .setRequired(true)) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for the kick') + .setRequired(true)) + .addNumberOption(option => + option.setName('delete_messages') + .setDescription('How much of their recent message history to delete') + .addChoices(banMessageDeleteChoices)) + ) + .addSubcommand(subcommand => + subcommand.setName('multiuser') + .setDescription('Kick multiple users') + .addStringOption(option => + option.setName('users') + .setDescription('User(s) to kick') + .setRequired(true)) + .addStringOption(option => + option.setName('reason') + .setDescription('Reason for the kick') + .setRequired(true)) + .addNumberOption(option => + option.setName('delete_messages') + .setDescription('How much of their recent message history to delete') + .addChoices(banMessageDeleteChoices)) + ); export default { name: command.name, diff --git a/src/commands/warn.ts b/src/commands/warn.ts index 1ec22a4..9609819 100644 --- a/src/commands/warn.ts +++ b/src/commands/warn.ts @@ -15,10 +15,19 @@ async function warnHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + let userIds; + if (subcommand === 'user') { + const user = interaction.options.getUser('user', true); + userIds = [user.id]; + } else if (subcommand === 'multiuser') { + const users = interaction.options.getString('users', true); + userIds = [...new Set(Array.from(users.matchAll(new RegExp(/\d{17,18}/g)), match => match[0]))]; + } else { + throw new Error(`Unknown warn subcommand: ${subcommand}`); + } const warningListEmbed = new EmbedBuilder(); warningListEmbed.setTitle('User Warnings :thumbsdown:'); @@ -261,15 +270,33 @@ const command = new SlashCommandBuilder() .setDefaultMemberPermissions('0') .setName('warn') .setDescription('Warn user(s)') - .addStringOption(option => { - return option.setName('users') - .setDescription('User(s) to warn') - .setRequired(true); + .addSubcommand(subcommand => { + return subcommand.setName('user') + .setDescription('Warn a user') + .addUserOption(option => { + return option.setName('user') + .setDescription('User to warn') + .setRequired(true); + }) + .addStringOption(option => { + return option.setName('reason') + .setDescription('Reason for the warning') + .setRequired(true); + }); }) - .addStringOption(option => { - return option.setName('reason') - .setDescription('Reason for the warning') - .setRequired(true); + .addSubcommand(subcommand => { + return subcommand.setName('multiuser') + .setDescription('Warn multiple users') + .addStringOption(option => { + return option.setName('users') + .setDescription('User(s) to warn') + .setRequired(true); + }) + .addStringOption(option => { + return option.setName('reason') + .setDescription('Reason for the warning') + .setRequired(true); + }); }); export default { diff --git a/src/util.ts b/src/util.ts index 67080bb..f46b8be 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,6 @@ import { getDB, getDBList } from '@/db'; import { ChannelType } from 'discord.js'; -import type { Channel, EmbedBuilder, Guild, Role, Message } from 'discord.js'; +import type { Channel, EmbedBuilder, Guild, Role, Message, APIApplicationCommandOptionChoice } from 'discord.js'; const ordinalRules = new Intl.PluralRules('en', { type: 'ordinal' @@ -13,6 +13,17 @@ const suffixes: Record = { other: 'th' }; +export const banMessageDeleteChoices: Array> = [ + { name: 'Previous 30 Minutes', value: 30 * 60 }, + { name: 'Previous Hour', value: 60 * 60 }, + { name: 'Previous 3 Hours', value: 3 * 60 * 60 }, + { name: 'Previous 6 Hours', value: 6 * 60 * 60 }, + { name: 'Previous 12 Hours', value: 12 * 60 * 60 }, + { name: 'Previous Day', value: 24 * 60 * 60 }, + { name: 'Previous 3 Days', value: 3 * 24 * 60 * 60 }, + { name: 'Previous Week', value: 7 * 24 * 60 * 60 }, +]; + export function ordinal(number: number): string { const category = ordinalRules.select(number); const suffix = suffixes[category]; From de372fcde7dc4c4f8ffc595221cfce1f4a831833 Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:59:35 -0400 Subject: [PATCH 4/8] enhancement: add context menus to commands --- src/commands/ban.ts | 25 ++++++++------ src/commands/kick.ts | 25 ++++++++------ src/commands/warn.ts | 25 ++++++++------ src/context-menus/users/ban.ts | 58 +++++++++++++++++++++++++++++++++ src/context-menus/users/kick.ts | 58 +++++++++++++++++++++++++++++++++ src/context-menus/users/warn.ts | 58 +++++++++++++++++++++++++++++++++ src/events/interactionCreate.ts | 4 +-- src/events/ready.ts | 6 ++++ 8 files changed, 227 insertions(+), 32 deletions(-) create mode 100644 src/context-menus/users/ban.ts create mode 100644 src/context-menus/users/kick.ts create mode 100644 src/context-menus/users/warn.ts diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 61f317e..8665b29 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -4,15 +4,9 @@ import { Ban } from '@/models/bans'; import { banMessageDeleteChoices, sendEventLogMessage, ordinal } from '@/util'; import { untrustUser } from '@/leveling'; import { notifyUser } from '@/notifications'; -import type { ChatInputCommandInteraction } from 'discord.js'; +import type { ChatInputCommandInteraction, CommandInteraction, ModalSubmitInteraction } from 'discord.js'; -async function banHandler(interaction: ChatInputCommandInteraction): Promise { - await interaction.deferReply({ - ephemeral: true - }); - - const guild = await interaction.guild!.fetch(); - const executor = interaction.user; +async function banCommandHandler(interaction: ChatInputCommandInteraction): Promise { const subcommand = interaction.options.getSubcommand(); const reason = interaction.options.getString('reason', true); const deleteMessages = interaction.options.getNumber('delete_messages'); @@ -28,6 +22,17 @@ async function banHandler(interaction: ChatInputCommandInteraction): Promise { + await interaction.deferReply({ + ephemeral: true + }); + + const guild = await interaction.guild!.fetch(); + const executor = interaction.user; + const bansListEmbed = new EmbedBuilder(); bansListEmbed.setTitle('User Bans :thumbsdown:'); bansListEmbed.setColor(0xFFA500); @@ -166,7 +171,7 @@ async function banHandler(interaction: ChatInputCommandInteraction): Promise { - await interaction.deferReply({ - ephemeral: true - }); - - const guild = await interaction.guild!.fetch(); - const executor = interaction.user; +async function kickCommandHandler(interaction: ChatInputCommandInteraction): Promise { const subcommand = interaction.options.getSubcommand(); const reason = interaction.options.getString('reason', true); const deleteMessages = interaction.options.getNumber('delete_messages'); @@ -29,6 +23,17 @@ async function kickHandler(interaction: ChatInputCommandInteraction): Promise { + await interaction.deferReply({ + ephemeral: true + }); + + const guild = await interaction.guild!.fetch(); + const executor = interaction.user; + const kicksListEmbed = new EmbedBuilder(); kicksListEmbed.setTitle('User Kicks :thumbsdown:'); kicksListEmbed.setColor(0xFFA500); @@ -228,7 +233,7 @@ async function kickHandler(interaction: ChatInputCommandInteraction): Promise { - await interaction.deferReply({ - ephemeral: true - }); - - const guild = await interaction.guild!.fetch(); - const executor = interaction.user; +async function warnCommandHandler(interaction: ChatInputCommandInteraction): Promise { const subcommand = interaction.options.getSubcommand(); const reason = interaction.options.getString('reason', true); @@ -29,6 +23,17 @@ async function warnHandler(interaction: ChatInputCommandInteraction): Promise { + await interaction.deferReply({ + ephemeral: true + }); + + const guild = await interaction.guild!.fetch(); + const executor = interaction.user; + const warningListEmbed = new EmbedBuilder(); warningListEmbed.setTitle('User Warnings :thumbsdown:'); warningListEmbed.setColor(0xFFA500); @@ -263,7 +268,7 @@ async function warnHandler(interaction: ChatInputCommandInteraction): Promise { + const target = interaction.targetUser; + + const reasonInput = new TextInputBuilder() + .setCustomId('banReason') + .setLabel('Reason') + .setRequired(true) + .setStyle(TextInputStyle.Short); + + const reasonActionRow = new ActionRowBuilder() + .addComponents(reasonInput); + + const banModal = new ModalBuilder() + .setCustomId('banModal') + .setTitle(`Ban ${target.tag}`) + .addComponents(reasonActionRow); + + await interaction.showModal(banModal); + + let modalSubmitInteraction; + try { + modalSubmitInteraction = await interaction.awaitModalSubmit({ + time: 5 * 60 * 1000, + filter: modalSubmitInteraction => + modalSubmitInteraction.customId === 'banModal' && + modalSubmitInteraction.user.id === interaction.user.id + }); + } catch { + // * The user dismissed the modal or took too long to respond + return; + } + + const reason = modalSubmitInteraction.fields.getTextInputValue('banReason'); + + await banHandler(modalSubmitInteraction, [target.id], reason); +} + +const banContextMenu = new ContextMenuCommandBuilder() + .setName('Ban user') + .setDefaultMemberPermissions('0') + .setType(ApplicationCommandType.User); + +export default { + name: banContextMenu.name, + handler: banContextMenuHandler, + deploy: banContextMenu.toJSON() +}; diff --git a/src/context-menus/users/kick.ts b/src/context-menus/users/kick.ts new file mode 100644 index 0000000..0ad6e0f --- /dev/null +++ b/src/context-menus/users/kick.ts @@ -0,0 +1,58 @@ +import { + ActionRowBuilder, + ApplicationCommandType, + ContextMenuCommandBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle +} from 'discord.js'; +import { kickHandler } from '@/commands/kick'; +import type { UserContextMenuCommandInteraction, ModalActionRowComponentBuilder} from 'discord.js'; + +export async function kickContextMenuHandler(interaction: UserContextMenuCommandInteraction): Promise { + const target = interaction.targetUser; + + const reasonInput = new TextInputBuilder() + .setCustomId('kickReason') + .setLabel('Reason') + .setRequired(true) + .setStyle(TextInputStyle.Short); + + const reasonActionRow = new ActionRowBuilder() + .addComponents(reasonInput); + + const kickModal = new ModalBuilder() + .setCustomId('kickModal') + .setTitle(`Kick ${target.tag}`) + .addComponents(reasonActionRow); + + await interaction.showModal(kickModal); + + let modalSubmitInteraction; + try { + modalSubmitInteraction = await interaction.awaitModalSubmit({ + time: 5 * 60 * 1000, + filter: modalSubmitInteraction => + modalSubmitInteraction.customId === 'kickModal' && + modalSubmitInteraction.user.id === interaction.user.id + }); + } catch { + // * The user dismissed the modal or took too long to respond + return; + } + + const reason = modalSubmitInteraction.fields.getTextInputValue('kickReason'); + + await kickHandler(modalSubmitInteraction, [target.id], reason); +} + +const kickContextMenu = new ContextMenuCommandBuilder() + .setName('Kick user') + .setDefaultMemberPermissions('0') + .setType(ApplicationCommandType.User); + +export default { + name: kickContextMenu.name, + handler: kickContextMenuHandler, + deploy: kickContextMenu.toJSON() +}; diff --git a/src/context-menus/users/warn.ts b/src/context-menus/users/warn.ts new file mode 100644 index 0000000..764d170 --- /dev/null +++ b/src/context-menus/users/warn.ts @@ -0,0 +1,58 @@ +import { + ActionRowBuilder, + ApplicationCommandType, + ContextMenuCommandBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle +} from 'discord.js'; +import { warnHandler } from '@/commands/warn'; +import type { UserContextMenuCommandInteraction, ModalActionRowComponentBuilder} from 'discord.js'; + +export async function warnContextMenuHandler(interaction: UserContextMenuCommandInteraction): Promise { + const target = interaction.targetUser; + + const reasonInput = new TextInputBuilder() + .setCustomId('warnReason') + .setLabel('Reason') + .setRequired(true) + .setStyle(TextInputStyle.Short); + + const reasonActionRow = new ActionRowBuilder() + .addComponents(reasonInput); + + const warnModal = new ModalBuilder() + .setCustomId('warnModal') + .setTitle(`Warn ${target.tag}`) + .addComponents(reasonActionRow); + + await interaction.showModal(warnModal); + + let modalSubmitInteraction; + try { + modalSubmitInteraction = await interaction.awaitModalSubmit({ + time: 5 * 60 * 1000, + filter: modalSubmitInteraction => + modalSubmitInteraction.customId === 'warnModal' && + modalSubmitInteraction.user.id === interaction.user.id + }); + } catch { + // * The user dismissed the modal or took too long to respond + return; + } + + const reason = modalSubmitInteraction.fields.getTextInputValue('warnReason'); + + await warnHandler(modalSubmitInteraction, [target.id], reason); +} + +const warnContextMenu = new ContextMenuCommandBuilder() + .setName('Warn user') + .setDefaultMemberPermissions('0') + .setType(ApplicationCommandType.User); + +export default { + name: warnContextMenu.name, + handler: warnContextMenuHandler, + deploy: warnContextMenu.toJSON() +}; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index a6a4024..8495347 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,7 +1,7 @@ import buttonHandler from '@/handlers/button-handler'; import commandHandler from '@/handlers/command-handler'; import messageContextMenuHandler from '@/handlers/context-menu-handler'; -import type { Interaction } from 'discord.js'; +import type { Interaction } from 'discord.js'; export default async function interactionCreateHandler(interaction: Interaction): Promise { try { @@ -24,7 +24,7 @@ export default async function interactionCreateHandler(interaction: Interaction) try { if (interaction.replied || interaction.deferred) { - await interaction.editReply(payload); + await interaction.followUp(payload); } else { await interaction.reply(payload); } diff --git a/src/events/ready.ts b/src/events/ready.ts index 246054c..c49ab56 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -8,6 +8,9 @@ import warnCommand from '@/commands/warn'; import modpingCommand from '@/commands/modping'; import messageLogContextMenu from '@/context-menus/messages/message-log'; import slowModeCommand from '@/commands/slow-mode'; +import warnContextMenu from '@/context-menus/users/warn'; +import kickContextMenu from '@/context-menus/users/kick'; +import banContextMenu from '@/context-menus/users/ban'; import { checkMatchmakingThreads } from '@/matchmaking-threads'; import { loadModel } from '@/check-nsfw'; import { SlowMode } from '@/models/slow-mode'; @@ -55,6 +58,9 @@ function loadBotHandlersCollection(client: Client): void { client.commands.set(slowModeCommand.name, slowModeCommand); client.contextMenus.set(messageLogContextMenu.name, messageLogContextMenu); + client.contextMenus.set(warnContextMenu.name, warnContextMenu); + client.contextMenus.set(kickContextMenu.name, kickContextMenu); + client.contextMenus.set(banContextMenu.name, banContextMenu); } async function setupSlowMode(client: Client): Promise { From 907c973e3599029acd9a1001789ed829d502d9fd Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Mon, 5 Aug 2024 23:01:40 -0400 Subject: [PATCH 5/8] enhancement: add delete message history option to context menus --- src/context-menus/users/ban.ts | 24 ++++++++++++++++++++++-- src/context-menus/users/kick.ts | 24 ++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/context-menus/users/ban.ts b/src/context-menus/users/ban.ts index d17e67f..c7f3db9 100644 --- a/src/context-menus/users/ban.ts +++ b/src/context-menus/users/ban.ts @@ -18,13 +18,23 @@ export async function banContextMenuHandler(interaction: UserContextMenuCommandI .setRequired(true) .setStyle(TextInputStyle.Short); + const deleteMessagesInput = new TextInputBuilder() + .setCustomId('deleteMessages') + .setLabel('Delete recent message history (hours)') + .setPlaceholder('Leave blank to not delete any messages') + .setRequired(false) + .setStyle(TextInputStyle.Short); + const reasonActionRow = new ActionRowBuilder() .addComponents(reasonInput); + const deleteMessagesActionRow = new ActionRowBuilder() + .addComponents(deleteMessagesInput); + const banModal = new ModalBuilder() .setCustomId('banModal') .setTitle(`Ban ${target.tag}`) - .addComponents(reasonActionRow); + .addComponents(reasonActionRow, deleteMessagesActionRow); await interaction.showModal(banModal); @@ -42,8 +52,18 @@ export async function banContextMenuHandler(interaction: UserContextMenuCommandI } const reason = modalSubmitInteraction.fields.getTextInputValue('banReason'); + const deleteMessagesText = modalSubmitInteraction.fields.getTextInputValue('deleteMessages'); + const deleteMessages = parseFloat(deleteMessagesText) * 60 * 60 || null; + + if (deleteMessages && (deleteMessages < 0 || deleteMessages > 7 * 24 * 60 * 60)) { + await modalSubmitInteraction.reply({ + content: 'Message deletion time must be between 0 and 168 hours (7 days).', + ephemeral: true + }); + return; + } - await banHandler(modalSubmitInteraction, [target.id], reason); + await banHandler(modalSubmitInteraction, [target.id], reason, deleteMessages); } const banContextMenu = new ContextMenuCommandBuilder() diff --git a/src/context-menus/users/kick.ts b/src/context-menus/users/kick.ts index 0ad6e0f..fbc5e1e 100644 --- a/src/context-menus/users/kick.ts +++ b/src/context-menus/users/kick.ts @@ -18,13 +18,23 @@ export async function kickContextMenuHandler(interaction: UserContextMenuCommand .setRequired(true) .setStyle(TextInputStyle.Short); + const deleteMessagesInput = new TextInputBuilder() + .setCustomId('deleteMessages') + .setLabel('Delete recent message history (hours)') + .setPlaceholder('Leave blank to not delete any messages') + .setRequired(false) + .setStyle(TextInputStyle.Short); + const reasonActionRow = new ActionRowBuilder() .addComponents(reasonInput); + const deleteMessagesActionRow = new ActionRowBuilder() + .addComponents(deleteMessagesInput); + const kickModal = new ModalBuilder() .setCustomId('kickModal') .setTitle(`Kick ${target.tag}`) - .addComponents(reasonActionRow); + .addComponents(reasonActionRow, deleteMessagesActionRow); await interaction.showModal(kickModal); @@ -42,8 +52,18 @@ export async function kickContextMenuHandler(interaction: UserContextMenuCommand } const reason = modalSubmitInteraction.fields.getTextInputValue('kickReason'); + const deleteMessagesText = modalSubmitInteraction.fields.getTextInputValue('deleteMessages'); + const deleteMessages = parseFloat(deleteMessagesText) * 60 * 60 || null; + + if (deleteMessages && (deleteMessages < 0 || deleteMessages > 7 * 24 * 60 * 60)) { + await modalSubmitInteraction.reply({ + content: 'Message deletion time must be between 0 and 168 hours (7 days).', + ephemeral: true + }); + return; + } - await kickHandler(modalSubmitInteraction, [target.id], reason); + await kickHandler(modalSubmitInteraction, [target.id], reason, deleteMessages); } const kickContextMenu = new ContextMenuCommandBuilder() From a0ce4d2a57464587c246a180182b1b67f3f8e47f Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:13:04 -0400 Subject: [PATCH 6/8] chore: simplify regex literals --- src/commands/ban.ts | 2 +- src/commands/kick.ts | 2 +- src/commands/warn.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 2d5ed84..265b877 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -23,7 +23,7 @@ async function banHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + userIds = [...new Set(Array.from(users.matchAll(/\d{17,18}/g), match => match[0]))]; } else { throw new Error(`Unknown ban subcommand: ${subcommand}`); } diff --git a/src/commands/kick.ts b/src/commands/kick.ts index 0f11de0..78b26bd 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -24,7 +24,7 @@ async function kickHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + userIds = [...new Set(Array.from(users.matchAll(/\d{17,18}/g), match => match[0]))]; } else { throw new Error(`Unknown kick subcommand: ${subcommand}`); } diff --git a/src/commands/warn.ts b/src/commands/warn.ts index 9609819..931f2ad 100644 --- a/src/commands/warn.ts +++ b/src/commands/warn.ts @@ -24,7 +24,7 @@ async function warnHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + userIds = [...new Set(Array.from(users.matchAll(/\d{17,18}/g), match => match[0]))]; } else { throw new Error(`Unknown warn subcommand: ${subcommand}`); } From ce48b1b2166b960d02a9db82ed59f2c052b4f2ce Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:04:36 -0400 Subject: [PATCH 7/8] chore: increase modal timeout to 14.5 minutes --- src/context-menus/users/ban.ts | 3 ++- src/context-menus/users/kick.ts | 3 ++- src/context-menus/users/warn.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/context-menus/users/ban.ts b/src/context-menus/users/ban.ts index c7f3db9..74aaae9 100644 --- a/src/context-menus/users/ban.ts +++ b/src/context-menus/users/ban.ts @@ -40,8 +40,9 @@ export async function banContextMenuHandler(interaction: UserContextMenuCommandI let modalSubmitInteraction; try { + // * Interaction tokens are only valid for 15 minutes, leave some time for ban processing modalSubmitInteraction = await interaction.awaitModalSubmit({ - time: 5 * 60 * 1000, + time: 14.5 * 60 * 1000, filter: modalSubmitInteraction => modalSubmitInteraction.customId === 'banModal' && modalSubmitInteraction.user.id === interaction.user.id diff --git a/src/context-menus/users/kick.ts b/src/context-menus/users/kick.ts index fbc5e1e..aa6b2a4 100644 --- a/src/context-menus/users/kick.ts +++ b/src/context-menus/users/kick.ts @@ -40,8 +40,9 @@ export async function kickContextMenuHandler(interaction: UserContextMenuCommand let modalSubmitInteraction; try { + // * Interaction tokens are only valid for 15 minutes, leave some time for kick processing modalSubmitInteraction = await interaction.awaitModalSubmit({ - time: 5 * 60 * 1000, + time: 14.5 * 60 * 1000, filter: modalSubmitInteraction => modalSubmitInteraction.customId === 'kickModal' && modalSubmitInteraction.user.id === interaction.user.id diff --git a/src/context-menus/users/warn.ts b/src/context-menus/users/warn.ts index 764d170..7685b1e 100644 --- a/src/context-menus/users/warn.ts +++ b/src/context-menus/users/warn.ts @@ -30,8 +30,9 @@ export async function warnContextMenuHandler(interaction: UserContextMenuCommand let modalSubmitInteraction; try { + // * Interaction tokens are only valid for 15 minutes, leave some time for warn processing modalSubmitInteraction = await interaction.awaitModalSubmit({ - time: 5 * 60 * 1000, + time: 14.5 * 60 * 1000, filter: modalSubmitInteraction => modalSubmitInteraction.customId === 'warnModal' && modalSubmitInteraction.user.id === interaction.user.id From 85f56e1e31b5bfa221f92d776d2a7e71b146c5e2 Mon Sep 17 00:00:00 2001 From: Matthew Lopez <73856503+MatthewL246@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:51:40 -0400 Subject: [PATCH 8/8] chore: correct naming for ID variables --- src/commands/ban.ts | 10 +++++----- src/commands/kick.ts | 10 +++++----- src/commands/warn.ts | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 265b877..61f317e 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -17,13 +17,13 @@ async function banHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + userIDs = [...new Set(Array.from(users.matchAll(/\d{17,18}/g), match => match[0]))]; } else { throw new Error(`Unknown ban subcommand: ${subcommand}`); } @@ -32,8 +32,8 @@ async function banHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + userIDs = [...new Set(Array.from(users.matchAll(/\d{17,18}/g), match => match[0]))]; } else { throw new Error(`Unknown kick subcommand: ${subcommand}`); } @@ -33,8 +33,8 @@ async function kickHandler(interaction: ChatInputCommandInteraction): Promise match[0]))]; + userIDs = [...new Set(Array.from(users.matchAll(/\d{17,18}/g), match => match[0]))]; } else { throw new Error(`Unknown warn subcommand: ${subcommand}`); } @@ -33,8 +33,8 @@ async function warnHandler(interaction: ChatInputCommandInteraction): Promise