ticket creation works

This commit is contained in:
Isaac 2022-08-08 21:55:09 +01:00
parent 01e479dab5
commit bc3ccdcb82
No known key found for this signature in database
GPG Key ID: F4EAABEB0FFCC06A
10 changed files with 226 additions and 95 deletions

12
src/buttons/edit.js Normal file
View File

@ -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) { }
};

View File

@ -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) { }
};

View File

@ -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) { }
};

View File

@ -9,28 +9,6 @@ module.exports = class TopicSlashCommand extends SlashCommand {
const nameLocalizations = {}; const nameLocalizations = {};
client.i18n.locales.forEach(l => (nameLocalizations[l] = client.i18n.getMessage(l, 'commands.slash.topic.name'))); 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, { super(client, {
...options, ...options,
description: descriptionLocalizations['en-GB'], description: descriptionLocalizations['en-GB'],
@ -38,7 +16,6 @@ module.exports = class TopicSlashCommand extends SlashCommand {
dmPermission: false, dmPermission: false,
name: nameLocalizations['en-GB'], name: nameLocalizations['en-GB'],
nameLocalizations, nameLocalizations,
options: opts,
}); });
} }

View File

@ -1,10 +1,22 @@
buttons: buttons:
claim:
emoji: 🙌
text: Claim
close:
emoji: ✖️
text: Close
confirm_open: confirm_open:
emoji: emoji:
text: Create ticket text: Create ticket
create: create:
emoji: 🎫 emoji: 🎫
text: Create a ticket text: Create a ticket
edit:
emoji: ✏️
text: Edit
unclaim:
emoji: ♻️
text: Release
commands: commands:
message: message:
create: create:
@ -22,6 +34,9 @@ commands:
ticket: ticket:
description: The ticket to add the member to description: The ticket to add the member to
name: ticket name: ticket
claim:
description: Claim a ticket
name: claim
close: close:
description: Close a ticket description: Close a ticket
name: close name: close
@ -79,6 +94,9 @@ commands:
LOW: 🟢 Low LOW: 🟢 Low
description: The priority of the ticket description: The priority of the ticket
name: priority name: priority
release:
description: Release (unclaim) a ticket
name: release
remove: remove:
description: Remove a member from a ticket description: Remove a member from a ticket
name: remove name: remove
@ -99,10 +117,6 @@ commands:
topic: topic:
description: Change the topic of a ticket description: Change the topic of a ticket
name: topic name: topic
options:
new-topic:
description: The new topic of the ticket
name: new-topic
tickets: tickets:
description: List your own or someone else's tickets description: List your own or someone else's tickets
name: tickets name: tickets
@ -152,10 +166,6 @@ menus:
placeholder: Select a ticket category placeholder: Select a ticket category
guild: guild:
placeholder: Select a server placeholder: Select a server
modals:
feedback:
title: 'Feedback'
topic: 'Topic'
misc: misc:
no_categories: no_categories:
description: No ticket categories have been configured. description: No ticket categories have been configured.
@ -166,7 +176,16 @@ misc:
unknown_category: unknown_category:
description: Please try a different category. description: Please try a different category.
title: ❌ That ticket category doesn't exist title: ❌ That ticket category doesn't exist
modals:
feedback:
title: Feedback
topic:
label: Topic
placeholder: What is this ticket about?
ticket: ticket:
created:
description: 'Your ticket channel has been created: {channel}.'
title: ✅ Ticket created
answers: answers:
no_value: '*No response*' no_value: '*No response*'
opening_message: opening_message:

8
src/lib/embed.js Normal file
View File

@ -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);
}
};

View File

@ -1,6 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const { const {
ActionRowBuilder, ActionRowBuilder,
ButtonStyle,
ModalBuilder, ModalBuilder,
SelectMenuBuilder, SelectMenuBuilder,
SelectMenuOptionBuilder, SelectMenuOptionBuilder,
@ -9,7 +10,8 @@ const {
} = require('discord.js'); } = require('discord.js');
const emoji = require('node-emoji'); const emoji = require('node-emoji');
const ms = require('ms'); 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 * @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({ async create({
categoryId, interaction, topic, referencesMessage, referencesTicket, categoryId, interaction, topic, referencesMessage, referencesTicket,
}) { }) {
categoryId = Number(categoryId);
const cacheKey = `cache/category+guild+questions:${categoryId}`; const cacheKey = `cache/category+guild+questions:${categoryId}`;
/** @type {CategoryGuildQuestions} */ /** @type {CategoryGuildQuestions} */
let category = await this.client.keyv.get(cacheKey); 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 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({ 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, ephemeral: true,
}); });
} }
@ -74,24 +75,24 @@ module.exports = class TicketManager {
const rlKey = `ratelimits/guild-user:${category.guildId}-${interaction.user.id}`; const rlKey = `ratelimits/guild-user:${category.guildId}-${interaction.user.id}`;
const rl = await this.client.keyv.get(rlKey); const rl = await this.client.keyv.get(rlKey);
if (rl) { 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({ 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, ephemeral: true,
}); });
} else { } else {
this.client.keyv.set(rlKey, true, ms('10s')); this.client.keyv.set(rlKey, true, ms('10s'));
} }
// TODO: if blacklisted role -> stop
// TODO: if member !required roles -> stop // TODO: if member !required roles -> stop
// TODO: if discordCategory has 50 channels -> stop // TODO: if discordCategory has 50 channels -> stop
@ -124,7 +125,7 @@ module.exports = class TicketManager {
.setCustomId(q.id) .setCustomId(q.id)
.setLabel(q.label) .setLabel(q.label)
.setStyle(q.style) .setStyle(q.style)
.setMaxLength(q.maxLength) .setMaxLength(Math.min(q.maxLength, 1000))
.setMinLength(q.minLength) .setMinLength(q.minLength)
.setPlaceholder(q.placeholder) .setPlaceholder(q.placeholder)
.setRequired(q.required) .setRequired(q.required)
@ -168,8 +169,12 @@ module.exports = class TicketManager {
.setComponents( .setComponents(
new TextInputBuilder() new TextInputBuilder()
.setCustomId('topic') .setCustomId('topic')
.setLabel(getMessage('modals.topic')) .setLabel(getMessage('modals.topic.label'))
.setStyle(TextInputStyle.Long), .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] * @param {string?} [data.topic]
*/ */
async postQuestions({ async postQuestions({
categoryId, interaction, topic, referencesMessage, referencesTicket, action, categoryId, interaction, topic, referencesMessage, referencesTicket,
}) { }) {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
@ -199,12 +204,16 @@ module.exports = class TicketManager {
let answers; let answers;
if (interaction.isModalSubmit()) { if (interaction.isModalSubmit()) {
answers = category.questions.map(q => ({ if (action === 'questions') {
questionId: q.id, answers = category.questions.map(q => ({
userId: interaction.user.id, questionId: q.id,
value: interaction.fields.getTextInputValue(q.id), userId: interaction.user.id,
})); value: interaction.fields.getTextInputValue(q.id),
if (category.customTopic) topic = interaction.fields.getTextInputValue(category.customTopic); }));
if (category.customTopic) topic = interaction.fields.getTextInputValue(category.customTopic);
} else if (action === 'topic') {
topic = interaction.fields.getTextInputValue('topic');
}
} }
/** @type {import("discord.js").Guild} */ /** @type {import("discord.js").Guild} */
@ -244,48 +253,101 @@ module.exports = class TicketManager {
topic: `${creator}${topic?.length > 0 ? ` | ${topic}` : ''}`, topic: `${creator}${topic?.length > 0 ? ` | ${topic}` : ''}`,
}); });
const embed = new EmbedBuilder() if (category.image) await channel.send(category.image);
.setColor(category.guild.primaryColour)
.setAuthor({
iconURL: creator.displayAvatarURL(),
name: creator.displayName,
})
.setDescription(
category.openingMessage
.replace(/{+\s?(user)?name\s?}+/gi, creator.user.toString()),
); 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) { if (answers) {
embed.setFields( embeds.push(
category.questions.map(q => ({ new ExtendedEmbedBuilder()
name: q.label, .setColor(category.guild.primaryColour)
value: interaction.fields.getTextInputValue(q.id) || getMessage('ticket.answers.no_value'), .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) { } else if (topic) {
embed.setFields({ embeds.push(
name: getMessage('ticket.opening_message.fields.topic'), new ExtendedEmbedBuilder()
value: topic, .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) { if (category.guild.footer) {
embed.setFooter({ embeds[embeds.length - 1].setFooter({
iconURL: guild.iconURL(), iconURL: guild.iconURL(),
text: category.guild.footer, text: category.guild.footer,
}); });
} }
// TODO: add edit button (if topic or questions) const components = new ActionRowBuilder();
// TODO: add close and claim buttons if enabled
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 pings = category.pingRoles.map(r => `<@&${r}>`).join(' ');
const sent = await channel.send({ const sent = await channel.send({
components: components.components.length >=1 ? [components] : [],
content: getMessage('ticket.opening_message.content', { content: getMessage('ticket.opening_message.content', {
creator: interaction.user.toString(), creator: interaction.user.toString(),
staff: pings ? pings + ',' : '', staff: pings ? pings + ',' : '',
}), }),
embeds: [embed], embeds,
}); });
await sent.pin({ reason: 'Ticket opening message' }); await sent.pin({ reason: 'Ticket opening message' });
const pinned = channel.messages.cache.last(); 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 (message) data.referencesMessage = { connect: { id: referencesMessage } }; // only add if the message has been archived ^^
if (answers) data.questionAnswers = { createMany: { data: answers } }; if (answers) data.questionAnswers = { createMany: { data: answers } };
const ticket = await this.client.prisma.ticket.create({ data }); const ticket = await this.client.prisma.ticket.create({ data });
console.log(ticket);
interaction.editReply({ interaction.editReply({
components: [], 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 // TODO: log channel
} }

View File

@ -178,6 +178,7 @@ module.exports = class extends Listener {
} }
} else { } else {
// TODO: archive messages in tickets // TODO: archive messages in tickets
// TODO: first response
// TODO: auto tag // TODO: auto tag
} }
} }

View File

@ -32,7 +32,7 @@ module.exports = class extends Listener {
cached = { cached = {
avgResolutionTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / closedTickets.length), 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), 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, totalTickets: tickets.length,
}; };
await this.client.keyv.set(cacheKey, cached, ms('15m')); await this.client.keyv.set(cacheKey, cached, ms('15m'));

View File

@ -9,10 +9,9 @@ module.exports = class TopicModal extends Modal {
} }
async run(id, interaction) { async run(id, interaction) {
console.log(id); await this.client.tickets.postQuestions({
console.log(require('util').inspect(interaction, { ...id,
colors: true, interaction,
depth: 10, });
}));
} }
}; };