Files
SupportBot/Commands/close.js
T
2024-11-09 00:37:51 +00:00

251 lines
9.2 KiB
JavaScript

const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
const Command = require("../Structures/Command.js");
module.exports = new Command({
name: cmdconfig.CloseTicket.Command,
description: cmdconfig.CloseTicket.Description,
type: Discord.ApplicationCommandType.ChatInput,
options: [
{
name: "reason",
description: "Ticket Close Reason",
type: Discord.ApplicationCommandOptionType.String,
},
],
permissions: cmdconfig.CloseTicket.Permission,
async run(interaction) {
const { getRole, getChannel } = interaction.client;
if (supportbot.Ticket.Close.StaffOnly) {
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!SupportStaff || !Admin) {
return interaction.reply("Some roles seem to be missing! Please check for errors when starting the bot.");
}
const NoPerms = new Discord.EmbedBuilder()
.setTitle("Invalid Permissions!")
.setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``)
.setColor(supportbot.Embed.Colours.Warn);
if (!interaction.member.roles.cache.has(SupportStaff.id) && !interaction.member.roles.cache.has(Admin.id)) {
return interaction.reply({ embeds: [NoPerms] });
}
}
if (
(supportbot.Ticket.TicketType === "threads" && interaction.channel.type !== Discord.ChannelType.PrivateThread) ||
(supportbot.Ticket.TicketType === "channels" && interaction.channel.type !== Discord.ChannelType.GuildText)
) {
const NotTicketChannel = new Discord.EmbedBuilder()
.setTitle("Invalid Channel!")
.setDescription(`This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`)
.setColor(supportbot.Embed.Colours.Warn); // Ensure valid color
return interaction.reply({ embeds: [NotTicketChannel], ephemeral: true });
}
await interaction.deferReply(); // Deferring to avoid double replies
// Reading TicketData.json once for efficiency
let tickets;
try {
tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
} catch (err) {
console.error('Error reading ticket data file:', err);
return interaction.followUp({ content: "There was an error loading ticket data." });
}
let TicketData = tickets.tickets.findIndex((t) => t.id === interaction.channel.id);
let ticket = tickets.tickets[TicketData];
if (TicketData === -1) {
const Exists = new Discord.EmbedBuilder()
.setTitle("No Ticket Found!")
.setDescription(msgconfig.Error.NoValidTicket)
.setColor(supportbot.Embed.Colours.Warn); // Ensure valid color
return interaction.followUp({ embeds: [Exists] });
}
let reason = interaction.options?.getString("reason") || "No Reason Provided.";
if (supportbot.Ticket.ReviewSystem.Enabled) {
const rating = await collectReviewRating(interaction);
const comment = await collectReviewComment(interaction);
const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
const reviewer = supportbot.Ticket.ClaimTickets ? `<@${ticket.claimedBy}>` : null;
const reviewEmbed = new Discord.EmbedBuilder()
.setTitle(msgconfig.ReviewSystem.ReviewEmbed.Title)
.addFields(
{
name: msgconfig.ReviewSystem.ReviewEmbed.RatingTitle,
value: `${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(rating)}`,
inline: false
},
{
name: msgconfig.ReviewSystem.ReviewEmbed.CommentTitle,
value: `\`\`\`${comment}\`\`\``,
inline: false
}
)
.setColor(msgconfig.ReviewSystem.ReviewEmbed.Color);
if (supportbot.Ticket.ClaimTickets.Enabled) {
reviewEmbed.setDescription(
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${reviewer}\n**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** <@${interaction.user.id}>` || `N/A`
);
} else {
reviewEmbed.setDescription(
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** <@${interaction.user.id}>`
);
}
if (reviewChannel) {
await reviewChannel.send({ embeds: [reviewEmbed] });
}
if (!interaction.replied && !interaction.deferred) {
await interaction.followUp({ embeds: [reviewEmbed] });
}
await handleCloseTicket(interaction, reason, ticket, TicketData);
} else {
await handleCloseTicket(interaction, reason, ticket, TicketData);
}
},
});
async function collectReviewRating(interaction) {
const reviewPrompt = new Discord.EmbedBuilder()
.setTitle(msgconfig.ReviewSystem.Rate.Title)
.setDescription(msgconfig.ReviewSystem.Rate.Description)
.setColor(supportbot.Embed.Colours.General); // Ensure valid color
const stars = new Discord.ActionRowBuilder().addComponents(
new Discord.StringSelectMenuBuilder()
.setCustomId("starRating")
.setPlaceholder("Select a rating")
.addOptions([
{ label: msgconfig.ReviewSystem.Stars.One, value: "1" },
{ label: msgconfig.ReviewSystem.Stars.Two, value: "2" },
{ label: msgconfig.ReviewSystem.Stars.Three, value: "3" },
{ label: msgconfig.ReviewSystem.Stars.Four, value: "4" },
{ label: msgconfig.ReviewSystem.Stars.Five, value: "5" },
])
);
await interaction.followUp({
embeds: [reviewPrompt],
components: [stars],
ephemeral: true,
});
const starRating = await interaction.channel.awaitMessageComponent({
componentType: Discord.ComponentType.StringSelect,
filter: (i) => i.customId === "starRating" && i.user.id === interaction.user.id,
time: 30000,
});
await starRating.deferUpdate();
return starRating.values[0];
}
async function collectReviewComment(interaction) {
const commentPrompt = new Discord.EmbedBuilder()
.setTitle(msgconfig.ReviewSystem.Comment.Title)
.setDescription(msgconfig.ReviewSystem.Comment.Description)
.setColor(supportbot.Embed.Colours.General); // Ensure valid color
await interaction.followUp({
embeds: [commentPrompt],
ephemeral: true,
});
const filter = (response) => response.author.id === interaction.user.id;
const commentCollection = await interaction.channel.awaitMessages({ filter, max: 1, time: 30000 });
const comment = commentCollection.first()?.content;
return comment && comment.toLowerCase() !== "no" ? comment : "No Comment Provided";
}
async function handleCloseTicket(interaction, reason, ticket, TicketData) {
const { getChannel } = interaction.client;
let tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
let tUser = interaction.client.users.cache.get(ticket.user);
let transcriptChannel = await getChannel(supportbot.Ticket.Log.TicketDataLog, interaction.guild);
if (!transcriptChannel)
return console.log("Ticket Log Missing");
try {
tickets.tickets[TicketData].open = false;
fs.writeFileSync("./Data/TicketData.json", JSON.stringify(tickets, null, 4));
const transcriptEmbed = new Discord.EmbedBuilder()
.setTitle(msgconfig.TicketLog.Title)
.setColor(msgconfig.TicketLog.Colour) // Ensure valid color
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
.setDescription(`> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n> **User:** ${tUser?.username || "N/A"}#${tUser?.discriminator || "N/A"} (\`${tUser?.id || ticket.user}\`)\n> **Closed by:** <@${interaction.user.id}>`)
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: true });
let msgs = await interaction.channel.messages.fetch({ limit: 100 });
let html = createTranscriptHTML(interaction, msgs);
const fileName = `${interaction.channel.name}.html`;
let file = new Discord.AttachmentBuilder(Buffer.from(html), { name: `SPOILER_${fileName}` });
transcriptChannel.send({
embeds: [transcriptEmbed],
files: [file],
});
} catch (err) {
console.log("Error closing ticket:", err);
}
await interaction.channel.delete();
}
function createTranscriptHTML(interaction, msgs) {
let html = `
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.message { padding: 10px; margin: 5px 0; border: 1px solid #ddd; }
.username { font-weight: bold; }
</style>
</head>
<body>
<h1>Ticket Transcript</h1>
<h2>Ticket: ${interaction.channel.name}</h2>
<h3>User: ${interaction.user.tag}</h3>
<div id="messages">
`;
msgs.reverse().forEach((msg) => {
html += `
<div class="message">
<span class="username">${msg.author.tag}</span>: ${msg.content}
</div>
`;
});
html += `
</div>
</body>
</html>
`;
return html;
}