diff --git a/config/config.example.jsonc b/config/config.example.jsonc index 205fbee6..df913234 100644 --- a/config/config.example.jsonc +++ b/config/config.example.jsonc @@ -85,6 +85,7 @@ "createTranscript": true, // If set to true, when the ticket is closed a transcript will be generated and sent in the logs channel "askReason": true, // If false the ticket will be closed without asking the reason "whoCanCloseTicket": "STAFFONLY", // STAFFONLY (roles configured at "rolesWhoHaveAccessToTheTickets") or EVERYONE + "deleteTicket": false, // when enabled, it will delete the ticket on clicking close button "closeTicketCategoryId": "" // The id of the category where a closed ticket will be moved to. Leave blank to disable this feature }, "uuidType": "uuid", // uuid or emoji diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index ac5c89dd..3a4d6b21 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -1,11 +1,21 @@ -import { ActionRowBuilder, GuildChannel, GuildMember, Interaction, ModalBuilder, SelectMenuComponentOptionData, StringSelectMenuBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; +import { + ActionRowBuilder, + GuildChannel, + GuildMember, + Interaction, + ModalBuilder, + SelectMenuComponentOptionData, + StringSelectMenuBuilder, + TextInputBuilder, + TextInputStyle, +} from "discord.js"; import { log } from "../utils/logs"; -import {createTicket} from "../utils/createTicket"; +import { createTicket } from "../utils/createTicket"; import { close } from "../utils/close"; import { claim } from "../utils/claim"; import { closeAskReason } from "../utils/close_askReason"; import { deleteTicket } from "../utils/delete"; -import {BaseEvent, ExtendedClient} from "../structure"; +import { BaseEvent, ExtendedClient } from "../structure"; /* Copyright 2023 Sayrix (github.com/Sayrix) @@ -19,7 +29,7 @@ export default class InteractionCreateEvent extends BaseEvent { super(client); } - public async execute(interaction: Interaction): Promise { + public async execute(interaction: Interaction): Promise { if (interaction.isChatInputCommand()) { const command = this.client.commands.get(interaction.commandName); if (!command) return; @@ -30,7 +40,7 @@ export default class InteractionCreateEvent extends BaseEvent { console.error(error); await interaction.reply({ content: "There was an error while executing this command!", - ephemeral: true + ephemeral: true, }); } } @@ -40,8 +50,8 @@ export default class InteractionCreateEvent extends BaseEvent { await interaction.deferReply({ ephemeral: true }).catch((e) => console.log(e)); const tCount = this.client.config.ticketTypes.length; - if(tCount === 0 || tCount > 25) { - await interaction.followUp({content: this.client.locales.getValue("invalidConfig"), ephemeral: true}); + if (tCount === 0 || tCount > 25) { + await interaction.followUp({ content: this.client.locales.getValue("invalidConfig"), ephemeral: true }); throw new Error("ticketTypes either has nothing or exceeded 25 entries. Please check the config and restart the bot"); } @@ -49,23 +59,26 @@ export default class InteractionCreateEvent extends BaseEvent { if (role && (interaction.member as GuildMember | null)?.roles.cache.has(role)) { interaction .editReply({ - content: "You can't create a ticket because you are blacklisted" + content: "You can't create a ticket because you are blacklisted", }) .catch((e) => console.log(e)); return; } } - + // Max Ticket if (this.client.config.maxTicketOpened > 0) { - const ticketsOpened = (await this.client.prisma.$queryRaw<[{count: bigint}]> - `SELECT COUNT(*) as count FROM tickets WHERE closedby IS NULL AND creator = ${interaction.user.id}`)[0].count; + const ticketsOpened = ( + await this.client.prisma.$queryRaw< + [{ count: bigint }] + >`SELECT COUNT(*) as count FROM tickets WHERE closedby IS NULL AND creator = ${interaction.user.id}` + )[0].count; // If maxTicketOpened is 0, it means that there is no limit if (ticketsOpened >= this.client.config.maxTicketOpened) { interaction .editReply({ - content: this.client.locales.getValue("ticketLimitReached").replace("TICKETLIMIT", this.client.config.maxTicketOpened.toString()) + content: this.client.locales.getValue("ticketLimitReached").replace("TICKETLIMIT", this.client.config.maxTicketOpened.toString()), }) .catch((e) => console.log(e)); return; @@ -101,7 +114,7 @@ export default class InteractionCreateEvent extends BaseEvent { if (options.length <= 0) { interaction.editReply({ - content: this.client.locales.getValue("noTickets") + content: this.client.locales.getValue("noTickets"), }); return; } @@ -111,7 +124,7 @@ export default class InteractionCreateEvent extends BaseEvent { .setCustomId("selectTicketType") .setPlaceholder(this.client.locales.getSubValue("other", "selectTicketTypePlaceholder")) .setMaxValues(1) - .addOptions(options) + .addOptions(options), ); interaction @@ -127,11 +140,11 @@ export default class InteractionCreateEvent extends BaseEvent { if (interaction.customId === "close") { await interaction.deferReply({ ephemeral: true }).catch((e) => console.log(e)); - close(interaction, this.client, this.client.locales.getSubValue("other", "noReasonGiven")); + close(interaction, this.client, this.client.locales.getSubValue("other", "noReasonGiven"), this.client.config.closeOption.deleteTicket); } if (interaction.customId === "close_askReason") { - closeAskReason(interaction, this.client); + closeAskReason(interaction, this.client, this.client.config.closeOption.deleteTicket); } if (interaction.customId === "deleteTicket") { @@ -142,8 +155,11 @@ export default class InteractionCreateEvent extends BaseEvent { if (interaction.isStringSelectMenu()) { if (interaction.customId === "selectTicketType") { if (this.client.config.maxTicketOpened > 0) { - const ticketsOpened = (await this.client.prisma.$queryRaw<[{count: bigint}]> - `SELECT COUNT(*) as count FROM tickets WHERE closedby IS NULL AND creator = ${interaction.user.id}`)[0].count; + const ticketsOpened = ( + await this.client.prisma.$queryRaw< + [{ count: bigint }] + >`SELECT COUNT(*) as count FROM tickets WHERE closedby IS NULL AND creator = ${interaction.user.id}` + )[0].count; // If maxTicketOpened is 0, it means that there is no limit if (ticketsOpened >= this.client.config.maxTicketOpened) { interaction @@ -161,7 +177,7 @@ export default class InteractionCreateEvent extends BaseEvent { if (ticketType.askQuestions) { // Sanity Check const qCount = ticketType.questions.length; - if(qCount === 0 || qCount > 5) + if (qCount === 0 || qCount > 5) throw new Error(`${ticketType.codeName} has either no questions or exceeded 5 questions. Check your config and restart the bot`); const modal = new ModalBuilder().setCustomId("askReason").setTitle(this.client.locales.getSubValue("modals", "reasonTicketOpen", "title")); @@ -191,8 +207,8 @@ export default class InteractionCreateEvent extends BaseEvent { invited: true, }, where: { - channelid: interaction.message.channelId - } + channelid: interaction.message.channelId, + }, }); for (const value of interaction.values) { await (interaction.channel as GuildChannel | null)?.permissionOverwrites.delete(value).catch((e) => console.log(e)); @@ -206,28 +222,26 @@ export default class InteractionCreateEvent extends BaseEvent { id: value, }, }, - this.client + this.client, ); } // Update the data in the database await this.client.prisma.tickets.update({ data: { - invited: JSON.stringify((JSON.parse(ticket?.invited ?? "[]") as string[]) - .filter(userid=>interaction.values.find(rUID=>rUID===userid) === undefined)) + invited: JSON.stringify( + (JSON.parse(ticket?.invited ?? "[]") as string[]).filter((userid) => interaction.values.find((rUID) => rUID === userid) === undefined), + ), }, where: { - channelid: interaction.channel?.id - } + channelid: interaction.channel?.id, + }, }); - await interaction - .update({ - content: `> Removed ${ - interaction.values.length < 1 ? interaction.values : interaction.values.map((a) => `<@${a}>`).join(", ") - } from the ticket`, - components: [], - }); + await interaction.update({ + content: `> Removed ${interaction.values.length < 1 ? interaction.values : interaction.values.map((a) => `<@${a}>`).join(", ")} from the ticket`, + components: [], + }); } } @@ -243,6 +257,9 @@ export default class InteractionCreateEvent extends BaseEvent { if (interaction.customId === "askReasonClose") { await interaction.deferReply().catch((e) => console.log(e)); close(interaction, this.client, interaction.fields.fields.first()?.value); + } else if (interaction.customId === "askReasonDelete") { + await interaction.deferReply().catch((e) => console.log(e)); + close(interaction, this.client, interaction.fields.fields.first()?.value, true); } } } diff --git a/src/structure/ExtendedClient.ts b/src/structure/ExtendedClient.ts index 6413249e..ba49631b 100644 --- a/src/structure/ExtendedClient.ts +++ b/src/structure/ExtendedClient.ts @@ -1,6 +1,6 @@ import {Client, ClientOptions, Collection, Routes} from "discord.js"; import {BaseCommand, ConfigType} from "./"; -import {PrismaClient} from "@prisma/client"; +import {PrismaClient} from "../../node_modules/.prisma/client"; import fs from "fs-extra"; import path from "node:path"; import {AddCommand, MassAddCommand, ClaimCommand, CloseCommand, RemoveCommand, RenameCommand, clearDM} from "../commands"; diff --git a/src/structure/types.ts b/src/structure/types.ts index 3e58bb70..b5aa4ca8 100644 --- a/src/structure/types.ts +++ b/src/structure/types.ts @@ -29,6 +29,7 @@ export type ConfigType = { askReason: boolean; whoCanCloseTicket: "STAFFONLY" | "EVERYONE"; closeTicketCategoryId?: string; + deleteTicket: boolean, }; uuidType: "uuid" | "emoji"; status: { diff --git a/src/utils/close.ts b/src/utils/close.ts index 6076ee6a..7b335b51 100644 --- a/src/utils/close.ts +++ b/src/utils/close.ts @@ -1,9 +1,23 @@ import { generateMessages } from "ticket-bot-transcript-uploader"; import zlib from "zlib"; import axios from "axios"; -import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, Collection, ColorResolvable, CommandInteraction, ComponentType, EmbedBuilder, GuildMember, Message, ModalSubmitInteraction, TextChannel } from "discord.js"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + Collection, + ColorResolvable, + CommandInteraction, + ComponentType, + EmbedBuilder, + GuildMember, + Message, + ModalSubmitInteraction, + TextChannel, +} from "discord.js"; import { log } from "./logs"; -import {ExtendedClient, TicketType} from "../structure"; +import { ExtendedClient, TicketType } from "../structure"; let domain = "https://ticket.pm/"; /* @@ -14,51 +28,57 @@ please check https://creativecommons.org/licenses/by/4.0 for more informations. */ type ticketType = { - id: number; - channelid: string; - messageid: string; - category: string; - invited: string; - reason: string; - creator: string; - createdat: bigint; - claimedby: string | null; - claimedat: bigint | null; - closedby: string | null; - closedat: bigint | null; - closereason: string | null; - transcript: string | null; -} + id: number; + channelid: string; + messageid: string; + category: string; + invited: string; + reason: string; + creator: string; + createdat: bigint; + claimedby: string | null; + claimedat: bigint | null; + closedby: string | null; + closedat: bigint | null; + closereason: string | null; + transcript: string | null; +}; -export async function close(interaction: ButtonInteraction | CommandInteraction | ModalSubmitInteraction, client: ExtendedClient, reason?: string) { - if (!client.config.closeOption.createTranscript) domain = client.locales.getSubValue("other","unavailable"); +export async function close( + interaction: ButtonInteraction | CommandInteraction | ModalSubmitInteraction, + client: ExtendedClient, + reason?: string, + deleteTicket: boolean = false, +) { + if (!client.config.closeOption.createTranscript) domain = client.locales.getSubValue("other", "unavailable"); const ticket = await client.prisma.tickets.findUnique({ where: { - channelid: interaction.channel?.id - } + channelid: interaction.channel?.id, + }, }); const ticketClosed = ticket?.closedat && ticket.closedby; if (!ticket) return interaction.editReply({ content: "Ticket not found" }).catch((e) => console.log(e)); // @TODO: Breaking change refactor happens here as well.. - const ticketType = ticket ? JSON.parse(ticket.category) as TicketType : undefined; - + const ticketType = ticket ? (JSON.parse(ticket.category) as TicketType) : undefined; + if ( client.config.closeOption.whoCanCloseTicket === "STAFFONLY" && - !(interaction.member as GuildMember | null)?.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id) || - ticketType?.staffRoles?.includes(r.id)) + !(interaction.member as GuildMember | null)?.roles.cache.some( + (r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id) || ticketType?.staffRoles?.includes(r.id), + ) ) return interaction .editReply({ - content: client.locales.getValue("ticketOnlyClosableByStaff") + content: client.locales.getValue("ticketOnlyClosableByStaff"), }) .catch((e) => console.log(e)); if (ticketClosed) return interaction .editReply({ - content: client.locales.getValue("ticketAlreadyClosed") + content: client.locales.getValue("ticketAlreadyClosed"), }) .catch((e) => console.log(e)); @@ -69,11 +89,11 @@ export async function close(interaction: ButtonInteraction | CommandInteraction ticketId: ticket.id, ticketChannelId: interaction.channel?.id, ticketCreatedAt: ticket.createdat, - reason: reason + reason: reason, }, - client + client, ); - + // Normally the user that closes the ticket will get posted here, but we'll do it when the ticket finalizes const creator = ticket.creator; @@ -81,105 +101,131 @@ export async function close(interaction: ButtonInteraction | CommandInteraction (interaction.channel as TextChannel | null)?.permissionOverwrites .edit(creator, { - ViewChannel: false + ViewChannel: false, }) .catch((e: unknown) => console.log(e)); invited.forEach(async (user) => { (interaction.channel as TextChannel | null)?.permissionOverwrites .edit(user, { - ViewChannel: false + ViewChannel: false, }) .catch((e) => console.log(e)); }); interaction .editReply({ - content: client.locales.getValue("ticketCreatingTranscript") + content: client.locales.getValue("ticketCreatingTranscript"), }) .catch((e) => console.log(e)); async function _close(id: string, ticket: ticketType) { - if (client.config.closeOption.closeTicketCategoryId) (interaction.channel as TextChannel | null)?.setParent(client.config.closeOption.closeTicketCategoryId).catch((e) => console.log(e)); + if (client.config.closeOption.closeTicketCategoryId) + (interaction.channel as TextChannel | null)?.setParent(client.config.closeOption.closeTicketCategoryId).catch((e) => console.log(e)); const msg = await interaction.channel?.messages.fetch(ticket.messageid); const embed = new EmbedBuilder(msg?.embeds[0].data); const rowAction = new ActionRowBuilder(); msg?.components[0]?.components?.map((x) => { - if(x.type !== ComponentType.Button) return; + if (x.type !== ComponentType.Button) return; const builder = new ButtonBuilder(x.data); if (x.customId === "close") builder.setDisabled(true); if (x.customId === "close_askReason") builder.setDisabled(true); rowAction.addComponents(builder); }); - msg?.edit({ - content: msg.content, - embeds: [embed], - components: [rowAction] - }) + msg + ?.edit({ + content: msg.content, + embeds: [embed], + components: [rowAction], + }) + .catch((e) => console.log(e)); + + interaction.channel + ?.send({ + content: client.locales + .getValue("ticketTranscriptCreated") + .replace( + "TRANSCRIPTURL", + domain === client.locales.getSubValue("other", "unavailable") ? client.locales.getSubValue("other", "unavailable") : `<${domain}${id}>`, + ), + }) .catch((e) => console.log(e)); - interaction.channel?.send({ - content: client.locales.getValue("ticketTranscriptCreated").replace( - "TRANSCRIPTURL", - domain === client.locales.getSubValue("other", "unavailable") ? client.locales.getSubValue("other", "unavailable") : `<${domain}${id}>` - ) - }).catch((e) => console.log(e)); - ticket = await client.prisma.tickets.update({ data: { closedby: interaction.user.id, closedat: Date.now(), closereason: reason, - transcript: domain === client.locales.getSubValue("other", "unavailable") ? client.locales.getSubValue("other", "unavailable") : `${domain}${id}` + transcript: domain === client.locales.getSubValue("other", "unavailable") ? client.locales.getSubValue("other", "unavailable") : `${domain}${id}`, }, where: { - channelid: interaction.channel?.id - } + channelid: interaction.channel?.id, + }, }); const row = new ActionRowBuilder().addComponents( - new ButtonBuilder().setCustomId("deleteTicket").setLabel(client.locales.getSubValue("other", "deleteTicketButtonMSG")).setStyle(ButtonStyle.Danger) + new ButtonBuilder() + .setCustomId("deleteTicket") + .setLabel(client.locales.getSubValue("other", "deleteTicketButtonMSG")) + .setStyle(ButtonStyle.Danger) + .setDisabled(deleteTicket), ); const locale = client.locales; - interaction.channel?.send({ - embeds: [ - JSON.parse( - JSON.stringify(locale.getSubRawValue("embeds", "ticketClosed")) - .replace("TICKETCOUNT", ticket.id.toString()) - .replace("REASON", (ticket.closereason ?? client.locales.getSubValue("other", "noReasonGiven")).replace(/[\n\r]/g, "\\n")) - .replace("CLOSERNAME", interaction.user.tag) - ) - ], - components: [row] - }) + interaction.channel + ?.send({ + embeds: [ + JSON.parse( + JSON.stringify(locale.getSubRawValue("embeds", "ticketClosed")) + .replace("TICKETCOUNT", ticket.id.toString()) + .replace("REASON", (ticket.closereason ?? client.locales.getSubValue("other", "noReasonGiven")).replace(/[\n\r]/g, "\\n")) + .replace("CLOSERNAME", interaction.user.tag), + ), + ], + components: [row], + }) .catch((e) => console.log(e)); + if (deleteTicket) { + log( + { + LogType: "ticketDelete", + user: interaction.user, + ticketId: ticket.id, + ticketCreatedAt: ticket.createdat, + transcriptURL: ticket.transcript ?? undefined, + }, + client, + ); + + setTimeout(() => interaction.channel?.delete().catch((e) => console.log(e)), 15000); // ticket will be deleted within 15 seconds + } - if(!client.config.closeOption.dmUser) return; + if (!client.config.closeOption.dmUser) return; const footer = locale.getSubValue("embeds", "ticketClosedDM", "footer", "text").replace("ticket.pm", ""); const ticketClosedDMEmbed = new EmbedBuilder({ color: 0, }) - .setColor(locale.getNoErrorSubValue("embeds", "ticketClosedDM", "color") as ColorResolvable ?? client.config.mainColor) + .setColor((locale.getNoErrorSubValue("embeds", "ticketClosedDM", "color") as ColorResolvable) ?? client.config.mainColor) .setDescription( - client.locales.getSubValue("embeds", "ticketClosedDM", "description") + client.locales + .getSubValue("embeds", "ticketClosedDM", "description") .replace("TICKETCOUNT", ticket.id.toString()) .replace("TRANSCRIPTURL", `${domain}${id}`) .replace("REASON", ticket.closereason ?? client.locales.getSubValue("other", "noReasonGiven")) - .replace("CLOSERNAME", interaction.user.tag) + .replace("CLOSERNAME", interaction.user.tag), ) .setFooter({ // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) text: `ticket.pm ${footer.trim() !== "" ? `- ${footer}` : ""}`, // Please respect the LICENSE :D // Please respect the project by keeping the credits, (if it is too disturbing you can credit me in the "about me" of the bot discord) - iconURL: locale.getNoErrorSubValue("embeds", "ticketClosedDM", "footer", "iconUrl") + iconURL: locale.getNoErrorSubValue("embeds", "ticketClosedDM", "footer", "iconUrl"), }); client.users.fetch(creator).then((user) => { user .send({ - embeds: [ticketClosedDMEmbed] + embeds: [ticketClosedDMEmbed], }) .catch((e) => console.log(e)); }); @@ -196,13 +242,12 @@ export async function close(interaction: ButtonInteraction | CommandInteraction // eslint-disable-next-line no-constant-condition while (true) { // using if statement for this check causes a TypeScript bug. Hard to reproduce; thus, bug report won't be accepted. - if(!lastID) break; + if (!lastID) break; const fetched = await interaction.channel?.messages.fetch({ limit: 100, before: lastID }); if (fetched?.size === 0) { break; } - if(fetched) - collArray.push(fetched); + if (fetched) collArray.push(fetched); lastID = fetched?.last()?.id; if (fetched?.size !== 100) { break; @@ -223,8 +268,8 @@ export async function close(interaction: ButtonInteraction | CommandInteraction const ts = await axios .post(`${domain}upload?key=${premiumKey}&uuid=${client.config.uuidType}`, JSON.stringify(compressed), { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, }) .catch(console.error); _close(ts?.data, ticket); diff --git a/src/utils/close_askReason.ts b/src/utils/close_askReason.ts index 13ad9b00..098b53fe 100644 --- a/src/utils/close_askReason.ts +++ b/src/utils/close_askReason.ts @@ -6,22 +6,22 @@ please check https://creativecommons.org/licenses/by/4.0 for more informations. */ import { ActionRowBuilder, ButtonInteraction, CommandInteraction, GuildMember, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; -import {ExtendedClient, TicketType} from "../structure"; - -export const closeAskReason = async(interaction: CommandInteraction | ButtonInteraction, client: ExtendedClient) => { +import { ExtendedClient, TicketType } from "../structure"; +export const closeAskReason = async (interaction: CommandInteraction | ButtonInteraction, client: ExtendedClient, deleteTicket: boolean = false) => { // @TODO: Breaking change refactor happens here as well.. const ticket = await client.prisma.tickets.findUnique({ where: { - channelid: interaction.channel?.id - } + channelid: interaction.channel?.id, + }, }); - const ticketType = ticket ? JSON.parse(ticket.category) as TicketType : undefined; - + const ticketType = ticket ? (JSON.parse(ticket.category) as TicketType) : undefined; + if ( client.config.closeOption.whoCanCloseTicket === "STAFFONLY" && - !(interaction.member as GuildMember | null)?.roles.cache.some((r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id) || - ticketType?.staffRoles?.includes(r.id)) + !(interaction.member as GuildMember | null)?.roles.cache.some( + (r) => client.config.rolesWhoHaveAccessToTheTickets.includes(r.id) || ticketType?.staffRoles?.includes(r.id), + ) ) return interaction .reply({ @@ -30,11 +30,11 @@ export const closeAskReason = async(interaction: CommandInteraction | ButtonInte }) .catch((e) => console.log(e)); - const modal = new ModalBuilder().setCustomId("askReasonClose").setTitle(client.locales.getSubValue("modals", "reasonTicketClose", "title")); + const modal = new ModalBuilder().setCustomId(!deleteTicket ? "askReasonClose" : "askReasonDelete").setTitle(client.locales.getSubValue("modals", "reasonTicketClose", "title")); const input = new TextInputBuilder() .setCustomId("reason") - .setLabel(client.locales.getSubValue("modals","reasonTicketClose", "label")) + .setLabel(client.locales.getSubValue("modals", "reasonTicketClose", "label")) .setStyle(TextInputStyle.Paragraph) .setPlaceholder(client.locales.getSubValue("modals", "reasonTicketClose", "placeholder")) .setMaxLength(256);