Compare commits

...
22 Commits
Author SHA1 Message Date
1tzEmerald e3fa8b6d72 Merge pull request #185 from Emerald-Services/dev
Update close.js
2024-11-10 13:31:17 +00:00
1tzEmerald 5c6a9f7306 Update close.js 2024-11-10 13:30:42 +00:00
1tzEmerald 71e33dbf66 Update close.js 2024-11-10 13:30:27 +00:00
1tzEmerald d468ab1575 Delete Data/Profiles directory 2024-11-10 13:18:50 +00:00
1tzEmerald 74482af471 Merge pull request #184 from Emerald-Services/dev
Dev
2024-11-09 13:04:16 +00:00
1tzEmerald 0b8b092cb9 Update profile.js 2024-11-09 13:03:55 +00:00
1tzEmerald 3cdfaef942 Update profile.js 2024-11-09 13:03:41 +00:00
1tzEmerald fcf441a190 Update close.js 2024-11-09 02:46:35 +00:00
1tzEmerald 5a1c4ad99c Update close.js 2024-11-09 02:46:25 +00:00
1tzEmerald d9cb72f22e Update close.js 2024-11-09 02:44:48 +00:00
1tzEmerald 58eb37124e Update close.js 2024-11-09 02:44:39 +00:00
1tzEmerald afeeda3a8d Merge pull request #183 from Emerald-Services/dev
Dev
2024-11-09 02:38:20 +00:00
1tzEmerald fece90d2bd Update close.js 2024-11-09 02:37:26 +00:00
1tzEmerald 357a85fa3d Update close.js 2024-11-09 02:37:14 +00:00
1tzEmerald f34158caa6 Update close.js 2024-11-09 02:32:52 +00:00
1tzEmerald 1bb343e49a Update ticket.js 2024-11-09 02:32:28 +00:00
1tzEmerald dffa5d4161 Update close.js 2024-11-09 02:32:06 +00:00
1tzEmerald 13590b08a8 Update ticket.js 2024-11-09 01:07:16 +00:00
1tzEmerald 6e14ce209d Merge pull request #182 from Emerald-Services/dev
8.3 Dev -> Release
2024-11-09 00:45:40 +00:00
1tzEmerald 72b8c2247e Merge branch 'release' into dev 2024-11-09 00:45:28 +00:00
1tzEmerald 925504a928 Merge branch 'dev' of https://github.com/Emerald-Services/SupportBot into dev 2024-11-09 00:37:53 +00:00
1tzEmerald 236d67707f 8.3 Update 2024-11-09 00:37:51 +00:00
20 changed files with 1622 additions and 2111 deletions

No files matched your search

+332 -210
View File
@@ -17,7 +17,7 @@ module.exports = new Command({
name: "reason",
description: "Ticket Close Reason",
type: Discord.ApplicationCommandOptionType.String,
},
}
],
permissions: cmdconfig.CloseTicket.Permission,
@@ -28,21 +28,23 @@ 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);
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
const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread;
if (
(supportbot.Ticket.TicketType === "threads" && interaction.channel.type !== Discord.ChannelType.PrivateThread) ||
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
(supportbot.Ticket.TicketType === "channels" && interaction.channel.type !== Discord.ChannelType.GuildText)
) {
const NotTicketChannel = new Discord.EmbedBuilder()
@@ -55,7 +57,14 @@ module.exports = new Command({
await interaction.deferReply();
let tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
let tickets;
try {
tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
} catch (err) {
console.error("Error reading ticket data file:", err);
return interaction.followUp({ content: "There was an error loading ticket data." });
}
let TicketData = tickets.tickets.findIndex((t) => t.id === interaction.channel.id);
let ticket = tickets.tickets[TicketData];
@@ -68,237 +77,350 @@ module.exports = new Command({
}
let reason = interaction.options?.getString("reason") || "No Reason Provided.";
const ticketUserId = ticket.user;
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, ticketUserId);
const comment = await collectReviewComment(interaction, ticketUserId);
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}** <@${ticketUserId}>` || `N/A`
);
} else {
reviewEmbed.setDescription(
`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** <@${ticketUserId}>`
);
}
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, ticketUserId) {
const reviewPrompt = new Discord.EmbedBuilder()
.setTitle(msgconfig.ReviewSystem.Rate.Title)
.setDescription(`${msgconfig.ReviewSystem.Rate.Description}`)
.setColor(supportbot.Embed.Colours.General);
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({
content: `<@${ticketUserId}>`,
embeds: [reviewPrompt],
components: [stars],
ephemeral: true,
});
const starRating = await interaction.channel.awaitMessageComponent({
componentType: Discord.ComponentType.StringSelect,
filter: (i) => i.customId === "starRating" && i.user.id === ticketUserId,
});
await starRating.deferUpdate();
return starRating.values[0];
}
async function collectReviewComment(interaction, ticketUserId) {
const commentPrompt = new Discord.EmbedBuilder()
.setTitle(msgconfig.ReviewSystem.Comment.Title)
.setDescription(`${msgconfig.ReviewSystem.Comment.Description}`)
.setColor(supportbot.Embed.Colours.General);
await interaction.followUp({
content: `<@${ticketUserId}>`,
embeds: [commentPrompt],
ephemeral: true,
});
const filter = (response) => response.author.id === ticketUserId;
const commentCollection = await interaction.channel.awaitMessages({ filter, max: 1 });
const comment = commentCollection.first()?.content;
return comment && comment.toLowerCase() !== "no" ? comment : "No Comment Provided";
}
async function handleCloseTicket(interaction, reason, ticket, TicketData) {
const { getChannel } = interaction.client;
let tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
let tickets;
try {
tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
} catch (err) {
console.error("Error reading ticket data:", err);
return interaction.followUp("Failed to load ticket data.");
}
let tUser = interaction.client.users.cache.get(ticket.user);
let transcriptChannel = await getChannel(supportbot.Ticket.Log.TicketDataLog, interaction.guild);
if (!transcriptChannel)
return interaction.followUp("Some Channels seem to be missing!");
if (!transcriptChannel) {
console.log("Transcript channel missing or inaccessible");
return interaction.followUp("Error: Transcript log channel is missing or bot lacks permission.");
}
try {
tickets.tickets[TicketData].open = false;
fs.writeFileSync(
"./Data/TicketData.json",
JSON.stringify(tickets, null, 4),
(err) => {
if (err) console.error(err);
// Fetch all messages from the channel
let allMessages = [];
let lastId = null;
while (true) {
const options = { limit: 100 };
if (lastId) {
options.before = lastId;
}
);
const messages = await interaction.channel.messages.fetch(options);
if (messages.size === 0) break;
allMessages = [...allMessages, ...messages.values()];
lastId = messages.last().id;
if (messages.size < 100) break;
}
// Sort messages by timestamp
allMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
// Create transcript data
const transcriptData = allMessages.map(msg => ({
content: msg.content || "No content",
username: msg.author.username,
userId: msg.author.id,
avatar: msg.author.displayAvatarURL(),
timestamp: msg.createdAt.toISOString(),
attachments: Array.from(msg.attachments.values()).map(att => ({
url: att.url,
name: att.name
})),
embeds: msg.embeds.map(embed => ({
title: embed.title,
description: embed.description,
fields: embed.fields
}))
}));
// Update ticket status
tickets.tickets[TicketData].open = false;
tickets.tickets[TicketData].messages = transcriptData;
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 }
);
.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
.setDescription(
`> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` +
`> **User:** ${tUser?.tag || "Unknown User"} (\`${tUser?.id || ticket.user}\`)\n` +
`> **Closed by:** <@${interaction.user.id}>\n` +
`> **Message Count:** ${transcriptData.length}`
)
.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>
`;
}
});
const fileName = `${interaction.channel.name}.html`;
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 }
const html = createTranscriptHTML(
{
id: interaction.channel.id,
messages: transcriptData,
name: interaction.channel.name
},
reason
);
await transcriptChannel
.send({ embeds: [transcriptEmbed], files: [file] })
.catch((err) => {
console.error(err);
});
const fileName = `${interaction.channel.id}-transcript.html`;
fs.writeFileSync(`./Data/Transcripts/${fileName}`, html);
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);
}
await transcriptChannel.send({
embeds: [transcriptEmbed],
files: [{
attachment: `./Data/Transcripts/${fileName}`,
name: `SPOILER_${fileName}`,
spoiler: true
}]
});
} catch (error) {
console.error(error);
if (interaction.channel.type === Discord.ChannelType.GuildText ||
interaction.channel.type === Discord.ChannelType.PrivateThread) {
await interaction.channel.delete();
}
} catch (err) {
console.error("Error handling ticket close:", err);
interaction.followUp("An error occurred while closing the ticket.");
}
}
function convertMarkdownToHTML(text) {
// Convert **bold** to <strong>bold</strong>
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Convert *italic* to <em>italic</em>
text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
// 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;
function parseMarkdown(content) {
// Convert **bold** text to <strong> tags
content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// Convert _italic_ text to <em> tags
content = content.replace(/_(.*?)_/g, '<em>$1</em>');
// Convert inline `code` to <code> tags
content = content.replace(/`(.*?)`/g, '<code>$1</code>');
// Convert [text](link) to <a> tags
content = content.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>');
// Convert multiline code blocks ```code``` to <pre><code> tags
content = content.replace(/```(.*?)```/gs, '<pre><code>$1</code></pre>');
return content;
}
function createTranscriptHTML(ticket, reason) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 15px;
padding: 20px;
}
.message {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
margin-bottom: 15px;
}
.message-header {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 15px;
}
.username {
font-weight: bold;
color: #4CAF50;
}
.timestamp {
margin-left: auto;
color: #888;
font-size: 0.9em;
}
.content {
word-break: break-word;
}
.embed {
border-left: 4px solid #4CAF50;
padding-left: 10px;
margin: 10px 0;
background-color: rgba(0, 0, 0, 0.2);
}
.attachment {
display: inline-block;
background-color: rgba(76, 175, 80, 0.1);
border: 1px solid #4CAF50;
border-radius: 5px;
padding: 5px 10px;
margin: 5px 0;
color: #4CAF50;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<div class="mb-6">
<h1 class="text-3xl font-bold mb-4">Ticket Transcript</h1>
<p><strong>Channel:</strong> ${ticket.name}</p>
<p><strong>Ticket ID:</strong> ${ticket.id}</p>
<p><strong>Message Count:</strong> ${ticket.messages.length}</p>
<p><strong>Close Reason:</strong> ${reason}</p>
</div>
<div class="messages">
${ticket.messages.map(msg => `
<div class="message">
<div class="message-header">
<img src="${msg.avatar}" alt="Avatar" class="avatar">
<span class="username">${msg.username}</span>
<span class="timestamp">${new Date(msg.timestamp).toLocaleString()}</span>
</div>
<div class="content">
${parseMarkdown(msg.content)}
${msg.embeds.map(embed => `
<div class="embed">
${embed.title ? `<div class="font-bold">${embed.title}</div>` : ''}
${embed.description ? `<div>${embed.description}</div>` : ''}
${embed.fields.map(field => `
<div class="mt-2">
<strong>${field.name}:</strong>
<div>${field.value}</div>
</div>
`).join('')}
</div>
`).join('')}
${msg.attachments.map(att => `
<a href="${att.url}" class="attachment" target="_blank">
📎 ${att.name}
</a>
`).join('')}
</div>
</div>
`).join('')}
</div>
</div>
</body>
</html>
`;
}
+450 -138
View File
@@ -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
View File
@@ -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 });
}
}
});
+1 -5
View File
@@ -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') {
@@ -39,7 +35,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;
+95 -15
View File
@@ -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;
}
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
@@ -231,20 +312,6 @@ module.exports = new Command({
});
}
if (User.roles.cache.has(supportbot.Roles.ModRoles.Blacklisted)) {
return interaction.reply({
content: msgconfig.Ticket.Blacklisted,
ephemeral: true,
});
}
if (User.roles.cache.has(supportbot.Roles.ModRoles.Muted)) {
return interaction.reply({
content: msgconfig.Ticket.Muted,
ephemeral: true,
});
}
let ticketNumberID = await TicketNumberID.pad();
const TicketSubject = TicketReason || msgconfig.Ticket.InvalidSubject;
@@ -352,6 +419,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 +480,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 +496,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 +542,7 @@ module.exports = new Command({
components: [row2],
});
} catch (error) {
console.error("Error in run method:", error);
}
+30 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+5 -2
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
{
"blacklistedUsers": []
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"tickets": [
{}
]
}
+2 -2
View File
@@ -1,4 +1,4 @@
{
"id": 0,
"TicketPanelID": "0000000000000000000"
}
"TicketPanelID": "000000000000000"
}
+141 -1
View File
@@ -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") {
+43
View File
@@ -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]
});
}
});
+57
View File
@@ -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
View File
@@ -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);
+7 -2
View File
@@ -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}`));
+1 -1
View 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);
-1671
View File
File diff suppressed because it is too large. Load diff
+3 -4
View File
@@ -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"
}
}