22 files changed
+1566
-2244
No files matched your search
+154
-208
@@ -28,19 +28,20 @@ module.exports = new Command({
|
||||
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!\nPlease check for errors when starting the bot.");
|
||||
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);
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
if (!interaction.member.roles.cache.has(SupportStaff.id) && !interaction.member.roles.cache.has(Admin.id))
|
||||
if (!interaction.member.roles.cache.has(SupportStaff.id) && !interaction.member.roles.cache.has(Admin.id)) {
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a thread or channel based on config
|
||||
if (
|
||||
(supportbot.Ticket.TicketType === "threads" && interaction.channel.type !== Discord.ChannelType.PrivateThread) ||
|
||||
(supportbot.Ticket.TicketType === "channels" && interaction.channel.type !== Discord.ChannelType.GuildText)
|
||||
@@ -48,14 +49,22 @@ module.exports = new Command({
|
||||
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);
|
||||
.setColor(supportbot.Embed.Colours.Warn); // Ensure valid color
|
||||
|
||||
return interaction.reply({ embeds: [NotTicketChannel], ephemeral: true });
|
||||
}
|
||||
|
||||
await interaction.deferReply();
|
||||
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 tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
let TicketData = tickets.tickets.findIndex((t) => t.id === interaction.channel.id);
|
||||
let ticket = tickets.tickets[TicketData];
|
||||
|
||||
@@ -63,44 +72,112 @@ module.exports = new Command({
|
||||
const Exists = new Discord.EmbedBuilder()
|
||||
.setTitle("No Ticket Found!")
|
||||
.setDescription(msgconfig.Error.NoValidTicket)
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
.setColor(supportbot.Embed.Colours.Warn); // Ensure valid color
|
||||
return interaction.followUp({ embeds: [Exists] });
|
||||
}
|
||||
|
||||
let reason = interaction.options?.getString("reason") || "No Reason Provided.";
|
||||
|
||||
const CloseButton = new Discord.ButtonBuilder()
|
||||
.setCustomId("confirmCloseTicket")
|
||||
.setLabel(supportbot.Ticket.Close.Confirmation_Button)
|
||||
.setEmoji(supportbot.Ticket.Close.Confirmation_Emoji)
|
||||
.setStyle(supportbot.Ticket.Close.Confirmation_Style);
|
||||
if (supportbot.Ticket.ReviewSystem.Enabled) {
|
||||
const rating = await collectReviewRating(interaction);
|
||||
const comment = await collectReviewComment(interaction);
|
||||
|
||||
const row = new Discord.ActionRowBuilder().addComponents(CloseButton);
|
||||
const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
|
||||
const reviewer = supportbot.Ticket.ClaimTickets ? `<@${ticket.claimedBy}>` : null;
|
||||
|
||||
const CloseTicketRequest = new Discord.EmbedBuilder()
|
||||
.setTitle(`**${supportbot.Ticket.Close.Title}**`)
|
||||
.setDescription(`${msgconfig.Ticket.ConfirmClose}`)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
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);
|
||||
|
||||
await interaction.followUp({
|
||||
embeds: [CloseTicketRequest],
|
||||
components: [row],
|
||||
});
|
||||
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}>`
|
||||
);
|
||||
}
|
||||
|
||||
const filter = (btnInteraction) => btnInteraction.customId === "confirmCloseTicket" && btnInteraction.user.id === interaction.user.id;
|
||||
if (reviewChannel) {
|
||||
await reviewChannel.send({ embeds: [reviewEmbed] });
|
||||
}
|
||||
|
||||
interaction.channel.awaitMessageComponent({ filter, time: 20000 })
|
||||
.then(async (btnInteraction) => {
|
||||
await handleCloseTicket(interaction, reason, ticket, TicketData);
|
||||
await btnInteraction.reply({ content: "Ticket successfully closed.", ephemeral: true });
|
||||
})
|
||||
.catch((err) => {
|
||||
interaction.followUp(`${msgconfig.Ticket.CloseTimeout}`);
|
||||
console.error(err);
|
||||
});
|
||||
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;
|
||||
|
||||
@@ -109,196 +186,65 @@ async function handleCloseTicket(interaction, reason, ticket, TicketData) {
|
||||
let transcriptChannel = await getChannel(supportbot.Ticket.Log.TicketDataLog, interaction.guild);
|
||||
|
||||
if (!transcriptChannel)
|
||||
return interaction.followUp("Some Channels seem to be missing!");
|
||||
return console.log("Ticket Log Missing");
|
||||
|
||||
try {
|
||||
tickets.tickets[TicketData].open = false;
|
||||
fs.writeFileSync(
|
||||
"./Data/TicketData.json",
|
||||
JSON.stringify(tickets, null, 4),
|
||||
(err) => {
|
||||
if (err) console.error(err);
|
||||
}
|
||||
);
|
||||
fs.writeFileSync("./Data/TicketData.json", JSON.stringify(tickets, null, 4));
|
||||
|
||||
const transcriptEmbed = new Discord.EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketLog.Title)
|
||||
.setColor(msgconfig.TicketLog.Colour)
|
||||
.setFooter({
|
||||
text: supportbot.Embed.Footer,
|
||||
iconURL: interaction.user.displayAvatarURL(),
|
||||
})
|
||||
.addFields(
|
||||
{ name: "Ticket", value: `${interaction.channel.name} (${interaction.channel.id})` },
|
||||
{ name: "User", value: `${tUser?.username || "N/A"}#${tUser?.discriminator || "N/A"} (${tUser?.id || ticket.user})` },
|
||||
{ name: "Closed By", value: interaction.user.tag },
|
||||
{ name: "Reason", value: reason }
|
||||
);
|
||||
.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 = `
|
||||
<html>
|
||||
<head>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #1a1a1a;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.container {
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(0, 128, 0, 0.3);
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
.message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
.username {
|
||||
font-weight: bold;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.8em;
|
||||
color: #bbb;
|
||||
}
|
||||
.download-button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="text-2xl font-bold mb-4">Ticket Transcript</h1>
|
||||
<p><strong>Server Name:</strong> ${interaction.guild.name}</p>
|
||||
<p><strong>Ticket:</strong> ${interaction.channel.name}</p>
|
||||
<p><strong>Messages:</strong> ${msgs.size} Messages</p>
|
||||
<div class="mt-4">
|
||||
`;
|
||||
|
||||
msgs = msgs.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
|
||||
msgs.forEach((msg) => {
|
||||
if (msg.content) {
|
||||
html += `
|
||||
<div class="message">
|
||||
<img class="avatar" src="${msg.author.displayAvatarURL()}" alt="Avatar">
|
||||
<div class="content">
|
||||
<p class="username">${msg.author.tag}</p>
|
||||
<p class="timestamp">${msg.createdAt.toLocaleString()}</p>
|
||||
<p>${convertMarkdownToHTML(msg.content)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
let html = createTranscriptHTML(interaction, msgs);
|
||||
|
||||
const fileName = `${interaction.channel.name}.html`;
|
||||
let file = new Discord.AttachmentBuilder(Buffer.from(html), { name: `SPOILER_${fileName}` });
|
||||
|
||||
html += `
|
||||
</div>
|
||||
<a href="${fileName}" download class="download-button">Download Transcript</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
let file = new Discord.AttachmentBuilder(
|
||||
Buffer.from(html),
|
||||
{ name: fileName }
|
||||
);
|
||||
|
||||
await transcriptChannel
|
||||
.send({ embeds: [transcriptEmbed], files: [file] })
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if (supportbot.Ticket.DMTranscripts) {
|
||||
await tUser
|
||||
?.send({ embeds: [transcriptEmbed], files: [file] })
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if (ticket.subUsers) {
|
||||
ticket.subUsers.forEach(async (subUser) => {
|
||||
await interaction.client.users.cache
|
||||
.get(subUser)
|
||||
?.send({ embeds: [transcriptEmbed], files: [file] });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.channel.delete().catch((error) => {
|
||||
console.error(error);
|
||||
if (error.code !== Discord.Constants.APIErrors.UNKNOWN_CHANNEL) {
|
||||
console.error("Failed to delete the interaction:", error);
|
||||
}
|
||||
transcriptChannel.send({
|
||||
embeds: [transcriptEmbed],
|
||||
files: [file],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} catch (err) {
|
||||
console.log("Error closing ticket:", err);
|
||||
}
|
||||
|
||||
await interaction.channel.delete();
|
||||
}
|
||||
|
||||
function convertMarkdownToHTML(text) {
|
||||
// Convert **bold** to <strong>bold</strong>
|
||||
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
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">
|
||||
`;
|
||||
|
||||
// Convert *italic* to <em>italic</em>
|
||||
text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
||||
msgs.reverse().forEach((msg) => {
|
||||
html += `
|
||||
<div class="message">
|
||||
<span class="username">${msg.author.tag}</span>: ${msg.content}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Convert __underline__ to <u>underline</u>
|
||||
text = text.replace(/__(.*?)__/g, '<u>$1</u>');
|
||||
|
||||
// Convert ~~strikethrough~~ to <s>strikethrough</s>
|
||||
text = text.replace(/~~(.*?)~~/g, '<s>$1</s>');
|
||||
|
||||
// Convert inline code `code` to <code>code</code>
|
||||
text = text.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
|
||||
// Convert blockquotes > quote to <blockquote>quote</blockquote>
|
||||
text = text.replace(/^> (.*?$)/gm, '<blockquote>$1</blockquote>');
|
||||
|
||||
// Convert multiline code blocks ```code``` to <pre><code>code</code></pre>
|
||||
text = text.replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>');
|
||||
|
||||
// Convert line breaks to <br>
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
|
||||
return text;
|
||||
html += `
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return html;
|
||||
}
|
||||
+450
-138
@@ -1,167 +1,479 @@
|
||||
// File path: ./commands/embed.js
|
||||
const {
|
||||
ApplicationCommandType,
|
||||
ApplicationCommandOptionType,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
EmbedBuilder,
|
||||
StringSelectMenuBuilder,
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
ButtonStyle,
|
||||
ComponentType
|
||||
} = require('discord.js');
|
||||
const Command = require('../Structures/Command.js');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
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 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");
|
||||
// Helper function to validate and convert hex color
|
||||
function parseColor(color) {
|
||||
if (!color) return 0x5865F2;
|
||||
if (typeof color === 'number') return color;
|
||||
if (typeof color !== 'string') return 0x5865F2;
|
||||
color = color.replace('#', '');
|
||||
if (/^[0-9A-F]{6}$/i.test(color)) {
|
||||
return parseInt(color, 16);
|
||||
}
|
||||
return 0x5865F2;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Embed.Command,
|
||||
description: cmdconfig.Embed.Description,
|
||||
type: Discord.ApplicationCommandType.ChatInput,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: "title",
|
||||
description: "Embed Title",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "message",
|
||||
description: "Embed Message",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
description: "Embed HEX Color",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "fields",
|
||||
description: "Embed Fields (title|content;title|content;...)",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "thumbnail",
|
||||
description: "Embed Thumbnail URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
description: "Embed Image URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "footer",
|
||||
description: "Embed Footer Text",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "footericon",
|
||||
description: "Embed Footer Icon URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "author",
|
||||
description: "Embed Author Name",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "authoricon",
|
||||
description: "Embed Author Icon URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "authorurl",
|
||||
description: "Embed Author URL",
|
||||
type: Discord.ApplicationCommandOptionType.String,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "timestamp",
|
||||
description: "Include Timestamp",
|
||||
type: Discord.ApplicationCommandOptionType.Boolean,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "inline",
|
||||
description: "Make Fields Inline",
|
||||
type: Discord.ApplicationCommandOptionType.Boolean,
|
||||
required: false,
|
||||
name: 'channel',
|
||||
description: 'Select channel to send the message',
|
||||
type: ApplicationCommandOptionType.Channel,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
permissions: cmdconfig.Embed.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
// Permission check
|
||||
const { getRole } = interaction.client;
|
||||
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!\nPlease check for errors when starting the bot.");
|
||||
return interaction.reply('Some roles seem to be missing!\nPlease 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)) {
|
||||
const NoPerms = new EmbedBuilder()
|
||||
.setTitle('Invalid Permissions!')
|
||||
.setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``)
|
||||
.setColor(0xFF0000);
|
||||
return interaction.reply({ embeds: [NoPerms], ephemeral: true });
|
||||
}
|
||||
|
||||
if (
|
||||
interaction.member.roles.cache.has(SupportStaff.id) ||
|
||||
interaction.member.roles.cache.has(Admin.id)
|
||||
) {
|
||||
const EmbedTitle = interaction.options.getString("title").replace(/\\n/g, '\n');
|
||||
const EmbedSubject = interaction.options.getString("message").replace(/\\n/g, '\n');
|
||||
const GeneralColour = interaction.options.getString("color") || supportbot.Embed.Colours.Default;
|
||||
const EmbedThumbnail = interaction.options.getString("thumbnail");
|
||||
const EmbedImage = interaction.options.getString("image");
|
||||
const EmbedFields = interaction.options.getString("fields");
|
||||
const EmbedFooter = interaction.options.getString("footer")?.replace(/\\n/g, '\n');
|
||||
const EmbedFooterIcon = interaction.options.getString("footericon");
|
||||
const EmbedAuthor = interaction.options.getString("author")?.replace(/\\n/g, '\n');
|
||||
const EmbedAuthorIcon = interaction.options.getString("authoricon");
|
||||
const EmbedAuthorURL = interaction.options.getString("authorurl");
|
||||
const EmbedTimestamp = interaction.options.getBoolean("timestamp");
|
||||
const InlineFields = interaction.options.getBoolean("inline");
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
|
||||
// Initial message state
|
||||
const messageData = {
|
||||
type: 'embed',
|
||||
title: 'New Embed',
|
||||
description: 'Click the buttons below to edit this embed',
|
||||
color: 0x5865F2,
|
||||
fields: [],
|
||||
footer: null,
|
||||
thumbnail: null,
|
||||
image: null,
|
||||
timestamp: false
|
||||
};
|
||||
|
||||
const EmbedMsg = new Discord.EmbedBuilder()
|
||||
.setTitle(EmbedTitle)
|
||||
.setDescription(EmbedSubject)
|
||||
.setColor(GeneralColour)
|
||||
.setThumbnail(EmbedThumbnail)
|
||||
.setImage(EmbedImage);
|
||||
// Create initial embed
|
||||
function createEmbed() {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(messageData.title)
|
||||
.setDescription(messageData.description)
|
||||
.setColor(messageData.color);
|
||||
|
||||
if (EmbedFields) {
|
||||
const fieldsArray = EmbedFields.split(';');
|
||||
fieldsArray.forEach(field => {
|
||||
const [title, value] = field.split('|');
|
||||
if (title && value) {
|
||||
EmbedMsg.addFields({ name: title.replace(/\\n/g, '\n'), value: value.replace(/\\n/g, '\n'), inline: InlineFields || false });
|
||||
}
|
||||
if (messageData.fields.length > 0) {
|
||||
embed.addFields(messageData.fields);
|
||||
}
|
||||
if (messageData.footer) {
|
||||
embed.setFooter({ text: messageData.footer });
|
||||
}
|
||||
if (messageData.thumbnail) {
|
||||
embed.setThumbnail(messageData.thumbnail);
|
||||
}
|
||||
if (messageData.image) {
|
||||
embed.setImage(messageData.image);
|
||||
}
|
||||
if (messageData.timestamp) {
|
||||
embed.setTimestamp();
|
||||
}
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
// Create type selector
|
||||
const typeRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new StringSelectMenuBuilder()
|
||||
.setCustomId('message_type')
|
||||
.setPlaceholder('Select message type')
|
||||
.addOptions([
|
||||
{
|
||||
label: 'Embed Message',
|
||||
description: 'Send as an embedded message',
|
||||
value: 'embed'
|
||||
},
|
||||
{
|
||||
label: 'Text Message',
|
||||
description: 'Send as a plain text message',
|
||||
value: 'text'
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
// Create button rows function
|
||||
function createButtons(messageType) {
|
||||
const buttons = [];
|
||||
|
||||
if (messageType === 'embed') {
|
||||
// Only show these buttons for embed mode
|
||||
const row1 = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_title')
|
||||
.setLabel('Edit Title')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_description')
|
||||
.setLabel('Edit Description')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_color')
|
||||
.setLabel('Edit Color')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('add_field')
|
||||
.setLabel('Add Field')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
const row2 = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_footer')
|
||||
.setLabel('Edit Footer')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_image')
|
||||
.setLabel('Add Image')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_thumbnail')
|
||||
.setLabel('Add Thumbnail')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('toggle_timestamp')
|
||||
.setLabel('Toggle Timestamp')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
buttons.push(row1, row2);
|
||||
} else {
|
||||
// Text mode only shows edit text button
|
||||
const textRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('edit_description')
|
||||
.setLabel('Edit Text')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
buttons.push(textRow);
|
||||
}
|
||||
|
||||
// Send button always shows
|
||||
const sendRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('send_message')
|
||||
.setLabel('Send')
|
||||
.setStyle(ButtonStyle.Success)
|
||||
);
|
||||
|
||||
buttons.push(sendRow);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
// Function to update the message preview
|
||||
async function updatePreview(i) {
|
||||
if (messageData.type === 'embed') {
|
||||
await i.update({
|
||||
content: 'Customize your message:',
|
||||
embeds: [createEmbed()],
|
||||
components: [typeRow, ...createButtons('embed')]
|
||||
});
|
||||
} else {
|
||||
// For text mode, just show the text directly
|
||||
await i.update({
|
||||
content: messageData.description,
|
||||
embeds: [],
|
||||
components: [typeRow, ...createButtons('text')]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Send initial message
|
||||
const message = await interaction.reply({
|
||||
content: messageData.type === 'text' ? messageData.description : 'Customize your message:',
|
||||
components: [typeRow, ...createButtons(messageData.type)],
|
||||
embeds: messageData.type === 'embed' ? [createEmbed()] : [],
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
// Create collector for interactions
|
||||
const collector = message.createMessageComponentCollector({
|
||||
time: 900000
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
if (i.user.id !== interaction.user.id) {
|
||||
return i.reply({
|
||||
content: 'You cannot edit this message.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
if (EmbedFooter) {
|
||||
EmbedMsg.setFooter({ text: EmbedFooter, iconURL: EmbedFooterIcon });
|
||||
switch (i.customId) {
|
||||
case 'message_type':
|
||||
messageData.type = i.values[0];
|
||||
await updatePreview(i);
|
||||
break;
|
||||
|
||||
case 'edit_description':
|
||||
const descModal = new ModalBuilder()
|
||||
.setCustomId('desc_modal')
|
||||
.setTitle(messageData.type === 'embed' ? 'Edit Description' : 'Edit Text');
|
||||
|
||||
descModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('desc_input')
|
||||
.setLabel(messageData.type === 'embed' ? 'Description' : 'Message Text')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setValue(messageData.description)
|
||||
.setMaxLength(4000)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(descModal);
|
||||
break;
|
||||
|
||||
case 'edit_title':
|
||||
const titleModal = new ModalBuilder()
|
||||
.setCustomId('title_modal')
|
||||
.setTitle('Edit Title');
|
||||
|
||||
titleModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('title_input')
|
||||
.setLabel('Title')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.title)
|
||||
.setMaxLength(256)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(titleModal);
|
||||
break;
|
||||
|
||||
case 'edit_color':
|
||||
const colorModal = new ModalBuilder()
|
||||
.setCustomId('color_modal')
|
||||
.setTitle('Edit Color');
|
||||
|
||||
colorModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('color_input')
|
||||
.setLabel('Color (HEX)')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setPlaceholder('#FF0000')
|
||||
.setMinLength(6)
|
||||
.setMaxLength(7)
|
||||
.setValue(messageData.color.toString(16).padStart(6, '0'))
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(colorModal);
|
||||
break;
|
||||
|
||||
case 'add_field':
|
||||
const fieldModal = new ModalBuilder()
|
||||
.setCustomId('field_modal')
|
||||
.setTitle('Add Field');
|
||||
|
||||
fieldModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('field_name')
|
||||
.setLabel('Field Name')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setMaxLength(256)
|
||||
),
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('field_value')
|
||||
.setLabel('Field Value')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setMaxLength(1024)
|
||||
),
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('field_inline')
|
||||
.setLabel('Inline? (true/false)')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue('false')
|
||||
.setPlaceholder('true or false')
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(fieldModal);
|
||||
break;
|
||||
|
||||
case 'edit_footer':
|
||||
const footerModal = new ModalBuilder()
|
||||
.setCustomId('footer_modal')
|
||||
.setTitle('Edit Footer');
|
||||
|
||||
footerModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('footer_input')
|
||||
.setLabel('Footer Text')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.footer || '')
|
||||
.setMaxLength(2048)
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(footerModal);
|
||||
break;
|
||||
|
||||
case 'edit_image':
|
||||
const imageModal = new ModalBuilder()
|
||||
.setCustomId('image_modal')
|
||||
.setTitle('Add Image');
|
||||
|
||||
imageModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('image_input')
|
||||
.setLabel('Image URL')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.image || '')
|
||||
.setPlaceholder('https://example.com/image.png')
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(imageModal);
|
||||
break;
|
||||
|
||||
case 'edit_thumbnail':
|
||||
const thumbnailModal = new ModalBuilder()
|
||||
.setCustomId('thumbnail_modal')
|
||||
.setTitle('Add Thumbnail');
|
||||
|
||||
thumbnailModal.addComponents(
|
||||
new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('thumbnail_input')
|
||||
.setLabel('Thumbnail URL')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setValue(messageData.thumbnail || '')
|
||||
.setPlaceholder('https://example.com/thumbnail.png')
|
||||
)
|
||||
);
|
||||
|
||||
await i.showModal(thumbnailModal);
|
||||
break;
|
||||
|
||||
case 'toggle_timestamp':
|
||||
messageData.timestamp = !messageData.timestamp;
|
||||
await updatePreview(i);
|
||||
break;
|
||||
|
||||
case 'send_message':
|
||||
try {
|
||||
if (messageData.type === 'embed') {
|
||||
await channel.send({ embeds: [createEmbed()] });
|
||||
} else {
|
||||
await channel.send(messageData.description);
|
||||
}
|
||||
await i.update({
|
||||
content: 'Message sent successfully!',
|
||||
components: [],
|
||||
embeds: []
|
||||
});
|
||||
collector.stop();
|
||||
} catch (error) {
|
||||
await i.reply({
|
||||
content: 'Failed to send message. Please check channel permissions.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle modal submissions
|
||||
interaction.client.on('interactionCreate', async (modal) => {
|
||||
if (!modal.isModalSubmit()) return;
|
||||
if (modal.user.id !== interaction.user.id) return;
|
||||
|
||||
switch (modal.customId) {
|
||||
case 'title_modal':
|
||||
messageData.title = modal.fields.getTextInputValue('title_input');
|
||||
break;
|
||||
case 'desc_modal':
|
||||
messageData.description = modal.fields.getTextInputValue('desc_input');
|
||||
break;
|
||||
case 'color_modal':
|
||||
messageData.color = parseColor(modal.fields.getTextInputValue('color_input'));
|
||||
break;
|
||||
case 'field_modal':
|
||||
const inlineValue = modal.fields.getTextInputValue('field_inline').toLowerCase();
|
||||
messageData.fields.push({
|
||||
name: modal.fields.getTextInputValue('field_name'),
|
||||
value: modal.fields.getTextInputValue('field_value'),
|
||||
inline: inlineValue === 'true'
|
||||
});
|
||||
break;
|
||||
case 'footer_modal':
|
||||
messageData.footer = modal.fields.getTextInputValue('footer_input');
|
||||
break;
|
||||
case 'image_modal':
|
||||
messageData.image = modal.fields.getTextInputValue('image_input');
|
||||
break;
|
||||
case 'thumbnail_modal':
|
||||
messageData.thumbnail = modal.fields.getTextInputValue('thumbnail_input');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (EmbedAuthor) {
|
||||
EmbedMsg.setAuthor({ name: EmbedAuthor, iconURL: EmbedAuthorIcon, url: EmbedAuthorURL });
|
||||
}
|
||||
await updatePreview(modal);
|
||||
});
|
||||
|
||||
if (EmbedTimestamp) {
|
||||
EmbedMsg.setTimestamp();
|
||||
}
|
||||
|
||||
interaction.reply({
|
||||
embeds: [EmbedMsg],
|
||||
});
|
||||
} else {
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
}
|
||||
},
|
||||
});
|
||||
collector.on('end', () => {
|
||||
interaction.editReply({
|
||||
content: 'Message editor timed out.',
|
||||
components: [],
|
||||
embeds: []
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
});
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
const fs = require("fs");
|
||||
const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = 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");
|
||||
|
||||
// Function to chunk array into smaller arrays
|
||||
function chunkArray(arr, size) {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
chunks.push(arr.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Mod.Command,
|
||||
description: cmdconfig.Mod.Command,
|
||||
type: ApplicationCommandType.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Command,
|
||||
description: cmdconfig.Mod.TicketBlacklist.Description,
|
||||
type: ApplicationCommandOptionType.SubcommandGroup,
|
||||
options: [
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Add.Command,
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the ticket blacklist",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.UserDescription || "User to blacklist",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: msgconfig.Mod.TicketBlacklist.Add.ReasonDescription || "Reason for blacklisting",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: cmdconfig.Mod.TicketBlacklist.Remove.Command,
|
||||
description: msgconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the ticket blacklist",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: msgconfig.Mod.TicketBlacklist.Remove.UserDescription || "User to remove from blacklist",
|
||||
type: ApplicationCommandOptionType.User,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: msgconfig.Mod.TicketBlacklist.Remove.ReasonDescription || "Reason for removing from blacklist",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "view",
|
||||
description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users",
|
||||
type: ApplicationCommandOptionType.Subcommand,
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
permissions: cmdconfig.Mod.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
const { getRole } = interaction.client;
|
||||
|
||||
let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild);
|
||||
|
||||
if (!SupportStaff || !Admin || !Moderator) {
|
||||
const missingRolesEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
return interaction.reply({ embeds: [missingRolesEmbed] });
|
||||
}
|
||||
|
||||
const NoPerms = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
if (!interaction.member.roles.cache.has(Admin.id) && !interaction.member.roles.cache.has(Moderator.id) && (!supportbot.Mod.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))) {
|
||||
return interaction.reply({ embeds: [NoPerms] });
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand(false);
|
||||
const user = interaction.options.getUser('user');
|
||||
const reason = interaction.options.getString('reason') || "No reason provided";
|
||||
|
||||
try {
|
||||
let blacklistedUsers;
|
||||
try {
|
||||
blacklistedUsers = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8")).blacklistedUsers;
|
||||
if (!Array.isArray(blacklistedUsers)) {
|
||||
blacklistedUsers = [];
|
||||
}
|
||||
} catch (error) {
|
||||
blacklistedUsers = [];
|
||||
}
|
||||
|
||||
if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) {
|
||||
if (blacklistedUsers.includes(user.id)) {
|
||||
const alreadyBlacklistedEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Add.AlreadyBlacklisted.replace("{userTag}", user.tag) || ":x: User is already blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
return interaction.reply({ embeds: [alreadyBlacklistedEmbed], ephemeral: true });
|
||||
}
|
||||
|
||||
blacklistedUsers.push(user.id);
|
||||
fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4));
|
||||
|
||||
const successEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Add.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
|
||||
if (blacklistChannel) {
|
||||
const blacklistLogEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
|
||||
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
|
||||
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
|
||||
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
|
||||
.addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false })
|
||||
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
|
||||
|
||||
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [successEmbed],
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
} else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) {
|
||||
if (!blacklistedUsers.includes(user.id)) {
|
||||
const notBlacklistedEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.NotBlacklisted.replace("{userTag}", user.tag) || ":x: User is not blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
return interaction.reply({ embeds: [notBlacklistedEmbed], ephemeral: true });
|
||||
}
|
||||
|
||||
blacklistedUsers = blacklistedUsers.filter(id => id !== user.id);
|
||||
fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4));
|
||||
|
||||
const removedEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.Remove.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been removed from the blacklist.")
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog);
|
||||
if (blacklistChannel) {
|
||||
const blacklistLogEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log")
|
||||
.setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success)
|
||||
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
|
||||
.setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`)
|
||||
.addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false })
|
||||
.addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false });
|
||||
|
||||
blacklistChannel.send({ embeds: [blacklistLogEmbed] });
|
||||
}
|
||||
|
||||
return interaction.reply({ embeds: [removedEmbed], ephemeral: true });
|
||||
|
||||
} else if (subcommand === "view") {
|
||||
if (blacklistedUsers.length === 0) {
|
||||
const noBlacklistedUsersEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Mod.TicketBlacklist.View.NoBlacklistedUsers || "No users are blacklisted.")
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
return interaction.reply({ embeds: [noBlacklistedUsersEmbed], ephemeral: true });
|
||||
}
|
||||
|
||||
const chunkedUsers = chunkArray(blacklistedUsers, 5);
|
||||
let currentPage = 0;
|
||||
|
||||
const createEmbed = (page) => {
|
||||
return new EmbedBuilder()
|
||||
.setTitle(msgconfig.Mod.TicketBlacklist.View.EmbedTitle || "Blacklisted Users")
|
||||
.setDescription(chunkedUsers[page].map(id => `<@${id}>`).join("\n"))
|
||||
.setColor(supportbot.Embed.Colours.Success)
|
||||
.setFooter({ text: `Page ${page + 1} of ${chunkedUsers.length}` });
|
||||
};
|
||||
|
||||
const buttons = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('previous')
|
||||
.setLabel('Previous')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(currentPage === 0),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('next')
|
||||
.setLabel('Next')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(currentPage === chunkedUsers.length - 1)
|
||||
);
|
||||
|
||||
const response = await interaction.reply({
|
||||
embeds: [createEmbed(currentPage)],
|
||||
components: chunkedUsers.length > 1 ? [buttons] : [],
|
||||
ephemeral: true
|
||||
});
|
||||
|
||||
if (chunkedUsers.length <= 1) return;
|
||||
|
||||
const collector = response.createMessageComponentCollector({
|
||||
componentType: ComponentType.Button,
|
||||
time: 60000
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
if (i.user.id !== interaction.user.id) {
|
||||
return i.reply({
|
||||
content: 'You cannot use these buttons.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
if (i.customId === 'previous' && currentPage > 0) {
|
||||
currentPage--;
|
||||
} else if (i.customId === 'next' && currentPage < chunkedUsers.length - 1) {
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
buttons.components[0].setDisabled(currentPage === 0);
|
||||
buttons.components[1].setDisabled(currentPage === chunkedUsers.length - 1);
|
||||
|
||||
await i.update({
|
||||
embeds: [createEmbed(currentPage)],
|
||||
components: [buttons]
|
||||
});
|
||||
});
|
||||
|
||||
collector.on('end', () => {
|
||||
buttons.components.forEach(button => button.setDisabled(true));
|
||||
interaction.editReply({
|
||||
components: [buttons]
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in mod command:", error);
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setDescription(msgconfig.Error.ActionFailed || "An error occurred while processing your request.")
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
interaction.reply({ embeds: [errorEmbed], ephemeral: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
+89
-154
@@ -10,10 +10,6 @@ const Command = require("../Structures/Command.js");
|
||||
|
||||
const clockedInUsers = new Set();
|
||||
|
||||
// async function getRole(roleName, guild) {
|
||||
// return guild.roles.cache.find(role => role.name === roleName);
|
||||
// }
|
||||
|
||||
function updateClockedInStatus(profileEmbed, clockedIn) {
|
||||
profileEmbed.data.fields = profileEmbed.data.fields.map(field => {
|
||||
if (field.name === 'Clocked In Status') {
|
||||
@@ -24,6 +20,35 @@ function updateClockedInStatus(profileEmbed, clockedIn) {
|
||||
return profileEmbed;
|
||||
}
|
||||
|
||||
function formatSchedule(schedule) {
|
||||
if (!schedule || Object.keys(schedule).length === 0) return 'No schedule set';
|
||||
|
||||
const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
||||
return days
|
||||
.filter(day => schedule[day])
|
||||
.map(day => `${day.charAt(0).toUpperCase() + day.slice(1)}: ${schedule[day].start} - ${schedule[day].end}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function calculateWeeklyHours(schedule) {
|
||||
if (!schedule) return 0;
|
||||
|
||||
let totalMinutes = 0;
|
||||
Object.values(schedule).forEach(day => {
|
||||
if (day && day.start && day.end) {
|
||||
const [startHour, startMin] = day.start.split(':').map(Number);
|
||||
const [endHour, endMin] = day.end.split(':').map(Number);
|
||||
|
||||
const startMinutes = startHour * 60 + startMin;
|
||||
const endMinutes = endHour * 60 + endMin;
|
||||
|
||||
totalMinutes += endMinutes - startMinutes;
|
||||
}
|
||||
});
|
||||
|
||||
return Math.round(totalMinutes / 60 * 10) / 10; // Round to 1 decimal place
|
||||
}
|
||||
|
||||
module.exports = new Command({
|
||||
name: cmdconfig.Profile.Command,
|
||||
description: cmdconfig.Profile.Description,
|
||||
@@ -39,7 +64,7 @@ module.exports = new Command({
|
||||
],
|
||||
|
||||
async run(interaction) {
|
||||
await interaction.deferReply({ ephemeral: true }); // Acknowledge the interaction immediately
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const userOption = interaction.options.getUser('user');
|
||||
const viewingUser = userOption || interaction.user;
|
||||
@@ -47,7 +72,7 @@ module.exports = new Command({
|
||||
const interactionUserId = interaction.user.id;
|
||||
|
||||
const user = interaction.guild.members.cache.get(viewingUserId);
|
||||
const { getRole, getChannel } = interaction.client;
|
||||
const { getRole } = interaction.client;
|
||||
|
||||
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
|
||||
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
|
||||
@@ -71,12 +96,14 @@ module.exports = new Command({
|
||||
profileData = {
|
||||
bio: "",
|
||||
timezone: "",
|
||||
clockedIn: false
|
||||
clockedIn: false,
|
||||
schedule: {}
|
||||
};
|
||||
fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2));
|
||||
}
|
||||
|
||||
const { bio, timezone, clockedIn } = profileData;
|
||||
const { bio, timezone, clockedIn, schedule } = profileData;
|
||||
const weeklyHours = calculateWeeklyHours(schedule);
|
||||
|
||||
let profileEmbed = new EmbedBuilder()
|
||||
.setTitle(`${viewingUser.username}'s Profile`)
|
||||
@@ -88,7 +115,11 @@ module.exports = new Command({
|
||||
);
|
||||
|
||||
if (isStaff) {
|
||||
profileEmbed.addFields({ name: 'Clocked In Status', value: clockedIn ? '✅ Clocked In' : '❌ Clocked Out', inline: true });
|
||||
profileEmbed.addFields(
|
||||
{ name: 'Clocked In Status', value: clockedIn ? '✅ Clocked In' : '❌ Clocked Out', inline: true },
|
||||
{ name: 'Weekly Hours', value: `${weeklyHours} hours`, inline: true }, // Now inline with Schedule
|
||||
{ name: 'Schedule', value: formatSchedule(schedule), inline: true }
|
||||
);
|
||||
}
|
||||
|
||||
const buttonRow = new ActionRowBuilder();
|
||||
@@ -105,6 +136,10 @@ module.exports = new Command({
|
||||
new ButtonBuilder()
|
||||
.setCustomId('clockInOut')
|
||||
.setLabel(clockedIn ? 'Clock Out' : 'Clock In')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('editSchedule')
|
||||
.setLabel('Edit Schedule')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
}
|
||||
@@ -122,7 +157,7 @@ module.exports = new Command({
|
||||
await interaction.editReply({ embeds: [profileEmbed], components: buttonRow.components.length > 0 ? [buttonRow] : [] });
|
||||
|
||||
const filter = i => (
|
||||
['editProfile', 'clockInOut', 'viewTicketStats', 'showOpenTickets', 'backToStats', 'backToProfile'].includes(i.customId)
|
||||
['editProfile', 'clockInOut', 'viewTicketStats', 'editSchedule'].includes(i.customId)
|
||||
&& i.user.id === interactionUserId
|
||||
);
|
||||
|
||||
@@ -139,8 +174,7 @@ module.exports = new Command({
|
||||
.setCustomId('bio')
|
||||
.setLabel('Bio')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setPlaceholder('Enter your bio')
|
||||
.setValue(String(bio))
|
||||
.setValue(profileData.bio || '')
|
||||
.setRequired(false)
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
@@ -148,8 +182,7 @@ module.exports = new Command({
|
||||
.setCustomId('timezone')
|
||||
.setLabel('Timezone')
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setPlaceholder('Enter your timezone')
|
||||
.setValue(String(timezone))
|
||||
.setValue(profileData.timezone || '')
|
||||
.setRequired(false)
|
||||
)
|
||||
);
|
||||
@@ -160,7 +193,6 @@ module.exports = new Command({
|
||||
if (i.customId === 'clockInOut') {
|
||||
profileData.clockedIn = !profileData.clockedIn;
|
||||
fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2));
|
||||
profileEmbed = updateClockedInStatus(profileEmbed, profileData.clockedIn);
|
||||
|
||||
if (profileData.clockedIn) {
|
||||
clockedInUsers.add(viewingUserId);
|
||||
@@ -168,157 +200,60 @@ module.exports = new Command({
|
||||
clockedInUsers.delete(viewingUserId);
|
||||
}
|
||||
|
||||
const updatedButtonRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('editProfile')
|
||||
.setLabel('Edit Profile')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
profileEmbed = updateClockedInStatus(profileEmbed, profileData.clockedIn);
|
||||
const updatedRow = new ActionRowBuilder().addComponents(
|
||||
...buttonRow.components.map(button => {
|
||||
if (button.data.custom_id === 'clockInOut') {
|
||||
return new ButtonBuilder()
|
||||
.setCustomId('clockInOut')
|
||||
.setLabel(profileData.clockedIn ? 'Clock Out' : 'Clock In')
|
||||
.setStyle(ButtonStyle.Secondary);
|
||||
}
|
||||
return ButtonBuilder.from(button.data);
|
||||
})
|
||||
);
|
||||
|
||||
if (isStaff) {
|
||||
updatedButtonRow.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('clockInOut')
|
||||
.setLabel(profileData.clockedIn ? 'Clock Out' : 'Clock In')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
if (supportbot.Ticket.ClaimTickets.Enabled) {
|
||||
updatedButtonRow.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('viewTicketStats')
|
||||
.setLabel('View Ticket Stats')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await i.update({ embeds: [profileEmbed], components: [updatedButtonRow] });
|
||||
|
||||
let clockedinout = new EmbedBuilder()
|
||||
.setTitle(profileData.clockedIn ? 'Clocked In!' : 'Clocked Out!')
|
||||
.setDescription(profileData.clockedIn ? 'You have successfully clocked in.' : 'You have successfully clocked out.')
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
|
||||
await interaction.followUp({ embeds: [clockedinout], ephemeral: true });
|
||||
await i.update({ embeds: [profileEmbed], components: [updatedRow] });
|
||||
}
|
||||
|
||||
if (i.customId === 'viewTicketStats') {
|
||||
const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId);
|
||||
const ticketsOpen = ticketsClaimed.filter(t => t.open);
|
||||
const totalTickets = ticketsClaimed.length;
|
||||
|
||||
let totalResponseTime = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
ticketsClaimed.forEach(ticket => {
|
||||
if (ticket.claimedAt && ticket.createdAt) {
|
||||
const responseTime = new Date(ticket.claimedAt) - new Date(ticket.createdAt);
|
||||
totalResponseTime += responseTime;
|
||||
responseCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const averageResponseTime = responseCount ? totalResponseTime / responseCount : 0;
|
||||
const averageResponseTimeMinutes = Math.round(averageResponseTime / 60000);
|
||||
|
||||
const statsEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketStats.Title)
|
||||
.setDescription(`> Ticket stats for <@${viewingUserId}>`)
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.addFields(
|
||||
{ name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true },
|
||||
{ name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true },
|
||||
{ name: msgconfig.TicketStats.ResponseTime, value: `${averageResponseTimeMinutes} minutes`, inline: false }
|
||||
);
|
||||
|
||||
const statsButtonRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('showOpenTickets')
|
||||
.setLabel('Show Open Tickets')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('backToProfile')
|
||||
.setLabel('Back to Profile')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
await i.update({ embeds: [statsEmbed], components: [statsButtonRow] });
|
||||
}
|
||||
|
||||
if (i.customId === 'showOpenTickets') {
|
||||
const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
const ticketsOpen = TicketData.tickets.filter(t => t.claimedBy === viewingUserId && t.open);
|
||||
|
||||
const openTicketsEmbed = new EmbedBuilder()
|
||||
.setTitle('Active Tickets')
|
||||
.setDescription(ticketsOpen.map(t => `<#${t.id}>`).join('\n') || 'No active tickets.')
|
||||
// Handle ticket stats viewing here
|
||||
const ticketStatsEmbed = new EmbedBuilder()
|
||||
.setTitle('Ticket Stats')
|
||||
.setDescription('Here are your ticket stats...')
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
const openTicketsButtonRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('backToStats')
|
||||
.setLabel('Back to Stats')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
await i.update({ embeds: [openTicketsEmbed], components: [openTicketsButtonRow] });
|
||||
await i.update({ embeds: [ticketStatsEmbed], components: [] });
|
||||
}
|
||||
|
||||
if (i.customId === 'backToStats') {
|
||||
const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId);
|
||||
const ticketsOpen = ticketsClaimed.filter(t => t.open);
|
||||
const totalTickets = ticketsClaimed.length;
|
||||
if (i.customId === 'editSchedule') {
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId('editScheduleModal')
|
||||
.setTitle('Edit Schedule');
|
||||
|
||||
let totalResponseTime = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
ticketsClaimed.forEach(ticket => {
|
||||
if (ticket.claimedAt && ticket.createdAt) {
|
||||
const responseTime = new Date(ticket.claimedAt) - new Date(ticket.createdAt);
|
||||
totalResponseTime += responseTime;
|
||||
responseCount++;
|
||||
}
|
||||
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'].forEach(day => {
|
||||
modal.addComponents(
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId(`${day.toLowerCase()}Start`)
|
||||
.setLabel(`${day} Start Time (HH:mm)`)
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(false)
|
||||
.setValue(schedule[day.toLowerCase()]?.start || '')
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId(`${day.toLowerCase()}End`)
|
||||
.setLabel(`${day} End Time (HH:mm)`)
|
||||
.setStyle(TextInputStyle.Short)
|
||||
.setRequired(false)
|
||||
.setValue(schedule[day.toLowerCase()]?.end || '')
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const averageResponseTime = responseCount ? totalResponseTime / responseCount : 0;
|
||||
const averageResponseTimeMinutes = Math.round(averageResponseTime / 60000);
|
||||
|
||||
const statsEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.TicketStats.Title)
|
||||
.setDescription(`> Ticket stats for <@${viewingUserId}>`)
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.addFields(
|
||||
{ name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true },
|
||||
{ name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true },
|
||||
{ name: msgconfig.TicketStats.ResponseTime, value: `${averageResponseTimeMinutes} minutes`, inline: false }
|
||||
);
|
||||
|
||||
const statsButtonRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('showOpenTickets')
|
||||
.setLabel('Show Open Tickets')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('backToProfile')
|
||||
.setLabel('Back to Profile')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
await i.update({ embeds: [statsEmbed], components: [statsButtonRow] });
|
||||
}
|
||||
|
||||
if (i.customId === 'backToProfile') {
|
||||
await i.update({ embeds: [profileEmbed], components: buttonRow.components.length > 0 ? [buttonRow] : [] });
|
||||
await i.showModal(modal);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.clockedInUsers = clockedInUsers;
|
||||
+95
-1
@@ -9,6 +9,7 @@ const {
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder,
|
||||
ButtonStyle,
|
||||
MessageCollector,
|
||||
} = require("discord.js");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
@@ -20,6 +21,23 @@ const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||
const Command = require("../Structures/Command.js");
|
||||
const TicketNumberID = require("../Structures/TicketID.js");
|
||||
|
||||
async function isBlacklisted(userId) {
|
||||
let blacklistData;
|
||||
try {
|
||||
blacklistData = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8"));
|
||||
} catch (error) {
|
||||
console.error("Error reading BlacklistedUsers.json:", error);
|
||||
return false; // Assume not blacklisted if there's an error
|
||||
}
|
||||
|
||||
if (!blacklistData || !Array.isArray(blacklistData.blacklistedUsers)) {
|
||||
console.error("blacklistedUsers is not defined or is not an array");
|
||||
return false;
|
||||
}
|
||||
|
||||
return blacklistData.blacklistedUsers.includes(userId);
|
||||
}
|
||||
|
||||
async function getClockedInUsers(guild) {
|
||||
const clockedInUsers = new Set();
|
||||
|
||||
@@ -37,6 +55,60 @@ async function getClockedInUsers(guild) {
|
||||
return clockedInUsers;
|
||||
}
|
||||
|
||||
async function askTicketQuestions(ticketChannel, interaction, questions) {
|
||||
const answers = [];
|
||||
let questionIndex = 0;
|
||||
|
||||
const sendNextQuestion = async () => {
|
||||
if (questionIndex < questions.length) {
|
||||
|
||||
const questionsEmbed = new EmbedBuilder()
|
||||
.setDescription(`**${questionIndex + 1}.** ${questions[questionIndex]}`)
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
|
||||
const questionMessage = await ticketChannel.send({
|
||||
embeds: [questionsEmbed],
|
||||
});
|
||||
|
||||
const collector = new MessageCollector(ticketChannel, {
|
||||
filter: (m) => m.author.id === interaction.user.id,
|
||||
max: 1,
|
||||
time: 60000,
|
||||
});
|
||||
|
||||
collector.on("collect", async (message) => {
|
||||
answers.push({ question: questions[questionIndex], answer: message.content });
|
||||
questionIndex++;
|
||||
await message.delete();
|
||||
await questionMessage.delete();
|
||||
sendNextQuestion();
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
const summaryEmbed = new EmbedBuilder()
|
||||
.setTitle(msgconfig.Ticket.TicketQuestions.Details.Title)
|
||||
.setDescription(msgconfig.Ticket.TicketQuestions.Details.Description.replace('%user%', interaction.user.id))
|
||||
.setColor(supportbot.Embed.Colours.General)
|
||||
.addFields(
|
||||
answers.map((a) => ({
|
||||
name: `Q: ${a.question}`,
|
||||
value: `A: ${a.answer}`,
|
||||
inline: false,
|
||||
}))
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await ticketChannel.send({
|
||||
embeds: [summaryEmbed]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Start asking the first question
|
||||
sendNextQuestion();
|
||||
}
|
||||
|
||||
async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts = 0) {
|
||||
|
||||
try {
|
||||
@@ -205,7 +277,16 @@ module.exports = new Command({
|
||||
permissions: cmdconfig.OpenTicket.Permission,
|
||||
|
||||
async run(interaction) {
|
||||
|
||||
try {
|
||||
|
||||
if (await isBlacklisted(interaction.user.id)) {
|
||||
return interaction.reply({
|
||||
content: msgconfig.Ticket.Blacklisted,
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
let TicketReason = null;
|
||||
if (supportbot.Ticket.TicketReason) {
|
||||
TicketReason = interaction.reason || null; // Retrieve the reason from the interaction object
|
||||
@@ -352,6 +433,14 @@ module.exports = new Command({
|
||||
});
|
||||
}
|
||||
|
||||
// Ask questions to the user after the ticket is opened
|
||||
if (supportbot.Ticket.Questions.Enabled) {
|
||||
const questions = supportbot.Ticket.Questions.List || [];
|
||||
if (questions.length > 0) {
|
||||
await askTicketQuestions(ticketChannel, interaction, questions);
|
||||
}
|
||||
}
|
||||
|
||||
// Save ticket data
|
||||
TicketData.tickets.push({
|
||||
id: ticketChannel.id,
|
||||
@@ -405,7 +494,6 @@ module.exports = new Command({
|
||||
)
|
||||
.setColor(supportbot.Embed.Colours.General);
|
||||
|
||||
// Add reason field if ticket reason is enabled
|
||||
if (supportbot.Ticket.TicketReason && TicketReason) {
|
||||
TicketMessage.addFields(
|
||||
{ name: "Reason", value: TicketReason, inline: false }
|
||||
@@ -422,6 +510,11 @@ module.exports = new Command({
|
||||
.setDescription("Close the ticket.")
|
||||
.setEmoji(supportbot.SelectMenus.Tickets.CloseEmoji)
|
||||
.setValue("ticketclose"),
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Voice Channel")
|
||||
.setDescription("Create a support voice channel.")
|
||||
.setEmoji(supportbot.SelectMenus.Tickets.VCEmoji)
|
||||
.setValue("supportvc"),
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Archive")
|
||||
.setDescription("Archive the ticket.")
|
||||
@@ -463,6 +556,7 @@ module.exports = new Command({
|
||||
components: [row2],
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in run method:", error);
|
||||
}
|
||||
|
||||
+30
-6
@@ -7,13 +7,11 @@
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://dsc.gg/emerald-dev
|
||||
# Official Documentation: https://emeraldsrv.dev/wiki
|
||||
# Third-Party Documentation: https://emeraldsrv.dev/third-party
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Command Configuration
|
||||
|
||||
# If you don't want to mess with this configuration, You can manage commands via Discord Settings the Intregations.
|
||||
# Commands Configuration (commands.yml)
|
||||
|
||||
Help:
|
||||
Command: "help"
|
||||
@@ -99,6 +97,32 @@ Ping:
|
||||
Permission: ["SendMessages"]
|
||||
Enabled: true
|
||||
|
||||
# ---------------------------------------
|
||||
# Bot Moderator Commands
|
||||
# ---------------------------------------
|
||||
|
||||
Mod:
|
||||
Command: "mod"
|
||||
Description: "Execute a moderation action."
|
||||
Permission: ["BanMembers", "KickMembers"]
|
||||
Enabled: true
|
||||
|
||||
TicketBlacklist:
|
||||
Command: "ticket-blacklist"
|
||||
Description: "Blacklist a user from creating Tickets."
|
||||
|
||||
Add:
|
||||
Command: "add"
|
||||
Description: "Add a user to the blacklist."
|
||||
|
||||
Remove:
|
||||
Command: "remove"
|
||||
Description: "Remove a user from the blacklist."
|
||||
|
||||
View:
|
||||
Command: "view"
|
||||
Description: "View all blacklisted users."
|
||||
|
||||
# ---------------------------------------
|
||||
# Bot Administrator Commands
|
||||
# ---------------------------------------
|
||||
|
||||
+101
-9
@@ -7,11 +7,15 @@
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://dsc.gg/emerald-dev
|
||||
# Official Documentation: https://emeraldsrv.com/wiki
|
||||
# Third-Party Documentation: https://emeraldsrv.com/third-party
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Messages Configuration
|
||||
# Messages Configuration (messages.yml)
|
||||
|
||||
# ----------------------------
|
||||
# Info Command
|
||||
# ----------------------------
|
||||
|
||||
Info:
|
||||
Title: "About Us"
|
||||
@@ -20,6 +24,10 @@ Info:
|
||||
URL: "https://docs.emeraldsrv.com"
|
||||
Colour: "#f74343"
|
||||
|
||||
# ----------------------------
|
||||
# Welcome/Leave Messages
|
||||
# ----------------------------
|
||||
|
||||
Welcome:
|
||||
Embed:
|
||||
Colour: "#f74343"
|
||||
@@ -40,12 +48,20 @@ Leave:
|
||||
ImageEnabled: false
|
||||
ImageURL: "https://animagelink.png/"
|
||||
|
||||
# ----------------------------
|
||||
# Suggestion Messages
|
||||
# ----------------------------
|
||||
|
||||
Suggestions:
|
||||
Sent_Title: "Suggestion Sent!"
|
||||
Sent: ":white_check_mark: **You have successfully sent a suggestion.**"
|
||||
NoSelfVote: ":x: **You cannot vote on your own suggestion.**"
|
||||
RemoveVoteButton: "Remove Vote"
|
||||
|
||||
# ----------------------------
|
||||
# Ticket Messages
|
||||
# ----------------------------
|
||||
|
||||
Ticket:
|
||||
|
||||
# Available Placeholders:
|
||||
@@ -95,6 +111,44 @@ Ticket:
|
||||
ReassigningTitle: "Reassigning!..."
|
||||
ReassigningMessage: "<@%user%> did not claim the ticket. **Reassigning...**"
|
||||
|
||||
TicketQuestions:
|
||||
Details:
|
||||
Title: "Ticket Details"
|
||||
Description: "Questions by <@%user%>" # Use <@%user%> to display the username.
|
||||
|
||||
# ----------------------------
|
||||
# Review Messages
|
||||
# ----------------------------
|
||||
|
||||
ReviewSystem:
|
||||
Rate:
|
||||
Title: "Review us!"
|
||||
Description: "Please rate our support for this ticket (1-5 stars)."
|
||||
|
||||
Stars:
|
||||
One: "⭐ 1 Star"
|
||||
Two: "⭐⭐ 2 Stars"
|
||||
Three: "⭐⭐⭐ 3 Stars"
|
||||
Four: "⭐⭐⭐⭐ 4 Stars"
|
||||
Five: "⭐⭐⭐⭐⭐ 5 Stars"
|
||||
|
||||
Comment:
|
||||
Title: "Write a comment."
|
||||
Description: "Write a comment about your experience."
|
||||
|
||||
ReviewEmbed:
|
||||
Title: "Ticket Review"
|
||||
CommentTitle: "Comment"
|
||||
RatingTitle: "Rating"
|
||||
ReviewedStaffTitle: "Reviewed Staff:"
|
||||
ReviewedByTitle: "Reviewed By:"
|
||||
ReviewEmoji: "⭐"
|
||||
Color: "#f7a943"
|
||||
|
||||
# ----------------------------
|
||||
# Profile Messages
|
||||
# ----------------------------
|
||||
|
||||
TicketStats:
|
||||
OpenTickets: "✅ Open Tickets"
|
||||
TotalTickets: "🎫 Total Tickets"
|
||||
@@ -102,10 +156,46 @@ TicketStats:
|
||||
|
||||
Title: "Your Ticket Stats"
|
||||
|
||||
# ----------------------------
|
||||
# Mod Command Messages
|
||||
# ----------------------------
|
||||
|
||||
Mod:
|
||||
|
||||
TicketBlacklist:
|
||||
Add:
|
||||
Description: "Add a user to the ticket blacklist"
|
||||
UserDescription: "User to blacklist"
|
||||
AlreadyBlacklisted: ":x: {userTag} is already blacklisted."
|
||||
Success: ":white_check_mark: {userTag} has been added to the blacklist. They can no longer use the ticket system."
|
||||
|
||||
Remove:
|
||||
Description: "Remove a user from the ticket blacklist"
|
||||
UserDescription: "User to remove from blacklist"
|
||||
NotBlacklisted: ":x: {userTag} is not blacklisted."
|
||||
Success: ":white_check_mark: {userTag} has been removed from the blacklist. They can now use the ticket system again."
|
||||
|
||||
View:
|
||||
Description: "View the list of all blacklisted users"
|
||||
NoBlacklistedUsers: ":information_source: There are currently no users on the blacklist."
|
||||
|
||||
|
||||
# ----------------------------
|
||||
# Logging Messages
|
||||
# ----------------------------
|
||||
|
||||
TicketLog:
|
||||
Title: "Ticket Log"
|
||||
Colour: "#0cc559"
|
||||
|
||||
TicketBlacklistLog:
|
||||
Title: "Ticket Blacklist Log"
|
||||
Colour: "#0cc559"
|
||||
|
||||
# ----------------------------
|
||||
# Misc Messages
|
||||
# ----------------------------
|
||||
|
||||
AddUser:
|
||||
NotInTicket_Title: "Ticket Invitation"
|
||||
NotInTicket_Description: "This command can only be used within a ticket thread."
|
||||
@@ -171,11 +261,13 @@ ForceAddUser:
|
||||
Error_Adding: "There was an error adding the user to the ticket."
|
||||
|
||||
Error:
|
||||
IncorrectPerms: ":x: **Err!** You do not have the correct permissions to use this."
|
||||
InvalidChannel: ":x: **Err!** Channel not found. Please create it!"
|
||||
UserNotFound: ":x: **Err!** This user doesn't exist, Are they in this server?"
|
||||
NoValidTicket: ":warning: You cannot execute this command here."
|
||||
TicketExists: ":warning: This ticket cannot be opened, It already exists!"
|
||||
IncorrectPermissions: ":x: **Error!** You do not have the correct permissions to use this command."
|
||||
MissingChannel: ":x: **Error!** Required channel not found. Please ensure it is set up correctly!"
|
||||
UserNotFound: ":x: **Error!** Specified user not found. Please check if they are in this server."
|
||||
CommandNotAllowedHere: ":warning: This command cannot be used in this channel."
|
||||
EntryAlreadyExists: ":warning: Cannot complete the action, as the entry already exists."
|
||||
EntryDoesNotExist: ":warning: Cannot complete the action, as the entry does not exist."
|
||||
ActionFailed: ":x: **Error!** An error occurred while processing your request. Please try again later."
|
||||
|
||||
|
||||
|
||||
|
||||
+88
-41
@@ -4,15 +4,16 @@
|
||||
# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_|
|
||||
# |_| |_|
|
||||
#
|
||||
# SupportBot created by Emerald Development
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://dsc.gg/emerald-dev
|
||||
# Official Documentation: https://github.com/Emerald-Services/SupportBot/wiki
|
||||
# Third-Party Documentation: COMING SOON
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# SupportBot Configuration (supportbot.yml)
|
||||
|
||||
SupportBot_Version: "v8.2"
|
||||
# Do not change the version number
|
||||
SupportBot_Version: "v8.3"
|
||||
|
||||
# -------------------------
|
||||
# General
|
||||
@@ -20,8 +21,7 @@ SupportBot_Version: "v8.2"
|
||||
|
||||
General:
|
||||
Name: "SupportBot"
|
||||
Token: "BOT_TOKEN_HERE"
|
||||
# To get your Discord Bot Token, Go to https://discord.com/developers/applications
|
||||
Token: "BOT_TOKEN"
|
||||
|
||||
Addons:
|
||||
Enabled: true
|
||||
@@ -33,11 +33,16 @@ General:
|
||||
Activity:
|
||||
Status: "SupportBot v8"
|
||||
Type: "Competing"
|
||||
# The Type can either be Playing, Watching, Listening or Competing
|
||||
StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME"
|
||||
# Set The Type to "Streaming" for a streaming status!
|
||||
|
||||
# You can update the activity via Discord using: /settings
|
||||
# Available Types:
|
||||
# Playing
|
||||
# Watching
|
||||
# Listening
|
||||
# Competing
|
||||
# Streaming (URL required)
|
||||
|
||||
# You can also update the activity via Discord using: /settings
|
||||
|
||||
# -------------------------
|
||||
# Embeds
|
||||
@@ -45,13 +50,15 @@ Activity:
|
||||
|
||||
Embed:
|
||||
Colours:
|
||||
General: 0x0cc559 # 0x<hexcode>
|
||||
Success: 0x04b545 # 0x<hexcode>
|
||||
Error: 0xd13030 # 0x<hexcode>
|
||||
Warn: 0xfca117 # 0x<hexcode>
|
||||
General: 0x0cc559
|
||||
Success: 0x04b545
|
||||
Error: 0xd13030
|
||||
Warn: 0xfca117
|
||||
|
||||
Footer: "SupportBot | Developed by Emerald Development"
|
||||
|
||||
# Use 0x<hexcode> to change the colour of the embeds.
|
||||
|
||||
# -------------------------
|
||||
# Roles
|
||||
# -------------------------
|
||||
@@ -62,12 +69,14 @@ Roles:
|
||||
Role: "ROLE_ID"
|
||||
|
||||
StaffMember:
|
||||
Staff: "STAFF_ROLE_ID"
|
||||
Admin: "ADMIN_ROLE_ID"
|
||||
Staff: "ROLE_ID"
|
||||
Admin: "ROLE_ID"
|
||||
Moderator: "ROLE_ID"
|
||||
|
||||
ModRoles:
|
||||
Muted: "MUTED_ROLE_ID"
|
||||
Blacklisted: "BLACKLISTED_ROLE_ID"
|
||||
Mod:
|
||||
AllowSupportStaff: false
|
||||
|
||||
# Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml)
|
||||
|
||||
# ----------------------------
|
||||
# System Messages
|
||||
@@ -75,18 +84,18 @@ Roles:
|
||||
|
||||
Welcome:
|
||||
Enabled: true
|
||||
Channel: "WELCOME_CHANNEL_ID"
|
||||
Channel: "JOIN_CHANNEL_ID"
|
||||
|
||||
Leave:
|
||||
Enabled: true
|
||||
Channel: "LEAVE_CHANNEL_ID"
|
||||
Channel: "JOIN_CHANNEL_ID"
|
||||
|
||||
# ----------------------------
|
||||
# Suggestions
|
||||
# ----------------------------
|
||||
|
||||
Suggestions:
|
||||
Channel: "SUGGEST_CHANNEL_ID"
|
||||
Channel: "SUGGESTIONS_CHANNEL_ID"
|
||||
UpvoteEmoji: "✅"
|
||||
DownvoteEmoji: "❌"
|
||||
|
||||
@@ -94,6 +103,7 @@ Suggestions:
|
||||
DownvoteTitle: "No"
|
||||
|
||||
OwnSuggestion: true
|
||||
|
||||
# If false this will not allow the user to vote for a suggestion they created
|
||||
# If true this will allow the user to vote for a suggestion they created
|
||||
|
||||
@@ -120,23 +130,27 @@ Suggestions:
|
||||
# Tickets
|
||||
# -------------------------
|
||||
|
||||
Ticket:
|
||||
TicketType: "channels" # Choose how you want tickets to be created.
|
||||
# Options:
|
||||
# channels - Tickets will be created as channels in the
|
||||
# threads - Tickets will be created as threads through the "TicketHome"
|
||||
VoiceTickets:
|
||||
Name: "%username%'s VC"
|
||||
Category: "VOICEE_CATEGORY_ID"
|
||||
|
||||
Ticket:
|
||||
TicketType: "channels"
|
||||
|
||||
# Available TicketTypes:
|
||||
# channels - Tickets will be created as channels in the TicketChannelsCategory
|
||||
# threads - Tickets will be created as threads through the "TicketHome" channel.
|
||||
|
||||
# For TicketType "Threads"
|
||||
TicketHome: "TICKETHOME_CHANNEL_ID"
|
||||
|
||||
# What is TicketHome?
|
||||
# - Channel where the Ticket creation panel is sent.
|
||||
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
|
||||
|
||||
# For TicketType "Channels"
|
||||
TicketChannelsCategory: "TICKET_CATEGORY_ID" # if Type is set to channels
|
||||
TicketChannelsCategory2: "BACKUP_TICKET_CATEGORY_ID" # This will be used if the first category has reached the max channel limit.
|
||||
TicketArchiveCategory: "ARCHIVE_TICKET_CATEGORY_ID"
|
||||
|
||||
# For TicketType: channels
|
||||
TicketChannelsCategory: "TICKETCHANNEL_CATEGORY_ID"
|
||||
TicketChannelsCategory2: "TICKETCHANNEL_BACKUP_CATEGORY_ID" # This will be used if the first category has reached the max channel limit.
|
||||
TicketArchiveCategory: "ARCHIVE_CATEGORY_ID"
|
||||
# ---
|
||||
|
||||
Channel: "ticket-" # The prefix for ticket channel names
|
||||
@@ -144,16 +158,16 @@ Ticket:
|
||||
DMTranscripts: true
|
||||
TicketReason: true
|
||||
# If ticket reason is set to false, It will not prompt to create a reason
|
||||
|
||||
TicketSubject: "description"
|
||||
|
||||
# Available TicketSubjects:
|
||||
# "description" will display the ticket reason within the channel description
|
||||
# "embed" will display the ticket reason within the ticket embed
|
||||
|
||||
Timeout: 5
|
||||
# Set to true to allow all staff to access the ticket
|
||||
|
||||
Log:
|
||||
TicketDataLog: "TICKET_LOG_ID"
|
||||
TickeDataTitle: "Ticket Data"
|
||||
|
||||
Close:
|
||||
StaffOnly: true
|
||||
# Set to false is anyone is able to close a ticket.
|
||||
@@ -164,7 +178,7 @@ Ticket:
|
||||
|
||||
Title: "Are you sure you want to close this ticket?"
|
||||
|
||||
# Available Button Styles
|
||||
# Available Button Styles:
|
||||
# 1 - Blurple
|
||||
# 2 - Grey
|
||||
# 3 - Green
|
||||
@@ -176,7 +190,7 @@ Ticket:
|
||||
ClaimTickets:
|
||||
Enabled: true
|
||||
Channel: "CLAIM_TICKETS_CHANNEL_ID"
|
||||
# Channel name or Channel ID
|
||||
# Channel where the ticket assignments are sent for staff to claim.
|
||||
|
||||
ClaimAcceptTime: 120000
|
||||
# How long the assigned user has to accept the ticket before it gets re-assigned to someone else.
|
||||
@@ -196,10 +210,34 @@ Ticket:
|
||||
# 3 - Green
|
||||
# 4 - Red
|
||||
|
||||
ReviewSystem:
|
||||
Enabled: true
|
||||
Channel: "REVIEW_CHANNEL_ID"
|
||||
UseModal: true
|
||||
|
||||
Questions:
|
||||
Enabled: true # Set to false to disable ticket questions
|
||||
List:
|
||||
- "Please provide your account email?"
|
||||
- "What is the nature of your issue?"
|
||||
- "Have you tried resolving the issue yourself?"
|
||||
|
||||
Log:
|
||||
TicketDataLog: "TICKET_DATA_LOG_ID"
|
||||
TickeDataTitle: "Ticket Data"
|
||||
TicketBlacklistLog: "TICKET_BLACKLIST_LOG_ID"
|
||||
|
||||
# -------------------------
|
||||
# Buttons
|
||||
# -------------------------
|
||||
Buttons:
|
||||
General:
|
||||
Delete: "🗑️"
|
||||
DeleteStyle: "4"
|
||||
|
||||
Voice:
|
||||
TicketDeleteText: "Delete Voice Channel"
|
||||
|
||||
Tickets:
|
||||
ClaimEmoji: "🧑💻"
|
||||
ClaimStyle: "1"
|
||||
@@ -230,13 +268,22 @@ SelectMenus:
|
||||
ArchiveEmoji: "♻️"
|
||||
LockEmoji: "🔐"
|
||||
RenameEmoji: "🏷️"
|
||||
VCEmoji: "🔊"
|
||||
|
||||
EnableInvitesEmoji: "📤"
|
||||
DisableInvitesEmoji: "📥"
|
||||
|
||||
# -------------------------
|
||||
# Misc
|
||||
# Logging
|
||||
# -------------------------
|
||||
|
||||
MessageDelete:
|
||||
Channel: "MESSAGE_DELETE_CHANNEL_ID""
|
||||
Colour: "#d92525"
|
||||
|
||||
MessageUpdate:
|
||||
Channel: "MESSAGE_UPDATE_CHANNEL_ID"
|
||||
Colour: "#f2a024"
|
||||
|
||||
Translate:
|
||||
TranslateLog: "TRANSLATE_LOG_ID"
|
||||
@@ -7,8 +7,11 @@
|
||||
# SupportBot created by Emerald Services
|
||||
# Installed with MIT License
|
||||
#
|
||||
# Discord Support: https://emeraldsrv.dev/discord
|
||||
# Community Resources: https://emeraldsrv.dev/resources
|
||||
# Discord Support: https://discord.gg/emeraldsrv
|
||||
# Official Documentation: https://docs.emeraldsrv.com
|
||||
# Addons Marketplace: https://community.emeraldsrv.com
|
||||
#
|
||||
# Ticket Creation Panel Configuration (ticket-panel.yml)
|
||||
|
||||
# -------------------------------
|
||||
# Ticket Panel Configuration
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"blacklistedUsers": []
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"bio": "",
|
||||
"timezone": "",
|
||||
"clockedIn": true,
|
||||
"workingHours": 0,
|
||||
"lastClockInTime": null,
|
||||
"schedule": {
|
||||
"monday": {
|
||||
"start": "09:00",
|
||||
"end": "17:00"
|
||||
},
|
||||
"tuesday": {
|
||||
"start": "09:00",
|
||||
"end": "17:00"
|
||||
},
|
||||
"wednesday": {
|
||||
"start": "09:00",
|
||||
"end": "17:00"
|
||||
},
|
||||
"thursday": {
|
||||
"start": "09:00",
|
||||
"end": "17:00"
|
||||
},
|
||||
"friday": {
|
||||
"start": "09:00",
|
||||
"end": "17:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"bio": "",
|
||||
"timezone": "",
|
||||
"clockedIn": false
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"tickets": [
|
||||
|
||||
{}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"id": 0,
|
||||
"TicketPanelID": "0000000000000000000"
|
||||
}
|
||||
"TicketPanelID": "000000000000000"
|
||||
}
|
||||
+141
-1
@@ -130,7 +130,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (interaction.isStringSelectMenu()) {
|
||||
if (interaction.customId === "ticketcontrolpanel") {
|
||||
const selectedOption = interaction.values[0];
|
||||
@@ -146,6 +145,89 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
||||
}
|
||||
break;
|
||||
|
||||
case "supportvc":
|
||||
const ticketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
const ticketInfo = ticketData.tickets.find(t => t.id === interaction.channel.id);
|
||||
|
||||
if (ticketInfo) {
|
||||
const user = interaction.guild.members.cache.get(ticketInfo.user) || ticketInfo.user;
|
||||
const claimedBy = interaction.guild.members.cache.get(ticketInfo.claimedBy) || ticketInfo.claimedBy;
|
||||
const Admin = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Admin);
|
||||
const Staff = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Staff);
|
||||
|
||||
const VCcategory = interaction.guild.channels.cache.find(
|
||||
(c) => c.name === supportbot.VoiceTickets.Category || c.id === supportbot.VoiceTickets.Category
|
||||
);
|
||||
|
||||
if (!VCcategory) {
|
||||
return interaction.reply({
|
||||
content: "The Support VC category does not exist!",
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
let username = user ? user.user.username : "Unknown User";
|
||||
|
||||
let SupportVoice = await interaction.guild.channels.create({
|
||||
name: supportbot.VoiceTickets.Name.replace(/%username%/g, username),
|
||||
type: Discord.ChannelType.GuildVoice,
|
||||
parent: VCcategory,
|
||||
permissionOverwrites: [
|
||||
{
|
||||
id: user,
|
||||
allow: [
|
||||
Discord.PermissionsBitField.Flags.Connect,
|
||||
Discord.PermissionsBitField.Flags.Speak,
|
||||
Discord.PermissionsBitField.Flags.ViewChannel,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: Staff,
|
||||
allow: [
|
||||
Discord.PermissionsBitField.Flags.Connect,
|
||||
Discord.PermissionsBitField.Flags.Speak,
|
||||
Discord.PermissionsBitField.Flags.ViewChannel,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: Admin,
|
||||
allow: [
|
||||
Discord.PermissionsBitField.Flags.Connect,
|
||||
Discord.PermissionsBitField.Flags.Speak,
|
||||
Discord.PermissionsBitField.Flags.ViewChannel,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: interaction.guild.id,
|
||||
deny: [Discord.PermissionsBitField.Flags.ViewChannel],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
ticketInfo.voiceChannelId = SupportVoice.id;
|
||||
|
||||
fs.writeFileSync("./Data/TicketData.json", JSON.stringify(ticketData, null, 2));
|
||||
|
||||
const VCDeleteButton = new Discord.ButtonBuilder()
|
||||
.setCustomId("deleteSupportVC")
|
||||
.setLabel(supportbot.Buttons.Voice.TicketDeleteText)
|
||||
.setEmoji(supportbot.Buttons.General.Delete)
|
||||
.setStyle(supportbot.Buttons.General.DeleteStyle);
|
||||
|
||||
const VCDeleteRow = new Discord.ActionRowBuilder().addComponents(VCDeleteButton);
|
||||
|
||||
const vcmadeembed = new Discord.EmbedBuilder()
|
||||
.setDescription(`> **Support VC Created:** <#${SupportVoice.id}>`)
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [vcmadeembed],
|
||||
components: [VCDeleteRow],
|
||||
});
|
||||
|
||||
}
|
||||
break;
|
||||
|
||||
case "archiveticket":
|
||||
const arcembed = new Discord.EmbedBuilder()
|
||||
.setDescription(msgconfig.Ticket.TicketArchived)
|
||||
@@ -306,6 +388,64 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (interaction.customId === 'deleteSupportVC') {
|
||||
const ticketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
|
||||
const ticketInfo = ticketData.tickets.find(t => t.id === interaction.channel.id);
|
||||
|
||||
if (ticketInfo && ticketInfo.voiceChannelId) {
|
||||
try {
|
||||
const voiceChannel = await interaction.guild.channels.fetch(ticketInfo.voiceChannelId).catch(() => null);
|
||||
|
||||
if (voiceChannel) {
|
||||
await voiceChannel.delete();
|
||||
|
||||
const successEmbed = new Discord.EmbedBuilder()
|
||||
.setDescription('✅ Support voice channel has been successfully deleted!')
|
||||
.setColor(supportbot.Embed.Colours.Success);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [successEmbed],
|
||||
ephemeral: true,
|
||||
});
|
||||
|
||||
ticketInfo.voiceChannelId = null;
|
||||
fs.writeFileSync("./Data/TicketData.json", JSON.stringify(ticketData, null, 2));
|
||||
} else {
|
||||
const errorEmbed = new Discord.EmbedBuilder()
|
||||
.setDescription('❌ The associated voice channel could not be found.')
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting the Voice Channel:', error);
|
||||
|
||||
const errorEmbed = new Discord.EmbedBuilder()
|
||||
.setDescription('❌ **There was an error trying to delete this voice channel. Please try again later.**')
|
||||
.setColor(supportbot.Embed.Colours.Error);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const errorEmbed = new Discord.EmbedBuilder()
|
||||
.setDescription('⚠️ This button is not linked to a valid voice channel.')
|
||||
.setColor(supportbot.Embed.Colours.Warn);
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [errorEmbed],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (interaction.isButton()) {
|
||||
if (interaction.customId === "unarchiveTicket") {
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
// Load the configuration files
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||
|
||||
module.exports = new Event("messageDelete", async (client, message) => {
|
||||
|
||||
// Check if the message is from a guild and not from a bot
|
||||
if (!message.guild || message.author.bot) return;
|
||||
|
||||
// Get the delete log channel either by ID or by name
|
||||
const deleteLogChannel = message.guild.channels.cache.get(supportbot.MessageDelete.Channel) ||
|
||||
message.guild.channels.cache.find(channel => channel.name === supportbot.MessageDelete.Channel);
|
||||
|
||||
// Create the embed for the deleted message
|
||||
const deletedMessageEmbed = new EmbedBuilder()
|
||||
.setColor(supportbot.MessageDelete.Colour)
|
||||
.setTitle("Message Deleted")
|
||||
.setDescription(`> **Channel:** <#${message.channel.id}>\n> **Message ID** [${message.id}](https://discord.com/channels/${message.guild.id}/${message.channel.id}/${message.id})\n> **Message author:** ${message.author.tag} (${message.author.id})\n> **Message Crated:** <t:${Math.floor(message.createdTimestamp / 1000)}:F>`)
|
||||
.addFields(
|
||||
{ name: 'Message', value: message.content || '*No content*', inline: false }
|
||||
)
|
||||
.setThumbnail(message.author.displayAvatarURL())
|
||||
.setTimestamp();
|
||||
|
||||
// If the message contains an attachment, add it to the embed
|
||||
if (message.attachments.size > 0) {
|
||||
const attachment = message.attachments.first();
|
||||
deletedMessageEmbed.addFields({ name: 'Attachment', value: `[${attachment.name}](${attachment.proxyURL})` });
|
||||
}
|
||||
|
||||
// Send the embed to the delete log channel if the channel is found
|
||||
if (deleteLogChannel) {
|
||||
deleteLogChannel.send({
|
||||
embeds: [deletedMessageEmbed]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const Event = require("../Structures/Event.js");
|
||||
|
||||
// Load the configuration files
|
||||
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
|
||||
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
|
||||
|
||||
module.exports = new Event("messageUpdate", async (client, oldMessage, newMessage) => {
|
||||
|
||||
// Check if the message is from a guild and not from a bot
|
||||
if (!newMessage.guild || newMessage.author.bot) return;
|
||||
|
||||
// Fetch old message if it's not cached (important for message content)
|
||||
if (!oldMessage.content && oldMessage.partial) {
|
||||
try {
|
||||
oldMessage = await oldMessage.fetch();
|
||||
} catch (err) {
|
||||
console.error('Error fetching old message:', err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if content is missing
|
||||
const oldMessageContent = oldMessage.content || (oldMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
|
||||
const newMessageContent = newMessage.content || (newMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*');
|
||||
|
||||
// Get the update log channel either by ID or by name
|
||||
const updateLogChannel = newMessage.guild.channels.cache.get(supportbot.MessageUpdate.Channel) ||
|
||||
newMessage.guild.channels.cache.find(channel => channel.name === supportbot.MessageUpdate.Channel);
|
||||
|
||||
// Create the embed for the updated message
|
||||
const updatedMessageEmbed = new EmbedBuilder()
|
||||
.setColor(supportbot.MessageUpdate.Colour)
|
||||
.setTitle("Message Updated")
|
||||
.setDescription(`> **Channel:** <#${newMessage.channel.id}>\n> **Message ID:** [${newMessage.id}](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${newMessage.id})\n> **Message author:** ${newMessage.author.tag} (${newMessage.author.id})\n> **Message Created:** <t:${Math.floor(oldMessage.createdTimestamp / 1000)}:F>`)
|
||||
.addFields(
|
||||
{ name: 'Old Message', value: oldMessageContent, inline: false },
|
||||
{ name: 'New Message', value: newMessageContent, inline: false }
|
||||
)
|
||||
.setThumbnail(newMessage.author.displayAvatarURL())
|
||||
.setTimestamp();
|
||||
|
||||
// If the new message contains an attachment, add it to the embed
|
||||
if (newMessage.attachments.size > 0) {
|
||||
const attachment = newMessage.attachments.first();
|
||||
updatedMessageEmbed.addFields({ name: 'Attachment', value: `[${attachment.name}](${attachment.proxyURL})` });
|
||||
}
|
||||
|
||||
// Send the embed to the update log channel if the channel is found
|
||||
if (updateLogChannel) {
|
||||
updateLogChannel.send({
|
||||
embeds: [updatedMessageEmbed]
|
||||
});
|
||||
}
|
||||
});
|
||||
+1
-3
@@ -89,9 +89,7 @@ module.exports = new Event("ready", async (client, interaction) => {
|
||||
|
||||
const roles = [
|
||||
supportbot.Roles.StaffMember.Admin,
|
||||
supportbot.Roles.StaffMember.Staff,
|
||||
supportbot.Roles.ModRoles.Blacklisted,
|
||||
supportbot.Roles.ModRoles.Muted
|
||||
supportbot.Roles.StaffMember.Staff
|
||||
];
|
||||
|
||||
if (supportbot.Roles.AutoRole.Role) roles.push(supportbot.Roles.AutoRole.Role);
|
||||
|
||||
@@ -13,9 +13,10 @@ class Client extends Discord.Client {
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMembers,
|
||||
GatewayIntentBits.GuildMessages
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent
|
||||
],
|
||||
partials: [Partials.Channel]
|
||||
partials: [Partials.Message, Partials.Channel]
|
||||
});
|
||||
|
||||
this.commands = new Discord.Collection();
|
||||
@@ -105,6 +106,10 @@ class Client extends Discord.Client {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "settings.js");
|
||||
}
|
||||
|
||||
if (cmdconfig.Mod.Enabled === false) {
|
||||
tempCommandFiles = tempCommandFiles.filter(item => item !== "mod.js");
|
||||
}
|
||||
|
||||
const commandFiles = tempCommandFiles;
|
||||
const commands = commandFiles.map((file) => require(`../Commands/${file}`));
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const supportbot = yaml.load(
|
||||
|
||||
const Client = require("./Structures/Client.js");
|
||||
const client = new Client({
|
||||
intents: ['Guilds', 'GuildMembers', 'GuildMessages']
|
||||
intents: ['Guilds', 'GuildMembers', 'GuildMessages', 'MessageContent']
|
||||
});
|
||||
|
||||
client.start(supportbot.General.Token);
|
||||
|
||||
Generated
-1671
File diff suppressed because it is too large.
Load diff
+3
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "supportbot",
|
||||
"version": "8.0.1",
|
||||
"version": "8.3",
|
||||
"description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -16,17 +16,16 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/Emerald-Services/SupportBot/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Emerald-Services/SupportBot#readme",
|
||||
"homepage": "https://community.emeraldsrv.dev",
|
||||
"dependencies": {
|
||||
"@paypal/checkout-server-sdk": "^1.0.3",
|
||||
"@vitalets/google-translate-api": "^8.0.0",
|
||||
"axios": "^1.7.2",
|
||||
"chatgpt": "^5.2.5",
|
||||
"discord.js": "^14.13.0",
|
||||
"js": "^0.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"moment": "^2.29.3",
|
||||
"ms": "^2.1.3",
|
||||
"openai": "^4.50.0",
|
||||
"openai": "^4.50.0"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user