more on closing and feedback (WIP)

This commit is contained in:
Isaac 2023-01-13 22:25:04 +00:00
parent 8bf01aa520
commit 2a8c1603f2
No known key found for this signature in database
GPG Key ID: 0DE40AE37BBA5C33
9 changed files with 158 additions and 69 deletions

View File

@ -1,4 +1,5 @@
const { Button } = require('@eartharoid/dbf'); const { Button } = require('@eartharoid/dbf');
const ExtendedEmbedBuilder = require('../lib/embed');
const { isStaff } = require('../lib/users'); const { isStaff } = require('../lib/users');
module.exports = class CloseButton extends Button { module.exports = class CloseButton extends Button {
@ -17,28 +18,55 @@ module.exports = class CloseButton extends Button {
/** @type {import("client")} */ /** @type {import("client")} */
const client = this.client; const client = this.client;
// the close button on th opening message, the same as using /close
if (id.accepted === undefined) { if (id.accepted === undefined) {
await client.tickets.beforeRequestClose(interaction); await client.tickets.beforeRequestClose(interaction);
} else { } 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(); await interaction.deferReply();
const ticket = await client.prisma.ticket.findUnique({ const ticket = await client.prisma.ticket.findUnique({
include: { guild: true }, include: {
category: true,
guild: true,
},
where: { id: interaction.channel.id }, 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)) { if (id.expect === 'staff' && !staff) {
return; return; // TODO: please wait for staff to close the ticket
} else if (interaction.user.id !== ticket.createdById) { } else if (id.expect === 'user' && staff) {
return; return; // TODO: please wait for the user to respond
// if user and expect user (or is creator), feedback modal (if enabled) } else {
// otherwise add "Give feedback" button in DM message (if enabled) 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);
}
}
} }
} }
} }

View File

@ -62,7 +62,7 @@ module.exports = class ForceCloseSlashCommand extends SlashCommand {
await interaction.deferReply(); await interaction.deferReply();
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); 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; let ticket;
if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff if (!(await isStaff(interaction.guild, interaction.user.id))) { // if user is not staff

View File

@ -352,6 +352,7 @@ ticket:
forbidden: forbidden:
description: You don't have permission to close this ticket. description: You don't have permission to close this ticket.
title: ❌ Error title: ❌ Error
rejected: ✋ {user} rejected a request to close this ticket.
staff_request: staff_request:
archived: | archived: |
@ -368,6 +369,7 @@ ticket:
edited: edited:
description: Your changes have been saved. description: Your changes have been saved.
title: ✅ Ticket updated title: ✅ Ticket updated
feedback: Thank you for your feedback.
opening_message: opening_message:
content: | content: |
{staff} {staff}

View File

@ -30,7 +30,7 @@ module.exports = async client => {
const guild = client.guilds.cache.get(ticket.guildId); const guild = client.guilds.cache.get(ticket.guildId);
if (guild && guild.available && !client.channels.cache.has(ticket.id)) { if (guild && guild.available && !client.channels.cache.has(ticket.id)) {
deleted += 0; deleted += 0;
await client.tickets.close(ticket.id, true, 'channel deleted'); await client.tickets.finallyClose(ticket.id, { reason: 'channel deleted' });
} }
} }

View File

@ -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 * @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({ const ticket = await this.client.prisma.ticket.findUnique({
include: { include: {
category: { select: { enableFeedback: true } }, category: { select: { enableFeedback: true } },
feedback: { select: { id: true } }, feedback: true,
guild: true, guild: true,
}, },
where: { id: interaction.channel.id }, where: { id: interaction.channel.id },
@ -836,7 +870,10 @@ module.exports = class TicketManager {
const getMessage = this.client.i18n.getLocale(locale); const getMessage = this.client.i18n.getLocale(locale);
return await interaction.editReply({ return await interaction.editReply({
embeds: [ embeds: [
new ExtendedEmbedBuilder() new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(),
text: ticket.guild.footer,
})
.setColor(errorColour) .setColor(errorColour)
.setTitle(getMessage('misc.not_ticket.title')) .setTitle(getMessage('misc.not_ticket.title'))
.setDescription(getMessage('misc.not_ticket.description')), .setDescription(getMessage('misc.not_ticket.description')),
@ -846,7 +883,7 @@ module.exports = class TicketManager {
const getMessage = this.client.i18n.getLocale(ticket.guild.locale); const getMessage = this.client.i18n.getLocale(ticket.guild.locale);
const staff = await isStaff(interaction.guild, interaction.user.id); 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) { if (ticket.createdById !== interaction.user.id && !staff) {
return await interaction.editReply({ return await interaction.editReply({
@ -859,42 +896,19 @@ module.exports = class TicketManager {
}); });
} }
if (ticket.createdById === interaction.user.id && ticket.category.enableFeedback && !ticket.feedback) { if (
return await interaction.showModal( ticket.createdById === interaction.user.id &&
new ModalBuilder() ticket.category.enableFeedback &&
.setCustomId(JSON.stringify({ !ticket.feedback
action: 'feedback', ) {
reason, return await interaction.showModal(this.buildFeedbackModal(ticket.guild.locale, {
})) next: 'requestClose',
.setTitle(getMessage('modals.feedback.title')) reason, // known issue: a reason longer than a few words will cause an error due to 100 character ID limit
.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),
),
),
);
} }
// not showing feedback, so send the close request
// defer asap // defer asap
await interaction.deferReply(); await interaction.deferReply();
@ -903,7 +917,7 @@ module.exports = class TicketManager {
try { try {
await interaction.guild.members.fetch(ticket.createdById); await interaction.guild.members.fetch(ticket.createdById);
} catch { } catch {
return this.close(ticket.id, true, reason); return this.finallyClose(ticket.id, { reason });
} }
await this.requestClose(interaction, 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 {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction|import("discord.js").ModalSubmitInteraction} interaction
* @param {string} reason
*/ */
async requestClose(interaction, reason) { async requestClose(interaction, reason) {
// interaction could be command, button. or modal // interaction could be command, button. or modal
@ -924,7 +939,10 @@ module.exports = class TicketManager {
action: 'close', action: 'close',
expect: staff ? 'user' : 'staff', expect: staff ? 'user' : 'staff',
}; };
const embed = new ExtendedEmbedBuilder() const embed = new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(),
text: ticket.guild.footer,
})
.setColor(ticket.guild.primaryColour) .setColor(ticket.guild.primaryColour)
.setTitle(getMessage(`ticket.close.${staff ? 'staff' : 'user'}_request.title`, { requestedBy: interaction.member.displayName })); .setTitle(getMessage(`ticket.close.${staff ? 'staff' : 'user'}_request.title`, { requestedBy: interaction.member.displayName }));
@ -965,6 +983,7 @@ module.exports = class TicketManager {
closeAt: ticket.guild.autoClose ? Date.now() + ticket.guild.autoClose : null, closeAt: ticket.guild.autoClose ? Date.now() + ticket.guild.autoClose : null,
closedBy: interaction.user.id, // null if set as stale due to inactivity closedBy: interaction.user.id, // null if set as stale due to inactivity
message: sent, message: sent,
messages: 0,
reason, reason,
staleSince: Date.now(), 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 * close a ticket
* @param {string} ticketId * @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/cat count
// TODO: update cache/member count // TODO: update cache/member count
// TODO: set messageCount on ticket // TODO: set messageCount on ticket

View File

@ -19,7 +19,7 @@ module.exports = class extends Listener {
}); });
if (!ticket) return; 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`); this.client.log.info(`Closed ticket ${ticket.id} because the channel was deleted`);
} }
}; };

View File

@ -26,7 +26,7 @@ module.exports = class extends Listener {
}); });
for (const ticket of tickets) { 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' });
} }
} }
}; };

View File

@ -218,8 +218,14 @@ module.exports = class extends Listener {
// if the ticket was set as stale, unset it // if the ticket was set as stale, unset it
if (client.tickets.$stale.has(ticket.id)) { if (client.tickets.$stale.has(ticket.id)) {
await message.channel.messages.delete(client.tickets.$stale.get(ticket.id).message.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); client.tickets.$stale.delete(ticket.id);
} else {
client.tickets.$stale.set(ticket.id, $ticket);
}
} }
} }

View File

@ -1,5 +1,5 @@
const { Modal } = require('@eartharoid/dbf'); const { Modal } = require('@eartharoid/dbf');
const ExtendedEmbedBuilder = require('../lib/embed');
module.exports = class FeedbackModal extends Modal { module.exports = class FeedbackModal extends Modal {
constructor(client, options) { constructor(client, options) {
super(client, { super(client, {
@ -8,24 +8,52 @@ module.exports = class FeedbackModal extends Modal {
}); });
} }
/**
* @param {*} id
* @param {import("discord.js").ModalSubmitInteraction} interaction
*/
async run(id, interaction) { async run(id, interaction) {
/** @type {import("client")} */ /** @type {import("client")} */
const client = this.client; const client = this.client;
await interaction.deferReply(); 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: { data: {
feedback: { feedback: {
create: { create: {
comment: interaction.fields.getTextInputValue('comment'), comment,
guild: { connect: { id: interaction.guild.id } }, guild: { connect: { id: interaction.guild.id } },
rating: parseInt(interaction.fields.getTextInputValue('rating')) || null, rating,
user: { connect: { id: interaction.user.id } }, user: { connect: { id: interaction.user.id } },
}, },
}, },
}, },
include: { guild: true },
where: { id: interaction.channel.id }, 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,
});
}
} }
}; };