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

View File

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

View File

@ -20,15 +20,16 @@ model ArchivedChannel {
}
model ArchivedMessage {
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
authorId String
content String
createdAt DateTime @default(now())
deleted Boolean @default(false)
edited Boolean @default(false)
id String @id
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketId String
author ArchivedUser @relation(fields: [ticketId, authorId], references: [ticketId, userId], onDelete: Cascade)
authorId String
content String
createdAt DateTime @default(now())
deleted Boolean @default(false)
edited Boolean @default(false)
id String @id
referencedBy Ticket[] @relation("MessageReferencedByTicket")
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketId String
@@map("archivedMessages")
}
@ -189,11 +190,11 @@ model Ticket {
archivedUsers ArchivedUser[]
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
categoryId Int?
claimedBy User @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id])
claimedById String
claimedBy User? @relation(name: "TicketsClaimedByUser", fields: [claimedById], references: [id])
claimedById String?
closedAt DateTime?
closedBy User @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id])
closedById String
closedBy User? @relation(name: "TicketsClosedByUser", fields: [closedById], references: [id])
closedById String?
closedReason String?
createdAt DateTime @default(now())
createdBy User @relation(name: "TicketsCreatedByUser", fields: [createdById], references: [id])
@ -213,7 +214,8 @@ model Ticket {
pinnedMessages String @default("[]")
priority String?
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)
referencesTicketId 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:
confirm_open:
emoji:
@ -165,7 +162,16 @@ misc:
title: ❌ There are no ticket categories
ratelimited:
description: Try again in a few seconds.
title: 🐢 Slow down
title: 🐢 Please slow down
unknown_category:
description: Please try a different category.
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 {
ActionRowBuilder,
ModalBuilder,
@ -10,6 +11,9 @@ const emoji = require('node-emoji');
const ms = require('ms');
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 {
constructor(client) {
/** @type {import("client")} */
@ -18,15 +22,15 @@ module.exports = class TicketManager {
/**
* @param {object} data
* @param {string} data.category
* @param {string} data.categoryId
* @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} data.interaction
* @param {string?} [data.topic]
*/
async create({
categoryId, interaction, topic, reference,
categoryId, interaction, topic, referencesMessage, referencesTicket,
}) {
const cacheKey = `cache/category+guild+questions:${categoryId}`;
/** @type {import('@prisma/client').Category} */
/** @type {CategoryGuildQuestions} */
let category = await this.client.keyv.get(cacheKey);
if (!category) {
category = await this.client.prisma.category.findUnique({
@ -104,7 +108,8 @@ module.exports = class TicketManager {
.setCustomId(JSON.stringify({
action: 'questions',
categoryId,
reference,
referencesMessage,
referencesTicket,
}))
.setTitle(category.name)
.setComponents(
@ -154,7 +159,8 @@ module.exports = class TicketManager {
.setCustomId(JSON.stringify({
action: 'topic',
categoryId,
reference,
referencesMessage,
referencesTicket,
}))
.setTitle(category.name)
.setComponents(
@ -183,20 +189,140 @@ module.exports = class TicketManager {
* @param {string?} [data.topic]
*/
async postQuestions({
categoryId, interaction, topic, reference,
categoryId, interaction, topic, referencesMessage, referencesTicket,
}) {
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({
components: [],
embeds: [],
});
// TODO: log channel
}
};

View File

@ -28,9 +28,10 @@ module.exports = class extends Listener {
firstResponseAt: true,
},
});
const closedTickets = tickets.filter(t => t.closedAt);
cached = {
avgResolutionTime: ms(tickets.reduce((total, ticket) => total + (ticket.closedAt - ticket.createdAt), 0) ?? 1 / tickets.length),
avgResponseTime: ms(tickets.reduce((total, ticket) => total + (ticket.firstResponseAt - ticket.createdAt), 0) ?? 1 / tickets.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),
openTickets: tickets.filter(t => t.open).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) {
console.log(id);
console.log(require('util').inspect(interaction, {
colors: true,
depth: 10,
}));
// TODO: custom topic
await this.client.tickets.postQuestions({
...id,
interaction,
});
}
};

View File

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

View File

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

View File

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