mirror of
https://github.com/Hessenuk/DiscordTickets.git
synced 2024-12-22 15:53:08 +02:00
fix: ticket closing
This commit is contained in:
parent
a60c998605
commit
d1c3620fcd
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,8 +6,7 @@ prisma/
|
|||||||
|
|
||||||
# files
|
# files
|
||||||
*.env*
|
*.env*
|
||||||
*.db
|
*.db*
|
||||||
*.db-journal
|
|
||||||
*.log
|
*.log
|
||||||
*-lock.*
|
*-lock.*
|
||||||
user/config.yml
|
user/config.yml
|
||||||
|
@ -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"
|
||||||
|
@ -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,25 +53,31 @@ 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)) {
|
||||||
await interaction.channel.messages.edit(
|
try {
|
||||||
client.tickets.$stale.get(ticket.id).message.id,
|
await interaction.channel.messages.edit(
|
||||||
{
|
client.tickets.$stale.get(ticket.id).message.id,
|
||||||
components: [],
|
{
|
||||||
embeds: [
|
components: [],
|
||||||
new ExtendedEmbedBuilder({
|
embeds: [
|
||||||
iconURL: interaction.guild.iconURL(),
|
new ExtendedEmbedBuilder({
|
||||||
text: ticket.guild.footer,
|
iconURL: interaction.guild.iconURL(),
|
||||||
})
|
text: ticket.guild.footer,
|
||||||
.setColor(ticket.guild.errorColour)
|
})
|
||||||
.setDescription(getMessage('ticket.close.rejected', { user: interaction.user.toString() })),
|
.setColor(ticket.guild.errorColour)
|
||||||
],
|
.setDescription(getMessage('ticket.close.rejected', { user: interaction.user.toString() }))
|
||||||
},
|
.setFooter({ text: null }),
|
||||||
);
|
],
|
||||||
client.tickets.$stale.delete(ticket.id);
|
},
|
||||||
|
);
|
||||||
|
} finally { // this should run regardless of whatever happens above
|
||||||
|
client.tickets.$stale.delete(ticket.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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, {
|
||||||
|
@ -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
|
||||||
|
@ -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 => {
|
||||||
|
@ -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,21 +687,21 @@ 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: {
|
create: { id: interaction.user.id },
|
||||||
create: { id: interaction.user.id },
|
where: { id: interaction.user.id },
|
||||||
where: { id: interaction.user.id },
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
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}` : '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -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) {
|
||||||
|
@ -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: {
|
||||||
|
@ -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) {
|
||||||
// ⌛
|
// ⌛
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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, {
|
||||||
|
@ -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, {
|
||||||
|
@ -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(),
|
||||||
|
Loading…
Reference in New Issue
Block a user