From 0df04e9169503f8632530736e5125fd4885ba581 Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Mon, 26 Oct 2026 23:08:24 +0800 Subject: [PATCH] (modals): Add `Modal` interface. --- src/context-menus/report.ts | 20 +--- src/index.ts | 15 ++- src/modals/add-knowledgebase-item-modal.ts | 49 -------- src/modals/add-note-modal.ts | 40 ------- src/modals/addKnowledgebaseItemModal.ts | 41 +++++++ src/modals/addNoteModal.ts | 38 +++++++ src/modals/edit-knowledgebase-item-modal.ts | 54 --------- src/modals/editKnowledgebaseItemModal.ts | 42 +++++++ src/modals/index.ts | 17 +++ src/modals/open-mod-ticket.ts | 31 ----- src/modals/open-ticket-modal.ts | 23 ---- src/modals/openModTicket.ts | 39 +++++++ src/modals/openTicketModal.ts | 31 +++++ src/modals/report-message.ts | 118 -------------------- src/modals/reportMessage.ts | 101 +++++++++++++++++ src/modals/sync-name-modal.ts | 32 ------ src/modals/syncNameModal.ts | 27 +++++ src/types/index.d.ts | 1 + src/types/modal.d.ts | 6 + src/utils/interaction-utils.ts | 2 +- src/utils/modal-utils.ts | 20 +--- src/utils/private-help-utils.ts | 5 +- 22 files changed, 362 insertions(+), 390 deletions(-) delete mode 100644 src/modals/add-knowledgebase-item-modal.ts delete mode 100644 src/modals/add-note-modal.ts create mode 100644 src/modals/addKnowledgebaseItemModal.ts create mode 100644 src/modals/addNoteModal.ts delete mode 100644 src/modals/edit-knowledgebase-item-modal.ts create mode 100644 src/modals/editKnowledgebaseItemModal.ts create mode 100644 src/modals/index.ts delete mode 100644 src/modals/open-mod-ticket.ts delete mode 100644 src/modals/open-ticket-modal.ts create mode 100644 src/modals/openModTicket.ts create mode 100644 src/modals/openTicketModal.ts delete mode 100644 src/modals/report-message.ts create mode 100644 src/modals/reportMessage.ts delete mode 100644 src/modals/sync-name-modal.ts create mode 100644 src/modals/syncNameModal.ts create mode 100644 src/types/modal.d.ts diff --git a/src/context-menus/report.ts b/src/context-menus/report.ts index aaa7fd3..815593f 100644 --- a/src/context-menus/report.ts +++ b/src/context-menus/report.ts @@ -1,5 +1,4 @@ -import { ApplicationCommandType, ApplicationIntegrationType, Client, ContextMenuCommandBuilder, InteractionContextType, LabelBuilder, MessageContextMenuCommandInteraction, MessageFlags, ModalBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; - +import { ApplicationCommandType, ApplicationIntegrationType, CheckboxBuilder, Client, ContextMenuCommandBuilder, InteractionContextType, LabelBuilder, MessageContextMenuCommandInteraction, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; import { Database } from '../shared/Database'; export default { @@ -28,24 +27,13 @@ export default { .setDescription('Any additional information? This will help staff understand your report. 1000 characters maximum.') .setTextInputComponent(additionalInfoInput); - const yesNoMenu = new StringSelectMenuBuilder() - .setCustomId('create-private-help-ticket') - .setRequired(false) - .addOptions( - new StringSelectMenuOptionBuilder() - .setLabel('No') - .setDefault(true) - .setValue('no'), - new StringSelectMenuOptionBuilder() - .setLabel('Yes') - .setDefault(false) - .setValue('yes') - ); + const createTicketCheckbox = new CheckboxBuilder() + .setCustomId('create-private-help-ticket'); const createPrivateTicket = new LabelBuilder() .setLabel('Create Private Help Ticket') .setDescription('Should a private help ticket be opened with this report? (Does not bypass ticket restrictions)') - .setStringSelectMenuComponent(yesNoMenu); + .setCheckboxComponent(createTicketCheckbox); modal.addLabelComponents(additionalInfoLabel, createPrivateTicket); diff --git a/src/index.ts b/src/index.ts index 9aebe04..c9ec596 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ -import { ButtonInteraction, Client, GatewayIntentBits, MessageFlags, Partials } from 'discord.js'; +import { ButtonInteraction, Client, GatewayIntentBits, MessageFlags, ModalSubmitInteraction, Partials } from 'discord.js'; import buttons from './buttons'; import { config } from './config'; import events from './events'; +import modals from './modals'; import { Database } from './shared/Database'; import { openRedisClient } from './shared/RedisClient'; import { Handler } from './types'; @@ -38,7 +39,6 @@ const client = new Client({ const commands: Handler[] = []; const contextMenus: Handler[] = []; -const modals: Handler[] = []; const menus: Handler[] = []; // Due to their reliance on each other, these two events (interactionCreate, and ready) have to stay here. @@ -104,10 +104,11 @@ client.on('interactionCreate', async (interaction) => { const id = interaction.customId.split('_')[0]; for (const modal of modals) { - if (id == modal.name) { - modal.handler(client, interaction, ...interaction.customId.split('_').slice(1)); - return; - } + if (id != modal.id) continue; + + Promise.resolve((modal.execute as (context: Client, interaction: ModalSubmitInteraction, ...args: unknown[]) => Promise)(client, interaction, ...interaction.customId.split('_').slice(1)) + .catch(e => console.error('An error occurred while handling a modal interaction: ', e))); + return; } } else if (interaction.isAnySelectMenu()) { // Handle menu selections. @@ -127,13 +128,11 @@ client.on('clientReady', async () => { await loadHandlersFrom('commands', commands); await loadHandlersFrom('context-menus', contextMenus); - await loadHandlersFrom('modals', modals); await loadHandlersFrom('menus', menus); await refreshCommands(client); await initIfNecessary(client, commands); - await initIfNecessary(client, modals); await initIfNecessary(client, menus); Encrypter.initialize(config.DATABASE_SECRET!); diff --git a/src/modals/add-knowledgebase-item-modal.ts b/src/modals/add-knowledgebase-item-modal.ts deleted file mode 100644 index 74c4418..0000000 --- a/src/modals/add-knowledgebase-item-modal.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js'; -import { Database } from '../shared/Database'; -import { deferInteraction, logCustomEvent } from '../utils'; - -export default { - name: 'add-knowledgebase-item-modal', - handler: async function (client: Client, interaction: ModalSubmitInteraction) { - if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' }); - - await deferInteraction(interaction); - - const name = interaction.fields.getTextInputValue('name'); - const content = interaction.fields.getTextInputValue('content'); - - if (content.length > 2000) return interaction.editReply('Content cannot be over 2000 characters long.'); - - const existingItem = await Database.getFromKnowledgebaseByName(interaction.guild.id, name); - - if (existingItem) return interaction.editReply(`Knowledgebase item ${name} already exists!`); - - logCustomEvent(interaction.guild!, { - title: 'Knowledgebase Item Added', - description: null, - color: 0x00FF00, - timestamp: new Date(), - fields: [ - { - name: 'User', - value: `<@${interaction.user.id}>\n${interaction.user.username}`, - inline: true - }, - { - name: 'Name', - value: name, - inline: true - }, - { - name: 'Content', - value: content, - inline: true - } - ] - }); - - await Database.addToKnowledgebase(interaction.guild.id, name, content); - - return interaction.editReply(`Entry \`${name}\` added to knowledgebase.`); - } -}; \ No newline at end of file diff --git a/src/modals/add-note-modal.ts b/src/modals/add-note-modal.ts deleted file mode 100644 index 6347710..0000000 --- a/src/modals/add-note-modal.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js'; -import { Database } from '../shared/Database'; -import { logCustomEvent } from '../utils'; - -export default { - name: 'add-note-modal', - handler: async function (client: Client, interaction: ModalSubmitInteraction, id: string) { - const message = interaction.fields.getTextInputValue('note-message'); - - const user = await client.users.fetch(id); - - logCustomEvent(interaction.guild!, { - title: 'Note Added', - description: null, - color: 0x00FF00, - timestamp: new Date(), - fields: [ - { - name: 'Moderator', - value: `<@${interaction.user.id}>\n${interaction.user.username}`, - inline: true - }, - { - name: 'User', - value: `<@${id}>\n${user.username}`, - inline: true - }, - { - name: 'Note', - value: message, - inline: true - } - ] - }); - - await Database.putNote(id, message, interaction.user.id); - - interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Note added' }); - } -}; \ No newline at end of file diff --git a/src/modals/addKnowledgebaseItemModal.ts b/src/modals/addKnowledgebaseItemModal.ts new file mode 100644 index 0000000..bb4811e --- /dev/null +++ b/src/modals/addKnowledgebaseItemModal.ts @@ -0,0 +1,41 @@ +import { Client, ModalSubmitInteraction } from 'discord.js'; +import { Database } from '../shared/Database'; +import { Modal } from '../types'; +import { deferInteraction, logCustomEvent } from '../utils'; + +class AddKnowledgebaseItemModal implements Modal { + id = 'add-knowledgebase-item-modal'; + + async execute(context: Client, interaction: ModalSubmitInteraction): Promise { + if (!interaction.inGuild()) return; + await deferInteraction(interaction); + + try { + const name = interaction.fields.getTextInputValue('name'); + const content = interaction.fields.getTextInputValue('content'); + if (content.length > 2000) throw new Error('Content cannot be longer than 2000 characters.'); + + const existing = await Database.getFromKnowledgebaseByName(interaction.guildId, name); + if (existing) throw new Error(`A knowledgebase item under \`${name}\` already exists.`); + + await logCustomEvent(interaction.guild!, { + title: 'Knowledgebase Item Added', + description: null, + color: 0x00FF00, + timestamp: new Date(), + fields: [ + { name: 'User', value: `${interaction.user} (${interaction.user.id})`, inline: false }, + { name: 'Name', value: name, inline: false }, + { name: 'Content', value: content, inline: false }, + ], + }); + + await Database.addToKnowledgebase(interaction.guildId, name, content); + await interaction.editReply({ content: `\`${name}\` has been added to the knowledgebase.` }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new AddKnowledgebaseItemModal(); \ No newline at end of file diff --git a/src/modals/addNoteModal.ts b/src/modals/addNoteModal.ts new file mode 100644 index 0000000..70e44dd --- /dev/null +++ b/src/modals/addNoteModal.ts @@ -0,0 +1,38 @@ +import { Client, ModalSubmitInteraction } from 'discord.js'; +import { Database } from '../shared/Database'; +import { deferInteraction, logCustomEvent } from '../utils'; +import { Modal } from '../types'; + +class AddNoteModal implements Modal { + id = 'add-note-modal'; + + async execute(context: Client, interaction: ModalSubmitInteraction, id: string): Promise { + if (!interaction.inGuild()) return; + await deferInteraction(interaction); + + try { + const message = interaction.fields.getTextInputValue('note-message'); + const guild = await context.guilds.fetch(interaction.guildId); + const user = await context.users.fetch(id); + + await logCustomEvent(guild, { + title: 'Note Added', + description: null, + color: 0x00FF00, + timestamp: new Date(), + fields: [ + { name: 'Moderator', value: `${interaction.user} (${interaction.user.id})`, inline: false }, + { name: 'User', value: `${user.username} (${user.id})`, inline: false }, + { name: 'Note', value: message, inline: false }, + ], + }); + + await Database.putNote(id, message, interaction.user.id); + await interaction.editReply({ content: 'Note added.' }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new AddNoteModal(); \ No newline at end of file diff --git a/src/modals/edit-knowledgebase-item-modal.ts b/src/modals/edit-knowledgebase-item-modal.ts deleted file mode 100644 index 52d612a..0000000 --- a/src/modals/edit-knowledgebase-item-modal.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js'; -import { Database } from '../shared/Database'; -import { deferInteraction, logCustomEvent } from '../utils'; - -export default { - name: 'edit-knowledgebase-item-modal', - handler: async function (client: Client, interaction: ModalSubmitInteraction, idString: string) { - if (!interaction.guild) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Must be ran in guild.' }); - - await deferInteraction(interaction); - - const id = parseInt(idString); - const content = interaction.fields.getTextInputValue('content'); - - const existingItem = await Database.getFromKnowledgebase(id); - - if (!existingItem) return interaction.editReply('Knowledgebase item not found.'); - - if (content.length > 2000) return interaction.editReply('Content cannot be over 2000 characters long.'); - - logCustomEvent(interaction.guild!, { - title: 'Knowledgebase Item Edited', - description: null, - color: 0xFFFF00, - timestamp: new Date(), - fields: [ - { - name: 'User', - value: `<@${interaction.user.id}>\n${interaction.user.username}`, - inline: true - }, - { - name: 'Name', - value: existingItem.name, - inline: true - }, - { - name: 'Old Content', - value: existingItem.content, - inline: true - }, - { - name: 'New Content', - value: content, - inline: true - } - ] - }); - - await Database.editKnowledgebaseItem(id, content); - - return interaction.editReply(`Edited knowledgebase entry \`${existingItem.name}\`.`); - } -}; \ No newline at end of file diff --git a/src/modals/editKnowledgebaseItemModal.ts b/src/modals/editKnowledgebaseItemModal.ts new file mode 100644 index 0000000..34a2461 --- /dev/null +++ b/src/modals/editKnowledgebaseItemModal.ts @@ -0,0 +1,42 @@ +import { Client, ModalSubmitInteraction } from 'discord.js'; +import { Database } from '../shared/Database'; +import { deferInteraction, logCustomEvent } from '../utils'; +import { Modal } from '../types'; + +class EditKnowledgebaseItemModal implements Modal { + id = 'edit-knowledgebase-item-modal'; + + async execute(context: Client, interaction: ModalSubmitInteraction, idString: string): Promise { + if (!interaction.inGuild()) return; + await deferInteraction(interaction); + + try { + const id = parseInt(idString); + const existing = await Database.getFromKnowledgebase(id); + if (!existing) throw new Error("A knowledgebase item with that ID doesn't exist."); + + const content = interaction.fields.getTextInputValue('content'); + if (content.length > 2000) throw new Error('Content cannot be longer than 2000 characters.'); + + await logCustomEvent(interaction.guild!, { + title: 'Knowledgebase Item Edited', + description: null, + color: 0xFFFF00, + timestamp: new Date(), + fields: [ + { name: 'User', value: `${interaction.user} (${interaction.user.id})`, inline: false }, + { name: 'Name', value: existing.name, inline: false }, + { name: 'Old Content', value: existing.content, inline: false }, + { name: 'New Content', value: content, inline: false }, + ], + }); + + await Database.editKnowledgebaseItem(id, content); + await interaction.editReply({ content: `\`${existing.name}\` has been edited.` }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new EditKnowledgebaseItemModal(); \ No newline at end of file diff --git a/src/modals/index.ts b/src/modals/index.ts new file mode 100644 index 0000000..83fed70 --- /dev/null +++ b/src/modals/index.ts @@ -0,0 +1,17 @@ +import addKnowledgebaseItemModal from './addKnowledgebaseItemModal'; +import addNoteModal from './addNoteModal'; +import editKnowledgebaseItemModal from './editKnowledgebaseItemModal'; +import openModTicket from './openModTicket'; +import openTicketModal from './openTicketModal'; +import reportMessage from './reportMessage'; +import syncNameModal from './syncNameModal'; + +export default [ + addKnowledgebaseItemModal, + addNoteModal, + editKnowledgebaseItemModal, + openModTicket, + openTicketModal, + reportMessage, + syncNameModal +]; \ No newline at end of file diff --git a/src/modals/open-mod-ticket.ts b/src/modals/open-mod-ticket.ts deleted file mode 100644 index 4c34913..0000000 --- a/src/modals/open-mod-ticket.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js'; -import { Database } from '../shared/Database'; -import { createPrivateHelpTicketThread } from '../utils'; - -const warning = '\n\n\nLeaving this thread without acknowledgement may result in punishment. Staff will close the thread when they deem your response acceptable.'; - -export default { - name: 'open-mod-ticket', - handler: async function (client: Client, interaction: ModalSubmitInteraction, userId: string) { - const guild = await client.guilds.fetch(interaction.guildId!); - const member = await guild.members.fetch(userId); - - const guildSettings = await Database.getOrCreateSettings(guild.id); - - if (!guildSettings || !guildSettings.private_help_channel_id) - return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a developer.' }); - - const title = interaction.fields.getTextInputValue('title') ? interaction.fields.getTextInputValue('title') : `Mod Ticket For ${member.displayName}`; - const reason = interaction.fields.getTextInputValue('initial-message') + warning; - const autoJoin = interaction.fields.getStringSelectValues('auto-join-thread')[0] == 'yes'; - - const membersToAdd = [userId]; - - if (autoJoin) membersToAdd.push(interaction.user.id); - - const thread = await createPrivateHelpTicketThread(client, guild, null, reason, title, membersToAdd); - - if (thread) interaction.reply({ flags: [MessageFlags.Ephemeral], content: `Mod ticket created: ${thread}.${!autoJoin ? "You've selected not to auto join the thread. You will not be notified of messages sent there. You will have to check the thread periodically for user response." : ''}` }); - else interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a developer.' }); - } -}; \ No newline at end of file diff --git a/src/modals/open-ticket-modal.ts b/src/modals/open-ticket-modal.ts deleted file mode 100644 index 59c9c79..0000000 --- a/src/modals/open-ticket-modal.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js'; -import { Database } from '../shared/Database'; -import { createPrivateHelpTicketThread } from '../utils'; - -export default { - name: 'open-ticket-modal', - handler: async function (client: Client, interaction: ModalSubmitInteraction) { - const guild = await client.guilds.fetch(interaction.guildId!); - const member = await guild.members.fetch(interaction.user.id); - - const guildSettings = await Database.getOrCreateSettings(guild.id); - - if (!guildSettings || !guildSettings.private_help_role_id) - return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a staff member.' }); - - const reason = interaction.fields.getTextInputValue('ticket-message'); - - const thread = await createPrivateHelpTicketThread(client, guild, member, reason); - - if (thread) interaction.reply({ flags: [MessageFlags.Ephemeral], content: `Your ticket has been created: ${thread}` }); - else interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to create ticket. Please report this to a staff member.' }); - } -}; \ No newline at end of file diff --git a/src/modals/openModTicket.ts b/src/modals/openModTicket.ts new file mode 100644 index 0000000..880421c --- /dev/null +++ b/src/modals/openModTicket.ts @@ -0,0 +1,39 @@ +import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js'; +import { Database } from '../shared/Database'; +import { Modal } from '../types'; +import { createPrivateHelpTicketThread } from '../utils'; + +const warning = '\n\n\nLeaving this thread without acknowledgement may result in punishment. Staff will close the thread when they deem your response acceptable.'; + +class OpenModTicketModal implements Modal { + id = 'open-mod-ticket'; + + async execute(context: Client, interaction: ModalSubmitInteraction, userId: string): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const settings = await Database.getOrCreateSettings(interaction.guildId); + if (!settings.private_help_channel_id) throw new Error("`private_help_channel_id` hasn't been configured. Please report this error to staff."); + + const guild = await context.guilds.fetch(interaction.guildId); + const member = await guild.members.fetch(userId); + + const title = interaction.fields.getTextInputValue('title') ? interaction.fields.getTextInputValue('title') : `Mod Ticket For ${member.displayName}`; + const reason = interaction.fields.getTextInputValue('initial-message') + warning; + const autoJoin = interaction.fields.getCheckbox('auto-join-thread'); + + const membersToAdd = [userId]; + if (autoJoin) membersToAdd.push(interaction.user.id); + + const thread = await createPrivateHelpTicketThread(context, guild, null, reason, title, membersToAdd); + if (!thread) throw new Error('Failed to create a ticket thread. Please report this to a developer.'); + + await interaction.editReply({ content: `Mod ticket created: ${thread}. ${!autoJoin ? "You've selected not to auto join the thread. You will not be notified of messages sent there. You will have to check the thread periodically for user response." : ''}` }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new OpenModTicketModal(); \ No newline at end of file diff --git a/src/modals/openTicketModal.ts b/src/modals/openTicketModal.ts new file mode 100644 index 0000000..647c6fa --- /dev/null +++ b/src/modals/openTicketModal.ts @@ -0,0 +1,31 @@ +import { Client, MessageFlags, ModalSubmitInteraction } from 'discord.js'; +import { Database } from '../shared/Database'; +import { Modal } from '../types'; +import { createPrivateHelpTicketThread } from '../utils'; + +class OpenTicketModal implements Modal { + id = 'open-ticket-modal'; + + async execute(context: Client, interaction: ModalSubmitInteraction): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const guild = await context.guilds.fetch(interaction.guildId!); + const member = await guild.members.fetch(interaction.user.id); + + const settings = await Database.getOrCreateSettings(interaction.guildId); + if (!settings.private_help_role_id) throw new Error("`private_help_role_id` hasn't been configured. Please report this error to staff."); + + const reason = interaction.fields.getTextInputValue('ticket-message'); + const thread = await createPrivateHelpTicketThread(context, guild, member, reason); + if (!thread) throw new Error('Failed to create ticket. Please report this to a staff member.'); + + await interaction.editReply({ content: `Your ticket has been created: ${thread}` }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new OpenTicketModal(); \ No newline at end of file diff --git a/src/modals/report-message.ts b/src/modals/report-message.ts deleted file mode 100644 index 3fcc0f2..0000000 --- a/src/modals/report-message.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder, GuildTextBasedChannel, MessageFlags, ModalSubmitInteraction } from 'discord.js'; -import { Database } from '../shared/Database'; -import { canOpenPrivateHelpTicket, createPrivateHelpTicketThread } from '../utils'; - -export default { - name: 'report-message', - handler: async function (client: Client, interaction: ModalSubmitInteraction, channelId: string, messageId: string) { - await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); - - const guild = await client.guilds.fetch(interaction.guildId!); - const member = await guild.members.fetch(interaction.user.id); - const reportedMessageChannel = await guild.channels.fetch(channelId) as GuildTextBasedChannel; - const reportedMessage = await reportedMessageChannel?.messages.fetch(messageId); - - const additionalInfo = interaction.fields.getTextInputValue('additional-info'); - const createPrivateHelpTicket = interaction.fields.getStringSelectValues('create-private-help-ticket')[0] == 'yes'; - - if (!reportedMessageChannel || !reportedMessage) - return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to submit report. Please report this to a staff member.' }); - - const guildSettings = await Database.getOrCreateSettings(interaction.guildId!); - - if (!guildSettings || !guildSettings.moderator_channel_id) - return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Report channel missing. Unable to submit report.' }); - - const reportsChannel = await interaction.guild!.channels.fetch(guildSettings.moderator_channel_id); - - if (!reportsChannel || !reportsChannel.isSendable()) - return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Report channel missing. Unable to submit report.' }); - - const embed = new EmbedBuilder() - .setTitle('New Message Report!') - .setColor(0xFF0000) - .addFields( - { - name: 'Message', - value: reportedMessage.url, - inline: false - }, - { - name: 'Message Author', - value: reportedMessage.author.toString(), - inline: false - }, - { - name: 'Reporter', - value: member.toString(), - inline: false - }); - - if (additionalInfo) { - embed.addFields( - { - name: 'Additional Information', - value: additionalInfo, - inline: false - } - ); - } - - let replyContent = "Thanks for making a report! I've notified the moderators who can take further action."; - - const wantsTicketButCantOpen = createPrivateHelpTicket && !await canOpenPrivateHelpTicket(member.id); - - if (wantsTicketButCantOpen) { - embed.addFields( - { - name: 'User Requested Private Ticket', - value: 'The user requested a private ticket be opened, but already has an open ticket. If additional information is needed, press the button below to open a ticket with the user.', - inline: false - } - ); - - replyContent += " Could not open private help ticket since you already have one open. Staff have been notified that you'd like to have a ticket opened, and can make one for you if they deem it necessary."; - } - - const openTicketButton = new ButtonBuilder() - .setCustomId('open-ticket-for-reported-message') - .setLabel('Open Private Ticket') - .setStyle(ButtonStyle.Primary); - - const row = new ActionRowBuilder().addComponents(openTicketButton); - - const reportMessage = await reportsChannel.send({ - embeds: [embed], - components: createPrivateHelpTicket && !wantsTicketButCantOpen ? [] : [row] - }); - - await reportsChannel.send({ files: [new AttachmentBuilder(Buffer.from(reportedMessage.content), { name: 'message-content.txt' })] }); - - if (createPrivateHelpTicket && !wantsTicketButCantOpen) { - if (!guildSettings.private_help_channel_id) { - replyContent += ' Could not open private help ticket. Private help channel not set. Please report this to a staff member.'; - } else { - const thread = await createPrivateHelpTicketThread(client, guild, member, `Ticket created with message report (${reportMessage.url}). ${member}, use this channel to talk with staff privately about the reported message (${reportedMessage.url}). ${additionalInfo ? `\n\nAdditional information provided in report:\n${additionalInfo.split('\n').map(c => `> ${c}`).join('\n')}` : ''}`); - if (thread) { - replyContent += ` Private help ticket created: ${thread}.`; - embed.addFields({ - name: 'Private Help Ticket', - value: thread.url, - inline: false - }); - - await reportMessage.edit({ embeds: [embed] }); - - } else { - replyContent += ' There was an issue opening a private help ticket, please report this to a staff member.'; - await reportMessage.edit({ - embeds: [embed], - components: [row] - }); - } - } - } - - await interaction.editReply(replyContent); - } -}; \ No newline at end of file diff --git a/src/modals/reportMessage.ts b/src/modals/reportMessage.ts new file mode 100644 index 0000000..aef683c --- /dev/null +++ b/src/modals/reportMessage.ts @@ -0,0 +1,101 @@ +import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, Client, EmbedBuilder, GuildTextBasedChannel, MessageFlags, ModalSubmitInteraction } from 'discord.js'; +import { Database } from '../shared/Database'; +import { Modal } from '../types'; +import { canOpenPrivateHelpTicket, createPrivateHelpTicketThread } from '../utils'; + +class ReportMessageModal implements Modal { + id = 'report-message'; + + async execute(context: Client, interaction: ModalSubmitInteraction, channelId: string, messageId: string): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); + + try { + const guild = await context.guilds.fetch(interaction.guildId!); + const member = await guild.members.fetch(interaction.user.id); + const reportedMessageChannel = await guild.channels.fetch(channelId) as GuildTextBasedChannel; + const reportedMessage = await reportedMessageChannel?.messages.fetch(messageId); + + const additionalInfo = interaction.fields.getTextInputValue('additional-info'); + const createPrivateHelpTicket = interaction.fields.getCheckbox('create-private-help-ticket'); + + if (!reportedMessageChannel || !reportedMessage) throw new Error('Failed to submit report. Please report this to a staff member.'); + + const settings = await Database.getOrCreateSettings(interaction.guildId); + if (!settings.moderator_channel_id) throw new Error('Report channel missing. Unable to submit report.'); + + const reportsChannel = await interaction.guild!.channels.fetch(settings.moderator_channel_id); + if (!reportsChannel || !reportsChannel.isSendable()) throw new Error('Report channel missing. Unable to submit report.'); + + const embed = new EmbedBuilder() + .setTitle('New Message Report!') + .setColor(0xFF0000) + .addFields( + { name: 'Message', value: reportedMessage.url, inline: false }, + { name: 'Author', value: reportedMessage.author.toString(), inline: false }, + { name: 'Reporter', value: member.toString(), inline: false } + ); + + if (additionalInfo) embed.addFields({ name: 'Additional Information', value: additionalInfo, inline: false }); + + let replyContent = "Thanks for making a report! I've notified the moderators who can take further action."; + const wantsTicketButCantOpen = createPrivateHelpTicket && !await canOpenPrivateHelpTicket(member.id); + + if (wantsTicketButCantOpen) { + embed.addFields({ + name: 'User Requested Private Ticket', + value: 'The user requested a private ticket be opened, but already has an open ticket. If additional information is needed, press the button below to open a ticket with the user.', + inline: false + }); + + replyContent += " Could not open private help ticket since you already have one open. Staff have been notified that you'd like to have a ticket opened, and can make one for you if they deem it necessary."; + } + + const openTicketButton = new ButtonBuilder() + .setCustomId('open-ticket-for-reported-message') + .setLabel('Open Private Ticket') + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(openTicketButton); + + const reportMessage = await reportsChannel.send({ + embeds: [embed], + components: createPrivateHelpTicket && !wantsTicketButCantOpen ? [] : [row] + }); + + await reportsChannel.send({ files: [new AttachmentBuilder(Buffer.from(reportedMessage.content), { name: 'message-content.txt' })] }); + + if (createPrivateHelpTicket && !wantsTicketButCantOpen) { + if (!settings.private_help_channel_id) { + replyContent += ' Could not open private help ticket. Private help channel not set. Please report this to a staff member.'; + } else { + const thread = await createPrivateHelpTicketThread(context, guild, member, `Ticket created with message report (${reportMessage.url}). ${member}, use this channel to talk with staff privately about the reported message (${reportedMessage.url}). ${additionalInfo ? `\n\nAdditional information provided in report:\n${additionalInfo.split('\n').map(c => `> ${c}`).join('\n')}` : ''}`); + if (thread) { + replyContent += ` Private help ticket created: ${thread}.`; + + embed.addFields({ + name: 'Private Help Ticket', + value: thread.url, + inline: false + }); + + await reportMessage.edit({ embeds: [embed] }); + } else { + replyContent += ' There was an issue opening a private help ticket, please report this to a staff member.'; + + await reportMessage.edit({ + embeds: [embed], + components: [row] + }); + } + } + } + + await interaction.editReply(replyContent); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new ReportMessageModal(); \ No newline at end of file diff --git a/src/modals/sync-name-modal.ts b/src/modals/sync-name-modal.ts deleted file mode 100644 index 12af741..0000000 --- a/src/modals/sync-name-modal.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Client, ModalSubmitInteraction } from 'discord.js'; -import { config } from '../config'; -import { deferInteraction, syncName } from '../utils'; - -export default { - name: 'sync-name-modal', - handler: async function (client: Client, interaction: ModalSubmitInteraction, id: string) { - await deferInteraction(interaction); - - const e621Id = Number(interaction.fields.getTextInputValue('id') ?? 0); - - if (isNaN(e621Id)) return await interaction.editReply('Provided id is not a number'); - - const member = await interaction.guild?.members.fetch(id); - - if (!member) { - return interaction.editReply('An error has occurred. Please try again later.'); - } - - const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!); - - if (!guild) { - return interaction.editReply('An error has occurred. Please try again later.'); - } - - if (!guild.members.me) { - return interaction.editReply('An error has occurred. Please try again later.'); - } - - await syncName(interaction, member, e621Id); - } -}; \ No newline at end of file diff --git a/src/modals/syncNameModal.ts b/src/modals/syncNameModal.ts new file mode 100644 index 0000000..0f49dd4 --- /dev/null +++ b/src/modals/syncNameModal.ts @@ -0,0 +1,27 @@ +import { Client, ModalSubmitInteraction } from 'discord.js'; +import { Modal } from '../types'; +import { deferInteraction, syncName } from '../utils'; + +class SyncNameModal implements Modal { + id = 'sync-name-modal'; + + async execute(context: Client, interaction: ModalSubmitInteraction, id: string): Promise { + if (!interaction.inGuild()) return; + await deferInteraction(interaction); + + try { + const e621Id = Number(interaction.fields.getTextInputValue('id') ?? 0); + if (isNaN(e621Id)) throw new Error('Provided id is not a number.'); + + const guild = await context.guilds.fetch(interaction.guildId); + const member = await guild.members.fetch(id); + if (!member) throw new Error('Unable to fetch the target member.'); + + await syncName(interaction, member, e621Id); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new SyncNameModal(); \ No newline at end of file diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 33d226b..3107663 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -5,4 +5,5 @@ export * from './event.d'; export * from './e621-types.d'; export * from './handler.d'; export * from './helper-types.d'; +export * from './modal.d'; export * from './task.d'; diff --git a/src/types/modal.d.ts b/src/types/modal.d.ts new file mode 100644 index 0000000..25f73b3 --- /dev/null +++ b/src/types/modal.d.ts @@ -0,0 +1,6 @@ +import { ModalSubmitInteraction } from 'discord.js'; + +export interface Modal { + id: string; + execute(context: Context, interaction: ModalSubmitInteraction, ...args: any): Promise; +} \ No newline at end of file diff --git a/src/utils/interaction-utils.ts b/src/utils/interaction-utils.ts index 6ae0183..7dd2009 100644 --- a/src/utils/interaction-utils.ts +++ b/src/utils/interaction-utils.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, ContextMenuCommandInteraction, GuildBasedChannel, MessageFlags, ModalSubmitInteraction } from 'discord.js'; -import { channelIsInStaffCategory } from './channel-utils'; +import { channelIsInStaffCategory } from '.'; export async function deferInteraction(interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction | ModalSubmitInteraction) { const isStaffChannel = await channelIsInStaffCategory(interaction.channel as GuildBasedChannel); diff --git a/src/utils/modal-utils.ts b/src/utils/modal-utils.ts index f0a4113..0fb7356 100644 --- a/src/utils/modal-utils.ts +++ b/src/utils/modal-utils.ts @@ -1,4 +1,4 @@ -import { LabelBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; +import { CheckboxBuilder, LabelBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; export function createTextInput(customId: string, labelTitle: string, description: string | null, required: boolean, style: TextInputStyle, maxLength: number | null, minLength: number | null): LabelBuilder { const input = new TextInputBuilder() @@ -19,23 +19,13 @@ export function createTextInput(customId: string, labelTitle: string, descriptio return label; } -export function createYesNoMenu(customId: string, labelTitle: string, description: string | null, defaultYes: boolean): LabelBuilder { - const yesNoMenu = new StringSelectMenuBuilder() - .setCustomId(customId) - .addOptions( - new StringSelectMenuOptionBuilder() - .setLabel('Yes') - .setDefault(defaultYes) - .setValue('yes'), - new StringSelectMenuOptionBuilder() - .setLabel('No') - .setDefault(!defaultYes) - .setValue('no') - ); +export function createCheckbox(customId: string, labelTitle: string, description: string | null, defaultYes: boolean): LabelBuilder { + const component = new CheckboxBuilder() + .setCustomId(customId); const yesNoLabel = new LabelBuilder() .setLabel(labelTitle) - .setStringSelectMenuComponent(yesNoMenu); + .setCheckboxComponent(component); if (description !== null) yesNoLabel.setDescription(description); diff --git a/src/utils/private-help-utils.ts b/src/utils/private-help-utils.ts index 89362bf..052e2d6 100644 --- a/src/utils/private-help-utils.ts +++ b/src/utils/private-help-utils.ts @@ -1,6 +1,6 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ChatInputCommandInteraction, Client, Guild, GuildMember, ModalBuilder, PrivateThreadChannel, TextChannel, TextInputStyle, ThreadAutoArchiveDuration, ThreadChannel, UserContextMenuCommandInteraction } from 'discord.js'; import { Database } from '../shared/Database'; -import { CreateDefaultEmbed, createTextInput, createYesNoMenu, SetSeverity } from '.'; +import { CreateDefaultEmbed, createTextInput, createCheckbox, SetSeverity } from '.'; export async function closeOldTickets(client: Client) { for (const ticket of await Database.getAllOpenPrivateHelpTickets()) { @@ -121,9 +121,8 @@ export async function openModTicketModal(interaction: UserContextMenuCommandInte const titleLabel = createTextInput('title', 'Title', `The name of the thread. Defaults to "Mod Ticket For ${member.displayName}" if left empty`, false, TextInputStyle.Short, 100, null); const initialMessageLabel = createTextInput('initial-message', 'Inital Message', 'The inital message sent in the thread', false, TextInputStyle.Paragraph, 1800, null); - const autoJoinLabel = createYesNoMenu('auto-join-thread', 'Auto Join Thread', 'Whether or not to join you to the thread. Selecting no will not notify you of messages sent!', true); + const autoJoinLabel = createCheckbox('auto-join-thread', 'Auto Join Thread', 'Whether or not to automatically add you to the thread.', true); modal.addLabelComponents(titleLabel, initialMessageLabel, autoJoinLabel); - interaction.showModal(modal); } \ No newline at end of file