From 584c342fca60fb0ee6c45b1ebe64231668e96cdf Mon Sep 17 00:00:00 2001 From: Nix Krystik Date: Mon, 26 Oct 2026 14:59:05 +0000 Subject: [PATCH] [Buttons] Update buttons to use classes (#24) --- src/buttons/claim-ticket.ts | 40 -------------- src/buttons/claimTicket.ts | 52 ++++++++++++++++++ src/buttons/close-mod-ticket.ts | 28 ---------- src/buttons/close-ticket.ts | 26 --------- src/buttons/closeModTicket.ts | 38 +++++++++++++ src/buttons/closeTicket.ts | 38 +++++++++++++ src/buttons/dev-watch.ts | 32 ----------- src/buttons/devWatch.ts | 38 +++++++++++++ src/buttons/index.ts | 27 ++++++++++ src/buttons/note-next.ts | 15 ------ src/buttons/note-previous.ts | 15 ------ src/buttons/noteNext.ts | 18 +++++++ src/buttons/notePrevious.ts | 18 +++++++ .../open-ticket-for-reported-message.ts | 40 -------------- src/buttons/openTicketForReportedMessage.ts | 45 ++++++++++++++++ src/buttons/private-help.ts | 20 ------- src/buttons/privateHelp.ts | 30 +++++++++++ src/buttons/records-next.ts | 15 ------ src/buttons/records-previous.ts | 15 ------ src/buttons/recordsNext.ts | 18 +++++++ src/buttons/recordsPrevious.ts | 18 +++++++ src/buttons/role-button.ts | 32 ----------- src/buttons/roleButton.ts | 38 +++++++++++++ src/buttons/unclaim-ticket.ts | 42 --------------- src/buttons/unclaimTicket.ts | 54 +++++++++++++++++++ src/commands/settings.ts | 2 +- src/index.ts | 15 +++--- src/types/button.d.ts | 6 +++ src/types/index.d.ts | 1 + 29 files changed, 447 insertions(+), 329 deletions(-) delete mode 100644 src/buttons/claim-ticket.ts create mode 100644 src/buttons/claimTicket.ts delete mode 100644 src/buttons/close-mod-ticket.ts delete mode 100644 src/buttons/close-ticket.ts create mode 100644 src/buttons/closeModTicket.ts create mode 100644 src/buttons/closeTicket.ts delete mode 100644 src/buttons/dev-watch.ts create mode 100644 src/buttons/devWatch.ts create mode 100644 src/buttons/index.ts delete mode 100644 src/buttons/note-next.ts delete mode 100644 src/buttons/note-previous.ts create mode 100644 src/buttons/noteNext.ts create mode 100644 src/buttons/notePrevious.ts delete mode 100644 src/buttons/open-ticket-for-reported-message.ts create mode 100644 src/buttons/openTicketForReportedMessage.ts delete mode 100644 src/buttons/private-help.ts create mode 100644 src/buttons/privateHelp.ts delete mode 100644 src/buttons/records-next.ts delete mode 100644 src/buttons/records-previous.ts create mode 100644 src/buttons/recordsNext.ts create mode 100644 src/buttons/recordsPrevious.ts delete mode 100644 src/buttons/role-button.ts create mode 100644 src/buttons/roleButton.ts delete mode 100644 src/buttons/unclaim-ticket.ts create mode 100644 src/buttons/unclaimTicket.ts create mode 100644 src/types/button.d.ts diff --git a/src/buttons/claim-ticket.ts b/src/buttons/claim-ticket.ts deleted file mode 100644 index 0b0452f..0000000 --- a/src/buttons/claim-ticket.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelType, Client, MessageFlags, PermissionFlagsBits } from 'discord.js'; -import { Database } from '../shared/Database'; - -export default { - name: 'claim-ticket', - handler: async function (client: Client, interaction: ButtonInteraction) { - const channel = await interaction.channel?.fetch(); - - if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread) - return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Oops. Something went wrong.' }); - - if (!interaction.memberPermissions) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'An error has occurred.' }); - - if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageMessages)) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You do not have permission to claim tickets.' }); - - const guild = await client.guilds.fetch(interaction.guildId!); - - const settings = await Database.getOrCreateSettings(guild.id); - if (!settings.private_help_role_id) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to claim ticket.' }); - - const closeButton = new ButtonBuilder() - .setCustomId('close-ticket') - .setLabel('Click here if you no longer need help') - .setStyle(ButtonStyle.Danger); - - const unclaimButton = new ButtonBuilder() - .setCustomId('unclaim-ticket') - .setLabel('Unclaim ticket') - .setStyle(ButtonStyle.Primary); - - const row = new ActionRowBuilder().addComponents(closeButton, unclaimButton); - - await interaction.message.edit({ - content: `${interaction.message.content}\n\nClaimed by: ${interaction.user}`, - components: [row] - }); - - await interaction.reply({ content: `Ticket claimed by ${interaction.user}.` }); - } -}; \ No newline at end of file diff --git a/src/buttons/claimTicket.ts b/src/buttons/claimTicket.ts new file mode 100644 index 0000000..f77e4ec --- /dev/null +++ b/src/buttons/claimTicket.ts @@ -0,0 +1,52 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelType, Client, MessageFlags, PermissionFlagsBits } from 'discord.js'; +import { Button } from '../types'; +import { SetSeverity } from '../utils'; + +class ClaimTicketButton implements Button { + id = 'claim-ticket'; + + async execute(context: Client, interaction: ButtonInteraction): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); + + try { + const channel = await context.channels.fetch(interaction.channelId); + + if (!channel) throw new Error("The interaction channel couldn't be fetched."); + if (!channel.isThread() || channel.type != ChannelType.PrivateThread) throw new Error('This interaction was not run in a ticket thread.'); + if (!channel.isSendable()) throw new Error("I don't have permission to send messages in this channel."); + + if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageMessages)) throw new Error("You don't have permission to claim tickets."); + + const closeButton = new ButtonBuilder() + .setCustomId('close-ticket') + .setLabel('Click here if you no longer need help') + .setStyle(ButtonStyle.Danger); + + const unclaimButton = new ButtonBuilder() + .setCustomId('unclaim-ticket') + .setLabel('Unclaim ticket') + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(closeButton, unclaimButton); + + await interaction.message.edit({ + content: `${interaction.message.content}\n\nClaimed by: ${interaction.user}`, + components: [row] + }); + + await channel.send({ + embeds: [{ + ...SetSeverity('success'), + description: `This ticket has been claimed by ${interaction.user}.` + }], + }); + + await interaction.editReply({ content: "You've claimed this ticket." }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new ClaimTicketButton(); diff --git a/src/buttons/close-mod-ticket.ts b/src/buttons/close-mod-ticket.ts deleted file mode 100644 index 91cabac..0000000 --- a/src/buttons/close-mod-ticket.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ButtonInteraction, Client, MessageFlags, ChannelType, PermissionFlagsBits } from 'discord.js'; -import { closeTicket } from '../utils'; - -export default { - name: 'close-mod-ticket', - handler: async function (client: Client, interaction: ButtonInteraction) { - await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); - const channel = await interaction.channel?.fetch(); - const guild = await interaction.guild?.fetch(); - const member = await guild?.members.fetch(interaction.user.id); - - if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread || !member) - return interaction.editReply({ content: 'Oops. Something went wrong. Please report this to a staff member.' }); - - if (!member.permissions.has(PermissionFlagsBits.KickMembers)) - return interaction.editReply({ content: 'Only staff members may close mod tickets.' }); - - await interaction.message.edit({ - content: interaction.message.content, - components: [] - }); - - if (!await closeTicket(client, channel, 'This ticket has been closed by staff.')) - return await interaction.editReply({ content: 'An error occurred while trying to close this ticket.' }); - - await interaction.editReply({ content: 'Ticket closed.' }); - } -}; \ No newline at end of file diff --git a/src/buttons/close-ticket.ts b/src/buttons/close-ticket.ts deleted file mode 100644 index e8caeec..0000000 --- a/src/buttons/close-ticket.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ButtonInteraction, Client, MessageFlags, ChannelType } from 'discord.js'; -import { closeTicket } from '../utils'; - -export default { - name: 'close-ticket', - handler: async function (client: Client, interaction: ButtonInteraction) { - const channel = await interaction.channel?.fetch(); - if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread) - return await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Oops. Something went wrong. Please report this to a staff member.' }); - - await interaction.reply({ - content: 'Closing, please wait...', - flags: MessageFlags.Ephemeral - }); - - await interaction.message.edit({ - content: interaction.message.content, - components: [] - }); - - if (!await closeTicket(client, channel, `This ticket has been closed by ${interaction.user}`)) - return await interaction.editReply({ content: 'An error occurred while trying to close this ticket.' }); - - await interaction.editReply({ content: 'Ticket closed.' }); - } -}; \ No newline at end of file diff --git a/src/buttons/closeModTicket.ts b/src/buttons/closeModTicket.ts new file mode 100644 index 0000000..978fbc3 --- /dev/null +++ b/src/buttons/closeModTicket.ts @@ -0,0 +1,38 @@ +import { ButtonInteraction, Client, MessageFlags, ChannelType } from 'discord.js'; +import { Button } from '../types'; +import { closeTicket } from '../utils'; + +class CloseModTicketButton implements Button { + id = 'close-mod-ticket'; + + async execute(context: Client, interaction: ButtonInteraction): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const channel = await context.channels.fetch(interaction.channelId); + + if (!channel) throw new Error("The interaction channel couldn't be fetched."); + if (!channel.isThread() || channel.type != ChannelType.PrivateThread) throw new Error('This interaction was not run in a ticket thread.'); + if (!channel.isSendable()) throw new Error("I don't have permission to send messages in this channel."); + + if (!interaction.memberPermissions.has('KickMembers')) throw new Error("You don't have permission to close this ticket."); + + await interaction.message.edit({ + content: interaction.message.content, + components: [] + }); + + if (!await closeTicket(context, channel, 'This ticket has been closed by staff.')) { + await interaction.editReply({ content: 'An error occurred while trying to close this ticket.' }); + return; + } + + await interaction.editReply({ content: 'Ticket closed.' }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new CloseModTicketButton(); diff --git a/src/buttons/closeTicket.ts b/src/buttons/closeTicket.ts new file mode 100644 index 0000000..da392ed --- /dev/null +++ b/src/buttons/closeTicket.ts @@ -0,0 +1,38 @@ +import { ButtonInteraction, Client, MessageFlags, ChannelType } from 'discord.js'; +import { Button } from '../types'; +import { closeTicket } from '../utils'; + +class CloseTicketButton implements Button { + id = 'close-ticket'; + + async execute(context: Client, interaction: ButtonInteraction): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const channel = await context.channels.fetch(interaction.channelId); + + if (!channel) throw new Error("The interaction channel couldn't be fetched."); + if (!channel.isThread() || channel.type != ChannelType.PrivateThread) throw new Error('This interaction was not run in a ticket thread.'); + if (!channel.isSendable()) throw new Error("I don't have permission to send messages in this channel."); + + await interaction.editReply({ content: 'Closing, please wait...' }); + + await interaction.message.edit({ + content: interaction.message.content, + components: [] + }); + + if (!await closeTicket(context, channel, `This ticket has been closed by ${interaction.user}`)) { + await interaction.editReply({ content: 'An error occurred while trying to close this ticket.' }); + return; + } + + await interaction.editReply({ content: 'Ticket closed.' }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new CloseTicketButton(); diff --git a/src/buttons/dev-watch.ts b/src/buttons/dev-watch.ts deleted file mode 100644 index 50b7e99..0000000 --- a/src/buttons/dev-watch.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ButtonInteraction, Client, MessageFlags } from 'discord.js'; -import { Database } from '../shared/Database'; - -export default { - name: 'dev-watch', - handler: async function (client: Client, interaction: ButtonInteraction) { - try { - if (!interaction.guild) throw new Error('This action must be run in a guild.'); - - const settings = await Database.getOrCreateSettings(interaction.guild.id); - if (!settings.devwatch_role_id) throw new Error('`devwatch_role_id` was not configured. Please configure a role then try again.'); - - const member = await interaction.guild.members.fetch(interaction.user.id); - if (!member) throw new Error('Unable to retreive the member executing this command.'); - - const bot = await interaction.guild.members.fetchMe(); - const role = await interaction.guild?.roles.fetch(settings.devwatch_role_id); - if (!role) throw new Error('The configured `devwatch_role_id` does not exist.'); - if (role.position > bot.roles.highest.position) throw new Error('The configured `devwatch_role_id` is higher than my highest role.'); - - if (member.roles.cache.has(settings.devwatch_role_id)) { - await member.roles.remove(settings.devwatch_role_id); - await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Removed role.' }); - } else { - await member.roles.add(settings.devwatch_role_id); - await interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Added role.' }); - } - } catch (error: Error | any) { - await interaction.reply({ flags: [MessageFlags.Ephemeral], content: error.message }); - } - } -}; \ No newline at end of file diff --git a/src/buttons/devWatch.ts b/src/buttons/devWatch.ts new file mode 100644 index 0000000..3137df4 --- /dev/null +++ b/src/buttons/devWatch.ts @@ -0,0 +1,38 @@ +import { ButtonInteraction, Client, MessageFlags } from 'discord.js'; +import { Database } from '../shared/Database'; +import { Button } from '../types'; + +class DevWatchButton implements Button { + id = 'dev-watch'; + + async execute(context: Client, interaction: ButtonInteraction): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const settings = await Database.getOrCreateSettings(interaction.guildId); + if (!settings.devwatch_role_id) throw new Error('`devwatch_role_id` was not configured. Please configure a role then try again.'); + + const guild = await context.guilds.fetch(interaction.guildId); + const member = await guild.members.fetch(interaction.user.id); + if (!member) throw new Error('Unable to fetch the member executing this command.'); + + const bot = await guild.members.fetchMe(); + const role = await guild.roles.fetch(settings.devwatch_role_id); + if (!role) throw new Error('The configured `devwatch_role_id` does not exist.'); + if (role.position > bot.roles.highest.position) throw new Error('The configured `devwatch_role_id` is higher than my highest role.'); + + if (member.roles.cache.has(settings.devwatch_role_id)) { + await member.roles.remove(settings.devwatch_role_id); + await interaction.editReply({ content: 'Removed role.' }); + } else { + await member.roles.add(settings.devwatch_role_id); + await interaction.editReply({ content: 'Added role.' }); + } + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new DevWatchButton(); diff --git a/src/buttons/index.ts b/src/buttons/index.ts new file mode 100644 index 0000000..4337c20 --- /dev/null +++ b/src/buttons/index.ts @@ -0,0 +1,27 @@ +import claimTicket from './claimTicket'; +import closeModTicket from './closeModTicket'; +import closeTicket from './closeTicket'; +import devWatch from './devWatch'; +import noteNext from './noteNext'; +import notePrevious from './notePrevious'; +import openTicketForReportedMessage from './openTicketForReportedMessage'; +import privateHelp from './privateHelp'; +import recordsNext from './recordsNext'; +import recordsPrevious from './recordsPrevious'; +import roleButton from './roleButton'; +import unclaimTicket from './unclaimTicket'; + +export default [ + claimTicket, + closeModTicket, + closeTicket, + devWatch, + noteNext, + notePrevious, + openTicketForReportedMessage, + privateHelp, + recordsNext, + recordsPrevious, + roleButton, + unclaimTicket +]; diff --git a/src/buttons/note-next.ts b/src/buttons/note-next.ts deleted file mode 100644 index dcca15f..0000000 --- a/src/buttons/note-next.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction, Client } from 'discord.js'; -import { getNoteMessage } from '../utils'; - -export default { - name: 'note-next', - handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) { - await interaction.deferUpdate(); - - const message = await getNoteMessage(userId, parseInt(page) + 1); - - if (!message) return; - - interaction.editReply(message); - } -}; \ No newline at end of file diff --git a/src/buttons/note-previous.ts b/src/buttons/note-previous.ts deleted file mode 100644 index 8e4b547..0000000 --- a/src/buttons/note-previous.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction, Client} from 'discord.js'; -import { getNoteMessage } from '../utils'; - -export default { - name: 'note-previous', - handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) { - await interaction.deferUpdate(); - - const message = await getNoteMessage(userId, parseInt(page) - 1); - - if (!message) return; - - interaction.editReply(message); - } -}; \ No newline at end of file diff --git a/src/buttons/noteNext.ts b/src/buttons/noteNext.ts new file mode 100644 index 0000000..b08f92f --- /dev/null +++ b/src/buttons/noteNext.ts @@ -0,0 +1,18 @@ +import { ButtonInteraction, Client } from 'discord.js'; +import { Button } from '../types'; +import { getNoteMessage } from '../utils'; + +class NoteNextButton implements Button { + id = 'note-next'; + + async execute(context: Client, interaction: ButtonInteraction, userId: string, page: string): Promise { + await interaction.deferUpdate(); + + const message = await getNoteMessage(userId, parseInt(page) + 1); + if (!message) return; + + await interaction.editReply(message); + } +} + +export default new NoteNextButton(); diff --git a/src/buttons/notePrevious.ts b/src/buttons/notePrevious.ts new file mode 100644 index 0000000..98e32d5 --- /dev/null +++ b/src/buttons/notePrevious.ts @@ -0,0 +1,18 @@ +import { ButtonInteraction, Client } from 'discord.js'; +import { Button } from '../types'; +import { getNoteMessage } from '../utils'; + +class NotePreviousButton implements Button { + id = 'note-previous'; + + async execute(context: Client, interaction: ButtonInteraction, userId: string, page: string): Promise { + await interaction.deferUpdate(); + + const message = await getNoteMessage(userId, parseInt(page) - 1); + if (!message) return; + + await interaction.editReply(message); + } +} + +export default new NotePreviousButton(); diff --git a/src/buttons/open-ticket-for-reported-message.ts b/src/buttons/open-ticket-for-reported-message.ts deleted file mode 100644 index 8a57b82..0000000 --- a/src/buttons/open-ticket-for-reported-message.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ButtonInteraction, Client, MessageFlags, MessageMentions } from 'discord.js'; -import { createPrivateHelpTicketThread } from '../utils'; - -export default { - name: 'open-ticket-for-reported-message', - handler: async function (client: Client, interaction: ButtonInteraction) { - const message = await interaction.message.fetch(); - const guild = await interaction.guild!.fetch(); - - const reportEmbed = message.embeds[0]!; - - const regex = new RegExp(MessageMentions.UsersPattern); - - const reportedMessageUrl = reportEmbed.fields[0].value; - const reporterId = regex.exec(reportEmbed.fields[2].value)!.groups!.id; - const additionalInfo = reportEmbed.fields[3]?.name == 'Additional Information' ? reportEmbed.fields[3].value : ''; - - const reporter = await guild.members.fetch(reporterId) ?? await client.users.fetch(reporterId); - - const thread = await createPrivateHelpTicketThread(client, guild, null, `Ticket creted by staff in response to message report: ${message.url}. Reported message: ${reportedMessageUrl}. ${additionalInfo ? `\n\nAdditional information provided in report:\n${additionalInfo.split('\n').map(c => `> ${c}`).join('\n')}` : ''}`, `Ticket: Message Report From ${reporter.displayName}`, [reporterId, interaction.user.id]); - - if (thread) { - await interaction.reply({ - flags: [MessageFlags.Ephemeral], - content: `Ticket created: ${thread}` - }); - - const requestIndex = reportEmbed.fields.findIndex(f => f.name == 'User Requested Private Ticket'); - if (requestIndex != -1) reportEmbed.fields.splice(requestIndex, 1); - - reportEmbed.fields.push({ - name: 'Private Help Ticket', - value: thread.url, - inline: false - }); - - await message.edit({ embeds: [reportEmbed], components: [] }); - } - } -}; \ No newline at end of file diff --git a/src/buttons/openTicketForReportedMessage.ts b/src/buttons/openTicketForReportedMessage.ts new file mode 100644 index 0000000..f1302aa --- /dev/null +++ b/src/buttons/openTicketForReportedMessage.ts @@ -0,0 +1,45 @@ +import { ButtonInteraction, Client, MessageFlags, MessageMentions } from 'discord.js'; +import { Button } from '../types'; +import { createPrivateHelpTicketThread } from '../utils'; + +class OpenTicketForReportedMessageButton implements Button { + id = 'open-ticket-for-reported-message'; + + async execute(context: Client, interaction: ButtonInteraction): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const guild = await context.guilds.fetch(interaction.guildId); + const message = await interaction.message.fetch(); + + const reportEmbed = message.embeds[0]!; + const regex = new RegExp(MessageMentions.UsersPattern); + + const reportedMessageUrl = reportEmbed.fields[0].value; + const reporterId = regex.exec(reportEmbed.fields[2].value)!.groups!.id; + const additionalInfo = reportEmbed.fields[3]?.name == 'Additional Information' ? reportEmbed.fields[3].value : ''; + const reporter = await guild.members.fetch(reporterId) ?? await context.users.fetch(reporterId); + + const thread = await createPrivateHelpTicketThread(context, guild, null, `Ticket creted by staff in response to message report: ${message.url}. Reported message: ${reportedMessageUrl}. ${additionalInfo ? `\n\nAdditional information provided in report:\n${additionalInfo.split('\n').map(c => `> ${c}`).join('\n')}` : ''}`, `Ticket: Message Report From ${reporter.displayName}`, [reporterId, interaction.user.id]); + if (!thread) throw new Error('An error occured and a thread was not created.'); + + await interaction.editReply({ content: `Ticket Created: ${thread}` }); + + const requestIndex = reportEmbed.fields.findIndex(f => f.name == 'User Requested Private Ticket'); + if (requestIndex != -1) reportEmbed.fields.splice(requestIndex, 1); + + reportEmbed.fields.push({ + name: 'Private Help Ticket', + value: thread.url, + inline: false + }); + + await message.edit({ embeds: [reportEmbed], components: [] }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new OpenTicketForReportedMessageButton(); diff --git a/src/buttons/private-help.ts b/src/buttons/private-help.ts deleted file mode 100644 index 82ef8fb..0000000 --- a/src/buttons/private-help.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ButtonInteraction, Client, ModalBuilder, TextInputStyle, MessageFlags } from 'discord.js'; -import { canOpenPrivateHelpTicket, createTextInput } from '../utils'; - -export default { - name: 'private-help', - handler: async function (client: Client, interaction: ButtonInteraction) { - if (!(await canOpenPrivateHelpTicket(interaction.user.id))) - return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You can only have one open ticket at a time that is less than a day old.' }); - - const modal = new ModalBuilder() - .setCustomId('open-ticket-modal') - .setTitle('Get in contact'); - - const label = createTextInput('ticket-message', 'What is the reason for your ticket?', null, true, TextInputStyle.Paragraph, 1500, 10); - - modal.addLabelComponents(label); - - await interaction.showModal(modal); - } -}; \ No newline at end of file diff --git a/src/buttons/privateHelp.ts b/src/buttons/privateHelp.ts new file mode 100644 index 0000000..65100de --- /dev/null +++ b/src/buttons/privateHelp.ts @@ -0,0 +1,30 @@ +import { ButtonInteraction, Client, ModalBuilder, TextInputStyle, MessageFlags } from 'discord.js'; +import { Button } from '../types'; +import { canOpenPrivateHelpTicket, createTextInput } from '../utils'; + +class PrivateHelpButton implements Button { + id = 'private-help'; + + async execute(context: Client, interaction: ButtonInteraction): Promise { + try { + if (!await canOpenPrivateHelpTicket(interaction.user.id)) + throw new Error('You can only have one open ticket at a time that is less than a day old.'); + + const modal = new ModalBuilder() + .setCustomId('open-ticket-modal') + .setTitle('Get in contact'); + + const label = createTextInput('ticket-message', 'What is the reason for your ticket?', null, true, TextInputStyle.Paragraph, 1500, 10); + modal.addLabelComponents(label); + + await interaction.showModal(modal); + } catch (error: Error | any) { + await interaction.reply({ + content: error.message, + flags: MessageFlags.Ephemeral, + }).catch(() => {}); + } + } +} + +export default new PrivateHelpButton(); diff --git a/src/buttons/records-next.ts b/src/buttons/records-next.ts deleted file mode 100644 index 9d909f0..0000000 --- a/src/buttons/records-next.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction, Client } from 'discord.js'; -import { getRecordMessageFromDiscordId } from '../utils'; - -export default { - name: 'records-next', - handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) { - await interaction.deferUpdate(); - - const message = await getRecordMessageFromDiscordId(userId, parseInt(page) + 1, interaction.guild!); - - if (!message) return; - - interaction.editReply(message); - } -}; \ No newline at end of file diff --git a/src/buttons/records-previous.ts b/src/buttons/records-previous.ts deleted file mode 100644 index 67b47e2..0000000 --- a/src/buttons/records-previous.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ButtonInteraction, Client} from 'discord.js'; -import { getRecordMessageFromDiscordId } from '../utils'; - -export default { - name: 'records-previous', - handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) { - await interaction.deferUpdate(); - - const message = await getRecordMessageFromDiscordId(userId, parseInt(page) - 1, interaction.guild!); - - if (!message) return; - - interaction.editReply(message); - } -}; \ No newline at end of file diff --git a/src/buttons/recordsNext.ts b/src/buttons/recordsNext.ts new file mode 100644 index 0000000..ddbfd12 --- /dev/null +++ b/src/buttons/recordsNext.ts @@ -0,0 +1,18 @@ +import { ButtonInteraction, Client } from 'discord.js'; +import { Button } from '../types'; +import { getRecordMessageFromDiscordId } from '../utils'; + +class RecordsNextButton implements Button { + id = 'records-next'; + + async execute(context: Client, interaction: ButtonInteraction, userId: string, page: string): Promise { + await interaction.deferUpdate(); + + const message = await getRecordMessageFromDiscordId(userId, parseInt(page) + 1, interaction.guild!); + if (!message) return; + + await interaction.editReply(message); + } +} + +export default new RecordsNextButton(); diff --git a/src/buttons/recordsPrevious.ts b/src/buttons/recordsPrevious.ts new file mode 100644 index 0000000..468ce79 --- /dev/null +++ b/src/buttons/recordsPrevious.ts @@ -0,0 +1,18 @@ +import { ButtonInteraction, Client } from 'discord.js'; +import { Button } from '../types'; +import { getRecordMessageFromDiscordId } from '../utils'; + +class RecordsPreviousButton implements Button { + id = 'records-previous'; + + async execute(context: Client, interaction: ButtonInteraction, userId: string, page: string): Promise { + await interaction.deferUpdate(); + + const message = await getRecordMessageFromDiscordId(userId, parseInt(page) - 1, interaction.guild!); + if (!message) return; + + await interaction.editReply(message); + } +} + +export default new RecordsPreviousButton(); diff --git a/src/buttons/role-button.ts b/src/buttons/role-button.ts deleted file mode 100644 index c20fc36..0000000 --- a/src/buttons/role-button.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ButtonInteraction, Client, MessageFlags } from 'discord.js'; -import { Database } from '../shared/Database'; - -export default { - name: 'role-button', - handler: async function (client: Client, interaction: ButtonInteraction) { - await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); - - const roleId = await Database.getRoleFromButton(interaction.message.id); - - if (!roleId) await interaction.editReply({ content: 'Could not find role button.' }); - - const guild = await interaction.guild?.fetch(); - const member = await guild?.members.fetch(interaction.user.id); - - if (!member) return interaction.editReply({ content: 'Member not found.' }); - - try { - if (member.roles.cache.has(roleId!)) { - await member.roles.remove(roleId!); - interaction.editReply({ content: 'Role removed.' }); - } else { - await member.roles.add(roleId!); - interaction.editReply({ content: 'Role added.' }); - } - - } catch (e) { - console.error(e); - interaction.editReply({ content: 'Could not add role. Is it higher than mine?' }); - } - } -}; \ No newline at end of file diff --git a/src/buttons/roleButton.ts b/src/buttons/roleButton.ts new file mode 100644 index 0000000..49f93a6 --- /dev/null +++ b/src/buttons/roleButton.ts @@ -0,0 +1,38 @@ +import { ButtonInteraction, Client, MessageFlags } from 'discord.js'; +import { Database } from '../shared/Database'; +import { Button } from '../types'; + +class RoleButtonButton implements Button { + id = 'role-button'; + + async execute(context: Client, interaction: ButtonInteraction): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const roleId = await Database.getRoleFromButton(interaction.message.id); + if (!roleId) throw new Error("I couldn't find an associated role ID with this button."); + + const guild = await context.guilds.fetch(interaction.guildId); + const member = await guild.members.fetch(interaction.user.id); + if (!member) throw new Error('I was unable to fetch you as a member.'); + + const bot = await guild.members.fetchMe(); + const role = await guild.roles.fetch(roleId); + if (!role) throw new Error("The configured role for this button doesn't exist."); + if (role.position > bot.roles.highest.position) throw new Error(`The configured role (\`${role.name}\`) is higher than my highest role.`); + + if (member.roles.cache.has(roleId)) { + await member.roles.remove(roleId); + await interaction.editReply({ content: 'Role removed.' }); + } else { + await member.roles.add(roleId); + await interaction.editReply({ content: 'Role Added' }); + } + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new RoleButtonButton(); \ No newline at end of file diff --git a/src/buttons/unclaim-ticket.ts b/src/buttons/unclaim-ticket.ts deleted file mode 100644 index 3ee34b7..0000000 --- a/src/buttons/unclaim-ticket.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelType, Client, MessageFlags, PermissionFlagsBits } from 'discord.js'; -import { Database } from '../shared/Database'; - -export default { - name: 'unclaim-ticket', - handler: async function (client: Client, interaction: ButtonInteraction) { - const channel = await interaction.channel?.fetch(); - - if (!channel || !channel.isThread() || !channel.isSendable() || channel.type != ChannelType.PrivateThread) - return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Oops. Something went wrong.' }); - - if (!interaction.memberPermissions) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'An error has occurred.' }); - - if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageMessages)) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You do not have permission to unclaim this ticket.' }); - - if (!interaction.message.content.split('\n').at(-1)!.includes(`<@${interaction.user.id}>`)) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'You did not claim this ticket.' }); - - const guild = await client.guilds.fetch(interaction.guildId!); - - const settings = await Database.getOrCreateSettings(guild.id); - if (!settings.private_help_role_id) return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Failed to unclaim ticket.' }); - - const closeButton = new ButtonBuilder() - .setCustomId('close-ticket') - .setLabel('Click here if you no longer need help') - .setStyle(ButtonStyle.Danger); - - const claimButton = new ButtonBuilder() - .setCustomId('claim-ticket') - .setLabel('Claim ticket') - .setStyle(ButtonStyle.Primary); - - const row = new ActionRowBuilder().addComponents(closeButton, claimButton); - - await interaction.message.edit({ - content: interaction.message.content.split('\n').slice(0, -1).join('\n').trim(), - components: [row] - }); - - await interaction.reply({ content: 'Ticket unclaimed.', flags: [MessageFlags.Ephemeral] }); - } -}; \ No newline at end of file diff --git a/src/buttons/unclaimTicket.ts b/src/buttons/unclaimTicket.ts new file mode 100644 index 0000000..2869383 --- /dev/null +++ b/src/buttons/unclaimTicket.ts @@ -0,0 +1,54 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelType, Client, MessageFlags, PermissionFlagsBits } from 'discord.js'; +import { Button } from '../types'; +import { SetSeverity } from '../utils'; + +class UnclaimTicketButton implements Button { + id = 'unclaim-ticket'; + + async execute(context: Client, interaction: ButtonInteraction): Promise { + if (!interaction.inGuild()) return; + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const channel = await context.channels.fetch(interaction.channelId); + + if (!channel) throw new Error("The interaction channel couldn't be fetched."); + if (!channel.isThread() || channel.type != ChannelType.PrivateThread) throw new Error('This interaction was not run in a ticket thread.'); + if (!channel.isSendable()) throw new Error("I don't have permission to send messages in this channel."); + + if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageMessages)) throw new Error("You don't have permission to unclaim tickets."); + + if (!interaction.message.content.split('\n').at(-1)!.includes(`<@${interaction.user.id}>`)) throw new Error("You haven't claimed this ticket."); + + const closeButton = new ButtonBuilder() + .setCustomId('close-ticket') + .setLabel('Click here if you no longer need help') + .setStyle(ButtonStyle.Danger); + + const claimButton = new ButtonBuilder() + .setCustomId('claim-ticket') + .setLabel('Claim ticket') + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(closeButton, claimButton); + + await interaction.message.edit({ + content: interaction.message.content.split('\n').slice(0, -1).join('\n').trim(), + components: [row] + }); + + await channel.send({ + embeds: [{ + ...SetSeverity('warning'), + description: 'This ticket has been unclaimed.' + }], + }); + + await interaction.editReply({ content: 'Ticket unclaimed.' }); + } catch (error: Error | any) { + await interaction.editReply({ content: error.message }).catch(() => {}); + } + } +} + +export default new UnclaimTicketButton(); diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 787ee31..387d611 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -189,7 +189,7 @@ export default { const moderatorChannel = interaction.options.getChannel('moderator-channel'); if (moderatorChannel) { await Database.updateSettings(interaction.guildId, 'moderator_channel_id', moderatorChannel.id); - response.push(`**moderator_channel_id** has been set to": ${moderatorChannel}.`); + response.push(`**moderator_channel_id** has been set to: ${moderatorChannel}.`); } const adminRole = interaction.options.getRole('admin-role'); diff --git a/src/index.ts b/src/index.ts index ddb79b0..9aebe04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ -import { Client, GatewayIntentBits, MessageFlags, Partials } from 'discord.js'; +import { ButtonInteraction, Client, GatewayIntentBits, MessageFlags, Partials } from 'discord.js'; +import buttons from './buttons'; import { config } from './config'; import events from './events'; import { Database } from './shared/Database'; @@ -37,7 +38,6 @@ const client = new Client({ const commands: Handler[] = []; const contextMenus: Handler[] = []; -const buttons: Handler[] = []; const modals: Handler[] = []; const menus: Handler[] = []; @@ -93,10 +93,11 @@ client.on('interactionCreate', async (interaction) => { const id = interaction.customId.split('_')[0]; for (const button of buttons) { - if (id == button.name) { - button.handler(client, interaction, ...interaction.customId.split('_').slice(1)); - return; - } + if (button.id != id) continue; + + Promise.resolve((button.execute as (context: Client, interaction: ButtonInteraction, ...args: unknown[]) => Promise)(client, interaction, ...interaction.customId.split('_').slice(1)) + .catch(e => console.error('An error occurred while handling a button interaction: ', e))); + return; } } else if (interaction.isModalSubmit()) { // Handle modal submissions. @@ -126,14 +127,12 @@ client.on('clientReady', async () => { await loadHandlersFrom('commands', commands); await loadHandlersFrom('context-menus', contextMenus); - await loadHandlersFrom('buttons', buttons); await loadHandlersFrom('modals', modals); await loadHandlersFrom('menus', menus); await refreshCommands(client); await initIfNecessary(client, commands); - await initIfNecessary(client, buttons); await initIfNecessary(client, modals); await initIfNecessary(client, menus); diff --git a/src/types/button.d.ts b/src/types/button.d.ts new file mode 100644 index 0000000..fa92fe4 --- /dev/null +++ b/src/types/button.d.ts @@ -0,0 +1,6 @@ +import { ButtonInteraction } from 'discord.js'; + +export interface Button { + id: string; + execute(context: Context, interaction: ButtonInteraction, ...args: any): Promise; +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 61a0722..33d226b 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,3 +1,4 @@ +export * from './button.d'; export * from './command.d'; export * from './database-types.d'; export * from './event.d';