Make progress on ticket creations + fixes

This commit is contained in:
Isaac 2022-08-08 01:36:14 +01:00
parent fcd390bc9d
commit 01e479dab5
No known key found for this signature in database
GPG Key ID: F4EAABEB0FFCC06A
11 changed files with 220 additions and 75 deletions

View File

@ -20,15 +20,16 @@ model ArchivedChannel {
} }
model ArchivedMessage { model ArchivedMessage {
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
authorId String @db.VarChar(19) authorId String @db.VarChar(19)
content String @db.Text content String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
deleted Boolean @default(false) deleted Boolean @default(false)
edited Boolean @default(false) edited Boolean @default(false)
id String @id @db.VarChar(19) id String @id @db.VarChar(19)
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) referencedBy Ticket[] @relation("MessageReferencedByTicket")
ticketId String @db.VarChar(19) ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketId String @db.VarChar(19)
@@map("archivedMessages") @@map("archivedMessages")
} }
@ -189,11 +190,11 @@ model Ticket {
archivedUsers ArchivedUser[] archivedUsers ArchivedUser[]
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
categoryId Int? categoryId Int?
claimedBy User @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id]) claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id])
claimedById String @db.VarChar(19) claimedById String? @db.VarChar(19)
closedAt DateTime? closedAt DateTime?
closedBy User @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id]) closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id])
closedById String @db.VarChar(19) closedById String? @db.VarChar(19)
closedReason String? @db.Text closedReason String? @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id]) createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id])
@ -213,7 +214,8 @@ model Ticket {
pinnedMessages Json @default("[]") pinnedMessages Json @default("[]")
priority TicketPriority? priority TicketPriority?
referencedBy Ticket[] @relation("TicketsReferencedByTicket") referencedBy Ticket[] @relation("TicketsReferencedByTicket")
referencesMessageId String @db.VarChar(19) referencesMessage ArchivedMessage? @relation(name: "MessageReferencedByTicket", fields: [referencesMessageId], references: [id], onDelete: SetNull)
referencesMessageId String? @db.VarChar(19)
referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull) referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull)
referencesTicketId String? @db.VarChar(19) referencesTicketId String? @db.VarChar(19)
topic String? @db.Text topic String? @db.Text

View File

@ -20,15 +20,16 @@ model ArchivedChannel {
} }
model ArchivedMessage { model ArchivedMessage {
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
authorId String @db.VarChar(19) authorId String @db.VarChar(19)
content String @db.Text content String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
deleted Boolean @default(false) deleted Boolean @default(false)
edited Boolean @default(false) edited Boolean @default(false)
id String @id @db.VarChar(19) id String @id @db.VarChar(19)
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) referencedBy Ticket[] @relation("MessageReferencedByTicket")
ticketId String @db.VarChar(19) ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketId String @db.VarChar(19)
@@map("archivedMessages") @@map("archivedMessages")
} }
@ -189,11 +190,11 @@ model Ticket {
archivedUsers ArchivedUser[] archivedUsers ArchivedUser[]
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
categoryId Int? categoryId Int?
claimedBy User @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id]) claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id])
claimedById String @db.VarChar(19) claimedById String? @db.VarChar(19)
closedAt DateTime? closedAt DateTime?
closedBy User @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id]) closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id])
closedById String @db.VarChar(19) closedById String? @db.VarChar(19)
closedReason String? @db.Text closedReason String? @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id]) createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id])
@ -213,7 +214,8 @@ model Ticket {
pinnedMessages Json @default("[]") pinnedMessages Json @default("[]")
priority TicketPriority? priority TicketPriority?
referencedBy Ticket[] @relation("TicketsReferencedByTicket") referencedBy Ticket[] @relation("TicketsReferencedByTicket")
referencesMessageId String @db.VarChar(19) referencesMessage ArchivedMessage? @relation(name: "MessageReferencedByTicket", fields: [referencesMessageId], references: [id], onDelete: SetNull)
referencesMessageId String? @db.VarChar(19)
referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull) referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull)
referencesTicketId String? @db.VarChar(19) referencesTicketId String? @db.VarChar(19)
topic String? @db.Text topic String? @db.Text

View File

@ -20,15 +20,16 @@ model ArchivedChannel {
} }
model ArchivedMessage { model ArchivedMessage {
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade) author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
authorId String authorId String
content String content String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
deleted Boolean @default(false) deleted Boolean @default(false)
edited Boolean @default(false) edited Boolean @default(false)
id String @id id String @id
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade) referencedBy Ticket[] @relation("MessageReferencedByTicket")
ticketId String ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketId String
@@map("archivedMessages") @@map("archivedMessages")
} }
@ -189,11 +190,11 @@ model Ticket {
archivedUsers ArchivedUser[] archivedUsers ArchivedUser[]
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
categoryId Int? categoryId Int?
claimedBy User @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id]) claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id])
claimedById String claimedById String?
closedAt DateTime? closedAt DateTime?
closedBy User @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id]) closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id])
closedById String closedById String?
closedReason String? closedReason String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id]) createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id])
@ -213,7 +214,8 @@ model Ticket {
pinnedMessages String @default("[]") pinnedMessages String @default("[]")
priority String? priority String?
referencedBy Ticket[] @relation("TicketsReferencedByTicket") referencedBy Ticket[] @relation("TicketsReferencedByTicket")
referencesMessageId String referencesMessage ArchivedMessage? @relation(name: "MessageReferencedByTicket", fields: [referencesMessageId], references: [id], onDelete: SetNull)
referencesMessageId String?
referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull) referencesTicket Ticket? @relation(name: "TicketsReferencedByTicket", fields: [referencesTicketId], references: [id], onDelete: SetNull)
referencesTicketId String? referencesTicketId String?
topic String? topic String?

View File

@ -13,5 +13,7 @@ module.exports = class CreateMessageCommand extends MessageCommand {
}); });
} }
async run(interaction) { } async run(interaction) {
// TODO: archive message
}
}; };

View File

@ -1,6 +1,3 @@
test: |
line 1
line 2
buttons: buttons:
confirm_open: confirm_open:
emoji: emoji:
@ -165,7 +162,16 @@ misc:
title: ❌ There are no ticket categories title: ❌ There are no ticket categories
ratelimited: ratelimited:
description: Try again in a few seconds. description: Try again in a few seconds.
title: 🐢 Slow down title: 🐢 Please slow down
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
ticket:
answers:
no_value: '*No response*'
opening_message:
content: |
{staff}
{creator} has created a new ticket
fields:
topic: Topic

View File

@ -1,3 +1,4 @@
/* eslint-disable max-lines */
const { const {
ActionRowBuilder, ActionRowBuilder,
ModalBuilder, ModalBuilder,
@ -10,6 +11,9 @@ const emoji = require('node-emoji');
const ms = require('ms'); const ms = require('ms');
const { EmbedBuilder } = require('discord.js'); const { EmbedBuilder } = require('discord.js');
/**
* @typedef {import('@prisma/client').Category & {guild: import('@prisma/client').Guild} & {questions: import('@prisma/client').Question[]}} CategoryGuildQuestions
*/
module.exports = class TicketManager { module.exports = class TicketManager {
constructor(client) { constructor(client) {
/** @type {import("client")} */ /** @type {import("client")} */
@ -18,15 +22,15 @@ module.exports = class TicketManager {
/** /**
* @param {object} data * @param {object} data
* @param {string} data.category * @param {string} data.categoryId
* @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} data.interaction * @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} data.interaction
* @param {string?} [data.topic] * @param {string?} [data.topic]
*/ */
async create({ async create({
categoryId, interaction, topic, reference, categoryId, interaction, topic, referencesMessage, referencesTicket,
}) { }) {
const cacheKey = `cache/category+guild+questions:${categoryId}`; const cacheKey = `cache/category+guild+questions:${categoryId}`;
/** @type {import('@prisma/client').Category} */ /** @type {CategoryGuildQuestions} */
let category = await this.client.keyv.get(cacheKey); let category = await this.client.keyv.get(cacheKey);
if (!category) { if (!category) {
category = await this.client.prisma.category.findUnique({ category = await this.client.prisma.category.findUnique({
@ -104,7 +108,8 @@ module.exports = class TicketManager {
.setCustomId(JSON.stringify({ .setCustomId(JSON.stringify({
action: 'questions', action: 'questions',
categoryId, categoryId,
reference, referencesMessage,
referencesTicket,
})) }))
.setTitle(category.name) .setTitle(category.name)
.setComponents( .setComponents(
@ -154,7 +159,8 @@ module.exports = class TicketManager {
.setCustomId(JSON.stringify({ .setCustomId(JSON.stringify({
action: 'topic', action: 'topic',
categoryId, categoryId,
reference, referencesMessage,
referencesTicket,
})) }))
.setTitle(category.name) .setTitle(category.name)
.setComponents( .setComponents(
@ -183,20 +189,140 @@ module.exports = class TicketManager {
* @param {string?} [data.topic] * @param {string?} [data.topic]
*/ */
async postQuestions({ async postQuestions({
categoryId, interaction, topic, reference, categoryId, interaction, topic, referencesMessage, referencesTicket,
}) { }) {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
console.log(require('util').inspect(interaction, {
colors: true,
depth: 10,
}));
if (interaction.isModalSubmit()) {
const cacheKey = `cache/category+guild+questions:${categoryId}`;
/** @type {CategoryGuildQuestions} */
const category = await this.client.keyv.get(cacheKey);
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);
} }
/** @type {import("discord.js").Guild} */
const guild = this.client.guilds.cache.get(category.guild.id);
const getMessage = this.client.i18n.getLocale(category.guild.locale);
const creator = await guild.members.fetch(interaction.user.id);
const number = (await this.client.prisma.ticket.count({ where: { guildId: category.guild.id } })) + 1;
const channelName = category.channelName
.replace(/{+\s?(user)?name\s?}+/gi, creator.user.username)
.replace(/{+\s?(nick|display)(name)?\s?}+/gi, creator.displayName)
.replace(/{+\s?num(ber)?\s?}+/gi, number === 1488 ? '1487b' : number);
const allow = ['ViewChannel', 'ReadMessageHistory', 'SendMessages', 'EmbedLinks', 'AttachFiles'];
/** @type {import("discord.js").TextChannel} */
const channel = await guild.channels.create({
name: channelName,
parent: category.discordCategory,
permissionOverwrites: [
{
deny: ['ViewChannel'],
id: guild.roles.everyone,
},
{
allow: allow,
id: this.client.user.id,
},
{
allow: allow,
id: creator.id,
},
...category.staffRoles.map(id => ({
allow: allow,
id,
})),
],
rateLimitPerUser: category.ratelimit,
reason: `${creator.user.tag} created a ticket`,
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 (answers) {
embed.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,
});
}
if (category.guild.footer) {
embed.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 pings = category.pingRoles.map(r => `<@&${r}>`).join(' ');
const sent = await channel.send({
content: getMessage('ticket.opening_message.content', {
creator: interaction.user.toString(),
staff: pings ? pings + ',' : '',
}),
embeds: [embed],
});
await sent.pin({ reason: 'Ticket opening message' });
const pinned = channel.messages.cache.last();
if (pinned.system) {
pinned
.delete({ reason: 'Cleaning up system message' })
.catch(() => this.client.log.warn('Failed to delete system pin message'));
}
// TODO: referenced msg or ticket
const data = {
category: { connect: { id: categoryId } },
createdBy: {
connectOrCreate: {
create: { id: interaction.user.id },
where: { id: interaction.user.id },
},
},
guild: { connect: { id: category.guild.id } },
id: channel.id,
number,
openingMessage: sent.id,
topic,
};
if (referencesTicket) data.referencesTicket = { connect: { id: referencesTicket } };
let message;
if (referencesMessage) message = this.client.prisma.archivedMessage.findUnique({ where: { id: referencesMessage } });
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({ interaction.editReply({
components: [], components: [],
embeds: [], embeds: [],
}); });
// TODO: log channel
} }
}; };

View File

@ -28,9 +28,10 @@ module.exports = class extends Listener {
firstResponseAt: true, firstResponseAt: true,
}, },
}); });
const closedTickets = tickets.filter(t => t.closedAt);
cached = { cached = {
avgResolutionTime: ms(tickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / tickets.length), avgResolutionTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / closedTickets.length),
avgResponseTime: ms(tickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / tickets.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.filter(t => t.open).length,
totalTickets: tickets.length, totalTickets: tickets.length,
}; };

View File

@ -8,13 +8,15 @@ module.exports = class QuestionsModal extends Modal {
}); });
} }
/**
*
* @param {*} id
* @param {import("discord.js").ModalSubmitInteraction} interaction
*/
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, });
}));
// TODO: custom topic
} }
}; };

View File

@ -27,13 +27,13 @@ module.exports.get = fastify => ({
}, },
where: { id: req.params.guild }, where: { id: req.params.guild },
}); });
categories = categories.map(c => { categories = categories.map(c => {
const closedTickets = c.tickets.filter(t => t.closedAt);
c = { c = {
...c, ...c,
stats: { stats: {
avgResolutionTime: ms(c.tickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / c.tickets.length), avgResolutionTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / closedTickets.length),
avgResponseTime: ms(c.tickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / c.tickets.length), avgResponseTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / closedTickets.length),
}, },
}; };
delete c.tickets; delete c.tickets;

View File

@ -28,14 +28,15 @@ module.exports.get = fastify => ({
}, },
where: { guildId: id }, where: { guildId: id },
}); });
const closedTickets = tickets.filter(t => t.closedAt);
cached = { cached = {
createdAt: settings.createdAt, createdAt: settings.createdAt,
id: guild.id, id: guild.id,
logo: guild.iconURL(), logo: guild.iconURL(),
name: guild.name, name: guild.name,
stats: { stats: {
avgResolutionTime: ms(tickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / tickets.length), avgResolutionTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / closedTickets.length),
avgResponseTime: ms(tickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / tickets.length), avgResponseTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / closedTickets.length),
categories: categories.map(c => ({ categories: categories.map(c => ({
id: c.id, id: c.id,
name: c.name, name: c.name,

View File

@ -14,6 +14,7 @@ module.exports.get = () => ({
firstResponseAt: true, firstResponseAt: true,
}, },
}); });
const closedTickets = tickets.filter(t => t.closedAt);
const users = await client.prisma.user.findMany({ select: { messageCount: true } }); const users = await client.prisma.user.findMany({ select: { messageCount: true } });
cached = { cached = {
avatar: client.user.avatarURL(), avatar: client.user.avatarURL(),
@ -23,8 +24,8 @@ module.exports.get = () => ({
stats: { stats: {
activatedUsers: users.length, activatedUsers: users.length,
archivedMessages: users.reduce((total, user) => total + user.messageCount, 0), // don't count archivedMessage table rows, they get deleted archivedMessages: users.reduce((total, user) => total + user.messageCount, 0), // don't count archivedMessage table rows, they get deleted
avgResolutionTime: ms(tickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / tickets.length), avgResolutionTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / closedTickets.length),
avgResponseTime: ms(tickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / tickets.length), avgResponseTime: ms(closedTickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / closedTickets.length),
categories: await client.prisma.category.count(), categories: await client.prisma.category.count(),
guilds: client.guilds.cache.size, guilds: client.guilds.cache.size,
members: client.guilds.cache.reduce((t, g) => t + g.memberCount, 0), members: client.guilds.cache.reduce((t, g) => t + g.memberCount, 0),