diff --git a/src/buttons/close.js b/src/buttons/close.js index bd7b151..50a25a3 100644 --- a/src/buttons/close.js +++ b/src/buttons/close.js @@ -8,5 +8,14 @@ module.exports = class CloseButton extends Button { }); } - async run(id, interaction) { } + /** + * @param {*} id + * @param {import("discord.js").ButtonInteraction} interaction + */ + async run(id, interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply(); + } }; \ No newline at end of file diff --git a/src/commands/slash/add.js b/src/commands/slash/add.js index 6768441..ccd3894 100644 --- a/src/commands/slash/add.js +++ b/src/commands/slash/add.js @@ -19,7 +19,7 @@ module.exports = class AddSlashCommand extends SlashCommand { autocomplete: true, name: 'ticket', required: false, - type: ApplicationCommandOptionType.Integer, + type: ApplicationCommandOptionType.String, }, ]; opts = opts.map(o => { diff --git a/src/commands/slash/claim.js b/src/commands/slash/claim.js index 814e702..cde6c67 100644 --- a/src/commands/slash/claim.js +++ b/src/commands/slash/claim.js @@ -1,5 +1,4 @@ const { SlashCommand } = require('@eartharoid/dbf'); -const { ApplicationCommandOptionType } = require('discord.js'); module.exports = class ClaimSlashCommand extends SlashCommand { constructor(client, options) { @@ -19,5 +18,7 @@ module.exports = class ClaimSlashCommand extends SlashCommand { }); } - async run(interaction) { } + async run(interaction) { + // tickets/manager.js + } }; \ No newline at end of file diff --git a/src/commands/slash/close.js b/src/commands/slash/close.js index 4e55840..3d7d1c2 100644 --- a/src/commands/slash/close.js +++ b/src/commands/slash/close.js @@ -19,11 +19,6 @@ module.exports = class CloseSlashCommand extends SlashCommand { autocomplete: true, name: 'ticket', required: false, - type: ApplicationCommandOptionType.Integer, - }, - { - name: 'time', - required: false, type: ApplicationCommandOptionType.String, }, ]; @@ -53,5 +48,10 @@ module.exports = class CloseSlashCommand extends SlashCommand { }); } - async run(interaction) { } + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + + } }; \ No newline at end of file diff --git a/src/commands/slash/force-close.js b/src/commands/slash/force-close.js index affc2ac..0410e4c 100644 --- a/src/commands/slash/force-close.js +++ b/src/commands/slash/force-close.js @@ -1,5 +1,14 @@ const { SlashCommand } = require('@eartharoid/dbf'); -const { ApplicationCommandOptionType } = require('discord.js'); +const { + ApplicationCommandOptionType, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, +} = require('discord.js'); +const ExtendedEmbedBuilder = require('../../lib/embed'); +const { isStaff } = require('../../lib/users'); +const ms = require('ms'); module.exports = class ForceCloseSlashCommand extends SlashCommand { constructor(client, options) { @@ -19,7 +28,7 @@ module.exports = class ForceCloseSlashCommand extends SlashCommand { autocomplete: true, name: 'ticket', required: false, - type: ApplicationCommandOptionType.Integer, + type: ApplicationCommandOptionType.String, }, { name: 'time', @@ -53,5 +62,183 @@ module.exports = class ForceCloseSlashCommand extends SlashCommand { }); } - async run(interaction) { } + /** + * @param {import("discord.js").ChatInputCommandInteraction} interaction + */ + async run(interaction) { + /** @type {import("client")} */ + const client = this.client; + + await interaction.deferReply(); + + const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); + const getMessage = this.client.i18n.getLocale(settings.locale); + let ticket; + + if (!isStaff(interaction.guild, interaction.user.id)) { // if user is not staff + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('commands.slash.force-close.not_staff.title')) + .setDescription(getMessage('commands.slash.force-close.not_staff.description')), + ], + }); + } + + if (interaction.options.getString('time', false)) { // if time option is passed + const time = ms(interaction.options.getString('time', false)); + + if (!time) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('commands.slash.close.invalid_time.title')) + .setDescription(getMessage('commands.slash.close.invalid_time.description', { input: interaction.options.getString('time', false) })), + ], + }); + } + + const tickets = await client.prisma.ticket.findMany({ + where: { + lastMessageAt: { lte: new Date(Date.now() - time) }, + open: true, + }, + }); + + if (tickets.length === 0) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('commands.slash.force-close.no_tickets.title')) + .setDescription(getMessage('commands.slash.force-close.no_tickets.description', { time: ms(time, { long: true }) })), + ], + }); + } + + let confirmed = false; + const collectorTime = ms('15s'); + const confirmationM = await interaction.editReply({ + components: [ + new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setCustomId(JSON.stringify({ + action: 'custom', + id: 'close', + })) + .setStyle(ButtonStyle.Danger) + .setEmoji(getMessage('buttons.close.emoji')) + .setLabel(getMessage('buttons.close.text')), + new ButtonBuilder() + .setCustomId(JSON.stringify({ + action: 'custom', + id: 'cancel', + })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.cancel.emoji')) + .setLabel(getMessage('buttons.cancel.text')), + ]), + ], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: getMessage('misc.expires_in', { time: ms(collectorTime, { long: true }) }), + }) + .setColor(settings.primaryColour) + .setTitle(getMessage('commands.slash.force-close.confirm_multiple.title')) + .setDescription(getMessage('commands.slash.force-close.confirm_multiple.description', { + count: tickets.length, + tickets: tickets.map(t => `> <#${t.id}>`).join('\n'), + time: ms(time, { long: true }), + })), + ], + }); + + + confirmationM.awaitMessageComponent({ + componentType: ComponentType.Button, + filter: i => { + i.deferUpdate(); + return i.user.id === interaction.user.id; + }, + time: collectorTime, + }) + .then(i => { + if (JSON.parse(i.customId).id === 'close') { + confirmed = true; + // TODO: i.editReply + } else { + // TODO: cancelled + } + }) + .catch(() => interaction.editReply({ + components: [], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.expired.title')) + .setDescription(getMessage('misc.expired.description', { time: ms(time, { long: true }) })), + ], + })); + + if (!confirmed) return; + + // TODO: tickets: for each, close (check reason) + } else if (interaction.options.getString('ticket', false)) { // if ticket option is passed + ticket = await client.prisma.ticket.findUnique({ + include: { category: true }, + where: { id: interaction.options.getString('ticket', false) }, + }); + + if (!ticket) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.invalid_ticket.title')) + .setDescription(getMessage('misc.invalid_ticket.description')), + ], + }); + } + } else { + ticket = await client.prisma.ticket.findUnique({ + include: { category: true }, + where: { id: interaction.channel.id }, + }); + + if (!ticket) { + return await interaction.editReply({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.not_ticket.title')) + .setDescription(getMessage('misc.not_ticket.description')), + ], + }); + } + } + + // TODO: close (reason) + } }; \ No newline at end of file diff --git a/src/commands/slash/release.js b/src/commands/slash/release.js index 7722fe2..6074705 100644 --- a/src/commands/slash/release.js +++ b/src/commands/slash/release.js @@ -1,5 +1,4 @@ const { SlashCommand } = require('@eartharoid/dbf'); -const { ApplicationCommandOptionType } = require('discord.js'); module.exports = class ReleaseSlashCommand extends SlashCommand { constructor(client, options) { diff --git a/src/commands/slash/remove.js b/src/commands/slash/remove.js index 6923bd4..29cc142 100644 --- a/src/commands/slash/remove.js +++ b/src/commands/slash/remove.js @@ -19,7 +19,7 @@ module.exports = class RemoveSlashCommand extends SlashCommand { autocomplete: true, name: 'ticket', required: false, - type: ApplicationCommandOptionType.Integer, + type: ApplicationCommandOptionType.String, }, ]; opts = opts.map(o => { diff --git a/src/commands/slash/transcript.js b/src/commands/slash/transcript.js index db9ad4b..3b6cc54 100644 --- a/src/commands/slash/transcript.js +++ b/src/commands/slash/transcript.js @@ -14,7 +14,7 @@ module.exports = class TranscriptSlashCommand extends SlashCommand { autocomplete: true, name: 'ticket', required: true, - type: ApplicationCommandOptionType.Integer, + type: ApplicationCommandOptionType.String, }, ]; opts = opts.map(o => { diff --git a/src/commands/slash/transfer.js b/src/commands/slash/transfer.js index dfa7967..3896f0e 100644 --- a/src/commands/slash/transfer.js +++ b/src/commands/slash/transfer.js @@ -14,7 +14,7 @@ module.exports = class TransferSlashCommand extends SlashCommand { autocomplete: true, name: 'category', required: true, - type: ApplicationCommandOptionType.String, + type: ApplicationCommandOptionType.Integer, }, ]; opts = opts.map(o => { diff --git a/src/commands/user/create.js b/src/commands/user/create.js index 648096d..5f0470a 100644 --- a/src/commands/user/create.js +++ b/src/commands/user/create.js @@ -14,6 +14,8 @@ module.exports = class CreateUserCommand extends UserCommand { } async run(interaction) { + // TODO: isStaff? + // TODO: user->create // select category // send button } diff --git a/src/i18n/en-GB.yml b/src/i18n/en-GB.yml index f1f7cb0..ceddfb3 100644 --- a/src/i18n/en-GB.yml +++ b/src/i18n/en-GB.yml @@ -259,6 +259,9 @@ misc: not_ticket: description: You can only use this command in tickets. title: ❌ This isn't a ticket channel + invalid_ticket: + description: Please specify a valid ticket. + title: ❌ Invalid ticket ratelimited: description: Try again in a few seconds. title: 🐢 Please slow down diff --git a/src/lib/sync.js b/src/lib/sync.js index ff30a66..0d186a5 100644 --- a/src/lib/sync.js +++ b/src/lib/sync.js @@ -30,7 +30,7 @@ module.exports = async client => { const guild = client.guilds.cache.get(ticket.guildId); if (guild && guild.available && !client.channels.cache.has(ticket.id)) { deleted += 0; - await client.tickets.close(ticket.id); + await client.tickets.close(ticket.id, true, 'channel deleted'); } } diff --git a/src/lib/tickets/manager.js b/src/lib/tickets/manager.js index fa7d162..3a46cbb 100644 --- a/src/lib/tickets/manager.js +++ b/src/lib/tickets/manager.js @@ -91,7 +91,7 @@ module.exports = class TicketManager { /** * @param {object} data * @param {string} data.categoryId - * @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} data.interaction + * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} data.interaction * @param {string?} [data.topic] */ async create({ @@ -634,4 +634,32 @@ module.exports = class TicketManager { }); } } + + + /** + * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction} interaction + */ + async preClose(interaction) { + const ticket = await this.client.prisma.ticket.findUnique({ + include: { + category: true, + guild: true, + }, + where: { id: interaction.channel.id }, + }); + const getMessage = this.client.i18n.getLocale(ticket.guild.locale); + } + + /** + * close a ticket + * @param {string} ticketId + * @param {boolean} skip + * @param {string} reason + */ + async close(ticketId, skip, reason) { + // TODO: update cache/cat count + // TODO: update cache/member count + // TODO: set messageCount on ticket + // delete + } }; \ No newline at end of file diff --git a/src/listeners/client/guildMemberRemove.js b/src/listeners/client/guildMemberRemove.js index 8e40061..40554b0 100644 --- a/src/listeners/client/guildMemberRemove.js +++ b/src/listeners/client/guildMemberRemove.js @@ -9,7 +9,24 @@ module.exports = class extends Listener { }); } - run(member) { - // TODO: close tickets + /** + * + * @param {import("discord.js").GuildMember} member + */ + async run(member) { + /** @type {import("client")} */ + const client = this.client; + + const tickets = await client.prisma.ticket.findMany({ + where: { + createdById: member.id, + guildId: member.guild.id, + open: true, + }, + }); + + for (const ticket of tickets) { + await client.tickets.close(ticket.id, true, 'user left server'); + } } };