mirror of
https://github.com/Hessenuk/DiscordTickets.git
synced 2024-11-05 04:13:08 +02:00
Make progress on ticket creations + fixes
This commit is contained in:
parent
fcd390bc9d
commit
01e479dab5
@ -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
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -13,5 +13,7 @@ module.exports = class CreateMessageCommand extends MessageCommand {
|
||||
});
|
||||
}
|
||||
|
||||
async run(interaction) { }
|
||||
async run(interaction) {
|
||||
// TODO: archive message
|
||||
}
|
||||
};
|
@ -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
|
||||
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
|
@ -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
|
||||
}
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user