From bc3ccdcb82b49f3e5c32c55d84181f081105e4c0 Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 8 Aug 2022 21:55:09 +0100 Subject: [PATCH] ticket creation works --- src/buttons/edit.js | 12 ++ src/commands/slash/claim.js | 23 ++++ src/commands/slash/release.js | 23 ++++ src/commands/slash/topic.js | 23 ---- src/i18n/en-GB.yml | 35 +++-- src/lib/embed.js | 8 ++ src/lib/tickets/manager.js | 185 ++++++++++++++++++-------- src/listeners/client/messageCreate.js | 1 + src/listeners/client/ready.js | 2 +- src/modals/topic.js | 9 +- 10 files changed, 226 insertions(+), 95 deletions(-) create mode 100644 src/buttons/edit.js create mode 100644 src/commands/slash/claim.js create mode 100644 src/commands/slash/release.js create mode 100644 src/lib/embed.js diff --git a/src/buttons/edit.js b/src/buttons/edit.js new file mode 100644 index 0000000..479d222 --- /dev/null +++ b/src/buttons/edit.js @@ -0,0 +1,12 @@ +const { Button } = require('@eartharoid/dbf'); + +module.exports = class EditButton extends Button { + constructor(client, options) { + super(client, { + ...options, + id: 'edit', + }); + } + + async run(id, interaction) { } +}; \ No newline at end of file diff --git a/src/commands/slash/claim.js b/src/commands/slash/claim.js new file mode 100644 index 0000000..814e702 --- /dev/null +++ b/src/commands/slash/claim.js @@ -0,0 +1,23 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); + +module.exports = class ClaimSlashCommand extends SlashCommand { + constructor(client, options) { + const descriptionLocalizations = {}; + client.i18n.locales.forEach(l => (descriptionLocalizations[l] = client.i18n.getMessage(l, 'commands.slash.claim.description'))); + + const nameLocalizations = {}; + client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.slash.claim.name'))); + + super(client, { + ...options, + description: descriptionLocalizations['en-GB'], + descriptionLocalizations, + dmPermission: false, + name: nameLocalizations['en-GB'], + nameLocalizations, + }); + } + + async run(interaction) { } +}; \ No newline at end of file diff --git a/src/commands/slash/release.js b/src/commands/slash/release.js new file mode 100644 index 0000000..7722fe2 --- /dev/null +++ b/src/commands/slash/release.js @@ -0,0 +1,23 @@ +const { SlashCommand } = require('@eartharoid/dbf'); +const { ApplicationCommandOptionType } = require('discord.js'); + +module.exports = class ReleaseSlashCommand extends SlashCommand { + constructor(client, options) { + const descriptionLocalizations = {}; + client.i18n.locales.forEach(l => (descriptionLocalizations[l] = client.i18n.getMessage(l, 'commands.slash.release.description'))); + + const nameLocalizations = {}; + client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.slash.release.name'))); + + super(client, { + ...options, + description: descriptionLocalizations['en-GB'], + descriptionLocalizations, + dmPermission: false, + name: nameLocalizations['en-GB'], + nameLocalizations, + }); + } + + async run(interaction) { } +}; \ No newline at end of file diff --git a/src/commands/slash/topic.js b/src/commands/slash/topic.js index 1774dc1..8fe6b39 100644 --- a/src/commands/slash/topic.js +++ b/src/commands/slash/topic.js @@ -9,28 +9,6 @@ module.exports = class TopicSlashCommand extends SlashCommand { const nameLocalizations = {}; client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.slash.topic.name'))); - let opts = [ - { - name: 'new-topic', - required: true, - type: ApplicationCommandOptionType.String, - }, - ]; - opts = opts.map(o => { - const descriptionLocalizations = {}; - client.i18n.locales.forEach(l => (descriptionLocalizations[l] = client.i18n.getMessage(l, `commands.slash.topic.options.${o.name}.description`))); - - const nameLocalizations = {}; - client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, `commands.slash.topic.options.${o.name}.name`))); - - return { - ...o, - description: descriptionLocalizations['en-GB'], - descriptionLocalizations, - nameLocalizations: nameLocalizations, - }; - }); - super(client, { ...options, description: descriptionLocalizations['en-GB'], @@ -38,7 +16,6 @@ module.exports = class TopicSlashCommand extends SlashCommand { dmPermission: false, name: nameLocalizations['en-GB'], nameLocalizations, - options: opts, }); } diff --git a/src/i18n/en-GB.yml b/src/i18n/en-GB.yml index 435bfbf..da77e06 100644 --- a/src/i18n/en-GB.yml +++ b/src/i18n/en-GB.yml @@ -1,10 +1,22 @@ buttons: + claim: + emoji: 🙌 + text: Claim + close: + emoji: ✖️ + text: Close confirm_open: emoji: ✅ text: Create ticket create: emoji: 🎫 text: Create a ticket + edit: + emoji: ✏️ + text: Edit + unclaim: + emoji: ♻️ + text: Release commands: message: create: @@ -22,6 +34,9 @@ commands: ticket: description: The ticket to add the member to name: ticket + claim: + description: Claim a ticket + name: claim close: description: Close a ticket name: close @@ -79,6 +94,9 @@ commands: LOW: 🟢 Low description: The priority of the ticket name: priority + release: + description: Release (unclaim) a ticket + name: release remove: description: Remove a member from a ticket name: remove @@ -99,10 +117,6 @@ commands: topic: description: Change the topic of a ticket name: topic - options: - new-topic: - description: The new topic of the ticket - name: new-topic tickets: description: List your own or someone else's tickets name: tickets @@ -152,10 +166,6 @@ menus: placeholder: Select a ticket category guild: placeholder: Select a server -modals: - feedback: - title: 'Feedback' - topic: 'Topic' misc: no_categories: description: No ticket categories have been configured. @@ -166,7 +176,16 @@ misc: unknown_category: description: Please try a different category. title: ❌ That ticket category doesn't exist +modals: + feedback: + title: Feedback + topic: + label: Topic + placeholder: What is this ticket about? ticket: + created: + description: 'Your ticket channel has been created: {channel}.' + title: ✅ Ticket created answers: no_value: '*No response*' opening_message: diff --git a/src/lib/embed.js b/src/lib/embed.js new file mode 100644 index 0000000..4b3504d --- /dev/null +++ b/src/lib/embed.js @@ -0,0 +1,8 @@ +const { EmbedBuilder } = require('discord.js'); + +module.exports = class ExtendedEmbedBuilder extends EmbedBuilder { + constructor(footer, opts) { + super(opts); + if (footer && footer.text) this.setFooter(footer); + } +}; \ No newline at end of file diff --git a/src/lib/tickets/manager.js b/src/lib/tickets/manager.js index a8e802d..7256c80 100644 --- a/src/lib/tickets/manager.js +++ b/src/lib/tickets/manager.js @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ const { ActionRowBuilder, + ButtonStyle, ModalBuilder, SelectMenuBuilder, SelectMenuOptionBuilder, @@ -9,7 +10,8 @@ const { } = require('discord.js'); const emoji = require('node-emoji'); const ms = require('ms'); -const { EmbedBuilder } = require('discord.js'); +const ExtendedEmbedBuilder = require('../embed'); +const { ButtonBuilder } = require('discord.js'); /** * @typedef {import('@prisma/client').Category & {guild: import('@prisma/client').Guild} & {questions: import('@prisma/client').Question[]}} CategoryGuildQuestions @@ -29,6 +31,7 @@ module.exports = class TicketManager { async create({ categoryId, interaction, topic, referencesMessage, referencesTicket, }) { + categoryId = Number(categoryId); const cacheKey = `cache/category+guild+questions:${categoryId}`; /** @type {CategoryGuildQuestions} */ let category = await this.client.keyv.get(cacheKey); @@ -51,18 +54,16 @@ module.exports = class TicketManager { }; } const getMessage = this.client.i18n.getLocale(settings.locale); - const embed = new EmbedBuilder() - .setColor(settings.errorColour) - .setTitle(getMessage('misc.unknown_category.title')) - .setDescription(getMessage('misc.unknown_category.description')); - if (settings.footer) { - embed.setFooter({ - iconURL: interaction.guild?.iconURL(), - text: settings.footer, - }); - } return await interaction.reply({ - embeds: [embed], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild?.iconURL(), + text: settings.footer, + }) + .setColor(settings.errorColour) + .setTitle(getMessage('misc.unknown_category.title')) + .setDescription(getMessage('misc.unknown_category.description')), + ], ephemeral: true, }); } @@ -74,24 +75,24 @@ module.exports = class TicketManager { const rlKey = `ratelimits/guild-user:${category.guildId}-${interaction.user.id}`; const rl = await this.client.keyv.get(rlKey); if (rl) { - const embed = new EmbedBuilder() - .setColor(category.guild.errorColour) - .setTitle(getMessage('misc.ratelimited.title')) - .setDescription(getMessage('misc.ratelimited.description')); - if (category.guild.footer) { - embed.setFooter({ - iconURL: interaction.guild.iconURL(), - text: category.guild.footer, - }); - } return await interaction.reply({ - embeds: [embed], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: interaction.guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.errorColour) + .setTitle(getMessage('misc.ratelimited.title')) + .setDescription(getMessage('misc.ratelimited.description')), + ], ephemeral: true, }); } else { this.client.keyv.set(rlKey, true, ms('10s')); } + // TODO: if blacklisted role -> stop + // TODO: if member !required roles -> stop // TODO: if discordCategory has 50 channels -> stop @@ -124,7 +125,7 @@ module.exports = class TicketManager { .setCustomId(q.id) .setLabel(q.label) .setStyle(q.style) - .setMaxLength(q.maxLength) + .setMaxLength(Math.min(q.maxLength, 1000)) .setMinLength(q.minLength) .setPlaceholder(q.placeholder) .setRequired(q.required) @@ -168,8 +169,12 @@ module.exports = class TicketManager { .setComponents( new TextInputBuilder() .setCustomId('topic') - .setLabel(getMessage('modals.topic')) - .setStyle(TextInputStyle.Long), + .setLabel(getMessage('modals.topic.label')) + .setStyle(TextInputStyle.Paragraph) + .setMaxLength(1000) + .setMinLength(5) + .setPlaceholder(getMessage('modals.topic.placeholder')) + .setRequired(true), ), ), ); @@ -189,7 +194,7 @@ module.exports = class TicketManager { * @param {string?} [data.topic] */ async postQuestions({ - categoryId, interaction, topic, referencesMessage, referencesTicket, + action, categoryId, interaction, topic, referencesMessage, referencesTicket, }) { await interaction.deferReply({ ephemeral: true }); @@ -199,12 +204,16 @@ module.exports = class TicketManager { let answers; if (interaction.isModalSubmit()) { - answers = category.questions.map(q => ({ - questionId: q.id, - userId: interaction.user.id, - value: interaction.fields.getTextInputValue(q.id), - })); - if (category.customTopic) topic = interaction.fields.getTextInputValue(category.customTopic); + if (action === 'questions') { + answers = category.questions.map(q => ({ + questionId: q.id, + userId: interaction.user.id, + value: interaction.fields.getTextInputValue(q.id), + })); + if (category.customTopic) topic = interaction.fields.getTextInputValue(category.customTopic); + } else if (action === 'topic') { + topic = interaction.fields.getTextInputValue('topic'); + } } /** @type {import("discord.js").Guild} */ @@ -244,48 +253,101 @@ module.exports = class TicketManager { topic: `${creator}${topic?.length > 0 ? ` | ${topic}` : ''}`, }); - const embed = new EmbedBuilder() - .setColor(category.guild.primaryColour) - .setAuthor({ - iconURL: creator.displayAvatarURL(), - name: creator.displayName, - }) - .setDescription( - category.openingMessage - .replace(/{+\s?(user)?name\s?}+/gi, creator.user.toString()), + if (category.image) await channel.send(category.image); - ); + const embeds = [ + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setAuthor({ + iconURL: creator.displayAvatarURL(), + name: creator.displayName, + }) + .setDescription( + category.openingMessage + .replace(/{+\s?(user)?name\s?}+/gi, creator.user.toString()), + + ), + ]; if (answers) { - embed.setFields( - category.questions.map(q => ({ - name: q.label, - value: interaction.fields.getTextInputValue(q.id) || getMessage('ticket.answers.no_value'), - })), + embeds.push( + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setFields( + category.questions.map(q => ({ + name: q.label, + value: interaction.fields.getTextInputValue(q.id) || getMessage('ticket.answers.no_value'), + })), + ), ); + // embeds[0].setFields( + // category.questions.map(q => ({ + // name: q.label, + // value: interaction.fields.getTextInputValue(q.id) || getMessage('ticket.answers.no_value'), + // })), + // ); } else if (topic) { - embed.setFields({ - name: getMessage('ticket.opening_message.fields.topic'), - value: topic, - }); + embeds.push( + new ExtendedEmbedBuilder() + .setColor(category.guild.primaryColour) + .setFields({ + name: getMessage('ticket.opening_message.fields.topic'), + value: topic, + }), + ); + // embeds[0].setFields({ + // name: getMessage('ticket.opening_message.fields.topic'), + // value: topic, + // }); } if (category.guild.footer) { - embed.setFooter({ + embeds[embeds.length - 1].setFooter({ iconURL: guild.iconURL(), text: category.guild.footer, }); } - // TODO: add edit button (if topic or questions) - // TODO: add close and claim buttons if enabled + const components = new ActionRowBuilder(); + + if (topic || answers) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'edit' })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.edit.emoji')) + .setLabel(getMessage('buttons.edit.text')), + ); + } + + if (category.guild.claimButton && category.claiming) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'claim' })) + .setStyle(ButtonStyle.Secondary) + .setEmoji(getMessage('buttons.claim.emoji')) + .setLabel(getMessage('buttons.claim.text')), + ); + } + + if (category.guild.closeButton) { + components.addComponents( + new ButtonBuilder() + .setCustomId(JSON.stringify({ action: 'close' })) + .setStyle(ButtonStyle.Danger) + .setEmoji(getMessage('buttons.close.emoji')) + .setLabel(getMessage('buttons.close.text')), + ); + } + const pings = category.pingRoles.map(r => `<@&${r}>`).join(' '); const sent = await channel.send({ + components: components.components.length >=1 ? [components] : [], content: getMessage('ticket.opening_message.content', { creator: interaction.user.toString(), staff: pings ? pings + ',' : '', }), - embeds: [embed], + embeds, }); await sent.pin({ reason: 'Ticket opening message' }); const pinned = channel.messages.cache.last(); @@ -318,10 +380,17 @@ module.exports = class TicketManager { if (message) data.referencesMessage = { connect: { id: referencesMessage } }; // only add if the message has been archived ^^ if (answers) data.questionAnswers = { createMany: { data: answers } }; const ticket = await this.client.prisma.ticket.create({ data }); - console.log(ticket); interaction.editReply({ components: [], - embeds: [], + embeds: [ + new ExtendedEmbedBuilder({ + iconURL: guild.iconURL(), + text: category.guild.footer, + }) + .setColor(category.guild.successColour) + .setTitle(getMessage('ticket.created.title')) + .setDescription(getMessage('ticket.created.description', { channel: channel.toString() })), + ], }); // TODO: log channel } diff --git a/src/listeners/client/messageCreate.js b/src/listeners/client/messageCreate.js index 64bcd37..f4b7012 100644 --- a/src/listeners/client/messageCreate.js +++ b/src/listeners/client/messageCreate.js @@ -178,6 +178,7 @@ module.exports = class extends Listener { } } else { // TODO: archive messages in tickets + // TODO: first response // TODO: auto tag } } diff --git a/src/listeners/client/ready.js b/src/listeners/client/ready.js index 2313360..5575f68 100644 --- a/src/listeners/client/ready.js +++ b/src/listeners/client/ready.js @@ -32,7 +32,7 @@ module.exports = class extends Listener { cached = { avgResolutionTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / closedTickets.length), avgResponseTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / closedTickets.length), - openTickets: tickets.filter(t => t.open).length, + openTickets: tickets.length - closedTickets.length, totalTickets: tickets.length, }; await this.client.keyv.set(cacheKey, cached, ms('15m')); diff --git a/src/modals/topic.js b/src/modals/topic.js index 3e98ba1..84544d3 100644 --- a/src/modals/topic.js +++ b/src/modals/topic.js @@ -9,10 +9,9 @@ module.exports = class TopicModal extends Modal { } async run(id, interaction) { - console.log(id); - console.log(require('util').inspect(interaction, { - colors: true, - depth: 10, - })); + await this.client.tickets.postQuestions({ + ...id, + interaction, + }); } }; \ No newline at end of file