mirror of
https://github.com/Hessenuk/DiscordTickets.git
synced 2024-12-23 00:03:09 +02:00
Finish pre-open checks, reduce database reads with more caching
This commit is contained in:
parent
c6abefe30d
commit
a190c1ac27
16
README.md
16
README.md
@ -30,4 +30,18 @@ creation requires an interaction:
|
|||||||
- message:create(staff) -> category? -> DM (channel fallback) button -> topic or questions -> create
|
- message:create(staff) -> category? -> DM (channel fallback) button -> topic or questions -> create
|
||||||
- DM -> guild? -> category? -> topic or questions -> create
|
- DM -> guild? -> category? -> topic or questions -> create
|
||||||
- panel(interaction) -> topic or questions -> create
|
- panel(interaction) -> topic or questions -> create
|
||||||
- ~~panel(message) -> DM (channel fallback) button -> topic or questions -> create~~
|
- ~~panel(message) -> DM (channel fallback) button -> topic or questions -> create~~
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
>
|
||||||
|
> test
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> test
|
||||||
|
|
||||||
|
<!-- <picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="...>
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="...">
|
||||||
|
<img alt="..." src="...">
|
||||||
|
</picture> -->
|
||||||
|
@ -186,6 +186,11 @@ misc:
|
|||||||
cooldown:
|
cooldown:
|
||||||
description: Please wait {time} before creating another ticket in this category.
|
description: Please wait {time} before creating another ticket in this category.
|
||||||
title: ❌ Please wait
|
title: ❌ Please wait
|
||||||
|
error:
|
||||||
|
description: Sorry, an unexpected error occurred.
|
||||||
|
fields:
|
||||||
|
identifier: Identifier
|
||||||
|
title: ⚠️ Something's wrong
|
||||||
member_limit:
|
member_limit:
|
||||||
description:
|
description:
|
||||||
- Please use your existing ticket or close it before creating another.
|
- Please use your existing ticket or close it before creating another.
|
||||||
|
@ -3,6 +3,7 @@ const {
|
|||||||
ActionRowBuilder,
|
ActionRowBuilder,
|
||||||
ButtonBuilder,
|
ButtonBuilder,
|
||||||
ButtonStyle,
|
ButtonStyle,
|
||||||
|
inlineCode,
|
||||||
ModalBuilder,
|
ModalBuilder,
|
||||||
SelectMenuBuilder,
|
SelectMenuBuilder,
|
||||||
SelectMenuOptionBuilder,
|
SelectMenuOptionBuilder,
|
||||||
@ -15,12 +16,73 @@ const ExtendedEmbedBuilder = require('../embed');
|
|||||||
const { logTicketEvent } = require('../logging');
|
const { logTicketEvent } = require('../logging');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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
|
||||||
*/
|
*/
|
||||||
module.exports = class TicketManager {
|
module.exports = class TicketManager {
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
/** @type {import("client")} */
|
/** @type {import("client")} */
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|
||||||
|
this.$ = { categories: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCategory(categoryId) {
|
||||||
|
const cacheKey = `cache/category+guild+questions:${categoryId}`;
|
||||||
|
/** @type {CategoryGuildQuestions} */
|
||||||
|
let category = await this.client.keyv.get(cacheKey);
|
||||||
|
if (!category) {
|
||||||
|
category = await this.client.prisma.category.findUnique({
|
||||||
|
include: {
|
||||||
|
guild: true,
|
||||||
|
questions: { orderBy: { order: 'asc' } },
|
||||||
|
},
|
||||||
|
where: { id: categoryId },
|
||||||
|
});
|
||||||
|
this.client.keyv.set(cacheKey, category, ms('5m'));
|
||||||
|
}
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: update when a ticket is closed or moved
|
||||||
|
async getTotalCount(categoryId) {
|
||||||
|
const category = this.$.categories[categoryId];
|
||||||
|
if (!category) this.$.categories[categoryId] = {};
|
||||||
|
let count = this.$.categories[categoryId].total;
|
||||||
|
if (!count) {
|
||||||
|
count = await this.client.prisma.ticket.count({
|
||||||
|
where: {
|
||||||
|
categoryId,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.$.categories[categoryId].total = count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: update when a ticket is closed or moved
|
||||||
|
async getMemberCount(categoryId, memberId) {
|
||||||
|
const category = this.$.categories[categoryId];
|
||||||
|
if (!category) this.$.categories[categoryId] = {};
|
||||||
|
let count = this.$.categories[categoryId][memberId];
|
||||||
|
if (!count) {
|
||||||
|
count = await this.client.prisma.ticket.count({
|
||||||
|
where: {
|
||||||
|
categoryId: categoryId,
|
||||||
|
createdById: memberId,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.$.categories[categoryId][memberId] = count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCooldown(categoryId, memberId) {
|
||||||
|
const cacheKey = `cooldowns/category-member:${categoryId}-${memberId}`;
|
||||||
|
return await this.client.keyv.get(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,42 +95,31 @@ module.exports = class TicketManager {
|
|||||||
categoryId, interaction, topic, referencesMessage, referencesTicket,
|
categoryId, interaction, topic, referencesMessage, referencesTicket,
|
||||||
}) {
|
}) {
|
||||||
categoryId = Number(categoryId);
|
categoryId = Number(categoryId);
|
||||||
const cacheKey = `cache/category+guild+questions:${categoryId}`;
|
const category = await this.getCategory(categoryId);
|
||||||
/** @type {CategoryGuildQuestions} */
|
|
||||||
let category = await this.client.keyv.get(cacheKey);
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
category = await this.client.prisma.category.findUnique({
|
let settings;
|
||||||
include: {
|
if (interaction.guild) {
|
||||||
guild: true,
|
settings = await this.client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
||||||
questions: { orderBy: { order: 'asc' } },
|
} else {
|
||||||
},
|
settings = {
|
||||||
where: { id: Number(categoryId) },
|
errorColour: 'Red',
|
||||||
});
|
locale: 'en-GB',
|
||||||
if (!category) {
|
};
|
||||||
let settings;
|
|
||||||
if (interaction.guild) {
|
|
||||||
settings = await this.client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
|
|
||||||
} else {
|
|
||||||
settings = {
|
|
||||||
errorColour: 'Red',
|
|
||||||
locale: 'en-GB',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const getMessage = this.client.i18n.getLocale(settings.locale);
|
|
||||||
return await interaction.reply({
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.client.keyv.set(cacheKey, category, ms('5m'));
|
const getMessage = this.client.i18n.getLocale(settings.locale);
|
||||||
|
return await interaction.reply({
|
||||||
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMessage = this.client.i18n.getLocale(category.guild.locale);
|
const getMessage = this.client.i18n.getLocale(category.guild.locale);
|
||||||
@ -122,23 +173,10 @@ module.exports = class TicketManager {
|
|||||||
const discordCategory = guild.channels.cache.get(category.discordCategory);
|
const discordCategory = guild.channels.cache.get(category.discordCategory);
|
||||||
if (discordCategory.children.cache.size === 50) return await sendError('category_full');
|
if (discordCategory.children.cache.size === 50) return await sendError('category_full');
|
||||||
|
|
||||||
// TODO: store locally and sync regularly so this isn't done during an interaction?
|
const totalCount = await this.getTotalCount(category.id);
|
||||||
const totalCount = await this.client.prisma.ticket.count({
|
|
||||||
where: {
|
|
||||||
categoryId: category.id,
|
|
||||||
open: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (totalCount >= category.totalLimit) return await sendError('category_full');
|
if (totalCount >= category.totalLimit) return await sendError('category_full');
|
||||||
|
|
||||||
const memberCount = await this.client.prisma.ticket.count({
|
const memberCount = await this.getMemberCount(category.id, interaction.user.id);
|
||||||
where: {
|
|
||||||
categoryId: category.id,
|
|
||||||
createdById: interaction.user.id,
|
|
||||||
open: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (memberCount >= category.memberLimit) {
|
if (memberCount >= category.memberLimit) {
|
||||||
return await interaction.reply({
|
return await interaction.reply({
|
||||||
embeds: [
|
embeds: [
|
||||||
@ -154,17 +192,33 @@ module.exports = class TicketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastTicket = await this.client.prisma.ticket.findFirst({
|
// const lastTicket = await this.client.prisma.ticket.findFirst({
|
||||||
orderBy: [{ closedAt: 'desc' }],
|
// orderBy: [{ closedAt: 'desc' }],
|
||||||
select: { closedAt: true },
|
// select: { closedAt: true },
|
||||||
where: {
|
// where: {
|
||||||
categoryId: category.id,
|
// categoryId: category.id,
|
||||||
createdById: interaction.user.id,
|
// createdById: interaction.user.id,
|
||||||
open: false,
|
// open: false,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (Date.now() - lastTicket.closedAt < category.cooldown) {
|
// if (Date.now() - lastTicket.closedAt < category.cooldown) {
|
||||||
|
// return await interaction.reply({
|
||||||
|
// embeds: [
|
||||||
|
// new ExtendedEmbedBuilder({
|
||||||
|
// iconURL: interaction.guild.iconURL(),
|
||||||
|
// text: category.guild.footer,
|
||||||
|
// })
|
||||||
|
// .setColor(category.guild.errorColour)
|
||||||
|
// .setTitle(getMessage('misc.cooldown.title'))
|
||||||
|
// .setDescription(getMessage('misc.cooldown.description', { time: ms(category.cooldown - (Date.now() - lastTicket.closedAt)) })),
|
||||||
|
// ],
|
||||||
|
// ephemeral: true,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
const cooldown = await this.getCooldown(category.id, interaction.member.id);
|
||||||
|
if (cooldown) {
|
||||||
return await interaction.reply({
|
return await interaction.reply({
|
||||||
embeds: [
|
embeds: [
|
||||||
new ExtendedEmbedBuilder({
|
new ExtendedEmbedBuilder({
|
||||||
@ -173,7 +227,7 @@ module.exports = class TicketManager {
|
|||||||
})
|
})
|
||||||
.setColor(category.guild.errorColour)
|
.setColor(category.guild.errorColour)
|
||||||
.setTitle(getMessage('misc.cooldown.title'))
|
.setTitle(getMessage('misc.cooldown.title'))
|
||||||
.setDescription(getMessage('misc.cooldown.description', { time: ms(category.cooldown - (Date.now() - lastTicket.closedAt)) })),
|
.setDescription(getMessage('misc.cooldown.description', { time: ms(cooldown - Date.now()) })),
|
||||||
],
|
],
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
});
|
});
|
||||||
@ -356,12 +410,6 @@ module.exports = class TicketManager {
|
|||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// 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) {
|
||||||
embeds.push(
|
embeds.push(
|
||||||
new ExtendedEmbedBuilder()
|
new ExtendedEmbedBuilder()
|
||||||
@ -371,10 +419,6 @@ module.exports = class TicketManager {
|
|||||||
value: topic,
|
value: topic,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// embeds[0].setFields({
|
|
||||||
// name: getMessage('ticket.opening_message.fields.topic'),
|
|
||||||
// value: topic,
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category.guild.footer) {
|
if (category.guild.footer) {
|
||||||
@ -455,7 +499,7 @@ module.exports = class TicketManager {
|
|||||||
if (referencesMessage) message = this.client.prisma.archivedMessage.findUnique({ where: { id: referencesMessage } });
|
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 (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 } };
|
||||||
interaction.editReply({
|
await interaction.editReply({
|
||||||
components: [],
|
components: [],
|
||||||
embeds: [
|
embeds: [
|
||||||
new ExtendedEmbedBuilder({
|
new ExtendedEmbedBuilder({
|
||||||
@ -467,14 +511,45 @@ module.exports = class TicketManager {
|
|||||||
.setDescription(getMessage('ticket.created.description', { channel: channel.toString() })),
|
.setDescription(getMessage('ticket.created.description', { channel: channel.toString() })),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const ticket = await this.client.prisma.ticket.create({ data });
|
|
||||||
logTicketEvent(this.client, {
|
try {
|
||||||
action: 'create',
|
const ticket = await this.client.prisma.ticket.create({ data });
|
||||||
target: {
|
this.$.categories[categoryId].total++;
|
||||||
id: ticket.id,
|
this.$.categories[categoryId][creator.id]++;
|
||||||
name: channel.toString(),
|
|
||||||
},
|
if (category.cooldown) {
|
||||||
userId: interaction.user.id,
|
const cacheKey = `cooldowns/category-member:${category.id}-${ticket.createdById}`;
|
||||||
});
|
const expiresAt = ticket.createdAt.getTime() + category.cooldown;
|
||||||
|
const TTL = category.cooldown;
|
||||||
|
await this.client.keyv.set(cacheKey, expiresAt, TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
logTicketEvent(this.client, {
|
||||||
|
action: 'create',
|
||||||
|
target: {
|
||||||
|
id: ticket.id,
|
||||||
|
name: channel.toString(),
|
||||||
|
},
|
||||||
|
userId: interaction.user.id,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const ref = require('crypto').randomUUID();
|
||||||
|
this.client.log.warn.tickets('An error occurred whilst creating ticket', channel.id);
|
||||||
|
this.client.log.error.tickets(ref);
|
||||||
|
this.client.log.error.tickets(error);
|
||||||
|
await interaction.editReply({
|
||||||
|
components: [],
|
||||||
|
embeds: [
|
||||||
|
new ExtendedEmbedBuilder()
|
||||||
|
.setColor('Orange')
|
||||||
|
.setTitle(getMessage('misc.error.title'))
|
||||||
|
.setDescription(getMessage('misc.error.description'))
|
||||||
|
.addFields({
|
||||||
|
name: getMessage('misc.error.fields.identifier'),
|
||||||
|
value: inlineCode(ref),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -11,18 +11,66 @@ module.exports = class extends Listener {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
run() {
|
async run() {
|
||||||
// process.title = `"[Discord Tickets] ${this.client.user.tag}"`; // too long and gets cut off
|
/** @type {import("client")} */
|
||||||
process.title = 'tickets';
|
const client = this.client;
|
||||||
this.client.log.success('Connected to Discord as "%s"', this.client.user.tag);
|
|
||||||
|
|
||||||
|
// process.title = `"[Discord Tickets] ${client.user.tag}"`; // too long and gets cut off
|
||||||
|
process.title = 'tickets';
|
||||||
|
client.log.success('Connected to Discord as "%s"', client.user.tag);
|
||||||
|
|
||||||
|
// load total number of open tickets
|
||||||
|
const categories = await client.prisma.category.findMany({
|
||||||
|
select: {
|
||||||
|
cooldown: true,
|
||||||
|
id: true,
|
||||||
|
tickets: {
|
||||||
|
select: { createdById: true },
|
||||||
|
where: { open: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let ticketCount = 0;
|
||||||
|
let cooldowns = 0;
|
||||||
|
for (const category of categories) {
|
||||||
|
ticketCount += category.tickets.length;
|
||||||
|
client.tickets.$.categories[category.id] = { total: category.tickets.length };
|
||||||
|
for (const ticket of category.tickets) {
|
||||||
|
if (client.tickets.$.categories[category.id][ticket.createdById]) client.tickets.$.categories[category.id][ticket.createdById]++;
|
||||||
|
else client.tickets.$.categories[category.id][ticket.createdById] = 1;
|
||||||
|
}
|
||||||
|
if (category.cooldown) {
|
||||||
|
const recent = await client.prisma.ticket.findMany({
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
select: {
|
||||||
|
createdAt: true,
|
||||||
|
createdById: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
categoryId: category.id,
|
||||||
|
createdAt: { gt: new Date(Date.now() - category.cooldown) },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
cooldowns += recent.length;
|
||||||
|
for (const ticket of recent) {
|
||||||
|
const cacheKey = `cooldowns/category-member:${category.id}-${ticket.createdById}`;
|
||||||
|
const expiresAt = ticket.createdAt.getTime() + category.cooldown;
|
||||||
|
const TTL = expiresAt - Date.now();
|
||||||
|
await client.keyv.set(cacheKey, expiresAt, TTL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// const ticketCount = categories.reduce((total, category) => total + category.tickets.length, 0);
|
||||||
|
client.log.info(`Cached ticket count of ${categories.length} categories (${ticketCount} open tickets)`);
|
||||||
|
client.log.info(`Loaded ${cooldowns} active cooldowns`);
|
||||||
|
|
||||||
|
// presence/activity
|
||||||
let next = 0;
|
let next = 0;
|
||||||
const setPresence = async () => {
|
const setPresence = async () => {
|
||||||
const cacheKey = 'cache/presence';
|
const cacheKey = 'cache/presence';
|
||||||
let cached = await this.client.keyv.get(cacheKey);
|
let cached = await client.keyv.get(cacheKey);
|
||||||
|
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
const tickets = await this.client.prisma.ticket.findMany({
|
const tickets = await client.prisma.ticket.findMany({
|
||||||
select: {
|
select: {
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
firstResponseAt: true,
|
firstResponseAt: true,
|
||||||
@ -35,24 +83,23 @@ module.exports = class extends Listener {
|
|||||||
openTickets: tickets.length - closedTickets.length,
|
openTickets: tickets.length - closedTickets.length,
|
||||||
totalTickets: tickets.length,
|
totalTickets: tickets.length,
|
||||||
};
|
};
|
||||||
await this.client.keyv.set(cacheKey, cached, ms('15m'));
|
await client.keyv.set(cacheKey, cached, ms('15m'));
|
||||||
}
|
}
|
||||||
const activity = this.client.config.presence.activities[next];
|
const activity = client.config.presence.activities[next];
|
||||||
activity.name = activity.name
|
activity.name = activity.name
|
||||||
.replace(/{+avgResolutionTime}+/gi, cached.avgResolutionTime)
|
.replace(/{+avgResolutionTime}+/gi, cached.avgResolutionTime)
|
||||||
.replace(/{+avgResponseTime}+/gi, cached.avgResponseTime)
|
.replace(/{+avgResponseTime}+/gi, cached.avgResponseTime)
|
||||||
.replace(/{+openTickets}+/gi, cached.openTickets)
|
.replace(/{+openTickets}+/gi, cached.openTickets)
|
||||||
.replace(/{+totalTickets}+/gi, cached.totalTickets);
|
.replace(/{+totalTickets}+/gi, cached.totalTickets);
|
||||||
this.client.user.setPresence({
|
client.user.setPresence({
|
||||||
activities: [activity],
|
activities: [activity],
|
||||||
status: this.client.config.presence.status,
|
status: client.config.presence.status,
|
||||||
});
|
});
|
||||||
next++;
|
next++;
|
||||||
if (next === this.client.config.presence.activities.length) next = 0;
|
if (next === client.config.presence.activities.length) next = 0;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setPresence();
|
setPresence();
|
||||||
if (this.client.config.presence.activities.length > 1) setInterval(() => setPresence(), this.client.config.presence.interval * 1000);
|
if (client.config.presence.activities.length > 1) setInterval(() => setPresence(), client.config.presence.interval * 1000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user