From 2a8c1603f2d27491048371beafd0ecc90118f32f Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 13 Jan 2023 22:25:04 +0000 Subject: [PATCH] more on closing and feedback (WIP) --- src/buttons/close.js | 56 ++++++++--- src/commands/slash/force-close.js | 2 +- src/i18n/en-GB.yml | 4 +- src/lib/sync.js | 2 +- src/lib/tickets/manager.js | 111 +++++++++++++--------- src/listeners/client/channelDelete.js | 2 +- src/listeners/client/guildMemberRemove.js | 2 +- src/listeners/client/messageCreate.js | 10 +- src/modals/feedback.js | 38 +++++++- 9 files changed, 158 insertions(+), 69 deletions(-) diff --git a/src/buttons/close.js b/src/buttons/close.js index 1a5cd54..7893884 100644 --- a/src/buttons/close.js +++ b/src/buttons/close.js @@ -1,4 +1,5 @@ const { Button } = require('@eartharoid/dbf'); +const ExtendedEmbedBuilder = require('../lib/embed'); const { isStaff } = require('../lib/users'); module.exports = class CloseButton extends Button { @@ -17,28 +18,55 @@ module.exports = class CloseButton extends Button { /** @type {import("client")} */ const client = this.client; + // the close button on th opening message, the same as using /close if (id.accepted === undefined) { await client.tickets.beforeRequestClose(interaction); } else { - // { - // action: 'close', - // expect: staff ? 'user' : 'staff', - // reason: interaction.options?.getString('reason', false) || null, // ?. because it could be a button interaction - // requestedBy: interaction.user.id, - // } - await interaction.deferReply(); const ticket = await client.prisma.ticket.findUnique({ - include: { guild: true }, + include: { + category: true, + guild: true, + }, where: { id: interaction.channel.id }, }); + const getMessage = client.i18n.getLocale(ticket.guild.locale); + const staff = await isStaff(interaction.guild, interaction.user.id); - if (id.expect === 'staff' && !await isStaff(interaction.guild, interaction.user.id)) { - return; - } else if (interaction.user.id !== ticket.createdById) { - return; - // if user and expect user (or is creator), feedback modal (if enabled) - // otherwise add "Give feedback" button in DM message (if enabled) + if (id.expect === 'staff' && !staff) { + return; // TODO: please wait for staff to close the ticket + } else if (id.expect === 'user' && staff) { + return; // TODO: please wait for the user to respond + } else { + if (id.accepted) { + if ( + ticket.createdById === interaction.user.id && + ticket.category.enableFeedback && + !ticket.feedback + ) { + return await interaction.showModal(client.tickets.buildFeedbackModal(ticket.guild.locale, { next: 'acceptClose' })); + } else { + await client.tickets.acceptClose(interaction); + } + } else { + if (client.tickets.$stale.has(ticket.id)) { + await interaction.channel.messages.edit( + client.tickets.$stale.get(ticket.id).message.id, + { + components: [], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.errorColour) + .setDescription(getMessage('ticket.close.rejected', { user: interaction.user.toString() })), + ], + }, + ); + client.tickets.$stale.delete(ticket.id); + } + } } } } diff --git a/src/commands/slash/force-close.js b/src/commands/slash/force-close.js index dea68f1..1876f60 100644 --- a/src/commands/slash/force-close.js +++ b/src/commands/slash/force-close.js @@ -62,7 +62,7 @@ module.exports = class ForceCloseSlashCommand extends SlashCommand { await interaction.deferReply(); const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); - const getMessage = this.client.i18n.getLocale(settings.locale); + const getMessage = client.i18n.getLocale(settings.locale); let ticket; if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff diff --git a/src/i18n/en-GB.yml b/src/i18n/en-GB.yml index 0be9cef..20783f6 100644 --- a/src/i18n/en-GB.yml +++ b/src/i18n/en-GB.yml @@ -339,7 +339,7 @@ modals: placeholder: Do you have any additional feedback? rating: label: Rating - placeholder: 1-5 + placeholder: 1-5 title: How did we do? topic: label: Topic @@ -352,6 +352,7 @@ ticket: forbidden: description: You don't have permission to close this ticket. title: ❌ Error + rejected: ✋ {user} rejected a request to close this ticket. staff_request: archived: | @@ -368,6 +369,7 @@ ticket: edited: description: Your changes have been saved. title: ✅ Ticket updated + feedback: Thank you for your feedback. opening_message: content: | {staff} diff --git a/src/lib/sync.js b/src/lib/sync.js index e7022b3..91f3d58 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, true, 'channel deleted'); + await client.tickets.finallyClose(ticket.id, { reason: 'channel deleted' }); } } diff --git a/src/lib/tickets/manager.js b/src/lib/tickets/manager.js index 9143a8e..a716368 100644 --- a/src/lib/tickets/manager.js +++ b/src/lib/tickets/manager.js @@ -807,6 +807,40 @@ module.exports = class TicketManager { }); } + buildFeedbackModal(locale, id) { + const getMessage = this.client.i18n.getLocale(locale); + return new ModalBuilder() + .setCustomId(JSON.stringify({ + action: 'feedback', + ...id, + })) + .setTitle(getMessage('modals.feedback.title')) + .setComponents( + new ActionRowBuilder() + .setComponents( + new TextInputBuilder() + .setCustomId('rating') + .setLabel(getMessage('modals.feedback.rating.label')) + .setStyle(TextInputStyle.Short) + .setMaxLength(3) + .setMinLength(1) + .setPlaceholder(getMessage('modals.feedback.rating.placeholder')) + .setRequired(true), + ), + new ActionRowBuilder() + .setComponents( + new TextInputBuilder() + .setCustomId('comment') + .setLabel(getMessage('modals.feedback.comment.label')) + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(1000) + .setMinLength(4) + .setPlaceholder(getMessage('modals.feedback.comment.placeholder')) + .setRequired(false), + ), + ); + } + /** * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction} interaction @@ -815,7 +849,7 @@ module.exports = class TicketManager { const ticket = await this.client.prisma.ticket.findUnique({ include: { category: { select: { enableFeedback: true } }, - feedback: { select: { id: true } }, + feedback: true, guild: true, }, where: { id: interaction.channel.id }, @@ -836,7 +870,10 @@ module.exports = class TicketManager { const getMessage = this.client.i18n.getLocale(locale); return await interaction.editReply({ embeds: [ - new ExtendedEmbedBuilder() + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) .setColor(errorColour) .setTitle(getMessage('misc.not_ticket.title')) .setDescription(getMessage('misc.not_ticket.description')), @@ -846,7 +883,7 @@ module.exports = class TicketManager { const getMessage = this.client.i18n.getLocale(ticket.guild.locale); const staff = await isStaff(interaction.guild, interaction.user.id); - const reason = interaction.options?.getString('reason', false) || null; // ?. because it could be a button interaction) + const reason = interaction.options?.getString('reason', false) || null; // ?. because it could be a button interaction if (ticket.createdById !== interaction.user.id && !staff) { return await interaction.editReply({ @@ -859,42 +896,19 @@ module.exports = class TicketManager { }); } - if (ticket.createdById === interaction.user.id && ticket.category.enableFeedback && !ticket.feedback) { - return await interaction.showModal( - new ModalBuilder() - .setCustomId(JSON.stringify({ - action: 'feedback', - reason, - })) - .setTitle(getMessage('modals.feedback.title')) - .setComponents( - new ActionRowBuilder() - .setComponents( - new TextInputBuilder() - .setCustomId('rating') - .setLabel(getMessage('modals.feedback.rating.label')) - .setStyle(TextInputStyle.Short) - .setMaxLength(3) - .setMinLength(1) - .setPlaceholder(getMessage('modals.feedback.rating.placeholder')) - .setRequired(false), - ), - new ActionRowBuilder() - .setComponents( - new TextInputBuilder() - .setCustomId('comment') - .setLabel(getMessage('modals.feedback.comment.label')) - .setStyle(TextInputStyle.Paragraph) - .setMaxLength(1000) - .setMinLength(4) - .setPlaceholder(getMessage('modals.feedback.comment.placeholder')) - .setRequired(false), - ), - ), - ); - + if ( + ticket.createdById === interaction.user.id && + ticket.category.enableFeedback && + !ticket.feedback + ) { + return await interaction.showModal(this.buildFeedbackModal(ticket.guild.locale, { + next: 'requestClose', + reason, // known issue: a reason longer than a few words will cause an error due to 100 character ID limit + })); } + // not showing feedback, so send the close request + // defer asap await interaction.deferReply(); @@ -903,7 +917,7 @@ module.exports = class TicketManager { try { await interaction.guild.members.fetch(ticket.createdById); } catch { - return this.close(ticket.id, true, reason); + return this.finallyClose(ticket.id, { reason }); } await this.requestClose(interaction, reason); @@ -911,6 +925,7 @@ module.exports = class TicketManager { /** * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction|import("discord.js").ModalSubmitInteraction} interaction + * @param {string} reason */ async requestClose(interaction, reason) { // interaction could be command, button. or modal @@ -924,14 +939,17 @@ module.exports = class TicketManager { action: 'close', expect: staff ? 'user' : 'staff', }; - const embed = new ExtendedEmbedBuilder() + const embed = new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) .setColor(ticket.guild.primaryColour) .setTitle(getMessage(`ticket.close.${staff ? 'staff' : 'user'}_request.title`, { requestedBy: interaction.member.displayName })); if (staff) { embed.setDescription( getMessage('ticket.close.staff_request.description', { requestedBy: interaction.user.toString() }) + - (ticket.guild.archive ? getMessage('ticket.close.staff_request.archived') : ''), + (ticket.guild.archive ? getMessage('ticket.close.staff_request.archived') : ''), ); } @@ -965,6 +983,7 @@ module.exports = class TicketManager { closeAt: ticket.guild.autoClose ? Date.now() + ticket.guild.autoClose : null, closedBy: interaction.user.id, // null if set as stale due to inactivity message: sent, + messages: 0, reason, staleSince: Date.now(), }); @@ -977,13 +996,19 @@ module.exports = class TicketManager { } } + /** + * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction|import("discord.js").ModalSubmitInteraction} interaction + */ + async acceptClose(interaction) {} + /** * close a ticket * @param {string} ticketId - * @param {boolean} skip - * @param {string} reason */ - async close(ticketId, skip, reason) { + async finallyClose(ticketId, { + closedBy, + reason, + }) { // TODO: update cache/cat count // TODO: update cache/member count // TODO: set messageCount on ticket diff --git a/src/listeners/client/channelDelete.js b/src/listeners/client/channelDelete.js index 58e5dc3..ff09f4e 100644 --- a/src/listeners/client/channelDelete.js +++ b/src/listeners/client/channelDelete.js @@ -19,7 +19,7 @@ module.exports = class extends Listener { }); if (!ticket) return; - await client.tickets.close(ticket.id, true, 'channel deleted'); + await client.tickets.finallyClose(ticket.id, { reason: 'channel deleted' }); this.client.log.info(`Closed ticket ${ticket.id} because the channel was deleted`); } }; diff --git a/src/listeners/client/guildMemberRemove.js b/src/listeners/client/guildMemberRemove.js index 40554b0..eb14514 100644 --- a/src/listeners/client/guildMemberRemove.js +++ b/src/listeners/client/guildMemberRemove.js @@ -26,7 +26,7 @@ module.exports = class extends Listener { }); for (const ticket of tickets) { - await client.tickets.close(ticket.id, true, 'user left server'); + await client.tickets.finallyClose(ticket.id, { reason: 'user left server' }); } } }; diff --git a/src/listeners/client/messageCreate.js b/src/listeners/client/messageCreate.js index a8eb04c..c55ced3 100644 --- a/src/listeners/client/messageCreate.js +++ b/src/listeners/client/messageCreate.js @@ -218,8 +218,14 @@ module.exports = class extends Listener { // if the ticket was set as stale, unset it if (client.tickets.$stale.has(ticket.id)) { - await message.channel.messages.delete(client.tickets.$stale.get(ticket.id).message.id); - client.tickets.$stale.delete(ticket.id); + const $ticket = client.tickets.$stale.get(ticket.id); + $ticket.messages++; + if ($ticket.messages >= 5) { + await message.channel.messages.delete($ticket.message.id); + client.tickets.$stale.delete(ticket.id); + } else { + client.tickets.$stale.set(ticket.id, $ticket); + } } } diff --git a/src/modals/feedback.js b/src/modals/feedback.js index cb68190..dcc355a 100644 --- a/src/modals/feedback.js +++ b/src/modals/feedback.js @@ -1,5 +1,5 @@ const { Modal } = require('@eartharoid/dbf'); - +const ExtendedEmbedBuilder = require('../lib/embed'); module.exports = class FeedbackModal extends Modal { constructor(client, options) { super(client, { @@ -8,24 +8,52 @@ module.exports = class FeedbackModal extends Modal { }); } + /** + * @param {*} id + * @param {import("discord.js").ModalSubmitInteraction} interaction + */ async run(id, interaction) { /** @type {import("client")} */ const client = this.client; await interaction.deferReply(); - await client.prisma.ticket.update({ + + const comment = interaction.fields.getTextInputValue('comment'); + const rating = parseInt(interaction.fields.getTextInputValue('rating')) || null; + + const ticket = await client.prisma.ticket.update({ data: { feedback: { create: { - comment: interaction.fields.getTextInputValue('comment'), + comment, guild: { connect: { id: interaction.guild.id } }, - rating: parseInt(interaction.fields.getTextInputValue('rating')) || null, + rating, user: { connect: { id: interaction.user.id } }, }, }, }, + include: { guild: true }, where: { id: interaction.channel.id }, }); - await client.tickets.requestClose(interaction, id.reason); + + if (id.next === 'requestClose') await client.tickets.requestClose(interaction, id.reason); + else if (id.next === 'acceptClose') await client.tickets.acceptClose(interaction); + + const getMessage = client.i18n.getLocale(ticket.guild.locale); + + // `followUp` must go after `reply`/`editReply` (the above) + if (comment?.length > 0 && rating !== null) { + await interaction.followUp({ + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: ticket.guild.footer, + }) + .setColor(ticket.guild.primaryColour) + .setDescription(getMessage('ticket.feedback')), + ], + ephemeral: true, + }); + } } }; \ No newline at end of file