Add message and ticket referencing, fixes

This commit is contained in:
Isaac 2022-08-13 23:01:44 +01:00
parent f6666b103e
commit c64b18a397
No known key found for this signature in database
GPG Key ID: F4EAABEB0FFCC06A
10 changed files with 262 additions and 38 deletions

View File

@ -8,5 +8,37 @@ module.exports = class ReferencesCompleter extends Autocompleter {
}); });
} }
async run(value, comamnd, interaction) { } /**
* @param {string} value
* @param {*} comamnd
* @param {import("discord.js").AutocompleteInteraction} interaction
*/
async run(value, comamnd, interaction) {
/** @type {import("client")} */
const client = this.client;
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
const tickets = await client.prisma.ticket.findMany({
where: {
createdById: interaction.user.id,
guildId: interaction.guild.id,
open: false,
},
});
const options = value ? tickets.filter(t =>
String(t.number).match(new RegExp(value, 'i')) ||
t.topic?.match(new RegExp(value, 'i')) ||
new Date(t.createdAt).toLocaleString(settings.locale, { dateStyle: 'short' })?.match(new RegExp(value, 'i')),
) : tickets;
await interaction.respond(
options
.slice(0, 25)
.map(t => {
const date = new Date(t.createdAt).toLocaleString(settings.locale, { dateStyle: 'short' });
return {
name: `#${t.number} - ${date} ${t.topic ? '| ' + t.topic.substring(0, 50) : ''}`,
value: t.id,
};
}),
);
}
}; };

View File

@ -1,4 +1,5 @@
const { MessageCommand } = require('@eartharoid/dbf'); const { MessageCommand } = require('@eartharoid/dbf');
const { useGuild } = require('../../lib/tickets/utils');
module.exports = class CreateMessageCommand extends MessageCommand { module.exports = class CreateMessageCommand extends MessageCommand {
constructor(client, options) { constructor(client, options) {
@ -13,7 +14,11 @@ module.exports = class CreateMessageCommand extends MessageCommand {
}); });
} }
/**
* @param {import("discord.js").MessageContextMenuCommandInteraction} interaction
*/
async run(interaction) { async run(interaction) {
// TODO: archive message // TODO: archive message
await useGuild(this.client, interaction, { referencesMessage: interaction.targetMessage.channelId + '/' + interaction.targetId });
} }
}; };

View File

@ -1,5 +1,6 @@
const { SlashCommand } = require('@eartharoid/dbf'); const { SlashCommand } = require('@eartharoid/dbf');
const { ApplicationCommandOptionType } = require('discord.js'); const { ApplicationCommandOptionType } = require('discord.js');
const { useGuild } = require('../../lib/tickets/utils');
module.exports = class NewSlashCommand extends SlashCommand { module.exports = class NewSlashCommand extends SlashCommand {
constructor(client, options) { constructor(client, options) {
@ -14,7 +15,7 @@ module.exports = class NewSlashCommand extends SlashCommand {
autocomplete: true, autocomplete: true,
name: 'references', name: 'references',
required: false, required: false,
type: ApplicationCommandOptionType.Integer, type: ApplicationCommandOptionType.String,
}, },
]; ];
opts = opts.map(o => { opts = opts.map(o => {
@ -43,5 +44,12 @@ module.exports = class NewSlashCommand extends SlashCommand {
}); });
} }
async run(interaction) { } /**
*
* @param {import("discord.js").ChatInputCommandInteraction} interaction
*/
async run(interaction) {
await useGuild(this.client, interaction, { referencesTicketId: interaction.options.getString('references', false) });
}
}; };

View File

@ -194,7 +194,9 @@ misc:
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.
- Please close a ticket before creating another. - |
Please close a ticket before creating another.
Use `/tickets` to view your existing tickets.
title: title:
- ❌ You already have a ticket - ❌ You already have a ticket
- ❌ You already have %d open tickets - ❌ You already have %d open tickets
@ -227,4 +229,14 @@ ticket:
{staff} {staff}
{creator} has created a new ticket {creator} has created a new ticket
fields: fields:
topic: Topic topic: Topic
references_message:
description: 'References [a message]({url}) sent {timestamp} by {author}.'
title: Reference
references_ticket:
description: 'This ticket is related to a previous ticket:'
fields:
date: Created at
number: Number
topic: Topic
title: Reference

View File

@ -0,0 +1,11 @@
const Cryptr = require('cryptr');
const cryptr = new Cryptr(process.env.ENCRYPTION_KEY);
module.exports = class TicketArchiver {
constructor(client) {
/** @type {import("client")} */
this.client = client;
}
async addMessage() {}
};

View File

@ -1,4 +1,5 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const TicketArchiver = require('./archiver');
const { const {
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
@ -24,7 +25,7 @@ module.exports = class TicketManager {
constructor(client) { constructor(client) {
/** @type {import("client")} */ /** @type {import("client")} */
this.client = client; this.client = client;
this.archiver = new TicketArchiver(client);
this.$ = { categories: {} }; this.$ = { categories: {} };
} }
@ -92,7 +93,7 @@ module.exports = class TicketManager {
* @param {string?} [data.topic] * @param {string?} [data.topic]
*/ */
async create({ async create({
categoryId, interaction, topic, referencesMessage, referencesTicket, categoryId, interaction, topic, referencesMessage, referencesTicketId,
}) { }) {
categoryId = Number(categoryId); categoryId = Number(categoryId);
const category = await this.getCategory(categoryId); const category = await this.getCategory(categoryId);
@ -122,6 +123,9 @@ module.exports = class TicketManager {
}); });
} }
/** @type {import("discord.js").Guild} */
const guild = this.client.guilds.cache.get(category.guild.id);
const member = interaction.member ?? await guild.members.fetch(interaction.user.id);
const getMessage = this.client.i18n.getLocale(category.guild.locale); const getMessage = this.client.i18n.getLocale(category.guild.locale);
const rlKey = `ratelimits/guild-user:${category.guildId}-${interaction.user.id}`; const rlKey = `ratelimits/guild-user:${category.guildId}-${interaction.user.id}`;
@ -130,7 +134,7 @@ module.exports = class TicketManager {
return await interaction.reply({ return await interaction.reply({
embeds: [ embeds: [
new ExtendedEmbedBuilder({ new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(), iconURL: guild.iconURL(),
text: category.guild.footer, text: category.guild.footer,
}) })
.setColor(category.guild.errorColour) .setColor(category.guild.errorColour)
@ -146,7 +150,7 @@ module.exports = class TicketManager {
const sendError = name => interaction.reply({ const sendError = name => interaction.reply({
embeds: [ embeds: [
new ExtendedEmbedBuilder({ new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(), iconURL: guild.iconURL(),
text: category.guild.footer, text: category.guild.footer,
}) })
.setColor(category.guild.errorColour) .setColor(category.guild.errorColour)
@ -156,10 +160,6 @@ module.exports = class TicketManager {
ephemeral: true, ephemeral: true,
}); });
/** @type {import("discord.js").Guild} */
const guild = this.client.guilds.cache.get(category.guild.id);
const member = interaction.member ?? await guild.members.fetch(interaction.user.id);
if (category.guild.blocklist.length !== 0) { if (category.guild.blocklist.length !== 0) {
const blocked = category.guild.blocklist.some(r => member.roles.cache.has(r)); const blocked = category.guild.blocklist.some(r => member.roles.cache.has(r));
if (blocked) return await sendError('blocked'); if (blocked) return await sendError('blocked');
@ -181,7 +181,7 @@ module.exports = class TicketManager {
return await interaction.reply({ return await interaction.reply({
embeds: [ embeds: [
new ExtendedEmbedBuilder({ new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(), iconURL: guild.iconURL(),
text: category.guild.footer, text: category.guild.footer,
}) })
.setColor(category.guild.errorColour) .setColor(category.guild.errorColour)
@ -206,7 +206,7 @@ module.exports = class TicketManager {
// return await interaction.reply({ // return await interaction.reply({
// embeds: [ // embeds: [
// new ExtendedEmbedBuilder({ // new ExtendedEmbedBuilder({
// iconURL: interaction.guild.iconURL(), // iconURL: guild.iconURL(),
// text: category.guild.footer, // text: category.guild.footer,
// }) // })
// .setColor(category.guild.errorColour) // .setColor(category.guild.errorColour)
@ -222,7 +222,7 @@ module.exports = class TicketManager {
return await interaction.reply({ return await interaction.reply({
embeds: [ embeds: [
new ExtendedEmbedBuilder({ new ExtendedEmbedBuilder({
iconURL: interaction.guild.iconURL(), iconURL: guild.iconURL(),
text: category.guild.footer, text: category.guild.footer,
}) })
.setColor(category.guild.errorColour) .setColor(category.guild.errorColour)
@ -240,7 +240,7 @@ module.exports = class TicketManager {
action: 'questions', action: 'questions',
categoryId, categoryId,
referencesMessage, referencesMessage,
referencesTicket, referencesTicketId,
})) }))
.setTitle(category.name) .setTitle(category.name)
.setComponents( .setComponents(
@ -290,7 +290,7 @@ module.exports = class TicketManager {
action: 'topic', action: 'topic',
categoryId, categoryId,
referencesMessage, referencesMessage,
referencesTicket, referencesTicketId,
})) }))
.setTitle(category.name) .setTitle(category.name)
.setComponents( .setComponents(
@ -311,6 +311,8 @@ module.exports = class TicketManager {
await this.postQuestions({ await this.postQuestions({
categoryId, categoryId,
interaction, interaction,
referencesMessage,
referencesTicketId,
topic, topic,
}); });
} }
@ -323,7 +325,7 @@ module.exports = class TicketManager {
* @param {string?} [data.topic] * @param {string?} [data.topic]
*/ */
async postQuestions({ async postQuestions({
action, categoryId, interaction, topic, referencesMessage, referencesTicket, action, categoryId, interaction, topic, referencesMessage, referencesTicketId,
}) { }) {
await interaction.deferReply({ ephemeral: true }); await interaction.deferReply({ ephemeral: true });
@ -480,6 +482,69 @@ module.exports = class TicketManager {
// TODO: referenced msg or ticket // TODO: referenced msg or ticket
if (referencesMessage) {
referencesMessage = referencesMessage.split('/');
/** @type {import("discord.js").Message} */
const message = await (await this.client.channels.fetch(referencesMessage[0]))?.messages.fetch(referencesMessage[1]);
if (message) {
await channel.send({
embeds: [
new ExtendedEmbedBuilder()
.setColor(category.guild.primaryColour)
.setTitle(getMessage('ticket.references_message.title'))
.setDescription(
getMessage('ticket.references_message.description', {
author: message.author.toString(),
timestamp: `<t:${Math.ceil(message.createdTimestamp / 1000)}:R>`,
url: message.url,
})),
new ExtendedEmbedBuilder({
iconURL: guild.iconURL(),
text: category.guild.footer,
})
.setColor(category.guild.primaryColour)
.setAuthor({
iconURL: message.member?.displayAvatarURL(),
name: message.member?.displayName || 'Unknown',
})
.setDescription(message.content.substring(0, 1000) + message.content.length > 1000 ? '...' : ''),
],
});
}
} else if (referencesTicketId) {
// TODO: add portal url
const ticket = await this.client.prisma.ticket.findUnique({ where: { id: referencesTicketId } });
if (ticket) {
const embed = new ExtendedEmbedBuilder({
iconURL: guild.iconURL(),
text: category.guild.footer,
})
.setColor(category.guild.primaryColour)
.setTitle(getMessage('ticket.references_ticket.title'))
.setDescription(getMessage('ticket.references_ticket.description'))
.setFields([
{
inline: true,
name: getMessage('ticket.references_ticket.fields.number'),
value: inlineCode(ticket.number),
},
{
inline: true,
name: getMessage('ticket.references_ticket.fields.date'),
value: `<t:${Math.ceil(ticket.createdAt / 1000)}:f>`,
},
]);
if (ticket.topic) {
embed.addFields({
inline: false,
name: getMessage('ticket.references_ticket.fields.topic'),
value: ticket.topic,
});
}
await channel.send({ embeds: [embed] });
}
}
const data = { const data = {
category: { connect: { id: categoryId } }, category: { connect: { id: categoryId } },
createdBy: { createdBy: {
@ -494,10 +559,10 @@ module.exports = class TicketManager {
openingMessageId: sent.id, openingMessageId: sent.id,
topic, topic,
}; };
if (referencesTicket) data.referencesTicket = { connect: { id: referencesTicket } }; if (referencesTicketId) data.referencesTicket = { connect: { id: referencesTicketId } };
let message; let message;
if (referencesMessage) message = this.client.prisma.archivedMessage.findUnique({ where: { id: referencesMessage } }); if (referencesMessage) message = await this.client.prisma.archivedMessage.findUnique({ where: { id: referencesMessage[1] } });
if (message) data.referencesMessage = { connect: { id: referencesMessage } }; // only add if the message has been archived ^^ if (message) data.referencesMessage = { connect: { id: referencesMessage[0] } }; // only add if the message has been archived ^^
if (answers) data.questionAnswers = { createMany: { data: answers } }; if (answers) data.questionAnswers = { createMany: { data: answers } };
await interaction.editReply({ await interaction.editReply({
components: [], components: [],

77
src/lib/tickets/utils.js Normal file
View File

@ -0,0 +1,77 @@
const {
ActionRowBuilder,
EmbedBuilder,
SelectMenuBuilder,
SelectMenuOptionBuilder,
} = require('discord.js');
const emoji = require('node-emoji');
module.exports = {
/**
* @param {import("client")} client
* @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} interaction
*/
async useGuild(client, interaction, {
referencesMessage,
referencesTicketId,
topic,
}) {
const settings = await client.prisma.guild.findUnique({
select: {
categories: true,
errorColour: true,
locale: true,
primaryColour: true,
},
where: { id: interaction.guild.id },
});
const getMessage = client.i18n.getLocale(settings.locale);
if (settings.categories.length === 0) {
interaction.reply({
components: [],
embeds: [
new EmbedBuilder()
.setColor(settings.errorColour)
.setTitle(getMessage('misc.no_categories.title'))
.setDescription(getMessage('misc.no_categories.description')),
],
ephemeral: true,
});
} else if (settings.categories.length === 1) {
await client.tickets.create({
categoryId: settings.categories[0].id,
interaction,
referencesMessage,
referencesTicketId,
topic,
});
} else {
await interaction.reply({
components: [
new ActionRowBuilder()
.setComponents(
new SelectMenuBuilder()
.setCustomId(JSON.stringify({
action: 'create',
referencesMessage,
referencesTicketId,
topic,
}))
.setPlaceholder(getMessage('menus.category.placeholder'))
.setOptions(
settings.categories.map(category =>
new SelectMenuOptionBuilder()
.setValue(String(category.id))
.setLabel(category.name)
.setDescription(category.description)
.setEmoji(emoji.hasEmoji(category.emoji) ? emoji.get(category.emoji) : { id: category.emoji }),
),
),
),
],
ephemeral: true,
});
}
},
};

View File

@ -23,13 +23,13 @@ module.exports = class extends Listener {
} }
/** /**
* @param {string} guildId * @param {import('@prisma/client').Guild} settings
* @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} interaction * @param {import("discord.js").ButtonInteraction|import("discord.js").SelectMenuInteraction} interaction
*/ */
async useGuild(settings, interaction, topic) { async useGuild(settings, interaction, topic) {
const getMessage = this.client.i18n.getLocale(settings.locale); const getMessage = this.client.i18n.getLocale(settings.locale);
if (settings.categories.length === 0) { if (settings.categories.length === 0) {
interaction.editReply({ interaction.update({
components: [], components: [],
embeds: [ embeds: [
new EmbedBuilder() new EmbedBuilder()
@ -45,7 +45,7 @@ module.exports = class extends Listener {
topic, topic,
}); });
} else { } else {
const sent = await interaction.editReply({ await interaction.update({
components: [ components: [
new ActionRowBuilder() new ActionRowBuilder()
.setComponents( .setComponents(
@ -67,17 +67,17 @@ module.exports = class extends Listener {
), ),
], ],
}); });
sent.awaitMessageComponent({ interaction.message.awaitMessageComponent({
componentType: ComponentType.SelectMenu, componentType: ComponentType.SelectMenu,
filter: () => true, filter: () => true,
time: ms('30s'), time: ms('30s'),
}) })
.then(async () => { .then(async () => {
await sent.delete(); interaction.message.delete();
}) })
.catch(error => { .catch(error => {
if (error) this.client.log.error(error); if (error) this.client.log.error(error);
sent.delete(); interaction.message.delete();
}); });
} }
@ -92,7 +92,7 @@ module.exports = class extends Listener {
if (message.channel.type === ChannelType.DM) { if (message.channel.type === ChannelType.DM) {
if (message.author.bot) return false; if (message.author.bot) return false;
const commonGuilds = await getCommonGuilds(this.client, message.author.id); const commonGuilds = await getCommonGuilds(client, message.author.id);
if (commonGuilds.size === 0) { if (commonGuilds.size === 0) {
return false; return false;
} else if (commonGuilds.size === 1) { } else if (commonGuilds.size === 1) {
@ -105,7 +105,7 @@ module.exports = class extends Listener {
}, },
where: { id: commonGuilds.at(0).id }, where: { id: commonGuilds.at(0).id },
}); });
const getMessage = this.client.i18n.getLocale(settings.locale); const getMessage = client.i18n.getLocale(settings.locale);
const sent = await message.reply({ const sent = await message.reply({
components: [ components: [
new ActionRowBuilder() new ActionRowBuilder()
@ -126,16 +126,16 @@ module.exports = class extends Listener {
}); });
sent.awaitMessageComponent({ sent.awaitMessageComponent({
componentType: ComponentType.Button, componentType: ComponentType.Button,
filter: interaction => interaction.deferUpdate(), filter: () => true,
time: ms('30s'), time: ms('30s'),
}) })
.then(async interaction => await this.useGuild(settings, interaction, message.content)) .then(async interaction => await this.useGuild(settings, interaction, message.content))
.catch(error => { .catch(error => {
if (error) this.client.log.error(error); if (error) client.log.error(error);
sent.delete(); sent.delete();
}); });
} else { } else {
const getMessage = this.client.i18n.getLocale(); const getMessage = client.i18n.getLocale();
const sent = await message.reply({ const sent = await message.reply({
components: [ components: [
new ActionRowBuilder() new ActionRowBuilder()
@ -156,7 +156,7 @@ module.exports = class extends Listener {
}); });
sent.awaitMessageComponent({ sent.awaitMessageComponent({
componentType: ComponentType.SelectMenu, componentType: ComponentType.SelectMenu,
filter: interaction => interaction.deferUpdate(), filter: () => true,
time: ms('30s'), time: ms('30s'),
}) })
.then(async interaction => { .then(async interaction => {
@ -172,7 +172,7 @@ module.exports = class extends Listener {
await this.useGuild(settings, interaction, message.content); await this.useGuild(settings, interaction, message.content);
}) })
.catch(error => { .catch(error => {
if (error) this.client.log.error(error); if (error) client.log.error(error);
sent.delete(); sent.delete();
}); });
} }

View File

@ -25,11 +25,16 @@ module.exports = class extends Listener {
cooldown: true, cooldown: true,
id: true, id: true,
tickets: { tickets: {
select: { createdById: true }, select: {
createdById: true,
guildId: true,
id: true,
},
where: { open: true }, where: { open: true },
}, },
}, },
}); });
let deleted = 0;
let ticketCount = 0; let ticketCount = 0;
let cooldowns = 0; let cooldowns = 0;
for (const category of categories) { for (const category of categories) {
@ -38,6 +43,13 @@ module.exports = class extends Listener {
for (const ticket of category.tickets) { for (const ticket of category.tickets) {
if (client.tickets.$.categories[category.id][ticket.createdById]) client.tickets.$.categories[category.id][ticket.createdById]++; if (client.tickets.$.categories[category.id][ticket.createdById]) client.tickets.$.categories[category.id][ticket.createdById]++;
else client.tickets.$.categories[category.id][ticket.createdById] = 1; else client.tickets.$.categories[category.id][ticket.createdById] = 1;
/** @type {import("discord.js").Guild} */
const guild = client.guilds.cache.get(ticket.guildId);
if (guild && guild.available && !client.channels.cache.has(ticket.id)) {
deleted += 0;
await client.tickets.close(ticket.id);
}
} }
if (category.cooldown) { if (category.cooldown) {
const recent = await client.prisma.ticket.findMany({ const recent = await client.prisma.ticket.findMany({
@ -63,6 +75,7 @@ module.exports = class extends Listener {
// const ticketCount = categories.reduce((total, category) => total + category.tickets.length, 0); // 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(`Cached ticket count of ${categories.length} categories (${ticketCount} open tickets)`);
client.log.info(`Loaded ${cooldowns} active cooldowns`); client.log.info(`Loaded ${cooldowns} active cooldowns`);
client.log.info(`Closed ${deleted} deleted tickets`);
// presence/activity // presence/activity
let next = 0; let next = 0;

View File

@ -1,4 +1,5 @@
const { Menu } = require('@eartharoid/dbf'); const { Menu } = require('@eartharoid/dbf');
const { MessageFlags } = require('discord.js');
module.exports = class CreateMenu extends Menu { module.exports = class CreateMenu extends Menu {
constructor(client, options) { constructor(client, options) {
@ -13,11 +14,11 @@ module.exports = class CreateMenu extends Menu {
* @param {import("discord.js").SelectMenuInteraction} interaction * @param {import("discord.js").SelectMenuInteraction} interaction
*/ */
async run(id, interaction) { async run(id, interaction) {
interaction.message.edit({ components: interaction.message.components }); // reset the select menu (minor client-side UI issue) if (!interaction.message.flags.has(MessageFlags.Ephemeral)) interaction.message.edit({ components: interaction.message.components }); // reset the select menu (minor client-side UI issue)
await this.client.tickets.create({ await this.client.tickets.create({
...id,
categoryId: interaction.values[0], categoryId: interaction.values[0],
interaction, interaction,
topic: id.topic,
}); });
} }
}; };