fix: ticket closing

This commit is contained in:
Isaac 2023-01-30 15:56:41 +00:00
parent a60c998605
commit d1c3620fcd
No known key found for this signature in database
GPG Key ID: 0DE40AE37BBA5C33
15 changed files with 213 additions and 100 deletions

3
.gitignore vendored
View File

@ -6,8 +6,7 @@ prisma/
# files # files
*.env* *.env*
*.db *.db*
*.db-journal
*.log *.log
*-lock.* *-lock.*
user/config.yml user/config.yml

View File

@ -43,7 +43,7 @@
"@fastify/http-proxy": "^8.4.0", "@fastify/http-proxy": "^8.4.0",
"@fastify/jwt": "^5.0.1", "@fastify/jwt": "^5.0.1",
"@fastify/oauth2": "^5.1.0", "@fastify/oauth2": "^5.1.0",
"@prisma/client": "^4.8.1", "@prisma/client": "^4.9.0",
"cryptr": "^6.1.0", "cryptr": "^6.1.0",
"discord.js": "^14.7.1", "discord.js": "^14.7.1",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@ -60,7 +60,7 @@
"node-emoji": "^1.11.0", "node-emoji": "^1.11.0",
"object-diffy": "^1.0.4", "object-diffy": "^1.0.4",
"pad": "^3.2.0", "pad": "^3.2.0",
"prisma": "^4.8.1", "prisma": "^4.9.0",
"semver": "^7.3.8", "semver": "^7.3.8",
"terminal-link": "^2.1.1", "terminal-link": "^2.1.1",
"yaml": "^1.10.2" "yaml": "^1.10.2"

View File

@ -18,25 +18,32 @@ module.exports = class CloseButton extends Button {
/** @type {import("client")} */ /** @type {import("client")} */
const client = this.client; const client = this.client;
// the close button on th opening message, the same as using /close
if (id.accepted === undefined) { if (id.accepted === undefined) {
// the close button on the opening message, the same as using /close
await client.tickets.beforeRequestClose(interaction); await client.tickets.beforeRequestClose(interaction);
} else { } else {
await interaction.deferReply(); const ticket = await client.tickets.getTicket(interaction.channel.id);
const ticket = await client.prisma.ticket.findUnique({
include: {
category: true,
guild: true,
},
where: { id: interaction.channel.id },
});
const getMessage = client.i18n.getLocale(ticket.guild.locale); const getMessage = client.i18n.getLocale(ticket.guild.locale);
const staff = await isStaff(interaction.guild, interaction.user.id); const staff = await isStaff(interaction.guild, interaction.user.id);
if (id.expect === 'staff' && !staff) { if (id.expect === 'staff' && !staff) {
return; // TODO: please wait for staff to close the ticket return await interaction.reply({
} else if (id.expect === 'user' && staff) { embeds: [
return; // TODO: please wait for the user to respond new ExtendedEmbedBuilder()
.setColor(ticket.guild.errorColour)
.setDescription(getMessage('ticket.close.wait_for_staff')),
],
ephemeral: true,
});
} else if (id.expect === 'user' && interaction.user.id !== ticket.createdById) {
return await interaction.reply({
embeds: [
new ExtendedEmbedBuilder()
.setColor(ticket.guild.errorColour)
.setDescription(getMessage('ticket.close.wait_for_user')),
],
ephemeral: true,
});
} else { } else {
if (id.accepted) { if (id.accepted) {
if ( if (
@ -46,10 +53,13 @@ module.exports = class CloseButton extends Button {
) { ) {
return await interaction.showModal(client.tickets.buildFeedbackModal(ticket.guild.locale, { next: 'acceptClose' })); return await interaction.showModal(client.tickets.buildFeedbackModal(ticket.guild.locale, { next: 'acceptClose' }));
} else { } else {
await interaction.deferReply();
await client.tickets.acceptClose(interaction); await client.tickets.acceptClose(interaction);
} }
} else { } else {
// TODO: reply
if (client.tickets.$stale.has(ticket.id)) { if (client.tickets.$stale.has(ticket.id)) {
try {
await interaction.channel.messages.edit( await interaction.channel.messages.edit(
client.tickets.$stale.get(ticket.id).message.id, client.tickets.$stale.get(ticket.id).message.id,
{ {
@ -60,14 +70,17 @@ module.exports = class CloseButton extends Button {
text: ticket.guild.footer, text: ticket.guild.footer,
}) })
.setColor(ticket.guild.errorColour) .setColor(ticket.guild.errorColour)
.setDescription(getMessage('ticket.close.rejected', { user: interaction.user.toString() })), .setDescription(getMessage('ticket.close.rejected', { user: interaction.user.toString() }))
.setFooter({ text: null }),
], ],
}, },
); );
} finally { // this should run regardless of whatever happens above
client.tickets.$stale.delete(ticket.id); client.tickets.$stale.delete(ticket.id);
} }
} }
} }
} }
} }
}
}; };

View File

@ -52,7 +52,12 @@ module.exports = class Client extends FrameworkClient {
async login(token) { async login(token) {
/** @type {PrismaClient} */ /** @type {PrismaClient} */
this.prisma = new PrismaClient(); this.prisma = new PrismaClient();
if (process.env.DB_PROVIDER === 'sqlite') this.prisma.$use(sqliteMiddleware); if (process.env.DB_PROVIDER === 'sqlite') {
this.prisma.$use(sqliteMiddleware);
// make sqlite faster (https://www.sqlite.org/wal.html),
// and the missing parentheses are not a mistake, `$queryRaw` is a tagged template literal
this.log.debug(await this.prisma.$queryRaw`PRAGMA journal_mode=WAL;`);
}
this.keyv = new Keyv(); this.keyv = new Keyv();
return super.login(token); return super.login(token);
} }

View File

@ -91,10 +91,10 @@ module.exports = class MoveSlashCommand extends SlashCommand {
$oldCategory.total--; $oldCategory.total--;
$oldCategory[ticket.createdById]--; $oldCategory[ticket.createdById]--;
if (!$newCategory.total) $newCategory.total = 0; $newCategory.total ||= 0;
$newCategory.total++; $newCategory.total++;
if (!$newCategory[ticket.createdById]) $newCategory[ticket.createdById] = 0; $newCategory[ticket.createdById] ||= 0;
$newCategory[ticket.createdById]++; $newCategory[ticket.createdById]++;
await interaction.channel.setParent(discordCategory, { await interaction.channel.setParent(discordCategory, {

View File

@ -349,6 +349,9 @@ ticket:
no_value: "*No response*" no_value: "*No response*"
claimed: 🙌 {user} has claimed this ticket. claimed: 🙌 {user} has claimed this ticket.
close: close:
closed:
description: This channel will be deleted in a few seconds...
title: ✅ Ticket closed
forbidden: forbidden:
description: You don't have permission to close this ticket. description: You don't have permission to close this ticket.
title: ❌ Error title: ❌ Error
@ -363,6 +366,8 @@ ticket:
title: ❓ Can this ticket be closed? title: ❓ Can this ticket be closed?
user_request: user_request:
title: ❓ {requestedBy} wants to close this ticket title: ❓ {requestedBy} wants to close this ticket
wait_for_staff: ✋ Please wait for staff to close this ticket.
wait_for_user: ✋ Please wait for the user to respond.
created: created:
description: "Your ticket channel has been created: {channel}." description: "Your ticket channel has been created: {channel}."
title: ✅ Ticket created title: ✅ Ticket created

View File

@ -2,7 +2,7 @@ const Cryptr = require('cryptr');
const { encrypt } = new Cryptr(process.env.ENCRYPTION_KEY); const { encrypt } = new Cryptr(process.env.ENCRYPTION_KEY);
/** /**
* Returns highest (roles.highest) hoisted role , or everyone * Returns highest (roles.highest) hoisted role, or everyone
* @param {import("discord.js").GuildMember} member * @param {import("discord.js").GuildMember} member
* @returns {import("discord.js").Role} * @returns {import("discord.js").Role}
*/ */
@ -121,7 +121,7 @@ module.exports = class TicketArchiver {
id: message.id, id: message.id,
}; };
await this.client.prisma.ticket.update({ return await this.client.prisma.ticket.update({
data: { data: {
archivedChannels: { archivedChannels: {
upsert: channels.map(channel => { upsert: channels.map(channel => {

View File

@ -26,6 +26,14 @@ const { encrypt } = new Cryptr(process.env.ENCRYPTION_KEY);
* {guild: import('@prisma/client').Guild} & * {guild: import('@prisma/client').Guild} &
* {questions: import('@prisma/client').Question[]}} CategoryGuildQuestions * {questions: import('@prisma/client').Question[]}} CategoryGuildQuestions
*/ */
/**
* @typedef {import('@prisma/client').Ticket &
* {category: import('@prisma/client').Category} &
* {feedback: import('@prisma/client').Feedback} &
* {guild: import('@prisma/client').Guild}} TicketCategoryFeedbackGuild
*/
module.exports = class TicketManager { module.exports = class TicketManager {
constructor(client) { constructor(client) {
/** @type {import("client")} */ /** @type {import("client")} */
@ -58,10 +66,32 @@ module.exports = class TicketManager {
return category; return category;
} }
// TODO: update when a ticket is closed or moved /**
* Retrieve cached ticket data for the closing sequence
* @param {string} ticketId the ticket ID
* @param {boolean} force bypass & update the cache?
* @returns {Promise<TicketCategoryFeedbackGuild>}
*/
async getTicket(ticketId, force) {
const cacheKey = `cache/ticket+category+feedback+guild:${ticketId}`;
/** @type {TicketCategoryFeedbackGuild} */
let ticket = await this.client.keyv.get(cacheKey);
if (!ticket || force) {
ticket = await this.client.prisma.ticket.findUnique({
include: {
category: true,
feedback: true,
guild: true,
},
where: { id: ticketId },
});
await this.client.keyv.set(cacheKey, ticket, ms('3m'));
}
return ticket;
}
async getTotalCount(categoryId) { async getTotalCount(categoryId) {
const category = this.$count.categories[categoryId]; this.$count.categories[categoryId] ||= {};
if (!category) this.$count.categories[categoryId] = {};
let count = this.$count.categories[categoryId].total; let count = this.$count.categories[categoryId].total;
if (!count) { if (!count) {
count = await this.client.prisma.ticket.count({ count = await this.client.prisma.ticket.count({
@ -75,10 +105,8 @@ module.exports = class TicketManager {
return count; return count;
} }
// TODO: update when a ticket is closed or moved
async getMemberCount(categoryId, memberId) { async getMemberCount(categoryId, memberId) {
const category = this.$count.categories[categoryId]; this.$count.categories[categoryId] ||= {};
if (!category) this.$count.categories[categoryId] = {};
let count = this.$count.categories[categoryId][memberId]; let count = this.$count.categories[categoryId][memberId];
if (!count) { if (!count) {
count = await this.client.prisma.ticket.count({ count = await this.client.prisma.ticket.count({
@ -314,9 +342,10 @@ module.exports = class TicketManager {
async postQuestions({ async postQuestions({
action, categoryId, interaction, topic, referencesMessage, referencesTicketId, action, categoryId, interaction, topic, referencesMessage, referencesTicketId,
}) { }) {
await interaction.deferReply({ ephemeral: true }); const [, category] = await Promise.all([
interaction.deferReply({ ephemeral: true }),
const category = await this.getCategory(categoryId); this.getCategory(categoryId),
]);
let answers; let answers;
if (interaction.isModalSubmit()) { if (interaction.isModalSubmit()) {
@ -604,11 +633,12 @@ module.exports = class TicketManager {
} }
if (category.guild.archive && message) { if (category.guild.archive && message) {
let row = await this.client.prisma.archivedMessage.findUnique({ where: { id: message.id } }); if (
if (!row) row = await this.archiver.saveMessage(ticket.id, message, true); await this.client.prisma.archivedMessage.findUnique({ where: { id: message.id } })||
if (row) { await this.archiver.saveMessage(ticket.id, message, true)
) {
await this.client.prisma.ticket.update({ await this.client.prisma.ticket.update({
data: { referencesMessageId: row.id }, data: { referencesMessageId: message.id },
where: { id: ticket.id }, where: { id: ticket.id },
}); });
} }
@ -657,11 +687,10 @@ module.exports = class TicketManager {
}); });
const getMessage = this.client.i18n.getLocale(ticket.guild.locale); const getMessage = this.client.i18n.getLocale(ticket.guild.locale);
await interaction.channel.permissionOverwrites.edit(interaction.user, { 'ViewChannel': true }, `Ticket claimed by ${interaction.user.tag}`); await Promise.all([
interaction.channel.permissionOverwrites.edit(interaction.user, { 'ViewChannel': true }, `Ticket claimed by ${interaction.user.tag}`),
for (const role of ticket.category.staffRoles) await interaction.channel.permissionOverwrites.edit(role, { 'ViewChannel': false }, `Ticket claimed by ${interaction.user.tag}`); ...ticket.category.staffRoles.map(role => interaction.channel.permissionOverwrites.edit(role, { 'ViewChannel': false }, `Ticket claimed by ${interaction.user.tag}`)),
this.client.prisma.ticket.update({
await this.client.prisma.ticket.update({
data: { data: {
claimedBy: { claimedBy: {
connectOrCreate: { connectOrCreate: {
@ -671,7 +700,8 @@ module.exports = class TicketManager {
}, },
}, },
where: { id: interaction.channel.id }, where: { id: interaction.channel.id },
}); }),
]);
const openingMessage = await interaction.channel.messages.fetch(ticket.openingMessageId); const openingMessage = await interaction.channel.messages.fetch(ticket.openingMessageId);
@ -735,6 +765,7 @@ module.exports = class TicketManager {
async release(interaction) { async release(interaction) {
const ticket = await this.client.prisma.ticket.findUnique({ const ticket = await this.client.prisma.ticket.findUnique({
include: { include: {
_count: { select: { questionAnswers: true } },
category: true, category: true,
guild: true, guild: true,
}, },
@ -742,14 +773,14 @@ module.exports = class TicketManager {
}); });
const getMessage = this.client.i18n.getLocale(ticket.guild.locale); const getMessage = this.client.i18n.getLocale(ticket.guild.locale);
await interaction.channel.permissionOverwrites.delete(interaction.user, `Ticket released by ${interaction.user.tag}`); await Promise.all([
interaction.channel.permissionOverwrites.delete(interaction.user, `Ticket released by ${interaction.user.tag}`),
for (const role of ticket.category.staffRoles) await interaction.channel.permissionOverwrites.edit(role, { 'ViewChannel': true }, `Ticket released by ${interaction.user.tag}`); ...ticket.category.staffRoles.map(role => interaction.channel.permissionOverwrites.edit(role, { 'ViewChannel': true }, `Ticket released by ${interaction.user.tag}`)),
this.client.prisma.ticket.update({
await this.client.prisma.ticket.update({
data: { claimedBy: { disconnect: true } }, data: { claimedBy: { disconnect: true } },
where: { id: interaction.channel.id }, where: { id: interaction.channel.id },
}); }),
]);
const openingMessage = await interaction.channel.messages.fetch(ticket.openingMessageId); const openingMessage = await interaction.channel.messages.fetch(ticket.openingMessageId);
@ -846,19 +877,12 @@ module.exports = class TicketManager {
* @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction} interaction * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction} interaction
*/ */
async beforeRequestClose(interaction) { async beforeRequestClose(interaction) {
const ticket = await this.client.prisma.ticket.findUnique({ const ticket = await this.getTicket(interaction.channel.id);
include: {
category: { select: { enableFeedback: true } },
feedback: true,
guild: true,
},
where: { id: interaction.channel.id },
});
if (!ticket) { if (!ticket) {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
const { const {
errorColour, errorColour,
footer,
locale, locale,
} = await this.client.prisma.guild.findUnique({ } = await this.client.prisma.guild.findUnique({
select: { select: {
@ -872,7 +896,7 @@ module.exports = class TicketManager {
embeds: [ embeds: [
new ExtendedEmbedBuilder({ new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(), iconURL: interaction.guild.iconURL(),
text: ticket.guild.footer, text: footer,
}) })
.setColor(errorColour) .setColor(errorColour)
.setTitle(getMessage('misc.not_ticket.title')) .setTitle(getMessage('misc.not_ticket.title'))
@ -903,7 +927,7 @@ module.exports = class TicketManager {
) { ) {
return await interaction.showModal(this.buildFeedbackModal(ticket.guild.locale, { return await interaction.showModal(this.buildFeedbackModal(ticket.guild.locale, {
next: 'requestClose', next: 'requestClose',
reason, // known issue: a reason longer than a few words will cause an error due to 100 character ID limit reason, // known issue: a reason longer than a few words will cause an error due to 100 character custom_id limit
})); }));
} }
@ -920,7 +944,7 @@ module.exports = class TicketManager {
return this.finallyClose(ticket.id, { reason }); return this.finallyClose(ticket.id, { reason });
} }
await this.requestClose(interaction, reason); this.requestClose(interaction, reason);
} }
/** /**
@ -929,12 +953,9 @@ module.exports = class TicketManager {
*/ */
async requestClose(interaction, reason) { async requestClose(interaction, reason) {
// interaction could be command, button. or modal // interaction could be command, button. or modal
const ticket = await this.client.prisma.ticket.findUnique({ const ticket = await this.getTicket(interaction.channel.id);
include: { guild: true },
where: { id: interaction.channel.id },
});
const getMessage = this.client.i18n.getLocale(ticket.guild.locale); const getMessage = this.client.i18n.getLocale(ticket.guild.locale);
const staff = await isStaff(interaction.guild, interaction.user.id); const staff = interaction.user.id !== ticket.createdById && await isStaff(interaction.guild, interaction.user.id);
const closeButtonId = { const closeButtonId = {
action: 'close', action: 'close',
expect: staff ? 'user' : 'staff', expect: staff ? 'user' : 'staff',
@ -999,20 +1020,71 @@ module.exports = class TicketManager {
/** /**
* @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction|import("discord.js").ModalSubmitInteraction} interaction * @param {import("discord.js").ChatInputCommandInteraction|import("discord.js").ButtonInteraction|import("discord.js").ModalSubmitInteraction} interaction
*/ */
async acceptClose(interaction) {} async acceptClose(interaction) {
const ticket = await this.getTicket(interaction.channel.id);
const $ticket = this.$stale.get(interaction.channel.id);
const getMessage = this.client.i18n.getLocale(ticket.guild.locale);
await interaction.editReply({
embeds: [
new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(),
text: ticket.guild.footer,
})
.setColor(ticket.guild.successColour)
.setTitle(getMessage('ticket.close.closed.title'))
.setDescription(getMessage('ticket.close.closed.description')),
],
});
await new Promise(resolve => setTimeout(resolve, 5000));
await this.finallyClose(interaction.channel.id, $ticket);
}
/** /**
* close a ticket * close a ticket
* @param {string} ticketId * @param {string} ticketId
*/ */
async finallyClose(ticketId, { async finallyClose(ticketId, {
closedBy, closedBy = null,
reason, reason = null,
}) { }) {
// TODO: update cache/cat count const ticket = await this.getTicket(ticketId);
// TODO: update cache/member count this.$count.categories[ticket.categoryId].total -= 1;
// TODO: set messageCount on ticket this.$count.categories[ticket.categoryId][ticket.createdById] -= 1;
// TODO: pinnedMessages, closedBy, closedAt
// delete const { _count: { archivedMessages } } = await this.client.prisma.ticket.findUnique({
select: { _count: { select: { archivedMessages: true } } },
where: { id: ticket.id },
});
/** @type {import("@prisma/client").Ticket} */
const data = {
closedAt: new Date(),
closedBy: closedBy && {
connectOrCreate: {
create: { id: closedBy },
where: { id: closedBy },
},
} || undefined, // Prisma wants undefined not null because it is a relation
closedReason: reason && encrypt(reason),
messageCount: archivedMessages,
};
/** @type {import("discord.js").TextChannel} */
const channel = this.client.channels.cache.get(ticketId);
if (channel) {
const pinned = await channel.messages.fetchPinned();
data.pinnedMessageIds = pinned.keys();
}
await this.client.prisma.ticket.update({
data,
where: { id: ticket.id },
});
if (channel?.deletable) {
const member = closedBy ? channel.guild.members.cache.get(closedBy) : null;
await channel.delete('Ticket closed' + (member ? ` by ${member.displayName}` : '') + reason ? `: ${reason}` : '');
}
} }
}; };

View File

@ -180,7 +180,7 @@ module.exports = class extends Listener {
}); });
} }
} else { } else {
const settings = await client.prisma.guild.findUnique({ where: { id:message.guild.id } }); const settings = await client.prisma.guild.findUnique({ where: { id: message.guild.id } });
let ticket = await client.prisma.ticket.findUnique({ where: { id: message.channel.id } }); let ticket = await client.prisma.ticket.findUnique({ where: { id: message.channel.id } });
if (ticket) { if (ticket) {

View File

@ -47,6 +47,8 @@ module.exports = class extends Listener {
} }
} }
if (newMessage.author.id === client.user.id) return;
await logMessageEvent(this.client, { await logMessageEvent(this.client, {
action: 'update', action: 'update',
diff: { diff: {

View File

@ -111,6 +111,14 @@ module.exports = class extends Listener {
setInterval(() => { setInterval(() => {
// TODO: check lastMessageAt and set stale // TODO: check lastMessageAt and set stale
// this.$stale.set(ticket.id, {
// closeAt: ticket.guild.autoClose ? Date.now() + ticket.guild.autoClose : null,
// closedBy: null, // null if set as stale due to inactivity
// message: sent,
// messages: 0,
// reason: 'inactivity',
// staleSince: Date.now(),
// });
for (const [ticketId, $] of client.tickets.$stale) { for (const [ticketId, $] of client.tickets.$stale) {
// ⌛ // ⌛

View File

@ -1,5 +1,8 @@
const { Modal } = require('@eartharoid/dbf'); const { Modal } = require('@eartharoid/dbf');
const ExtendedEmbedBuilder = require('../lib/embed'); const ExtendedEmbedBuilder = require('../lib/embed');
const Cryptr = require('cryptr');
const { encrypt } = new Cryptr(process.env.ENCRYPTION_KEY);
module.exports = class FeedbackModal extends Modal { module.exports = class FeedbackModal extends Modal {
constructor(client, options) { constructor(client, options) {
super(client, { super(client, {
@ -19,13 +22,14 @@ module.exports = class FeedbackModal extends Modal {
await interaction.deferReply(); await interaction.deferReply();
const comment = interaction.fields.getTextInputValue('comment'); const comment = interaction.fields.getTextInputValue('comment');
const rating = parseInt(interaction.fields.getTextInputValue('rating')) || null; let rating = parseInt(interaction.fields.getTextInputValue('rating')) || null; // any integer, or null if NaN
rating = Math.min(Math.max(rating, 1), 5); // clamp between 1 and 5 (0 and null become 1, 6 becomes 5)
const ticket = await client.prisma.ticket.update({ const ticket = await client.prisma.ticket.update({
data: { data: {
feedback: { feedback: {
create: { create: {
comment, comment: comment?.length > 0 ? encrypt(comment) : null,
guild: { connect: { id: interaction.guild.id } }, guild: { connect: { id: interaction.guild.id } },
rating, rating,
user: { connect: { id: interaction.user.id } }, user: { connect: { id: interaction.user.id } },
@ -36,6 +40,7 @@ module.exports = class FeedbackModal extends Modal {
where: { id: interaction.channel.id }, where: { id: interaction.channel.id },
}); });
if (id.next === 'requestClose') await client.tickets.requestClose(interaction, id.reason); if (id.next === 'requestClose') await client.tickets.requestClose(interaction, id.reason);
else if (id.next === 'acceptClose') await client.tickets.acceptClose(interaction); else if (id.next === 'acceptClose') await client.tickets.acceptClose(interaction);

View File

@ -140,6 +140,8 @@ module.exports.patch = fastify => ({
where: { id: categoryId }, where: { id: categoryId },
}); });
// update caches
await client.tickets.getCategory(categoryId, true);
await updateStaffRoles(guild); await updateStaffRoles(guild);
logAdminEvent(client, { logAdminEvent(client, {

View File

@ -94,6 +94,8 @@ module.exports.post = fastify => ({
}, },
}); });
// update caches
await client.tickets.getCategory(category.id, true);
await updateStaffRoles(guild); await updateStaffRoles(guild);
logAdminEvent(client, { logAdminEvent(client, {

View File

@ -1,6 +1,6 @@
module.exports = joi.object({ module.exports = joi.object({
archive: joi.boolean().optional(), archive: joi.boolean().optional(),
autoClose: joi.number().min(3600000).optional(), autoClose: joi.number().min(3_600_000).optional(),
autoTag: [joi.array(), joi.string().valid('ticket', '!ticket', 'all')].optional(), autoTag: [joi.array(), joi.string().valid('ticket', '!ticket', 'all')].optional(),
blocklist: joi.array().optional(), blocklist: joi.array().optional(),
createdAt: joi.string().optional(), createdAt: joi.string().optional(),
@ -9,7 +9,7 @@ module.exports = joi.object({
id: joi.string().optional(), id: joi.string().optional(),
logChannel: joi.string().optional(), logChannel: joi.string().optional(),
primaryColour: joi.string().optional(), primaryColour: joi.string().optional(),
staleAfter: joi.number().min(60000).optional(), staleAfter: joi.number().min(60_000).optional(),
successColour: joi.string().optional(), successColour: joi.string().optional(),
workingHours: joi.array().length(8).items( workingHours: joi.array().length(8).items(
joi.string(), joi.string(),