diff --git a/Addons/addon.js b/Addons/addon.js deleted file mode 100644 index 7609fd7..0000000 --- a/Addons/addon.js +++ /dev/null @@ -1,26 +0,0 @@ -const Discord = require("discord.js"); -const fs = require("fs"); -const yaml = require("js-yaml"); -const { Command } = require('../Structures/Addon.js'); // Adjust the path as needed -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); - -module.exports = new Command({ - name: "addon", - description: "This is a sample addon!", - options: [], - permissions: ["Administrator"], - - async run(interaction) { - let disableCommand = true; - - const PingEmbed = new Discord.EmbedBuilder() - .setDescription( - `This is a sample addon.\n\nPong! \`${interaction.client.ws.ping} ms\`` - ) - .setColor(supportbot.Embed.Colours.General); - - interaction.reply({ - embeds: [PingEmbed], - }); - }, - }); \ No newline at end of file diff --git a/Commands/help.js b/Commands/General/help.js similarity index 94% rename from Commands/help.js rename to Commands/General/help.js index 7e0f48a..9b0e469 100644 --- a/Commands/help.js +++ b/Commands/General/help.js @@ -5,7 +5,7 @@ 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 Command = require("../Structures/Command.js"); +const Command = require("../../Structures/Command.js"); module.exports = new Command({ name: cmdconfig.Help.Command, @@ -55,7 +55,7 @@ module.exports = new Command({ }); interaction.reply({ - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, embeds: [HelpEmbed1], }); }, diff --git a/Commands/General/info copy.js b/Commands/General/info copy.js new file mode 100644 index 0000000..4d47059 --- /dev/null +++ b/Commands/General/info copy.js @@ -0,0 +1,45 @@ +// SupportBot | Emerald Services +// Info Command + +const fs = require("fs"); + +const { ContainerBuilder, UserSelectMenuBuilder, ButtonStyle, MessageFlags } = 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 db = require("../../Structures/Database.js"); +const Command = require("../../Structures/Command.js"); + +module.exports = new Command({ + name: "test", + description: cmdconfig.Info.Description, + options: [], + permissions: cmdconfig.Info.Permission, + + async run(interaction, client) { + + const ticketPanelembed = new ContainerBuilder() + .addTextDisplayComponents( + textDisplay => textDisplay + .setContent("SupportBot"), + ) + + +await interaction.reply({ + components: [ticketPanelembed], + flags: MessageFlags.IsComponentsV2, +}); + + }, +}); \ No newline at end of file diff --git a/Commands/info.js b/Commands/General/info.js similarity index 95% rename from Commands/info.js rename to Commands/General/info.js index 34b5a2c..fe4dba3 100644 --- a/Commands/info.js +++ b/Commands/General/info.js @@ -18,7 +18,7 @@ const msgconfig = yaml.load( fs.readFileSync("./Configs/messages.yml", "utf8") ); -const Command = require("../Structures/Command.js"); +const Command = require("../../Structures/Command.js"); module.exports = new Command({ name: cmdconfig.Info.Command, diff --git a/Commands/ping.js b/Commands/General/ping.js similarity index 93% rename from Commands/ping.js rename to Commands/General/ping.js index 19942d4..99bb62c 100644 --- a/Commands/ping.js +++ b/Commands/General/ping.js @@ -10,7 +10,7 @@ const supportbot = yaml.load( ); const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const Command = require("../Structures/Command.js"); +const Command = require("../../Structures/Command.js"); module.exports = new Command({ name: cmdconfig.Ping.Command, diff --git a/Commands/General/profile.js b/Commands/General/profile.js new file mode 100644 index 0000000..855f9ed --- /dev/null +++ b/Commands/General/profile.js @@ -0,0 +1,296 @@ +const { + EmbedBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ApplicationCommandType, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ApplicationCommandOptionType +} = require("discord.js"); +const fs = require("fs"); +const yaml = require("js-yaml"); +const db = require("../../Structures/Database.js"); +const Command = require("../../Structures/Command.js"); + +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 clockedInUsers = new Set(); + +function updateClockedInStatus(embed, clockedIn) { + embed.data.fields = embed.data.fields.map(f => + f.name === "Clocked In Status" + ? { ...f, value: clockedIn ? "✅ Clocked In" : "❌ Clocked Out" } + : f + ); + return embed; +} + +module.exports = new Command({ + name: cmdconfig.Profile.Command, + description: cmdconfig.Profile.Description, + type: ApplicationCommandType.ChatInput, + permissions: cmdconfig.Profile.Permission, + options: [ + { + name: "user", + description: "The user to view the profile of", + type: ApplicationCommandOptionType.User, + required: false + } + ], + + async run(interaction) { + await interaction.deferReply({ ephemeral: true }); + + const viewingUser = interaction.options.getUser("user") || interaction.user; + const viewingUserId = viewingUser.id; + const interactionUserId = interaction.user.id; + + const member = await interaction.guild.members.fetch(viewingUserId); + const { getRole } = interaction.client; + const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); + const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); + + if (!Staff || !Admin) { + return interaction.editReply({ + content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", + ephemeral: true + }); + } + + const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id); + const isOwnProfile = interactionUserId === viewingUserId; + + const profileData = db.getProfile(viewingUserId); + let { bio, timezone, clockedIn } = profileData; + clockedIn = Boolean(clockedIn); + + let profileEmbed = new EmbedBuilder() + .setTitle(`${viewingUser.username}'s Profile`) + .setColor(supportbot.Embed.Colours.General) + .setThumbnail(viewingUser.displayAvatarURL()) + .addFields( + { name: "Bio", value: bio || "No bio set.", inline: false }, + { name: "Timezone", value: timezone || "No timezone set.", inline: true } + ); + + if (isStaff) { + profileEmbed.addFields({ + name: "Clocked In Status", + value: clockedIn ? "✅ Clocked In" : "❌ Clocked Out", + inline: true + }); + } + + const buttonRow = new ActionRowBuilder(); + if (isOwnProfile) { + buttonRow.addComponents( + new ButtonBuilder() + .setCustomId("editProfile") + .setLabel("Edit Profile") + .setStyle(ButtonStyle.Secondary) + ); + + if (isStaff) { + buttonRow.addComponents( + new ButtonBuilder() + .setCustomId("clockInOut") + .setLabel(clockedIn ? "Clock Out" : "Clock In") + .setStyle(ButtonStyle.Secondary) + ); + } + } + + if (isStaff && supportbot.Ticket.ClaimTickets.Enabled) { + buttonRow.addComponents( + new ButtonBuilder() + .setCustomId("viewTicketStats") + .setLabel("View Ticket Stats") + .setStyle(ButtonStyle.Secondary) + ); + } + + await interaction.editReply({ + embeds: [profileEmbed], + components: buttonRow.components.length ? [buttonRow] : [] + }); + + const filter = i => + ["editProfile", "clockInOut", "viewTicketStats", "showOpenTickets", "backToStats", "backToProfile"] + .includes(i.customId) && + i.user.id === interactionUserId; + + const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 }); + + collector.on("collect", async i => { + if (i.customId === "editProfile") { + const modal = new ModalBuilder() + .setCustomId("editProfileModal") + .setTitle("Edit Profile") + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("bio") + .setLabel("Bio") + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder("Enter your bio") + .setValue(String(bio || "")) + .setRequired(false) + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("timezone") + .setLabel("Timezone") + .setStyle(TextInputStyle.Short) + .setPlaceholder("Enter your timezone") + .setValue(String(timezone || "")) + .setRequired(false) + ) + ); + + return i.showModal(modal); + } + + if (i.customId === "clockInOut") { + const newStatus = !clockedIn; + db.setClockedIn(viewingUserId, newStatus); + clockedIn = newStatus; + profileEmbed = updateClockedInStatus(profileEmbed, newStatus); + + if (newStatus) clockedInUsers.add(viewingUserId); + else clockedInUsers.delete(viewingUserId); + + const updatedRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("editProfile") + .setLabel("Edit Profile") + .setStyle(ButtonStyle.Secondary) + ); + if (isStaff) { + updatedRow.addComponents( + new ButtonBuilder() + .setCustomId("clockInOut") + .setLabel(newStatus ? "Clock Out" : "Clock In") + .setStyle(ButtonStyle.Secondary) + ); + if (supportbot.Ticket.ClaimTickets.Enabled) { + updatedRow.addComponents( + new ButtonBuilder() + .setCustomId("viewTicketStats") + .setLabel("View Ticket Stats") + .setStyle(ButtonStyle.Secondary) + ); + } + } + + await i.update({ embeds: [profileEmbed], components: [updatedRow] }); + + const clockEmbed = new EmbedBuilder() + .setTitle(newStatus ? "Clocked In!" : "Clocked Out!") + .setDescription(newStatus ? "You have successfully clocked in." : "You have successfully clocked out.") + .setColor(supportbot.Embed.Colours.General); + + return interaction.followUp({ embeds: [clockEmbed], ephemeral: true }); + } + + 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(t => { + if (t.claimedAt && t.createdAt) { + totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt); + responseCount++; + } + }); + + const avgMinutes = responseCount ? Math.round(totalResponseTime / responseCount / 60000) : 0; + + 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: `${avgMinutes} minutes`, inline: false } + ); + + const statsRow = 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) + ); + + return i.update({ embeds: [statsEmbed], components: [statsRow] }); + } + + 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 openEmbed = new EmbedBuilder() + .setTitle("Active Tickets") + .setDescription(ticketsOpen.map(t => `<#${t.id}>`).join("\n") || "No active tickets.") + .setColor(supportbot.Embed.Colours.General); + + const backRow = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("backToStats").setLabel("Back to Stats").setStyle(ButtonStyle.Secondary) + ); + + return i.update({ embeds: [openEmbed], components: [backRow] }); + } + + 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; + + let totalResponseTime = 0; + let responseCount = 0; + ticketsClaimed.forEach(t => { + if (t.claimedAt && t.createdAt) { + totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt); + responseCount++; + } + }); + const avgMinutes = responseCount ? Math.round(totalResponseTime / responseCount / 60000) : 0; + + 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: `${avgMinutes} minutes`, inline: false } + ); + + const statsRow = 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) + ); + + return i.update({ embeds: [statsEmbed], components: [statsRow] }); + } + + if (i.customId === "backToProfile") { + return i.update({ + embeds: [profileEmbed], + components: buttonRow.components.length ? [buttonRow] : [] + }); + } + }); + } +}); + +module.exports.clockedInUsers = clockedInUsers; diff --git a/Commands/translate.js b/Commands/General/translate.js similarity index 97% rename from Commands/translate.js rename to Commands/General/translate.js index fe4e4cd..734d348 100644 --- a/Commands/translate.js +++ b/Commands/General/translate.js @@ -18,7 +18,7 @@ const msgconfig = yaml.load( fs.readFileSync("./Configs/messages.yml", "utf8") ) -const Command = require("../Structures/Command.js"); +const Command = require("../../Structures/Command.js"); const translate = require("@vitalets/google-translate-api"); module.exports = new Command({ @@ -89,7 +89,7 @@ module.exports = new Command({ "Click the title to see which 2 letter language codes are valid." ), ], - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); } }); diff --git a/Commands/Suggestions/suggest.js b/Commands/Suggestions/suggest.js new file mode 100644 index 0000000..7379099 --- /dev/null +++ b/Commands/Suggestions/suggest.js @@ -0,0 +1,147 @@ +const fs = require("fs"); +const Discord = require("discord.js"); +const yaml = require("js-yaml"); +const path = require("path"); + +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"); +const DB = require("../../Structures/Database.js"); + +module.exports = new Command({ + name: "suggest", + description: cmdconfig.Suggestion?.Description || "Submit a new suggestion", + type: Discord.ApplicationCommandType.ChatInput, + options: [ + { + name: "text", + description: "Your suggestion text", + type: Discord.ApplicationCommandOptionType.String, + required: true + } + ], + + async run(interaction) { + const suggestion = interaction.options.getString("text"); + const mode = supportbot.Suggestions.Mode || "Channel"; + + const embed = new Discord.EmbedBuilder() + .addFields( + { name: "Suggestion", value: suggestion, inline: true }, + { name: "From", value: `<@${interaction.user.id}>` }, + { name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, value: "0", inline: true }, + { name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, value: "0", inline: true } + ) + .setThumbnail(interaction.user.displayAvatarURL()) + .setColor(supportbot.Embed.Colours.General); + + const row = new Discord.ActionRowBuilder().addComponents( + new Discord.ButtonBuilder() + .setCustomId("upvote") + .setEmoji(supportbot.Suggestions.UpvoteEmoji) + .setStyle(supportbot.Suggestions.Buttons.Upvote), + new Discord.ButtonBuilder() + .setCustomId("downvote") + .setEmoji(supportbot.Suggestions.DownvoteEmoji) + .setStyle(supportbot.Suggestions.Buttons.Downvote), + new Discord.ButtonBuilder() + .setCustomId("removevote") + .setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title) + .setStyle(supportbot.Suggestions.Buttons.RemoveVote) + ); + + if (mode === "Forum") { + const forum = await interaction.guild.channels + .fetch(supportbot.Suggestions.ForumChannel) + .catch(() => null); + + if (!forum || forum.type !== Discord.ChannelType.GuildForum) { + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("⚠ Missing Forum Channel") + .setDescription(msgconfig.Error.MissingChannel) + .setColor(supportbot.Embed.Colours.Error) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + const appliedTags = supportbot.Suggestions.Tags?.Created + ? [supportbot.Suggestions.Tags.Created] + : []; + + const thread = await forum.threads.create({ + name: suggestion.slice(0, 100), + message: { + content: "\u200B", + embeds: [embed], + components: [row] + }, + appliedTags, + autoArchiveDuration: Discord.ThreadAutoArchiveDuration.OneDay, + reason: "New Suggestion" + }); + + embed.setFooter({ + text: `${supportbot.Embed.Footer} | Suggestion ID: ${thread.id}`, + iconURL: interaction.user.displayAvatarURL() + }); + const starter = await thread.fetchStarterMessage(); + await starter.edit({ embeds: [embed], components: [row] }); + + DB.add(thread.id, interaction.user.id, suggestion); + + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle(msgconfig.Suggestions.Sent_Title) + .setDescription(`${msgconfig.Suggestions.Sent}`) + .addFields({ name: 'Posted in:', value: `<#${forum.id}>` }) + .setColor(supportbot.Embed.Colours.Success) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + // ---------- Channel Mode ---------- + const channel = await interaction.guild.channels + .fetch(supportbot.Suggestions.Channel) + .catch(() => null); + + if (!channel) { + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("⚠ Missing Suggestion Channel") + .setDescription(msgconfig.Error.MissingChannel) + .setColor(supportbot.Embed.Colours.Error) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + const message = await channel.send({ embeds: [embed], components: [row] }); + + embed.setFooter({ + text: `${supportbot.Embed.Footer} | Suggestion ID: ${message.id}`, + iconURL: interaction.user.displayAvatarURL() + }); + await message.edit({ embeds: [embed], components: [row] }); + + DB.add(message.id, interaction.user.id, suggestion); + + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle(msgconfig.Suggestions.Sent_Title) + .setDescription(`${msgconfig.Suggestions.Sent}`) + .addFields({ name: 'Posted in:', value: `<#${channel.id}>` }) + .setColor(supportbot.Embed.Colours.Success) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } +}); diff --git a/Commands/Suggestions/suggestadmin.js b/Commands/Suggestions/suggestadmin.js new file mode 100644 index 0000000..33b70ec --- /dev/null +++ b/Commands/Suggestions/suggestadmin.js @@ -0,0 +1,183 @@ +const Discord = require("discord.js"); +const fs = require("fs"); +const yaml = require("js-yaml"); + +const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); +const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const SuggestionDB = require("../../Structures/Database.js"); +const Command = require("../../Structures/Command.js"); + +module.exports = new Command({ + name: cmdconfig.SuggestionAdmin.Command, + description: cmdconfig.SuggestionAdmin.Description, + type: Discord.ApplicationCommandType.ChatInput, + permissions: cmdconfig.SuggestionAdmin.Permission, + options: [ + { + name: "status", + description: "New status", + type: Discord.ApplicationCommandOptionType.String, + required: true, + choices: [ + { name: "Solved", value: "Solved" }, + { name: "Denied", value: "Denied" }, + { name: "Considering", value: "Considering" } + ] + }, + { + name: "id", + description: "Message ID (required in Channel mode)", + type: Discord.ApplicationCommandOptionType.String, + required: false + } + ], + + async run(interaction) { + const status = interaction.options.getString("status"); + const idOption = interaction.options.getString("id"); + const mode = supportbot.Suggestions.Mode || "Channel"; + + const { getRole } = interaction.client; + const staffRole = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); + const adminRole = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); + if ( + !interaction.member.roles.cache.has(staffRole?.id) && + !interaction.member.roles.cache.has(adminRole?.id) + ) { + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("Invalid Permissions!") + .setDescription( + `${msgconfig.Error.IncorrectPerms}\nRequired: ${supportbot.Roles.StaffMember.Staff} or ${supportbot.Roles.StaffMember.Admin}` + ) + .setColor(supportbot.Embed.Colours.Warn) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + if (mode === "Forum") { + const thread = interaction.channel; + if (!thread.isThread() || thread.parentId !== supportbot.Suggestions.ForumChannel) { + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("Not a Suggestion Thread") + .setDescription(msgconfig.Error.MissingChannel) + .setColor(supportbot.Embed.Colours.Error) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + const tagMap = { + Solved: supportbot.Suggestions.Tags.Solved, + Denied: supportbot.Suggestions.Tags.Denied, + Considering: supportbot.Suggestions.Tags.Considering + }; + const tagId = tagMap[status]; + if (!tagId) { + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("Tag Missing") + .setDescription("No forum tag configured for this status.") + .setColor(supportbot.Embed.Colours.Error) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + await thread.setAppliedTags([tagId]).catch(() => null); + SuggestionDB.setStatus(thread.id, status); + + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("Suggestion Updated") + .setDescription(`Status set to **${status}**.`) + .setColor(supportbot.Embed.Colours.Success) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + if (!idOption) { + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("Message ID Required") + .setDescription("Provide the suggestion’s message ID when in Channel mode.") + .setColor(supportbot.Embed.Colours.Error) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + const channel = await interaction.guild.channels + .fetch(supportbot.Suggestions.Channel) + .catch(() => null); + if (!channel) { + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("Missing Suggestion Channel") + .setDescription(msgconfig.Error.MissingChannel) + .setColor(supportbot.Embed.Colours.Error) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + const targetMsg = await channel.messages.fetch(idOption).catch(() => null); + if (!targetMsg) { + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("Message Not Found") + .setDescription("Could not fetch a suggestion with that ID.") + .setColor(supportbot.Embed.Colours.Error) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + const oldEmbed = targetMsg.embeds[0]; + if (!oldEmbed) { + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("No Embed Found") + .setDescription("The target message has no suggestion embed.") + .setColor(supportbot.Embed.Colours.Error) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + const colorMap = { + Solved: supportbot.Embed.Colours.Success, + Denied: supportbot.Embed.Colours.Error, + Considering: supportbot.Embed.Colours.Warn + }; + + const newEmbed = Discord.EmbedBuilder.from(oldEmbed) + .setColor(colorMap[status]) + .addFields({ name: "Status", value: status, inline: true }); + + await targetMsg.edit({ embeds: [newEmbed], components: targetMsg.components }); + SuggestionDB.setStatus(idOption, status); + + return interaction.reply({ + embeds: [ + new Discord.EmbedBuilder() + .setTitle("Suggestion Updated") + .setDescription(`Suggestion **${idOption}** marked as **${status}**.`) + .setColor(supportbot.Embed.Colours.Success) + ], + flags: Discord.MessageFlags.Ephemeral, + }); + } +}); diff --git a/Commands/Tickets/addUser.js b/Commands/Tickets/addUser.js new file mode 100644 index 0000000..f5de800 --- /dev/null +++ b/Commands/Tickets/addUser.js @@ -0,0 +1,119 @@ +const fs = require("fs"); +const { + EmbedBuilder, + ApplicationCommandOptionType, + ApplicationCommandType, + ChannelType, + MessageFlags, +} = require("discord.js"); +const yaml = require("js-yaml"); + +const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); +const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); + +const Command = require("../../Structures/Command.js"); +const db = require("../../Structures/Database.js"); + +module.exports = new Command({ + name: cmdconfig.ForceAddUser.Command, + description: cmdconfig.ForceAddUser.Description, + type: ApplicationCommandType.ChatInput, + options: [ + { + name: "user", + description: "The user to forcefully add", + type: ApplicationCommandOptionType.User, + required: true, + }, + ], + permissions: cmdconfig.ForceAddUser.Permission, + + async run(interaction) { + const userToAdd = interaction.options.getUser("user"); + const ticketChannel = interaction.channel; + + if ( + (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || + (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText) + ) { + const onlyInTicket = new EmbedBuilder() + .setTitle(msgconfig.ForceAddUser.NotInTicket_Title) + .setDescription(msgconfig.ForceAddUser.NotInTicket_Description) + .setColor(supportbot.Embed.Colours.Error); + + return interaction.reply({ + embeds: [onlyInTicket], + flags: MessageFlags.Ephemeral, + }); + } + + const ticketRow = db.getTicket(ticketChannel.id); + if (!ticketRow) { + const notFound = new EmbedBuilder() + .setTitle(msgconfig.ForceAddUser.NotInTicket_Title) + .setDescription(msgconfig.ForceAddUser.NotInTicket_Description) + .setColor(supportbot.Embed.Colours.Error); + + return interaction.reply({ + embeds: [notFound], + flags: Discord.MessageFlags.Ephemeral, + }); + } + + try { + if (supportbot.Ticket.TicketType === "threads") { + const botMember = await ticketChannel.members.fetch(interaction.client.user.id).catch(() => null); + if (!botMember) await ticketChannel.join(); + await new Promise(res => setTimeout(res, 1000)); + await ticketChannel.members.add(userToAdd.id); + } else if (supportbot.Ticket.TicketType === "channels") { + await ticketChannel.permissionOverwrites.create(userToAdd.id, { + ViewChannel: true, + SendMessages: true, + ReadMessageHistory: true, + }); + } + + db.addUserToTicket(ticketChannel.id, userToAdd.id); + + const addedEmbed = new EmbedBuilder() + .setTitle(msgconfig.ForceAddUser.Added_Title) + .setDescription( + msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username) + ) + .setColor(supportbot.Embed.Colours.Success); + + await interaction.reply({ + embeds: [addedEmbed], + flags: Discord.MessageFlags.Ephemeral, + }); + + const addedToTicketEmbed = new EmbedBuilder() + .setTitle(msgconfig.ForceAddUser.AddedToTicket_Title) + .setDescription( + msgconfig.ForceAddUser.AddedToTicket_Description.replace( + "%channel_link%", + `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})` + ) + ) + .setColor(supportbot.Embed.Colours.General); + + await userToAdd.send({ + embeds: [addedToTicketEmbed], + }); + } catch (err) { + console.error("Error adding user to the ticket:", err); + + const errorEmbed = new EmbedBuilder() + .setTitle(msgconfig.ForceAddUser.Error_Title) + .setDescription(msgconfig.ForceAddUser.Error_Adding) + .setColor(supportbot.Embed.Colours.Error); + + await interaction.reply({ + embeds: [errorEmbed], + flags: Discord.MessageFlags.Ephemeral, + }); + } + }, +}); diff --git a/Commands/close.js b/Commands/Tickets/close.js similarity index 62% rename from Commands/close.js rename to Commands/Tickets/close.js index ef853cf..6d618e8 100644 --- a/Commands/close.js +++ b/Commands/Tickets/close.js @@ -2,11 +2,13 @@ const fs = require("fs"); const Discord = require("discord.js"); const yaml = require("js-yaml"); +const db = require("../../Structures/Database.js"); + 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"); +const Command = require("../../Structures/Command.js"); module.exports = new Command({ name: cmdconfig.CloseTicket.Command, @@ -57,18 +59,9 @@ module.exports = new Command({ await interaction.deferReply(); - let tickets; - try { - tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); - } catch (err) { - console.error("Error reading ticket data file:", err); - return interaction.followUp({ content: "There was an error loading ticket data." }); - } - - let TicketData = tickets.tickets.findIndex((t) => t.id === interaction.channel.id); - let ticket = tickets.tickets[TicketData]; - - if (TicketData === -1) { + const ticket = db.getTicket(interaction.channel.id); + + if (!ticket) { const Exists = new Discord.EmbedBuilder() .setTitle("No Ticket Found!") .setDescription(msgconfig.Error.NoValidTicket) @@ -120,9 +113,9 @@ module.exports = new Command({ await interaction.followUp({ embeds: [reviewEmbed] }); } - await handleCloseTicket(interaction, reason, ticket, TicketData); + await handleCloseTicket(interaction, reason, ticket); } else { - await handleCloseTicket(interaction, reason, ticket, TicketData); + await handleCloseTicket(interaction, reason, ticket); } }, }); @@ -150,7 +143,7 @@ async function collectReviewRating(interaction, ticketUserId) { content: `<@${ticketUserId}>`, embeds: [reviewPrompt], components: [stars], - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); const starRating = await interaction.channel.awaitMessageComponent({ @@ -170,7 +163,7 @@ async function collectReviewComment(interaction, ticketUserId) { await interaction.followUp({ content: `<@${ticketUserId}>`, embeds: [commentPrompt], - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); const filter = (response) => response.author.id === ticketUserId; @@ -179,18 +172,10 @@ async function collectReviewComment(interaction, ticketUserId) { const comment = commentCollection.first()?.content; return comment && comment.toLowerCase() !== "no" ? comment : "No Comment Provided"; } -async function handleCloseTicket(interaction, reason, ticket, TicketData) { +async function handleCloseTicket(interaction, reason, ticket) { const { getChannel } = interaction.client; - 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 tUser = interaction.client.users.cache.get(ticket.user_id); let transcriptChannel = await getChannel(supportbot.Ticket.Log.TicketDataLog, interaction.guild); if (!transcriptChannel) { @@ -199,7 +184,6 @@ async function handleCloseTicket(interaction, reason, ticket, TicketData) { } try { - // Fetch all messages from the channel let allMessages = []; let lastId = null; @@ -218,10 +202,8 @@ async function handleCloseTicket(interaction, reason, ticket, TicketData) { 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, @@ -239,10 +221,7 @@ async function handleCloseTicket(interaction, reason, ticket, TicketData) { })) })); - // Update ticket status - tickets.tickets[TicketData].open = false; - tickets.tickets[TicketData].messages = transcriptData; - fs.writeFileSync("./Data/TicketData.json", JSON.stringify(tickets, null, 4)); + db.updateTicketStatus(interaction.channel.id, "closed"); const transcriptEmbed = new Discord.EmbedBuilder() .setTitle(msgconfig.TicketLog.Title) @@ -250,7 +229,7 @@ async function handleCloseTicket(interaction, reason, ticket, TicketData) { .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` + + `> **User:** ${tUser?.tag || "Unknown User"} (\`${tUser?.id || ticket.user_id}\`)` + `> **Closed by:** <@${interaction.user.id}>\n` + `> **Message Count:** ${transcriptData.length}` ) @@ -264,6 +243,12 @@ async function handleCloseTicket(interaction, reason, ticket, TicketData) { }, reason ); + + const transcriptDir = "./Data/Transcripts"; + + if (!fs.existsSync(transcriptDir)) { + fs.mkdirSync(transcriptDir, { recursive: true }); + } const fileName = `${interaction.channel.id}-transcript.html`; fs.writeFileSync(`./Data/Transcripts/${fileName}`, html); @@ -288,19 +273,14 @@ async function handleCloseTicket(interaction, reason, ticket, TicketData) { } function parseMarkdown(content) { - // Convert **bold** text to tags content = content.replace(/\*\*(.*?)\*\*/g, '$1'); - // Convert _italic_ text to tags content = content.replace(/_(.*?)_/g, '$1'); - // Convert inline `code` to tags content = content.replace(/`(.*?)`/g, '$1'); - // Convert [text](link) to tags content = content.replace(/\[(.*?)\]\((.*?)\)/g, '$1'); - // Convert multiline code blocks ```code``` to
 tags
   content = content.replace(/```(.*?)```/gs, '
$1
'); return content; @@ -308,119 +288,181 @@ function parseMarkdown(content) { function createTranscriptHTML(ticket, reason) { return ` - - - - - - - - - -
-
-

Ticket Transcript

-

Channel: ${ticket.name}

-

Ticket ID: ${ticket.id}

-

Message Count: ${ticket.messages.length}

-

Close Reason: ${reason}

-
- -
- ${ticket.messages.map(msg => ` -
-
- Avatar - ${msg.username} - ${new Date(msg.timestamp).toLocaleString()} -
-
- ${parseMarkdown(msg.content)} - - ${msg.embeds.map(embed => ` -
- ${embed.title ? `
${embed.title}
` : ''} - ${embed.description ? `
${embed.description}
` : ''} - ${embed.fields.map(field => ` -
- ${field.name}: -
${field.value}
-
- `).join('')} -
- `).join('')} - - ${msg.attachments.map(att => ` - - 📎 ${att.name} - - `).join('')} -
-
- `).join('')} -
+ + + + + + Ticket Transcript + + + + +
+

Ticket Transcript

+
+

Channel: ${ticket.name}

+

Ticket ID: ${ticket.id}

+

Message Count: ${ticket.messages.length}

+

Close Reason: ${reason}

- - + +
+ ${ticket.messages.map(msg => ` +
+
+ Avatar + ${msg.username} + ${new Date(msg.timestamp).toLocaleString()} +
+
+ ${parseMarkdown(msg.content)} + + ${msg.embeds.map(embed => ` +
+ ${embed.title ? `
${embed.title}
` : ''} + ${embed.description ? `
${embed.description}
` : ''} + ${embed.fields.map(field => ` +
+ ${field.name}: +
${field.value}
+
+ `).join('')} +
+ `).join('')} + + ${msg.attachments.map(att => ` + 📎 ${att.name} + `).join('')} +
+
+ `).join('')} +
+
+ + `; } diff --git a/Commands/forceaddUser.js b/Commands/Tickets/forceaddUser.js similarity index 64% rename from Commands/forceaddUser.js rename to Commands/Tickets/forceaddUser.js index fc02f8d..3ce1e2b 100644 --- a/Commands/forceaddUser.js +++ b/Commands/Tickets/forceaddUser.js @@ -4,6 +4,7 @@ const { ApplicationCommandOptionType, ApplicationCommandType, ChannelType, + MessageFlags, } = require("discord.js"); const yaml = require("js-yaml"); @@ -11,7 +12,8 @@ const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8") const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const Command = require("../Structures/Command.js"); +const Command = require("../../Structures/Command.js"); +const db = require("../../Structures/Database.js"); module.exports = new Command({ name: cmdconfig.ForceAddUser.Command, @@ -30,7 +32,6 @@ module.exports = new Command({ async run(interaction) { const userToAdd = interaction.options.getUser("user"); const ticketChannel = interaction.channel; - const ticketDataPath = "./Data/TicketData.json"; if ( (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || @@ -43,54 +44,38 @@ module.exports = new Command({ return interaction.reply({ embeds: [onlyInTicket], - ephemeral: true, + flags: MessageFlags.Ephemeral, + }); + } + + const ticketRow = db.getTicket(ticketChannel.id); + if (!ticketRow) { + const notFound = new EmbedBuilder() + .setTitle(msgconfig.ForceAddUser.NotInTicket_Title) + .setDescription(msgconfig.ForceAddUser.NotInTicket_Description) + .setColor(supportbot.Embed.Colours.Error); + + return interaction.reply({ + embeds: [notFound], + flags: MessageFlags.Ephemeral, }); } try { - if (supportbot.Ticket.TicketType === "threads") { - try { - // Explicitly ensure the bot is in the thread - const botMember = await ticketChannel.members.fetch(interaction.client.user.id).catch(() => null); - if (!botMember) { - await ticketChannel.join(); - } - - // Delay to ensure API has processed the bot's membership - await new Promise(res => setTimeout(res, 1000)); - - // Attempt to add the user to the thread - await ticketChannel.members.add(userToAdd.id); - - console.log("User successfully added to the thread."); - } catch (err) { - console.error("Error adding user to the thread:", err.message); - const errorEmbed = new EmbedBuilder() - .setTitle(msgconfig.ForceAddUser.Error_Title) - .setDescription(`An error occurred: ${err.message}`) - .setColor(supportbot.Embed.Colours.Error); - - return interaction.reply({ - embeds: [errorEmbed], - ephemeral: true, - }); - } - + const botMember = await ticketChannel.members.fetch(interaction.client.user.id).catch(() => null); + if (!botMember) await ticketChannel.join(); + await new Promise(res => setTimeout(res, 1000)); + await ticketChannel.members.add(userToAdd.id); } else if (supportbot.Ticket.TicketType === "channels") { await ticketChannel.permissionOverwrites.create(userToAdd.id, { ViewChannel: true, SendMessages: true, ReadMessageHistory: true, - }); + }); } - const ticketData = JSON.parse(fs.readFileSync(ticketDataPath, "utf8")); - const ticketIndex = ticketData.tickets.findIndex((t) => t.id === ticketChannel.id); - if (ticketIndex !== -1) { - ticketData.tickets[ticketIndex].subUsers.push(userToAdd.id); - fs.writeFileSync(ticketDataPath, JSON.stringify(ticketData, null, 4)); - } + db.addUserToTicket(ticketChannel.id, userToAdd.id); const addedEmbed = new EmbedBuilder() .setTitle(msgconfig.ForceAddUser.Added_Title) @@ -101,7 +86,7 @@ module.exports = new Command({ await interaction.reply({ embeds: [addedEmbed], - ephemeral: true, + flags: MessageFlags.Ephemeral, }); const addedToTicketEmbed = new EmbedBuilder() @@ -127,7 +112,7 @@ module.exports = new Command({ await interaction.reply({ embeds: [errorEmbed], - ephemeral: true, + flags: MessageFlags.Ephemeral, }); } }, diff --git a/Commands/Tickets/removeUser.js b/Commands/Tickets/removeUser.js new file mode 100644 index 0000000..9241480 --- /dev/null +++ b/Commands/Tickets/removeUser.js @@ -0,0 +1,108 @@ +// SupportBot | Emerald Services +// Remove user from a ticket + +const { + EmbedBuilder, + ApplicationCommandOptionType, + ApplicationCommandType, + ChannelType, + MessageFlags, +} = require("discord.js"); +const yaml = require("js-yaml"); +const fs = require("fs"); + +const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); +const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); + +const Command = require("../../Structures/Command.js"); +const db = require("../../Structures/Database.js"); + +module.exports = new Command({ + name: cmdconfig.RemoveUser.Command, + description: cmdconfig.RemoveUser.Description, + type: ApplicationCommandType.ChatInput, + options: [ + { + name: "user", + description: "The user to remove", + type: ApplicationCommandOptionType.User, + required: true, + }, + ], + permissions: cmdconfig.RemoveUser.Permission, + + async run(interaction) { + const { getRole } = interaction.client; + const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); + const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); + + if (!SupportStaff || !Admin) { + return interaction.reply("Missing staff/admin roles in config."); + } + 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(supportbot.Embed.Colours.Warn); + return interaction.reply({ embeds: [NoPerms], flags: MessageFlags.Ephemeral }); + } + + const userToRemove = interaction.options.getUser("user"); + const ticketChannel = interaction.channel; + + if ( + (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || + (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText) + ) { + const onlyInTicket = new EmbedBuilder() + .setTitle(msgconfig.RemoveUser.NotInTicket_Title) + .setDescription(msgconfig.RemoveUser.NotInTicket_Description) + .setColor(supportbot.Embed.Colours.Error); + return interaction.reply({ embeds: [onlyInTicket], flags: MessageFlags.Ephemeral }); + } + + try { + if (supportbot.Ticket.TicketType === "threads") { + await ticketChannel.members.remove(userToRemove.id); + } else { + await ticketChannel.permissionOverwrites.delete(userToRemove.id); + } + + db.removeUserFromTicket(ticketChannel.id, userToRemove.id); + + const removedEmbed = new EmbedBuilder() + .setTitle(msgconfig.RemoveUser.Removed_Title) + .setDescription(msgconfig.RemoveUser.Removed_Message) + .setColor(supportbot.Embed.Colours.Success); + + await interaction.reply({ embeds: [removedEmbed], flags: MessageFlags.Ephemeral }); + + const removedFromTicketEmbed = new EmbedBuilder() + .setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title) + .setDescription( + msgconfig.RemoveUser.RemovedFromTicket_Description.replace( + "%channel_link%", + `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})` + ) + ) + .setColor(supportbot.Embed.Colours.General); + + await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => {}); + } catch (err) { + console.error("Error removing user from the ticket:", err); + + const errorEmbed = new EmbedBuilder() + .setTitle(msgconfig.RemoveUser.Error_Title) + .setDescription(msgconfig.RemoveUser.Error_Removing) + .setColor(supportbot.Embed.Colours.Error); + + await interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral }); + } + }, +}); diff --git a/Commands/ticket.js b/Commands/Tickets/ticket.js similarity index 74% rename from Commands/ticket.js rename to Commands/Tickets/ticket.js index ed84267..fc110cf 100644 --- a/Commands/ticket.js +++ b/Commands/Tickets/ticket.js @@ -10,16 +10,22 @@ const { StringSelectMenuOptionBuilder, ButtonStyle, MessageCollector, + MessageFlags, + SeparatorBuilder, + ContainerBuilder, + TextDisplayBuilder } = require("discord.js"); const yaml = require("js-yaml"); +const db = require("../../Structures/Database.js"); + const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.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"); -const TicketNumberID = require("../Structures/TicketID.js"); +const Command = require("../../Structures/Command.js"); +const TicketNumberID = require("../../Structures/TicketID.js"); async function isBlacklisted(userId) { let blacklistData; @@ -40,16 +46,11 @@ async function isBlacklisted(userId) { async function getClockedInUsers(guild) { const clockedInUsers = new Set(); - - const profilesDir = './Data/Profiles'; - const profileFiles = fs.readdirSync(profilesDir); - - for (const file of profileFiles) { - const profileData = JSON.parse(fs.readFileSync(`${profilesDir}/${file}`, "utf8")); - if (profileData.clockedIn) { - const userId = file.replace('.json', ''); - const member = await guild.members.fetch(userId); - if (member) clockedInUsers.add(member.id); + const rows = guild.members.cache.map(m => m.id); + for (const id of rows) { + const profile = db.getProfile(id); + if (profile && profile.clockedIn) { + clockedInUsers.add(id); } } return clockedInUsers; @@ -61,14 +62,11 @@ async function askTicketQuestions(ticketChannel, interaction, questions) { const sendNextQuestion = async () => { if (questionIndex < questions.length) { - const questionsEmbed = new EmbedBuilder() .setDescription(`**${questionIndex + 1}.** ${questions[questionIndex]}`) - .setColor(supportbot.Embed.Colours.General) + .setColor(supportbot.Embed.Colours.General); - const questionMessage = await ticketChannel.send({ - embeds: [questionsEmbed], - }); + const questionMessage = await ticketChannel.send({ embeds: [questionsEmbed] }); const collector = new MessageCollector(ticketChannel, { filter: (m) => m.author.id === interaction.user.id, @@ -83,12 +81,12 @@ async function askTicketQuestions(ticketChannel, interaction, questions) { 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)) + .setDescription( + msgconfig.Ticket.TicketQuestions.Details.Description.replace("%user%", interaction.user.id) + ) .setColor(supportbot.Embed.Colours.General) .addFields( answers.map((a) => ({ @@ -99,18 +97,14 @@ async function askTicketQuestions(ticketChannel, interaction, questions) { ) .setTimestamp(); - await ticketChannel.send({ - embeds: [summaryEmbed] - }); + await ticketChannel.send({ embeds: [summaryEmbed] }); } }; - // Start asking the first question sendNextQuestion(); } async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts = 0) { - try { const clockedInUsers = await getClockedInUsers(interaction.guild); @@ -124,7 +118,7 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte const claimButton = new ButtonBuilder() .setCustomId(`claimticket-${ticketChannel.id}`) - .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || 'Claim Ticket') + .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || "Claim Ticket") .setStyle(ButtonStyle.Primary); const row = new ActionRowBuilder().addComponents(claimButton); @@ -135,20 +129,25 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte components: [row], }); - const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.member.roles.cache.has(Staff.id); + const filter = (i) => + i.customId === `claimticket-${ticketChannel.id}` && + i.member.roles.cache.has(Staff.id); const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 }); - collector.on('collect', async i => { + collector.on("collect", async (i) => { await ticketChannel.send({ content: `<@${i.user.id}> has claimed the ticket.` }); }); - collector.on('end', async collected => { + collector.on("end", async (collected) => { if (collected.size === 0) { await claimMessage.delete(); - await claimChannel.send({ - content: `Ticket is still unclaimed. \nCC: [<@&${Staff.id}> <@&${Admin.id}]`, - embeds: [new EmbedBuilder().setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`).setColor(supportbot.Embed.Colours.Warn)], + content: `Ticket is still unclaimed. \nCC: [<@&${Staff.id}> <@&${Admin.id}>]`, + embeds: [ + new EmbedBuilder() + .setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`) + .setColor(supportbot.Embed.Colours.Warn), + ], }); } }); @@ -161,22 +160,27 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte } const clockedInArray = Array.from(clockedInUsers); - const assignedUserId = clockedInArray[Math.floor(Math.random() * clockedInArray.length)]; + const assignedUserId = + clockedInArray[Math.floor(Math.random() * clockedInArray.length)]; const assignedUser = await interaction.guild.members.fetch(assignedUserId); const claimEmbed = new EmbedBuilder() .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle) - .setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage.replace('%user%', interaction.user.id)) + .setDescription( + msgconfig.Ticket.ClaimTickets.ClaimMessage.replace("%user%", interaction.user.id) + ) .setColor(supportbot.Embed.Colours.General); const claimButton = new ButtonBuilder() .setCustomId(`claimticket-${ticketChannel.id}`) - .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || 'Claim Ticket') + .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || "Claim Ticket") .setStyle(ButtonStyle.Primary); const row = new ActionRowBuilder().addComponents(claimButton); - const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel); + const claimChannel = await interaction.guild.channels.cache.get( + supportbot.Ticket.ClaimTickets.Channel + ); if (!claimChannel) { console.error("Claim channel not found"); return null; @@ -188,21 +192,28 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte components: [row], }); - const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.user.id === assignedUserId; + const filter = (i) => + i.customId === `claimticket-${ticketChannel.id}` && i.user.id === assignedUserId; const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 }); - collector.on('collect', async i => { - await ticketChannel.send({ content: `<@${assignedUserId}>` }).then(msg => setTimeout(() => msg.delete(), 5000)); + collector.on("collect", async (i) => { + await ticketChannel + .send({ content: `<@${assignedUserId}>` }) + .then((msg) => setTimeout(() => msg.delete(), 5000)); }); - collector.on('end', async collected => { + collector.on("end", async (collected) => { if (collected.size === 0) { await claimMessage.delete(); if (attempts >= 1) { await claimChannel.send({ content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`, - embeds: [new EmbedBuilder().setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`).setColor(supportbot.Embed.Colours.Warn)], + embeds: [ + new EmbedBuilder() + .setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`) + .setColor(supportbot.Embed.Colours.Warn), + ], }); const claimEmbed = new EmbedBuilder() @@ -223,17 +234,24 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte components: [row], }); - const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.member.roles.cache.has(Staff.id); - const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 }); + const filter = (i) => + i.customId === `claimticket-${ticketChannel.id}` && + i.member.roles.cache.has(Staff.id); + const collector = claimMessage.createMessageComponentCollector({ + filter, + time: 120000, + }); - collector.on('collect', async i => { + collector.on("collect", async (i) => { console.log(`Ticket ${ticketChannel.id} claimed by user ${i.user.id}`); await ticketChannel.member.add(i.user.id); }); - collector.on('end', async collected => { + collector.on("end", async (collected) => { if (collected.size === 0) { - console.log(`No one claimed the ticket (${ticketChannel.id}), notifying all staff`); + console.log( + `No one claimed the ticket (${ticketChannel.id}), notifying all staff` + ); await claimMessage.delete(); await claimChannel.send({ @@ -244,7 +262,9 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte } else { const ReassigningEmbed = new EmbedBuilder() .setTitle(msgconfig.Ticket.ClaimTickets.ReassigningTitle) - .setDescription(msgconfig.Ticket.ClaimTickets.ReassigningMessage.replace('%user%', assignedUserId)) + .setDescription( + msgconfig.Ticket.ClaimTickets.ReassigningMessage.replace("%user%", assignedUserId) + ) .setColor(supportbot.Embed.Colours.General); await claimChannel.send({ embeds: [ReassigningEmbed], components: [] }); @@ -256,9 +276,6 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte return assignedUserId; } catch (error) { console.error("Error in assignTicketToUser function:", error); - if (error instanceof CombinedError) { - console.error("CombinedError details:", error.errors); - } return null; } } @@ -283,16 +300,17 @@ module.exports = new Command({ if (await isBlacklisted(interaction.user.id)) { return interaction.reply({ content: msgconfig.Ticket.Blacklisted, - ephemeral: true, + flags: MessageFlags.Ephemeral }); } let TicketReason = null; if (supportbot.Ticket.TicketReason) { - TicketReason = interaction.reason || null; // Retrieve the reason from the interaction object + TicketReason = interaction.reason || null; } - let TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); + let TicketData = { tickets: db.getAllTickets() }; + const { getRole, getChannel } = interaction.client; let User = interaction.guild.members.cache.get(interaction.user.id); @@ -308,7 +326,7 @@ module.exports = new Command({ color: supportbot.Embed.Colours.Warn, }, ], - ephemeral: true, + flags: MessageFlags.Ephemeral }); } @@ -326,7 +344,7 @@ module.exports = new Command({ ) { return await interaction.reply({ embeds: [TicketExists], - ephemeral: true, + flags: MessageFlags.Ephemeral }); } @@ -335,7 +353,7 @@ module.exports = new Command({ if (!Staff || !Admin) return interaction.reply({ content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", - ephemeral: true, + flags: MessageFlags.Ephemeral }); let ticketChannel; @@ -361,7 +379,7 @@ module.exports = new Command({ if (!category) { return interaction.reply({ content: "The ticket category does not exist!", - ephemeral: true, + flags: MessageFlags.Ephemeral }); } @@ -372,7 +390,6 @@ module.exports = new Command({ reason: 'support ticket', }); - // Move to secondary category if the primary one is full if (category.children.size >= 50) { const secondaryCategory = interaction.guild.channels.cache.find( (c) => c.name === supportbot.Ticket.TicketChannelsCategory2 || c.id === supportbot.Ticket.TicketChannelsCategory2 @@ -383,13 +400,12 @@ module.exports = new Command({ } else { return interaction.reply({ content: "The secondary ticket category does not exist!", - ephemeral: true, + flags: MessageFlags.Ephemeral }); } } } - // Assign ticket to a user if claiming tickets is enabled if (supportbot.Ticket.ClaimTickets.Enabled) { const assignedUserId = await assignTicketToUser(ticketChannel, Staff, Admin, interaction); if (!assignedUserId) { @@ -418,17 +434,8 @@ module.exports = new Command({ ViewChannel: false, }); } - - // 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({ + db.addTicket({ id: ticketChannel.id, name: ticketChannel.name, user: interaction.user.id, @@ -439,16 +446,8 @@ module.exports = new Command({ createdAt: new Date().toISOString(), claimedAt: null, }); + TicketData.tickets = db.getAllTickets(); - fs.writeFileSync( - "./Data/TicketData.json", - JSON.stringify(TicketData, null, 4), - (err) => { - if (err) console.error(err); - } - ); - - // Notify the user that the ticket was created const CreatedTicket = new EmbedBuilder() .setDescription( msgconfig.Ticket.TicketCreatedAlert.replace(/%ticketauthor%/g, interaction.user.id) @@ -457,36 +456,59 @@ module.exports = new Command({ .replace(/%ticketreason%/g, TicketSubject) ) .setColor(supportbot.Embed.Colours.General); - await interaction.reply({ embeds: [CreatedTicket], ephemeral: true }); + await interaction.reply({ embeds: [CreatedTicket], flags: MessageFlags.Ephemeral }); - // Create the ticket message - const TicketMessage = new EmbedBuilder() - .setAuthor({ - name: msgconfig.Ticket.TicketAuthorTitle.replace(/%ticketauthor%/g, interaction.user.id) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username), - iconURL: interaction.user.displayAvatarURL(), - }) - .setTitle( - msgconfig.Ticket.TicketTitle.replace(/%ticketauthor%/g, interaction.user.id) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username) + // TICKET MESSAGE - UPDATED WITH COMPONENTS V2 + + const ticketMsgContainer = new ContainerBuilder() + + if (supportbot.Embed.Colours.General) { + ticketMsgContainer.setAccentColor( + parseInt(supportbot.Embed.Colours.General.replace("#", ""), 16) ) - .setDescription( - msgconfig.Ticket.TicketMessage.replace(/%ticketauthor%/g, interaction.user.id) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username) - .replace(/%ticketreason%/g, TicketSubject) - ) - .setColor(supportbot.Embed.Colours.General); - - if (supportbot.Ticket.TicketReason && TicketReason) { - TicketMessage.addFields( - { name: "Reason", value: TicketReason, inline: false } - ); } - // Create the control panel + const ticketAuthor = new TextDisplayBuilder().setContent( + msgconfig.Ticket.TicketAuthorTitle.replace(/%ticketauthor%/g, interaction.user.id) + .replace(/%ticketid%/g, ticketChannel.id) + .replace(/%ticketusername%/g, interaction.user.username), + ); + + ticketMsgContainer.addTextDisplayComponents(ticketAuthor) + + const ticketTitle = new TextDisplayBuilder().setContent( + msgconfig.Ticket.TicketTitle.replace(/%ticketauthor%/g, interaction.user.id) + .replace(/%ticketid%/g, ticketChannel.id) + .replace(/%ticketusername%/g, interaction.user.username) + ); + + ticketMsgContainer.addTextDisplayComponents(ticketTitle) + + const seperator2 = new SeparatorBuilder() + .setDivider(true) + + ticketMsgContainer.addSeparatorComponents(seperator2) + + const ticketDescription = new TextDisplayBuilder().setContent( + msgconfig.Ticket.TicketMessage.replace(/%ticketauthor%/g, interaction.user.id) + .replace(/%ticketid%/g, ticketChannel.id) + .replace(/%ticketusername%/g, interaction.user.username) + .replace(/%ticketreason%/g, TicketSubject) + ); + + ticketMsgContainer.addTextDisplayComponents(ticketDescription) + + const seperator3 = new SeparatorBuilder() + .setDivider(true) + + ticketMsgContainer.addSeparatorComponents(seperator3) + + const ticketReason = new TextDisplayBuilder().setContent( + `**Reason:**\n ${TicketReason}` + ); + + const selectMenuRow = new ActionRowBuilder(); + const SelectMenus = new StringSelectMenuBuilder() .setCustomId("ticketcontrolpanel") .setPlaceholder("Ticket Control Panel") @@ -535,13 +557,26 @@ module.exports = new Command({ ); } - const row2 = new ActionRowBuilder().addComponents(SelectMenus); + selectMenuRow.addComponents(SelectMenus) + + ticketMsgContainer.addActionRowComponents(selectMenuRow) + + if (supportbot.Ticket.TicketReason && TicketReason) { + ticketMsgContainer.addTextDisplayComponents(ticketReason) + } await ticketChannel.send({ - embeds: [TicketMessage], - components: [row2], + flags: MessageFlags.IsComponentsV2, + components: [ticketMsgContainer], }); + if (supportbot.Ticket.Questions.Enabled) { + const questions = supportbot.Ticket.Questions.List || []; + + if (questions.length > 0) { + await askTicketQuestions(ticketChannel, interaction, questions); + } +} } catch (error) { console.error("Error in run method:", error); diff --git a/Commands/Tickets/ticketpanel.js b/Commands/Tickets/ticketpanel.js new file mode 100644 index 0000000..f880c5a --- /dev/null +++ b/Commands/Tickets/ticketpanel.js @@ -0,0 +1,181 @@ +const fs = require("fs"); +const { + EmbedBuilder, + ApplicationCommandType, + ActionRowBuilder, + ButtonBuilder, + MessageFlags, + ContainerBuilder, + TextDisplayBuilder, + MediaGalleryBuilder, + SeparatorBuilder, + ThumbnailBuilder, + SectionBuilder +} = require("discord.js"); +const yaml = require("js-yaml"); + +const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8")); +const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); +const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); + +const Command = require("../../Structures/Command.js"); +const db = require("../../Structures/Database.js"); + +module.exports = new Command({ + name: cmdconfig.TicketPanel.Command, + description: cmdconfig.TicketPanel.Description, + type: ApplicationCommandType.ChatInput, + options: [], + permissions: cmdconfig.TicketPanel.Permission, + + async run(interaction) { + if (!panelconfig.Panel) { + console.log( + "\u001b[31m", + `[TICKET PANEL]`, + "\u001b[33m", + "Ticket Panel is not setup, You can set this in", `\u001b[31m`, '/Configs/ticket-panel.yml', `\n `, + ); + return interaction.reply({ + content: "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.", + flags: MessageFlags.Ephemeral + }); + } + + const { getChannel } = interaction.client; + + const channel = await getChannel( + supportbot.Ticket.TicketHome, + interaction.guild + ); + + if (!channel) { + console.log(`[TICKET PANEL] ${channelName} channel not found. Please check your config file.`); + return interaction.reply({ + content: `${channelName} channel not found. Please check your config file.`, + flags: MessageFlags.Ephemeral + }); + } + + const panelRow = db.getTicketPanel(); + + if (panelRow) { + try { + await channel.messages.fetch(panelRow.message_id); + return interaction.reply({ + content: "Ticket panel message already exists.", + flags: MessageFlags.Ephemeral + }); + } catch (err) {} + } + + const panelContainer = new ContainerBuilder() + + if (panelconfig.Style.Color) { + panelContainer.setAccentColor( + parseInt(panelconfig.Style.Color.replace("#", ""), 16) + ) + } + + const panelTitle = new TextDisplayBuilder().setContent( + panelconfig.Style.Title, + ); + + const topSeperator = new SeparatorBuilder() + .setDivider(true) + + if (panelconfig.Settings.TopTitle) { + panelContainer.addTextDisplayComponents(panelTitle) + } + + if (panelconfig.Settings.TopDivider) { + panelContainer.addSeparatorComponents(topSeperator) + } + + const panelDesc = new TextDisplayBuilder().setContent( + panelconfig.Style.Description, + ); + + if (panelconfig.Style.Layout === "3") { + panelContainer.addTextDisplayComponents(panelDesc) + } + + const createTicketButton = new ButtonBuilder() + .setCustomId("createticket") + .setLabel(panelconfig.Button.Text) + .setEmoji(panelconfig.Button.Emoji) + .setStyle(panelconfig.Button.Color); + + const middleSection = new SectionBuilder() + .addTextDisplayComponents(panelDesc) + .setButtonAccessory(createTicketButton) + + if (panelconfig.Style.Layout === "1") { + panelContainer.addSectionComponents(middleSection) + } + + const panelImage = new MediaGalleryBuilder() + .addItems([ + { + media: { + url: panelconfig.Style.Image, + }, + } + ]) + + if (panelconfig.Settings.Image) { + panelContainer.addMediaGalleryComponents(panelImage) + } + + // START OF STYLE 2 - TEXT UNDER THE IMAGE + + const bottomSeperator = new SeparatorBuilder() + .setDivider(true) + + if (panelconfig.Settings.BottomTitle) { + panelContainer.addTextDisplayComponents(panelTitle) + } + + if (panelconfig.Settings.BottomDivider) { + panelContainer.addSeparatorComponents(bottomSeperator) + } + + if (panelconfig.Style.Layout === "2") { + panelContainer.addSectionComponents(middleSection) + } + + // START OF STYLE 3 + + const buttonRow = new ActionRowBuilder(); + const ticketButtonRow = new ButtonBuilder() + .setCustomId("createticket") + .setLabel(panelconfig.Button.Text) + .setEmoji(panelconfig.Button.Emoji) + .setStyle(panelconfig.Button.Color); + buttonRow.addComponents(ticketButtonRow) + + if (panelconfig.Style.Layout === "3") { + panelContainer.addActionRowComponents(buttonRow) + } + + try { + const message = await channel.send({ + flags: MessageFlags.IsComponentsV2, + components: [panelContainer], + }); + + db.saveTicketPanel(message.id, channel.id); + + return interaction.reply({ + content: "Ticket panel message has been sent!", + flags: MessageFlags.Ephemeral + }); + } catch (e) { + console.log("Error sending message:", e); + return interaction.reply({ + content: "An error occurred while sending the ticket panel message.", + flags: MessageFlags.Ephemeral + }); + } + }, +}); diff --git a/Commands/embed.js b/Commands/Utils/embed.js similarity index 95% rename from Commands/embed.js rename to Commands/Utils/embed.js index b38470f..3024436 100644 --- a/Commands/embed.js +++ b/Commands/Utils/embed.js @@ -9,9 +9,10 @@ const { TextInputBuilder, TextInputStyle, ButtonStyle, - ComponentType + ComponentType, + MessageFlags, } = require('discord.js'); -const Command = require('../Structures/Command.js'); +const Command = require('../../Structures/Command.js'); const fs = require('fs'); const yaml = require('js-yaml'); @@ -19,7 +20,6 @@ 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')); -// Helper function to validate and convert hex color function parseColor(color) { if (!color) return 0x5865F2; if (typeof color === 'number') return color; @@ -46,7 +46,6 @@ module.exports = new Command({ 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); @@ -61,12 +60,11 @@ module.exports = new Command({ .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 }); + return interaction.reply({ embeds: [NoPerms], flags: MessageFlags.Ephemeral }); } const channel = interaction.options.getChannel('channel'); - // Initial message state const messageData = { type: 'embed', title: 'New Embed', @@ -79,7 +77,6 @@ module.exports = new Command({ timestamp: false }; - // Create initial embed function createEmbed() { const embed = new EmbedBuilder() .setTitle(messageData.title) @@ -105,7 +102,6 @@ module.exports = new Command({ return embed; } - // Create type selector const typeRow = new ActionRowBuilder() .addComponents( new StringSelectMenuBuilder() @@ -125,12 +121,10 @@ module.exports = new Command({ ]) ); - // 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() @@ -173,7 +167,6 @@ module.exports = new Command({ buttons.push(row1, row2); } else { - // Text mode only shows edit text button const textRow = new ActionRowBuilder() .addComponents( new ButtonBuilder() @@ -198,7 +191,6 @@ module.exports = new Command({ return buttons; } - // Function to update the message preview async function updatePreview(i) { if (messageData.type === 'embed') { await i.update({ @@ -207,7 +199,6 @@ module.exports = new Command({ components: [typeRow, ...createButtons('embed')] }); } else { - // For text mode, just show the text directly await i.update({ content: messageData.description, embeds: [], @@ -216,15 +207,13 @@ module.exports = new Command({ } } - // 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 + flags: MessageFlags.Ephemeral }); - // Create collector for interactions const collector = message.createMessageComponentCollector({ time: 900000 }); @@ -233,7 +222,7 @@ module.exports = new Command({ if (i.user.id !== interaction.user.id) { return i.reply({ content: 'You cannot edit this message.', - ephemeral: true + flags: MessageFlags.Ephemeral }); } @@ -422,7 +411,7 @@ module.exports = new Command({ } catch (error) { await i.reply({ content: 'Failed to send message. Please check channel permissions.', - ephemeral: true + flags: MessageFlags.Ephemeral }); } break; diff --git a/Commands/mod.js b/Commands/Utils/mod.js similarity index 95% rename from Commands/mod.js rename to Commands/Utils/mod.js index 719fa29..8cb7e56 100644 --- a/Commands/mod.js +++ b/Commands/Utils/mod.js @@ -1,11 +1,11 @@ const fs = require("fs"); -const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = require("discord.js"); +const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = 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"); +const Command = require("../../Structures/Command.js"); // Function to chunk array into smaller arrays function chunkArray(arr, size) { @@ -116,7 +116,7 @@ module.exports = new Command({ 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 }); + return interaction.reply({ embeds: [alreadyBlacklistedEmbed], flags: MessageFlags.Ephemeral }); } blacklistedUsers.push(user.id); @@ -141,7 +141,7 @@ module.exports = new Command({ return interaction.reply({ embeds: [successEmbed], - ephemeral: true + flags: MessageFlags.Ephemeral }); } else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) { @@ -149,7 +149,7 @@ module.exports = new Command({ 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 }); + return interaction.reply({ embeds: [notBlacklistedEmbed], flags: MessageFlags.Ephemeral }); } blacklistedUsers = blacklistedUsers.filter(id => id !== user.id); @@ -172,14 +172,14 @@ module.exports = new Command({ blacklistChannel.send({ embeds: [blacklistLogEmbed] }); } - return interaction.reply({ embeds: [removedEmbed], ephemeral: true }); + return interaction.reply({ embeds: [removedEmbed], flags: MessageFlags.Ephemeral }); } 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 }); + return interaction.reply({ embeds: [noBlacklistedUsersEmbed], flags: MessageFlags.Ephemeral }); } const chunkedUsers = chunkArray(blacklistedUsers, 5); @@ -210,7 +210,7 @@ module.exports = new Command({ const response = await interaction.reply({ embeds: [createEmbed(currentPage)], components: chunkedUsers.length > 1 ? [buttons] : [], - ephemeral: true + flags: MessageFlags.Ephemeral }); if (chunkedUsers.length <= 1) return; @@ -224,7 +224,7 @@ module.exports = new Command({ if (i.user.id !== interaction.user.id) { return i.reply({ content: 'You cannot use these buttons.', - ephemeral: true + flags: MessageFlags.Ephemeral }); } @@ -255,7 +255,7 @@ module.exports = new Command({ 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 }); + interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral }); } } }); \ No newline at end of file diff --git a/Commands/settings.js b/Commands/Utils/settings.js similarity index 77% rename from Commands/settings.js rename to Commands/Utils/settings.js index 255ee1c..6131921 100644 --- a/Commands/settings.js +++ b/Commands/Utils/settings.js @@ -1,33 +1,35 @@ -const { Command } = require('../Structures/Addon.js'); +const { Command } = require('../../Structures/Addon.js'); const Discord = require("discord.js"); const fs = require("fs"); const yaml = require("js-yaml"); -const path = require("path"); +const db = require("../../Structures/Database.js"); const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const settingsFilePath = path.resolve(__dirname, '../Data/settings.json'); -function loadSettings() { - try { - if (fs.existsSync(settingsFilePath)) { - const data = fs.readFileSync(settingsFilePath, 'utf8'); - return JSON.parse(data); - } else { - return {}; - } - } catch (error) { - console.error("Error reading settings file:", error); - return {}; - } +async function loadSettings() { + return new Promise((resolve, reject) => { + db.get("SELECT data FROM settings WHERE id = 1", (err, row) => { + if (err) return reject(err); + if (!row) return resolve({}); + try { + resolve(JSON.parse(row.data)); + } catch (e) { + resolve({}); + } + }); + }); } -function saveSettings(settings) { - try { - fs.writeFileSync(settingsFilePath, JSON.stringify(settings, null, 2), 'utf8'); - } catch (error) { - console.error("Error writing settings file:", error); - } +async function saveSettings(settings) { + const data = JSON.stringify(settings); + return new Promise((resolve, reject) => { + db.run( + "INSERT INTO settings (id, data) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data", + [data], + (err) => (err ? reject(err) : resolve()) + ); + }); } module.exports = new Command({ @@ -119,7 +121,7 @@ module.exports = new Command({ async run(interaction) { const subcommand = interaction.options.getSubcommand(); - const settings = loadSettings(); + const settings = await loadSettings(); if (subcommand === 'setstatus') { const status = interaction.options.getString('status'); @@ -131,15 +133,15 @@ module.exports = new Command({ }); settings.status = status; - saveSettings(settings); + await saveSettings(settings); const embed = new Discord.EmbedBuilder() .setDescription(`Status set to **${status.charAt(0).toUpperCase() + status.slice(1)}**`) .setColor(supportbot.Embed.Colours.General); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); } catch (error) { - return interaction.reply({ content: `Failed to set status: ${error.message}`, ephemeral: true }); + return interaction.reply({ content: `Failed to set status: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); } } @@ -154,15 +156,15 @@ module.exports = new Command({ }); settings.activity = { type: activityType, message: activityMessage }; - saveSettings(settings); + await saveSettings(settings); const embed = new Discord.EmbedBuilder() .setDescription(`Activity set to **${activityType.toLowerCase()} ${activityMessage}**`) .setColor(supportbot.Embed.Colours.General); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); } catch (error) { - return interaction.reply({ content: `Failed to set activity: ${error.message}`, ephemeral: true }); + return interaction.reply({ content: `Failed to set activity: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); } } else if (subcommand === 'streammode') { const toggle = interaction.options.getString('toggle'); @@ -170,7 +172,7 @@ module.exports = new Command({ if (toggle === 'on') { if (!twitchUrl) { - return interaction.reply({ content: 'Please provide a Twitch URL to stream.', ephemeral: true }); + return interaction.reply({ content: 'Please provide a Twitch URL to stream.', flags: Discord.MessageFlags.Ephemeral }); } try { @@ -180,15 +182,15 @@ module.exports = new Command({ }); settings.streaming = { active: true, url: twitchUrl }; - saveSettings(settings); + await saveSettings(settings); const embed = new Discord.EmbedBuilder() .setDescription(`**Streaming mode enabled!**\nStreaming: [Twitch](${twitchUrl})`) .setColor(supportbot.Embed.Colours.General); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); } catch (error) { - return interaction.reply({ content: `Failed to enable streaming mode: ${error.message}`, ephemeral: true }); + return interaction.reply({ content: `Failed to enable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); } } else if (toggle === 'off') { try { @@ -198,15 +200,15 @@ module.exports = new Command({ }); settings.streaming = { active: false }; - saveSettings(settings); + await saveSettings(settings); const embed = new Discord.EmbedBuilder() .setDescription(`**Streaming mode disabled!**`) .setColor(supportbot.Embed.Colours.General); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); } catch (error) { - return interaction.reply({ content: `Failed to disable streaming mode: ${error.message}`, ephemeral: true }); + return interaction.reply({ content: `Failed to disable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); } } } else if (subcommand === 'setnickname') { @@ -218,18 +220,18 @@ module.exports = new Command({ await botMember.setNickname(nickname); settings.nickname = nickname; - saveSettings(settings); + await saveSettings(settings); const embed = new Discord.EmbedBuilder() .setDescription(`Bot nickname set to **${nickname}**`) .setColor(supportbot.Embed.Colours.General); - return interaction.reply({ embeds: [embed], ephemeral: true }); + return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); } catch (error) { - return interaction.reply({ content: `Failed to set nickname: ${error.message}`, ephemeral: true }); + return interaction.reply({ content: `Failed to set nickname: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); } } else { - return interaction.reply({ content: 'Invalid command usage.', ephemeral: true }); + return interaction.reply({ content: 'Invalid command usage.', flags: Discord.MessageFlags.Ephemeral }); } }, -}); \ No newline at end of file +}); diff --git a/Commands/addUser.js b/Commands/addUser.js deleted file mode 100644 index 130f440..0000000 --- a/Commands/addUser.js +++ /dev/null @@ -1,217 +0,0 @@ -const fs = require("fs"); -const { - EmbedBuilder, - ApplicationCommandOptionType, - ApplicationCommandType, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - ChannelType, -} = require("discord.js"); -const yaml = require("js-yaml"); - -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); - -const Command = require("../Structures/Command.js"); - -module.exports = new Command({ - name: cmdconfig.AddUser.Command, - description: cmdconfig.AddUser.Description, - type: ApplicationCommandType.ChatInput, - options: [ - { - name: "user", - description: "The user to add", - type: ApplicationCommandOptionType.User, - required: true, - }, - ], - permissions: cmdconfig.AddUser.Permission, - - async run(interaction) { - const userToAdd = interaction.options.getUser("user"); - const ticketChannel = interaction.channel; - const ticketDataPath = "./Data/TicketData.json"; - - if ( - (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || - (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText) - ) { - const onlyInTicket = new EmbedBuilder() - .setTitle(msgconfig.AddUser.NotInTicket_Title) - .setDescription(msgconfig.AddUser.NotInTicket_Description) - .setColor(supportbot.Embed.Colours.Error); - - return interaction.reply({ - embeds: [onlyInTicket], - ephemeral: true, - }); - } - - const ticketInviteEmbed = new EmbedBuilder() - .setTitle(msgconfig.AddUser.Invite_Title) - .setDescription(msgconfig.AddUser.Invite_Description.replace("%username%", userToAdd.username)) - .setColor(supportbot.Embed.Colours.General); - - await interaction.reply({ - embeds: [ticketInviteEmbed], - ephemeral: true, - }); - - try { - const dmEmbed = new EmbedBuilder() - .setTitle(msgconfig.AddUser.DM_Title) - .setDescription(msgconfig.AddUser.DM_Description.replace("%channelname%", ticketChannel.name)) - .setColor(supportbot.Embed.Colours.General); - - const acceptButton = new ButtonBuilder() - .setCustomId("accept_invite") - .setLabel(msgconfig.AddUser.Accept_Label) - .setEmoji("✅") - .setStyle(ButtonStyle.Primary); - - const rejectButton = new ButtonBuilder() - .setCustomId("reject_invite") - .setLabel(msgconfig.AddUser.Reject_Label) - .setEmoji("❌") - .setStyle(ButtonStyle.Secondary); - - const row = new ActionRowBuilder().addComponents(acceptButton, rejectButton); - - const dmMessage = await userToAdd.send({ - embeds: [dmEmbed], - components: [row], - }); - - const filter = (i) => i.user.id === userToAdd.id; - const collector = dmMessage.createMessageComponentCollector({ - filter, - time: 60000, - }); - - collector.on("collect", async (i) => { - if (i.customId === "accept_invite") { - try { - // Add user to thread or channel based on TicketType - if (supportbot.Ticket.TicketType === "threads") { - await ticketChannel.members.add(userToAdd.id); // For threads - } else if (supportbot.Ticket.TicketType === "channels") { - await ticketChannel.permissionOverwrites.create(userToAdd.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); // For channels - } - - const ticketData = JSON.parse(fs.readFileSync(ticketDataPath, "utf8")); - const ticketIndex = ticketData.tickets.findIndex((t) => t.id === ticketChannel.id); - if (ticketIndex !== -1) { - ticketData.tickets[ticketIndex].subUsers.push(userToAdd.id); - fs.writeFileSync(ticketDataPath, JSON.stringify(ticketData, null, 4)); - } - - const acceptedEmbed = new EmbedBuilder() - .setTitle(msgconfig.AddUser.Accepted_Title) - .setDescription(msgconfig.AddUser.Accepted_Message) - .setColor(supportbot.Embed.Colours.Success); - - await i.update({ - embeds: [acceptedEmbed], - components: [], - }); - - // Send the added user ticket message in the ticket channel - - const addeduserTicket = new EmbedBuilder() - .setTitle(msgconfig.AddUser.Added_Title) - .setDescription(msgconfig.AddUser.Added_Description.replace('%username%', userToAdd.username)) - .setColor(supportbot.Embed.Colours.General); - - await ticketChannel.send({ - embeds: [addeduserTicket], - }); - - - const addedToTicketEmbed = new EmbedBuilder() - .setTitle(msgconfig.AddUser.AddedToTicket_Title) - .setDescription(msgconfig.AddUser.AddedToTicket_Description.replace( - "%channel_link%", - `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})` - )) - .setColor(supportbot.Embed.Colours.General); - - const ticketLinkButton = new ButtonBuilder() - .setLabel(msgconfig.AddUser.ViewTicket_Label) - .setURL(`https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id}`) - .setStyle(ButtonStyle.Link); - - await userToAdd.send({ - embeds: [addedToTicketEmbed], - components: [new ActionRowBuilder().addComponents(ticketLinkButton)], - }); - - } catch (err) { - console.error("Error adding user to the ticket:", err); - - const errorEmbed = new EmbedBuilder() - .setTitle(msgconfig.AddUser.Error_Title) - .setDescription(msgconfig.AddUser.Error_Adding) - .setColor(supportbot.Embed.Colours.Error); - - await i.update({ - embeds: [errorEmbed], - components: [], - }); - } - } else { - const declinedEmbed = new EmbedBuilder() - .setTitle(msgconfig.AddUser.Declined_Title) - .setDescription(msgconfig.AddUser.Declined_Message) - .setColor(supportbot.Embed.Colours.Error); - - await i.update({ - embeds: [declinedEmbed], - components: [], - }); - } - }); - - collector.on("end", (collected) => { - if (collected.size === 0) { - userToAdd.send({ - embeds: [ - new EmbedBuilder() - .setTitle(msgconfig.AddUser.NoResponse_Title) - .setDescription(msgconfig.AddUser.NoResponse_Message) - .setColor(supportbot.Embed.Colours.Warning), - ], - }); - } - }); - } catch (error) { - if (error.code === 50007) { - const errorEmbed = new EmbedBuilder() - .setTitle(msgconfig.AddUser.DM_Error_Title) - .setDescription(msgconfig.AddUser.DM_Error_Description.replace("%username%", userToAdd.username)) - .setColor(supportbot.Embed.Colours.Error); - - await ticketChannel.send({ - embeds: [errorEmbed], - }); - } else { - console.error("Error sending DM:", error); - - const unexpectedErrorEmbed = new EmbedBuilder() - .setTitle(msgconfig.AddUser.Unexpected_Error_Title) - .setDescription(msgconfig.AddUser.Unexpected_Error_Description.replace("%error%", error.message)) - .setColor(supportbot.Embed.Colours.Error); - - await ticketChannel.send({ - embeds: [unexpectedErrorEmbed], - }); - } - } - }, -}); diff --git a/Commands/profile.js b/Commands/profile.js deleted file mode 100644 index 2368525..0000000 --- a/Commands/profile.js +++ /dev/null @@ -1,320 +0,0 @@ -const { EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ApplicationCommandType, ActionRowBuilder, ButtonBuilder, ButtonStyle, ApplicationCommandOptionType } = require("discord.js"); -const fs = require("fs"); -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"); - -const clockedInUsers = new Set(); - -function updateClockedInStatus(profileEmbed, clockedIn) { - profileEmbed.data.fields = profileEmbed.data.fields.map(field => { - if (field.name === 'Clocked In Status') { - field.value = clockedIn ? '✅ Clocked In' : '❌ Clocked Out'; - } - return field; - }); - return profileEmbed; -} - -module.exports = new Command({ - name: cmdconfig.Profile.Command, - description: cmdconfig.Profile.Description, - type: ApplicationCommandType.ChatInput, - permissions: cmdconfig.Profile.Permission, - options: [ - { - name: 'user', - description: 'The user to view the profile of', - type: ApplicationCommandOptionType.User, - required: false - } - ], - - async run(interaction) { - await interaction.deferReply({ ephemeral: true }); - - const userOption = interaction.options.getUser('user'); - const viewingUser = userOption || interaction.user; - const viewingUserId = viewingUser.id; - const interactionUserId = interaction.user.id; - - const user = interaction.guild.members.cache.get(viewingUserId); - const { getRole, getChannel } = interaction.client; - - const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - - if (!Staff || !Admin) { - return interaction.editReply({ - content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", - ephemeral: true, - }); - } - - const isStaff = user.roles.cache.has(Staff.id) || user.roles.cache.has(Admin.id); - const isOwnProfile = interactionUserId === viewingUserId; - - const profilePath = `./Data/Profiles/${viewingUserId}.json`; - let profileData; - - if (fs.existsSync(profilePath)) { - profileData = JSON.parse(fs.readFileSync(profilePath, "utf8")); - } else { - profileData = { - bio: "", - timezone: "", - clockedIn: false - }; - fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2)); - } - - const { bio, timezone, clockedIn } = profileData; - - let profileEmbed = new EmbedBuilder() - .setTitle(`${viewingUser.username}'s Profile`) - .setColor(supportbot.Embed.Colours.General) - .setThumbnail(viewingUser.displayAvatarURL()) - .addFields( - { name: 'Bio', value: bio || 'No bio set.', inline: false }, - { name: 'Timezone', value: timezone || 'No timezone set.', inline: true } - ); - - if (isStaff) { - profileEmbed.addFields({ name: 'Clocked In Status', value: clockedIn ? '✅ Clocked In' : '❌ Clocked Out', inline: true }); - } - - const buttonRow = new ActionRowBuilder(); - if (isOwnProfile) { - buttonRow.addComponents( - new ButtonBuilder() - .setCustomId('editProfile') - .setLabel('Edit Profile') - .setStyle(ButtonStyle.Secondary) - ); - - if (isStaff) { - buttonRow.addComponents( - new ButtonBuilder() - .setCustomId('clockInOut') - .setLabel(clockedIn ? 'Clock Out' : 'Clock In') - .setStyle(ButtonStyle.Secondary) - ); - } - } - - if (isStaff && supportbot.Ticket.ClaimTickets.Enabled) { - buttonRow.addComponents( - new ButtonBuilder() - .setCustomId('viewTicketStats') - .setLabel('View Ticket Stats') - .setStyle(ButtonStyle.Secondary) - ); - } - - await interaction.editReply({ embeds: [profileEmbed], components: buttonRow.components.length > 0 ? [buttonRow] : [] }); - - const filter = i => ( - ['editProfile', 'clockInOut', 'viewTicketStats', 'showOpenTickets', 'backToStats', 'backToProfile'].includes(i.customId) - && i.user.id === interactionUserId - ); - - const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 }); - - collector.on('collect', async i => { - if (i.customId === 'editProfile') { - const modal = new ModalBuilder() - .setCustomId('editProfileModal') - .setTitle('Edit Profile') - .addComponents( - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId('bio') - .setLabel('Bio') - .setStyle(TextInputStyle.Paragraph) - .setPlaceholder('Enter your bio') - .setValue(String(bio)) - .setRequired(false) - ), - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId('timezone') - .setLabel('Timezone') - .setStyle(TextInputStyle.Short) - .setPlaceholder('Enter your timezone') - .setValue(String(timezone)) - .setRequired(false) - ) - ); - - await i.showModal(modal); - } - - 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); - } else { - clockedInUsers.delete(viewingUserId); - } - - const updatedButtonRow = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('editProfile') - .setLabel('Edit Profile') - .setStyle(ButtonStyle.Secondary) - ); - - 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 }); - } - - 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.') - .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] }); - } - - 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; - - 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 === 'backToProfile') { - await i.update({ embeds: [profileEmbed], components: buttonRow.components.length > 0 ? [buttonRow] : [] }); - } - }); - } -}); - -module.exports.clockedInUsers = clockedInUsers; diff --git a/Commands/removeUser.js b/Commands/removeUser.js deleted file mode 100644 index f5b6512..0000000 --- a/Commands/removeUser.js +++ /dev/null @@ -1,134 +0,0 @@ -const fs = require("fs"); -const { - EmbedBuilder, - ApplicationCommandOptionType, - ApplicationCommandType, - ChannelType, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, -} = require("discord.js"); -const yaml = require("js-yaml"); - -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); - -const Command = require("../Structures/Command.js"); - -module.exports = new Command({ - name: cmdconfig.RemoveUser.Command, - description: cmdconfig.RemoveUser.Description, - type: ApplicationCommandType.ChatInput, - options: [ - { - name: "user", - description: "The user to remove", - type: ApplicationCommandOptionType.User, - required: true, - }, - ], - permissions: cmdconfig.RemoveUser.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); - if (!SupportStaff || !Admin) { - return interaction.reply( - "Some roles seem to be missing!\nPlease check for errors when starting the bot." - ); - } - - const NoPerms = new EmbedBuilder() - .setTitle("Invalid Permissions!") - .setDescription( - `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\`` - ) - .setColor(supportbot.Embed.Colours.Warn); - - if ( - !interaction.member.roles.cache.has(SupportStaff.id) && - !interaction.member.roles.cache.has(Admin.id) - ) { - return interaction.reply({ embeds: [NoPerms] }); - } - - const userToRemove = interaction.options.getUser("user"); - const ticketChannel = interaction.channel; - const ticketDataPath = "./Data/TicketData.json"; - - // Check if the channel is a ticket thread or text channel based on TicketType - if ( - (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || - (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText) - ) { - const onlyInTicket = new EmbedBuilder() - .setTitle(msgconfig.RemoveUser.NotInTicket_Title) - .setDescription(msgconfig.RemoveUser.NotInTicket_Description) - .setColor(supportbot.Embed.Colours.Error); - - return interaction.reply({ - embeds: [onlyInTicket], - ephemeral: true, - }); - } - - try { - // Remove user from thread or channel based on TicketType - if (supportbot.Ticket.TicketType === "threads") { - await ticketChannel.members.remove(userToRemove.id); // For threads - } else if (supportbot.Ticket.TicketType === "channels") { - await ticketChannel.permissionOverwrites.delete(userToRemove.id); // For channels - } - - // Update the ticket data - const ticketData = JSON.parse(fs.readFileSync(ticketDataPath, "utf8")); - const ticketIndex = ticketData.tickets.findIndex((t) => t.id === ticketChannel.id); - if (ticketIndex !== -1) { - const subUsers = ticketData.tickets[ticketIndex].subUsers; - const userIndex = subUsers.indexOf(userToRemove.id); - if (userIndex !== -1) { - subUsers.splice(userIndex, 1); - fs.writeFileSync(ticketDataPath, JSON.stringify(ticketData, null, 4)); - } - } - - const removedEmbed = new EmbedBuilder() - .setTitle(msgconfig.RemoveUser.Removed_Title) - .setDescription(msgconfig.RemoveUser.Removed_Message) - .setColor(supportbot.Embed.Colours.Success); - - await interaction.reply({ - embeds: [removedEmbed], - ephemeral: true, - }); - - const removedFromTicketEmbed = new EmbedBuilder() - .setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title) - .setDescription(msgconfig.RemoveUser.RemovedFromTicket_Description.replace( - "%channel_link%", - `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})` - )) - .setColor(supportbot.Embed.Colours.General); - - await userToRemove.send({ - embeds: [removedFromTicketEmbed], - }); - - } catch (err) { - console.error("Error removing user from the ticket:", err); - - const errorEmbed = new EmbedBuilder() - .setTitle(msgconfig.RemoveUser.Error_Title) - .setDescription(msgconfig.RemoveUser.Error_Removing) - .setColor(supportbot.Embed.Colours.Error); - - await interaction.reply({ - embeds: [errorEmbed], - ephemeral: true, - }); - } - }, -}); diff --git a/Commands/suggest.js b/Commands/suggest.js deleted file mode 100644 index 65f1c2d..0000000 --- a/Commands/suggest.js +++ /dev/null @@ -1,115 +0,0 @@ -const fs = require("fs"); -const Discord = require("discord.js"); -const yaml = require("js-yaml"); - -// Load configurations -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"); - -// Ensure suggestions file exists and is correctly loaded -let suggestions; -try { - suggestions = require("../Data/SuggestionData.json"); -} catch (error) { - suggestions = {}; -} - -module.exports = new Command({ - name: cmdconfig.Suggestion.Command, - description: cmdconfig.Suggestion.Description, - type: Discord.ApplicationCommandType.ChatInput, - options: [ - { - name: "suggestion", - description: "Create a Suggestion", - type: Discord.ApplicationCommandOptionType.String, - required: true, - }, - ], - permissions: cmdconfig.Suggestion.Permission, - - async run(interaction) { - const { getChannel } = interaction.client; - - const suggestChannel = await getChannel( - supportbot.Suggestions.Channel, - interaction.guild - ); - - const NoChannel = new Discord.EmbedBuilder() - .setTitle("Missing Channel!") - .setDescription(msgconfig.Error.InvalidChannel) - .setColor(supportbot.Embed.Colours.Error); - - if (!suggestChannel) return interaction.reply({ embeds: [NoChannel] }); - - let suggestion = interaction.options.getString("suggestion"); - - const SuggestEmbed = new Discord.EmbedBuilder() - .addFields( - { name: "Suggestion", value: suggestion, inline: true }, - { name: "From", value: `<@${interaction.user.id}>` }, - { name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, value: "0", inline: true }, - { name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, value: "0", inline: true } - ) - .setThumbnail(interaction.user.displayAvatarURL()) - .setFooter({ - text: supportbot.Embed.Footer, - iconURL: interaction.user.displayAvatarURL(), - }) - .setColor(supportbot.Embed.Colours.General); - - const UpvoteButton = new Discord.ButtonBuilder() - .setCustomId("upvote") - .setEmoji(supportbot.Suggestions.UpvoteEmoji) - .setStyle(supportbot.Suggestions.Buttons.Upvote); - - const DownvoteButton = new Discord.ButtonBuilder() - .setCustomId("downvote") - .setEmoji(supportbot.Suggestions.DownvoteEmoji) - .setStyle(supportbot.Suggestions.Buttons.Downvote); - - const RemoveVoteButton = new Discord.ButtonBuilder() - .setCustomId("removevote") - .setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title) - .setStyle(supportbot.Suggestions.Buttons.RemoveVote); // Red button - - const row = new Discord.ActionRowBuilder().addComponents(UpvoteButton, DownvoteButton, RemoveVoteButton); - - const suggestionMsg = await suggestChannel.send({ embeds: [SuggestEmbed], components: [row] }); - - suggestions[suggestionMsg.id] = { - suggestion: suggestion, - author: interaction.user.id, - upvotes: [], - downvotes: [] - }; - - fs.writeFileSync("./Data/SuggestionData.json", JSON.stringify(suggestions, null, 2)); - - if (supportbot.Suggestions.Threads.Enabled) { - await suggestionMsg.startThread({ - name: supportbot.Suggestions.Threads.Title, - autoArchiveDuration: 60, - type: Discord.ChannelType.PublicThread, - reason: supportbot.Suggestions.Threads.Reason, - }); - } - - const Submitted = new Discord.EmbedBuilder() - .setTitle(`${msgconfig.Suggestions.Sent_Title}`) - .setDescription(`${msgconfig.Suggestions.Sent}`) - .addFields( - { name: "Sent to:", value: `<#${suggestChannel.id}>` }, - ) - .setColor(supportbot.Embed.Colours.Success); - - await interaction.reply({ - ephemeral: true, - embeds: [Submitted] - }); - }, -}); diff --git a/Commands/ticketpanel.js b/Commands/ticketpanel.js deleted file mode 100644 index e78f48a..0000000 --- a/Commands/ticketpanel.js +++ /dev/null @@ -1,120 +0,0 @@ -// ticketpanel.js -const fs = require("fs"); -const { - EmbedBuilder, - ApplicationCommandType, - ActionRowBuilder, - ButtonBuilder, -} = require("discord.js"); -const yaml = require("js-yaml"); - -const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8")); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); - -const Command = require("../Structures/Command.js"); - -module.exports = new Command({ - name: cmdconfig.TicketPanel.Command, - description: cmdconfig.TicketPanel.Description, - type: ApplicationCommandType.ChatInput, - options: [], - permissions: cmdconfig.TicketPanel.Permission, - - async run(interaction) { - if (!panelconfig.Panel) { - console.log( - "\u001b[31m", - `[TICKET PANEL]`, - "\u001b[33m", - "Ticket Panel is not setup, You can set this in", `\u001b[31m`, '/Configs/ticket-panel.yml', `\n `, - ); - return interaction.reply({ - content: "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.", - ephemeral: true, - }); - } - - const { getChannel } = interaction.client; - - const channel = await getChannel( - supportbot.Ticket.TicketHome, - interaction.guild - ); - - if (!channel) { - console.log(`[TICKET PANEL] ${channelName} channel not found. Please check your config file.`); - return interaction.reply({ - content: `${channelName} channel not found. Please check your config file.`, - ephemeral: true, - }); - } - - const panelid = JSON.parse(fs.readFileSync("./Data/ticket-panel-id.json", "utf8")); - - try { - await channel.messages.fetch(panelid.TicketPanelID); - return interaction.reply({ - content: "Ticket panel message already exists.", - ephemeral: true, - }); - } catch (error) { - let embed = new EmbedBuilder() - .setTitle(panelconfig.PanelTitle) - .setColor(panelconfig.PanelColour) - .setFooter({ - text: supportbot.Embed.Footer, - iconURL: interaction.user.displayAvatarURL(), - }); - - if (panelconfig.TicketPanel_Description) { - embed.setDescription(panelconfig.PanelMessage); - } - - if (panelconfig.TicketPanel_Thumbnail) { - embed.setThumbnail(panelconfig.PanelThumbnail); - } - - if (panelconfig.TicketPanel_Image) { - embed.setImage(panelconfig.PanelImage); - } - - const createTicketButton = new ButtonBuilder() - .setCustomId("createticket") - .setLabel(panelconfig.Button.Text) - .setEmoji(panelconfig.Button.Emoji) - .setStyle(panelconfig.Button.Color); - - let row = new ActionRowBuilder().addComponents(createTicketButton); - - try { - const message = await channel.send({ - embeds: [embed], - components: [row], - }); - - let data = { - id: panelid.id, - TicketPanelID: message.id, - }; - - fs.writeFileSync( - "./Data/ticket-panel-id.json", - JSON.stringify(data), - "utf8" - ); - - return interaction.reply({ - content: "Ticket panel message has been sent!", - ephemeral: true, - }); - } catch (e) { - console.log("Error sending message:", e); - return interaction.reply({ - content: "An error occurred while sending the ticket panel message.", - ephemeral: true, - }); - } - } - }, -}); \ No newline at end of file diff --git a/Configs/commands.yml b/Configs/commands.yml index 4f3a0cf..58506f1 100644 --- a/Configs/commands.yml +++ b/Configs/commands.yml @@ -73,6 +73,12 @@ Suggestion: Permission: ["SendMessages"] Enabled: true +SuggestionAdmin: + Command: "suggestadmin" + Description: "Update a suggestion’s status." + Permission: ['SendMessages'] + # Disable "Suggestion" to disable this command. + Embed: Command: "embed" Description: "Build a discord embed." diff --git a/Configs/messages.yml b/Configs/messages.yml index 3daca83..1d3504d 100644 --- a/Configs/messages.yml +++ b/Configs/messages.yml @@ -22,27 +22,74 @@ Info: 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." Button: "Visit Site" URL: "https://docs.emeraldsrv.com" - Colour: "#f74343" + Colour: "#45b463" # ---------------------------- # Welcome/Leave Messages # ---------------------------- Welcome: - Embed: - Colour: "#f74343" - Title: "Welcome to ServerName" - Message: ":smile: %joined_user% has just landed!" - Thumbnail: "BOT" + Enabled: true + LayoutStyle: "modern" + # Available Layout Styles + # "modern" - For the new Discord v2 components + # "classic" - Use the classic Discord look. - ImageEnabled: false - ImageURL: "https://animagelink.png/" + Embed: + Colour: "#45b463" + Title: "Welcome to ServerName" + Message: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#1202667199853232129>" + Icon: + Mode: "USER" # USER | BOT | CUSTOM + CustomURL: "https://example.com/custom.png" + Image: + Enabled: false + URL: "https://example.com/banner.png" + Buttons: + Enabled: true + List: + - label: "BuiltByBit" + style: "Link" + url: "https://your-website.com/how-to-play-java" + emoji: "⌨️" + - label: "GitHub" + style: "Link" + url: "https://your-website.com/how-to-play-bedrock" + emoji: "👾" + - label: "Tebex" + style: "Link" + url: "https://your-store.tebex.io/" + emoji: "🛒" + + Modern: + Colour: "#45b463" + Title: "**Welcome to ServerName**" + Body: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#1202667199853232129>" + Icon: + Mode: "BOT" # USER | BOT | CUSTOM + CustomURL: "https://example.com/custom-icon.png" + Image: + Enabled: false + URL: "https://example.com/banner.png" + Buttons: + - label: "BuiltByBit" + style: "Link" + url: "https://your-website.com/how-to-play-java" + emoji: "⌨️" + - label: "GitHub" + style: "Link" + url: "https://your-website.com/how-to-play-bedrock" + emoji: "👾" + - label: "Tebex" + style: "Link" + url: "https://your-store.tebex.io/" + emoji: "🛒" Leave: Embed: Colour: "#f74343" Title: "Goodbye!" - Message: ":slight_frown: %joined_user% has recently dropped out!" + Message: ":slight_frown: %left_user% has recently dropped out!" Thumbnail: "BOT" ImageEnabled: false @@ -70,8 +117,18 @@ Ticket: # <@%ticketauthor%> - Tags the user who created the ticket. # <#%ticketid%> - Mentions the ticket channel. - TicketAuthorTitle: "%ticketusername%'s Ticket" - TicketTitle: "Support Ticket" + # You can use Markdown formatting in this message: + # # Heading 1 + # ## Heading 2 + # ### Heading 3 + # **Bold** text + # *Italic* text + # -# Small text + # - Bullet points + # Links: [text](https://example.com) + + TicketAuthorTitle: "-# %ticketusername%'s Ticket" + TicketTitle: "## Support Ticket" TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you." TicketThumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png" TicketCreatedAlert: ":ticket: Ticked Created: <#%ticketid%>" @@ -268,4 +325,4 @@ Error: 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." - InvalidChannel: ":x: **Error!** The suggestions channel has not been set up correctly." \ No newline at end of file + InvalidChannel: ":x: **Error!** The suggestions channel has not been set up correctly." diff --git a/Configs/supportbot.yml b/Configs/supportbot.yml index 2587027..d57d7e0 100644 --- a/Configs/supportbot.yml +++ b/Configs/supportbot.yml @@ -8,12 +8,12 @@ # Installed with MIT License # # Discord Support: https://discord.gg/emeraldsrv -# Official Documentation: https://docs.emeraldsrv.com -# Addons Marketplace: https://community.emeraldsrv.com +# Official Documentation: https://docs.emeraldsrv.dev +# Addons Marketplace: https://community.emeraldsrv.dev # # SupportBot Configuration (supportbot.yml) -SupportBot_Version: "v8.3" +SupportBot_Version: "v8.4" # ------------------------- # General @@ -50,10 +50,10 @@ Activity: Embed: Colours: - General: 0x0cc559 - Success: 0x04b545 - Error: 0xd13030 - Warn: 0xfca117 + General: "#0cc559" + Success: "#04b545" + Error: "#d13030" + Warn: "#fca117" Footer: "SupportBot | Developed by Emerald Development" @@ -66,12 +66,12 @@ Embed: Roles: AutoRole: Enabled: true - Role: "ROLE_ID" + Role: "1111111111111111" StaffMember: - Staff: "ROLE_ID" - Admin: "ROLE_ID" - Moderator: "ROLE_ID" + Staff: "1111111111111111" + Admin: "1111111111111111" + Moderator: "1111111111111111" Mod: AllowSupportStaff: false @@ -84,18 +84,21 @@ Roles: Welcome: Enabled: true - Channel: "JOIN_CHANNEL_ID" + Channel: "1111111111111111" Leave: Enabled: true - Channel: "JOIN_CHANNEL_ID" + Channel: "1111111111111111" # ---------------------------- # Suggestions # ---------------------------- Suggestions: - Channel: "SUGGESTIONS_CHANNEL_ID" + Mode: "Channel" # Channel or Forum + ForumChannel: "1111111111111111" + Channel: "1111111111111111" + UpvoteEmoji: "✅" DownvoteEmoji: "❌" @@ -126,31 +129,37 @@ Suggestions: # 3 - Green # 4 - Red + Tags: + Created: "1111111111111111" + Considering: "1111111111111111" + Solved: "1111111111111111" + Denied: "1111111111111111" + # ------------------------- # Tickets # ------------------------- VoiceTickets: Name: "%username%'s VC" - Category: "VOICEE_CATEGORY_ID" + Category: "1111111111111111" Ticket: - TicketType: "channels" + TicketType: "threads" # Available TicketTypes: # channels - Tickets will be created as channels in the TicketChannelsCategory # threads - Tickets will be created as threads through the "TicketHome" channel. - TicketHome: "TICKETHOME_CHANNEL_ID" + TicketHome: "1111111111111111" # 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: "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" + TicketChannelsCategory: "1111111111111111" + TicketChannelsCategory2: "1111111111111111" # This will be used if the first category has reached the max channel limit. + TicketArchiveCategory: "1111111111111111" # --- Channel: "ticket-" # The prefix for ticket channel names @@ -189,7 +198,7 @@ Ticket: ClaimTickets: Enabled: true - Channel: "CLAIM_TICKETS_CHANNEL_ID" + Channel: "1111111111111111" # Channel where the ticket assignments are sent for staff to claim. ClaimAcceptTime: 120000 @@ -211,8 +220,8 @@ Ticket: # 4 - Red ReviewSystem: - Enabled: true - Channel: "REVIEW_CHANNEL_ID" + Enabled: false + Channel: "1111111111111111" UseModal: true Questions: @@ -223,9 +232,9 @@ Ticket: - "Have you tried resolving the issue yourself?" Log: - TicketDataLog: "TICKET_DATA_LOG_ID" + TicketDataLog: "1111111111111111" TickeDataTitle: "Ticket Data" - TicketBlacklistLog: "TICKET_BLACKLIST_LOG_ID" + TicketBlacklistLog: "1111111111111111" # ------------------------- # Buttons @@ -278,12 +287,12 @@ SelectMenus: # ------------------------- MessageDelete: - Channel: "MESSAGE_DELETE_CHANNEL_ID" + Channel: "1111111111111111" Colour: "#d92525" MessageUpdate: - Channel: "MESSAGE_UPDATE_CHANNEL_ID" + Channel: "1111111111111111" Colour: "#f2a024" Translate: - TranslateLog: "TRANSLATE_LOG_ID" + TranslateLog: "1111111111111111" diff --git a/Configs/ticket-panel.yml b/Configs/ticket-panel.yml index 5cb7f3d..b14eadf 100644 --- a/Configs/ticket-panel.yml +++ b/Configs/ticket-panel.yml @@ -23,13 +23,24 @@ Panel: true # Ticket Panel Message Configuration #2b2d31 # ---------------------------------------- -PanelTitle: "SupportBot" -PanelColour: "#0cc559" +Style: + Title: "## SupportBot Ticket Creation" + Description: "Click on the button to file a `Ticket` and our team will try their best to assist you in the best way we can!" + Image: "https://i.imgur.com/dejQvkg.png" + Color: "#45b463" + # Leave Color empty to remove the color from the embed/container. + + Layout: "3" + +# Available Layout Styles +# "1" - Style 1 - Title & Text, Button at the top -- Image at the bottom +# "2" - Style 2 - Title & Text, Button at the bottom -- Image at the top +# "3" - Style 3 - The classic look - Title & Text at the top -- Image in the middle -- Button at the bottom Button: Text: "Create Ticket" Emoji: "🎫" - Color: "2" + Color: "3" # Available Button Styles # 1 - Blurple @@ -37,10 +48,15 @@ Button: # 3 - Red # 4 - Green -TicketPanel_Description: true -TicketPanel_Thumbnail: false -TicketPanel_Image: true +Settings: + Image: true -PanelMessage: "Click on the button to file a `Ticket` and our team will try their best to assist you in the best way we can!" -PanelThumbnail: "https://i.imgur.com/ieeZejl.png" -PanelImage: "https://i.imgur.com/dejQvkg.png" \ No newline at end of file + # Text and divider will appear above the image. + TopTitle: true + TopDivider: true + + # Text and divider will appear below the image. + BottomTitle: false + BottomDivider: false + + # Note: If Image is set to false, the bottom and top settings will look the samme. \ No newline at end of file diff --git a/Data/Profiles.json b/Data/Profiles.json deleted file mode 100644 index 9e26dfe..0000000 --- a/Data/Profiles.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/Data/StaffData.json b/Data/StaffData.json deleted file mode 100644 index 9e26dfe..0000000 --- a/Data/StaffData.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/Data/SuggestionData.json b/Data/SuggestionData.json deleted file mode 100644 index 9e26dfe..0000000 --- a/Data/SuggestionData.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/Data/TicketData.json b/Data/TicketData.json deleted file mode 100644 index 987b7f9..0000000 --- a/Data/TicketData.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "tickets": [ - {} - ] -} \ No newline at end of file diff --git a/Data/profiles/0000000000000000.json b/Data/profiles/0000000000000000.json deleted file mode 100644 index 0088126..0000000 --- a/Data/profiles/0000000000000000.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "bio": "", - "timezone": "", - "clockedIn": false -} diff --git a/Data/settings.json b/Data/settings.json deleted file mode 100644 index 9e26dfe..0000000 --- a/Data/settings.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/Data/ticket-panel-id.json b/Data/ticket-panel-id.json deleted file mode 100644 index 571a32a..0000000 --- a/Data/ticket-panel-id.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id": 0, - "TicketPanelID": "000000000000000" -} \ No newline at end of file diff --git a/Events/guildMemberAdd.js b/Events/guildMemberAdd.js index f0b97fd..521b772 100644 --- a/Events/guildMemberAdd.js +++ b/Events/guildMemberAdd.js @@ -1,56 +1,153 @@ - -const Discord = require('discord.js'); +const { + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + MessageFlags, + ContainerBuilder, + TextDisplayBuilder, + SectionBuilder, + ThumbnailBuilder, + AttachmentBuilder +} = require("discord.js"); const fs = require("fs"); -const Event = require("../Structures/Event.js"); const yaml = require("js-yaml"); +const Event = require("../Structures/Event.js"); 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") ); -module.exports = new Event("guildMemberAdd", async (client, member, interaction, guild) => { +function getWelcomeThumbnail(client, member, iconCfg) { + switch ((iconCfg?.Mode || "").toUpperCase()) { + case "USER": + return member.user.displayAvatarURL(); + case "BOT": + return client.user.displayAvatarURL(); + case "CUSTOM": + return iconCfg.CustomURL; + default: + return null; + } +} - if (supportbot.Welcome.Enabled) { - const WelcomeChannel = member.guild.channels.cache.get(supportbot.Welcome.Channel) || - member.guild.channels.cache.find(channel => channel.name === supportbot.Welcome.Channel); +function buildModernComponents(client, member) { + const container = new ContainerBuilder() + .setAccentColor( + parseInt(msgconfig.Welcome.Modern.Colour.replace("#", ""), 16) + ); - const WelcomeEmbed = new Discord.EmbedBuilder() - .setColor(msgconfig.Welcome.Embed.Colour) - .setTitle(msgconfig.Welcome.Embed.Title) - .setDescription(msgconfig.Welcome.Embed.Message.replace(/%joined_user%/g, member.user)) - .setTimestamp(); - - if (msgconfig.Welcome.Embed.Thumbnail === "BOT") { - WelcomeEmbed.setThumbnail(client.user.displayAvatarURL()) - } else if (msgconfig.Welcome.Embed.Thumbnail === "USER") { - WelcomeEmbed.setThumbnail(member.user.displayAvatarURL()) - } - - if (msgconfig.Welcome.Embed.ImageEnabled) { - WelcomeEmbed.setImage(msgconfig.Welcome.Embed.ImageURL) - } - - if (WelcomeChannel) { - WelcomeChannel.send({ embeds: [WelcomeEmbed] }); - } - - if (supportbot.Roles.AutoRole.Enabled) { - const role = member.guild.roles.cache.get(supportbot.Roles.AutoRole.Role) || - member.guild.roles.cache.find(role => role.name === supportbot.Roles.AutoRole.Role); - if (role) { - member.roles.add(role); - } - } - - console.log(`\u001b[32m`, `[+]`, `\u001b[33m`, `${member.user.username} joined the server!`); + const section = new SectionBuilder().addTextDisplayComponents( + new TextDisplayBuilder().setContent( + msgconfig.Welcome.Modern.Title.replace(/%joined_user%/g, member.user) + ), + new TextDisplayBuilder().setContent( + msgconfig.Welcome.Modern.Body.replace(/%joined_user%/g, member.user) + ) + ); + + const thumbUrl = getWelcomeThumbnail(client, member, msgconfig.Welcome.Modern.Icon); + if (thumbUrl) { + section.setThumbnailAccessory( + new ThumbnailBuilder() + .setURL(thumbUrl) + ); } -}); \ No newline at end of file + container.addSectionComponents(section); + + if (msgconfig.Welcome.Modern.Image?.Enabled && msgconfig.Welcome.Modern.Image.URL) { + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `![image](${msgconfig.Welcome.Modern.Image.URL})` + ) + ); + } + + if (Array.isArray(msgconfig.Welcome.Modern.Buttons)) { + const row = new ActionRowBuilder(); + msgconfig.Welcome.Modern.Buttons.slice(0, 5).forEach(b => { + const btn = new ButtonBuilder() + .setLabel(b.label) + .setStyle(ButtonStyle[b.style]) + .setEmoji(b.emoji || null); + if (b.style === "Link") btn.setURL(b.url); + else if (b.customId) btn.setCustomId(b.customId); + row.addComponents(btn); + }); + + container.addActionRowComponents(row); + + } + + return [container]; +} + +module.exports = new Event("guildMemberAdd", async (client, member) => { + if (!supportbot.Welcome.Enabled) return; + + const WelcomeChannel = + member.guild.channels.cache.get(supportbot.Welcome.Channel) || + member.guild.channels.cache.find(c => c.name === supportbot.Welcome.Channel); + if (!WelcomeChannel) return; + + const style = (msgconfig.Welcome.LayoutStyle || "classic").toLowerCase(); + + if (style === "classic") { + const WelcomeEmbed = new EmbedBuilder() + .setColor(msgconfig.Welcome.Embed.Colour) + .setTitle(msgconfig.Welcome.Embed.Title || null) + .setDescription( + msgconfig.Welcome.Embed.Message.replace(/%joined_user%/g, member.user) + ) + .setTimestamp(); + + const thumb = getWelcomeThumbnail(client, member, msgconfig.Welcome.Embed.Icon); + if (thumb) WelcomeEmbed.setThumbnail(thumb); + + if (msgconfig.Welcome.Embed.Image?.Enabled && msgconfig.Welcome.Embed.Image.URL) { + WelcomeEmbed.setImage(msgconfig.Welcome.Embed.Image.URL); + } + + const components = []; + if ( + msgconfig.Welcome.Embed.Buttons?.Enabled && + Array.isArray(msgconfig.Welcome.Embed.Buttons.List) + ) { + const row = new ActionRowBuilder(); + msgconfig.Welcome.Embed.Buttons.List.slice(0, 5).forEach(b => { + const btn = new ButtonBuilder() + .setLabel(b.label) + .setStyle(ButtonStyle[b.style]) + .setEmoji(b.emoji || null); + if (b.style === "Link") btn.setURL(b.url); + else if (b.customId) btn.setCustomId(b.customId); + row.addComponents(btn); + }); + components.push(row); + } + + await WelcomeChannel.send({ embeds: [WelcomeEmbed], components }); + } else if (style === "modern") { + const components = buildModernComponents(client, member); + if (components.length > 0) { + await WelcomeChannel.send({ + components, + flags: MessageFlags.IsComponentsV2 + }); + } + } + + if (supportbot.Roles.AutoRole.Enabled) { + const role = + member.guild.roles.cache.get(supportbot.Roles.AutoRole.Role) || + member.guild.roles.cache.find(r => r.name === supportbot.Roles.AutoRole.Role); + if (role) await member.roles.add(role); + } + + console.log("\u001b[32m", "[+]", "\u001b[33m", `${member.user.username} joined the server!`); + +}); diff --git a/Events/guildMemberRemove.js b/Events/guildMemberRemove.js index 3004af7..f4d1bb2 100644 --- a/Events/guildMemberRemove.js +++ b/Events/guildMemberRemove.js @@ -1,50 +1,40 @@ - -const Discord = require('discord.js'); +const { EmbedBuilder } = require("discord.js"); const fs = require("fs"); -const Event = require("../Structures/Event.js"); const yaml = require("js-yaml"); +const Event = require("../Structures/Event.js"); -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 msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); -module.exports = new Event("guildMemberRemove", async (client, member, interaction, guild) => { +module.exports = new Event("guildMemberRemove", async (client, member) => { - if (supportbot.Leave.Enabled) { - const LeaveChannel = member.guild.channels.cache.get(supportbot.Leave.Channel) || - member.guild.channels.cache.find(channel => channel.name === supportbot.Leave.Channel); + if (!supportbot.Leave || !supportbot.Leave.Enabled) return; - const LeaveEmbed = new Discord.EmbedBuilder() - .setColor(msgconfig.Leave.Embed.Colour) - .setTitle(msgconfig.Leave.Embed.Title) - .setDescription(msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user)) - .setTimestamp(); - - if (msgconfig.Leave.Embed.Thumbnail === "BOT") { - LeaveEmbed.setThumbnail(client.user.displayAvatarURL()); - } else if (msgconfig.Leave.Embed.Thumbnail === "USER") { - LeaveEmbed.setThumbnail(member.user.displayAvatarURL()); - } - - if (msgconfig.Leave.Embed.ImageEnabled) { - LeaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL); - } - - if (LeaveChannel) { - LeaveChannel.send({ - embeds: [LeaveEmbed] - }); - } - - console.log(`\u001b[31m`, `[-]`, `\u001b[33m`, `${member.user.username} left the server!`); + const leaveChannel = + member.guild.channels.cache.get(supportbot.Leave.Channel) || + member.guild.channels.cache.find(c => c.name === supportbot.Leave.Channel); + + if (!leaveChannel) return; + + const leaveEmbed = new EmbedBuilder() + .setColor(msgconfig.Leave.Embed.Colour) + .setTitle(msgconfig.Leave.Embed.Title) + .setDescription( + msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user.toString()) + ) + .setTimestamp(); + + if (msgconfig.Leave.Embed.Thumbnail === "BOT") { + leaveEmbed.setThumbnail(client.user.displayAvatarURL()); + } else if (msgconfig.Leave.Embed.Thumbnail === "USER") { + leaveEmbed.setThumbnail(member.user.displayAvatarURL()); } -}); \ No newline at end of file + if (msgconfig.Leave.Embed.ImageEnabled) { + leaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL); + } + + await leaveChannel.send({ embeds: [leaveEmbed] }); + + console.log(`[-] ${member.user.tag} left the server`); +}); diff --git a/Events/interactionCreate.js b/Events/interactionCreate.js index 1849074..a52dd32 100644 --- a/Events/interactionCreate.js +++ b/Events/interactionCreate.js @@ -7,13 +7,7 @@ const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); const Event = require("../Structures/Event.js"); - -let suggestions; -try { - suggestions = require("../Data/SuggestionData.json"); -} catch (error) { - suggestions = {}; -} +const db = require("../Structures/Database.js"); module.exports = new Event("interactionCreate", async (client, interaction) => { @@ -31,7 +25,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { if (!command) return interaction.reply({ embeds: [NotValid], - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); const permission = interaction.member.permissions.has(command.permissions); @@ -45,7 +39,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { if (!permission) return interaction.reply({ embeds: [ValidPerms], - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); try { @@ -72,7 +66,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { console.error("Error executing command:", error); await interaction.reply({ content: "An error occurred while executing the command.", - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); } } @@ -83,7 +77,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { interaction.customId = `createticket-${reason}`; const cmd = client.commands.get(cmdconfig.OpenTicket.Command); if (cmd) { - interaction.reason = reason; // Attach the reason to the interaction object + interaction.reason = reason; await cmd.run(interaction); } else { console.error("Command not found."); @@ -93,43 +87,37 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { if (interaction.customId === "renameTicketModal") { const newTicketName = interaction.fields.getTextInputValue("renameTicket"); - - // Fixing the ticketChannelId extraction and lookup const ticketChannelId = interaction.channel.id; - let ticketChannel; if (supportbot.Ticket.TicketType === "threads") { ticketChannel = interaction.guild.channels.cache.get(ticketChannelId); } else if (supportbot.Ticket.TicketType === "channels") { ticketChannel = interaction.guild.channels.cache.get(ticketChannelId); } - if (!ticketChannel) { return interaction.reply({ content: "Ticket channel not found.", - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); } - try { await ticketChannel.setName(newTicketName); - const renamedsuccess = new Discord.EmbedBuilder() .setDescription(msgconfig.Ticket.RenamedSuccess) .setColor(supportbot.Embed.Colours.Success); - return interaction.reply({ embeds: [renamedsuccess], - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); } catch (error) { console.error("Error renaming the ticket:", error); return interaction.reply({ content: "There was an error renaming the ticket.", - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); } } + if (interaction.isStringSelectMenu()) { if (interaction.customId === "ticketcontrolpanel") { const selectedOption = interaction.values[0]; @@ -146,137 +134,107 @@ 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], - }, - ], + const ticketInfo = db.getTicket(interaction.channel.id); + if (!ticketInfo) { + return interaction.reply({ + content: "⚠️ This channel isn’t a valid ticket.", + flags: Discord.MessageFlags.Ephemeral }); - - 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; + + const user = interaction.guild.members.cache.get(ticketInfo.user_id) || ticketInfo.user_id; + const username = user?.user?.username || "Unknown User"; + + 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!", + flags: Discord.MessageFlags.Ephemeral, + }); + } + + const SupportVoice = await interaction.guild.channels.create({ + name: supportbot.VoiceTickets.Name.replace(/%username%/g, username), + type: Discord.ChannelType.GuildVoice, + parent: VCcategory, + permissionOverwrites: [ + { + id: interaction.guild.id, + deny: [Discord.PermissionsBitField.Flags.Connect] + }, + { + id: user.id || user, // works if user is a Member or just ID + allow: [ + Discord.PermissionsBitField.Flags.Connect, + Discord.PermissionsBitField.Flags.Speak, + Discord.PermissionsBitField.Flags.ViewChannel, + ], + }, + ], + }); + + db.updateTicketVoice(ticketInfo.ticket_id, SupportVoice.id); + + const vcmadeembed = new Discord.EmbedBuilder() + .setDescription(`> **Support VC Created:** <#${SupportVoice.id}>`) + .setColor(supportbot.Embed.Colours.Success); + + 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); + + await interaction.reply({ + embeds: [vcmadeembed], + components: [VCDeleteRow], + }); + break; case "archiveticket": const arcembed = new Discord.EmbedBuilder() .setDescription(msgconfig.Ticket.TicketArchived) .setColor(supportbot.Embed.Colours.Success); - 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); - const unarchiveButton = new Discord.ButtonBuilder() .setCustomId("unarchiveTicket") .setLabel(supportbot.Buttons.Tickets.Unarchive) .setEmoji(supportbot.Buttons.Tickets.Unarchive_Emoji) .setStyle(supportbot.Buttons.Tickets.Unarchive_Style); - const ArchivedOptions = new Discord.ActionRowBuilder() .addComponents(CloseButton, unarchiveButton); - await interaction.channel.send({ embeds: [arcembed], components: [ArchivedOptions], }); - if (supportbot.Ticket.TicketType === "threads") { await interaction.channel.setArchived(true); } - if (supportbot.Ticket.TicketType === "channels") { const { getRole, getChannel } = interaction.client; await interaction.channel.setParent(supportbot.Ticket.TicketArchiveCategory); - - const ticketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); - const ticketInfo = ticketData.tickets.find(t => t.id === interaction.channel.id); - + const tickets = db.getAllTickets(); + const ticketInfo = tickets.find(t => t.ticket_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 user = interaction.guild.members.cache.get(ticketInfo.user_id) || ticketInfo.user_id; + const claimedBy = interaction.guild.members.cache.get(ticketInfo.claimed_by) || ticketInfo.claimed_by; const Admin = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Admin); const Staff = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Staff); - if (user && user.id) { await interaction.channel.permissionOverwrites.edit(user.id, { ViewChannel: false, SendMessages: false, }); } - if (supportbot.ClaimTickets === true) { if (claimedBy && claimedBy.id) { await interaction.channel.permissionOverwrites.edit(claimedBy.id, { @@ -286,7 +244,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { }); } } - if (supportbot.ClaimTickets === false) { if (Staff && Staff.id) { await interaction.channel.permissionOverwrites.edit(claimedBy.id, { @@ -296,7 +253,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { }); } } - if (Admin && Admin.id) { await interaction.channel.permissionOverwrites.edit(Admin.id, { ViewChannel: true, @@ -304,59 +260,47 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { ReadMessageHistory: true, }); } - await interaction.channel.permissionOverwrites.edit(interaction.guild.roles.everyone.id, { ViewChannel: false, SendMessages: false, ReadMessageHistory: false, }); } - } - break; case "lockticket": const lockembed = new Discord.EmbedBuilder() .setDescription(msgconfig.Ticket.TicketLocked) .setColor(supportbot.Embed.Colours.Success); - const unlockButton = new Discord.ButtonBuilder() .setCustomId("unlockTicket") .setLabel(supportbot.Buttons.Tickets.Unlock) .setEmoji(supportbot.Buttons.Tickets.Unlock_Emoji) .setStyle(supportbot.Buttons.Tickets.Unlock_Style); - const LockedOptions = new Discord.ActionRowBuilder() .addComponents(unlockButton); - await interaction.channel.send({ embeds: [lockembed], components: [LockedOptions], ephemeral: false, }); - - if (supportbot.Ticket.TicketType === "threads") { await interaction.channel.setLocked(true); } - break; case "unlockTicket": const unlockEmbed = new Discord.EmbedBuilder() .setDescription(msgconfig.Ticket.TicketUnlocked) .setColor(supportbot.Embed.Colours.Success); - await interaction.channel.send({ embeds: [unlockEmbed], ephemeral: false, }); - if (supportbot.Ticket.TicketType === "threads") { await interaction.channel.setLocked(false); } - break; case "enableinvites": @@ -383,89 +327,70 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { ); await interaction.showModal(modal); break; - } } } 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); - + const ticketData = db.getAllTickets(); + const ticketInfo = ticketData.find(t => t.ticket_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, + flags: Discord.MessageFlags.Ephemeral, }); - - 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, + flags: Discord.MessageFlags.Ephemeral, }); } } 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, + flags: Discord.MessageFlags.Ephemeral, }); } } 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, + flags: Discord.MessageFlags.Ephemeral, }); } } - - if (interaction.isButton()) { if (interaction.customId === "unarchiveTicket") { - if (supportbot.Ticket.TicketType === "threads") { await interaction.channel.setArchived(false); } - if (supportbot.Ticket.TicketType === "channels") { const { getRole, getChannel } = interaction.client; await interaction.channel.setParent(supportbot.Ticket.TicketChannelsCategory); - - const ticketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); - const ticketInfo = ticketData.tickets.find(t => t.id === interaction.channel.id); - + const ticketData = db.getAllTickets(); + const ticketInfo = ticketData.find(t => t.ticket_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 user = interaction.guild.members.cache.get(ticketInfo.user_id) || ticketInfo.user_id; + const claimedBy = interaction.guild.members.cache.get(ticketInfo.claimed_by) || ticketInfo.claimed_by; const Admin = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Admin); const Staff = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Staff); - if (user && user.id) { await interaction.channel.permissionOverwrites.edit(user.id, { ViewChannel: true, @@ -473,7 +398,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { ReadMessageHistory: true, }); } - if (supportbot.ClaimTickets === true) { if (claimedBy && claimedBy.id) { await interaction.channel.permissionOverwrites.edit(claimedBy.id, { @@ -483,7 +407,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { }); } } - if (supportbot.ClaimTickets === false) { if (Staff && Staff.id) { await interaction.channel.permissionOverwrites.edit(claimedBy.id, { @@ -493,7 +416,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { }); } } - if (Admin && Admin.id) { await interaction.channel.permissionOverwrites.edit(Admin.id, { ViewChannel: true, @@ -501,7 +423,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { ReadMessageHistory: true, }); } - await interaction.channel.permissionOverwrites.edit(interaction.guild.roles.everyone.id, { ViewChannel: false, SendMessages: false, @@ -509,16 +430,13 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { }); } } - const unrcembed = new Discord.EmbedBuilder() .setDescription(msgconfig.Ticket.TicketUnarchived) .setColor(supportbot.Embed.Colours.Success); - await interaction.update({ embeds: [unrcembed], components: [], }); - } if (interaction.customId === "createticket") { @@ -535,7 +453,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { .setRequired(true) ) ); - await interaction.showModal(modal); } else { try { @@ -546,7 +463,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { console.error("Error executing the ticket command:", error); await interaction.reply({ content: "An error occurred while creating the ticket.", - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); } } @@ -566,7 +483,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { .setRequired(true) ) ); - await interaction.showModal(modal); } else { try { @@ -585,11 +501,11 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { if (!ticketChannel) { return interaction.reply({ content: "Ticket channel not found.", - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); } - 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); @@ -601,10 +517,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { .setColor(supportbot.Embed.Colours.Warn); if (!Staff || !Admin) { - return interaction.reply({ - embeds: [RolePerms], - ephemeral: true, - }); + return interaction.reply({ embeds: [RolePerms], ephemeral: true }); } const NoPermsClaim = new Discord.EmbedBuilder() @@ -615,65 +528,55 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { .setColor(supportbot.Embed.Colours.Error); if (!interaction.member.roles.cache.has(Staff.id) && !interaction.member.roles.cache.has(Admin.id)) { - return interaction.reply({ - embeds: [NoPermsClaim], - ephemeral: true, - }); + return interaction.reply({ embeds: [NoPermsClaim], ephemeral: true }); } if (supportbot.Ticket.TicketType === "threads") { - await ticketChannel.members.add(interaction.user.id); - - } else if (supportbot.Ticket.TicketType === "channels") { + await ticketChannel.members.add(interaction.user.id); + } else { await ticketChannel.permissionOverwrites.create(interaction.user.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - + ViewChannel: true, + SendMessages: true, + ReadMessageHistory: true, + }); await ticketChannel.permissionOverwrites.create(Admin, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - + ViewChannel: true, + SendMessages: true, + ReadMessageHistory: true, + }); await ticketChannel.permissionOverwrites.create(interaction.guild.roles.everyone, { ViewChannel: false, - }); - } - + }); + } + const pingclaimedstaff = await ticketChannel.send(`<@${interaction.user.id}>`); + setTimeout(() => pingclaimedstaff.delete(), 2000); - setTimeout(() => { - pingclaimedstaff.delete(); - }, 2000); - - const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); - const ticketIndex = TicketData.tickets.findIndex((t) => t.id === ticketChannelId); - if (ticketIndex !== -1) { - TicketData.tickets[ticketIndex].claimedBy = interaction.user.id; - TicketData.tickets[ticketIndex].claimedAt = new Date().toISOString(); - fs.writeFileSync("./Data/TicketData.json", JSON.stringify(TicketData, null, 4)); + const ticketInfo = db.getTicket(ticketChannelId); + if (ticketInfo) { + db.updateTicketStatus(ticketChannelId, ticketInfo.status); + ticketInfo.claimed_by = interaction.user.id; } const claimMessage = await interaction.message.fetch(); const claimedEmbed = new Discord.EmbedBuilder(claimMessage.embeds[0].data) .setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle) .setDescription( - msgconfig.Ticket.ClaimTickets.ClaimMessage_Edit.replace("%user%", interaction.user.id).replace("%channel%", ticketChannel.id) + msgconfig.Ticket.ClaimTickets.ClaimMessage_Edit + .replace("%user%", `${interaction.user.id}`) + .replace("%channel%", `${ticketChannel.id}`) ) .setColor(supportbot.Embed.Colours.Success); const successfullyClaimed = new Discord.EmbedBuilder() .setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle) - .setDescription(msgconfig.Ticket.ClaimTickets.Claimed.replace("%channel%", ticketChannel.id)) + .setDescription(msgconfig.Ticket.ClaimTickets.Claimed + .replace("%channel%", `${ticketChannel.id}`) + ) .setColor(supportbot.Embed.Colours.Success); await claimMessage.edit({ embeds: [claimedEmbed], components: [] }); - await interaction.reply({ - embeds: [successfullyClaimed], - ephemeral: true, - }); + await interaction.reply({ embeds: [successfullyClaimed], ephemeral: true }); } // SUGGESTION INTERACTIONS [START] @@ -773,11 +676,10 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { await interaction.reply({ embeds: [embed], - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); } - // Modal submission handler for editing profile if (interaction.type === Discord.InteractionType.ModalSubmit && interaction.customId === "editProfileModal") { const userId = interaction.user.id; const profilePath = `./Data/Profiles/${userId}.json`; @@ -824,7 +726,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { console.error("Failed to update profile data: ", error); await interaction.reply({ content: "There was an error while updating your profile. Please try again later.", - ephemeral: true, + flags: Discord.MessageFlags.Ephemeral, }); } } diff --git a/Events/messageDelete.js b/Events/messageDelete.js index 49807f7..5490f9c 100644 --- a/Events/messageDelete.js +++ b/Events/messageDelete.js @@ -3,41 +3,34 @@ 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) => { + if (!message.guild) return; - // Check if the message is from a guild and not from a bot - if (!message.guild || message.author.bot) return; + const author = message.author || (message.partial ? await message.fetch().then(m => m.author).catch(() => null) : null); + if (!author || author.bot) return; + + const content = message.content || (message.attachments.size > 0 ? "Contains attachment(s)" : "*No content*"); - // 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); + message.guild.channels.cache.find(c => c.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:** `) - .addFields( - { name: 'Message', value: message.content || '*No content*', inline: false } - ) - .setThumbnail(message.author.displayAvatarURL()) + .setDescription(`> **Channel:** <#${message.channel.id}>\n> **Message ID:** ${message.id}\n> **Message author:** ${author.tag} (${author.id})\n> **Message Created:** `) + .addFields({ name: "Deleted Content", value: content, inline: false }) + .setThumbnail(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})` }); + 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] - }); + deleteLogChannel.send({ embeds: [deletedMessageEmbed] }); } }); - diff --git a/Events/ready.js b/Events/ready.js index 69a7908..6921a17 100644 --- a/Events/ready.js +++ b/Events/ready.js @@ -1,143 +1,128 @@ -// SupportBot | Emerald Services -// Ready Event - const fs = require("fs"); - const Discord = require("discord.js"); -const { Client, GatewayIntentBits, ActivityType } = require('discord.js'); -const client = new Discord.Client({intents: 32767}) - const yaml = require("js-yaml"); - -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8") -); -const panelconfig = yaml.load( - fs.readFileSync("./Configs/ticket-panel.yml", "utf8") -); - -const cmdconfig = yaml.load( - fs.readFileSync("./Configs/commands.yml", "utf8") -); - -const msgconfig = yaml.load( - fs.readFileSync("./Configs/messages.yml", "utf8") -); - const Event = require("../Structures/Event.js"); +const gradient = require("gradient-string"); +const figlet = require("figlet"); -let chan1 = client.channels.cache.get(supportbot.Ticket.TicketHome); +// Load configs +const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); +const panel = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8")); +const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); -module.exports = new Event("ready", async (client, interaction) => { - const { getRole, getChannel, getCategory } = client; +const ACTIVITY_TYPES = { + PLAYING: Discord.ActivityType.Playing, + WATCHING: Discord.ActivityType.Watching, + LISTENING: Discord.ActivityType.Listening, + COMPETING: Discord.ActivityType.Competing +}; - if (supportbot.Activity.Type === "Playing", "playing") { +const setActivity = (client, type, status) => { + const activityType = ACTIVITY_TYPES[type?.toUpperCase()]; + if (activityType) { client.user.setPresence({ - activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Playing }], - status: supportbot.Activity.Type, + activities: [{ name: status || "Online", type: activityType }], + status: type?.toLowerCase() || "online" }); } +}; - if (supportbot.Activity.Type === "Watching", "watching") { - client.user.setPresence({ - activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Watching }], - status: supportbot.Activity.Type, - }); - } +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const logStartup = async (client) => { + console.clear(); - if (supportbot.Activity.Type === "Listening", "listening") { - client.user.setPresence({ - activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Listening }], - status: supportbot.Activity.Type, - }); + // Animated loading + const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + for (let i = 0; i < frames.length * 2; i++) { + process.stdout.write(`\r${frames[i % frames.length]} Initializing SupportBot...`); + await sleep(100); } + console.clear(); - if (supportbot.Activity.Type === "Competing", "competing") { - client.user.setPresence({ - activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Competing }], - status: supportbot.Activity.Type, - }); - } + // Main title + console.log("\n" + gradient.pastel.multiline(figlet.textSync("SupportBot", { font: "ANSI Shadow", horizontalLayout: "fitted" }))); - console.log(`\u001b[33m`, `――――――――――――――――――――――――――――――――――――――――――――`); - console.log(` `); - console.log(`\u001b[31m`, `┏━━━┓╋╋╋╋╋╋╋╋╋╋╋╋╋┏┓┏━━┓╋╋╋┏┓`); - console.log(`\u001b[31m`, `┃┏━┓┃╋╋╋╋╋╋╋╋╋╋╋╋┏┛┗┫┏┓┃╋╋┏┛┗┓`); - console.log(`\u001b[31m`, `┃┗━━┳┓┏┳━━┳━━┳━━┳┻┓┏┫┗┛┗┳━┻┓┏┛`); - console.log(`\u001b[31m`, `┗━━┓┃┃┃┃┏┓┃┏┓┃┏┓┃┏┫┃┃┏━┓┃┏┓┃┃`); - console.log(`\u001b[31m`, `┃┗━┛┃┗┛┃┗┛┃┗┛┃┗┛┃┃┃┗┫┗━┛┃┗┛┃┗┓`); - console.log(`\u001b[31m`, `┗━━━┻━━┫┏━┫┏━┻━━┻┛┗━┻━━━┻━━┻━┛`); - console.log(`\u001b[31m`, `┗╋╋╋╋╋╋╋┃┃╋┃┃`); - console.log(`\u001b[31m`, `╋╋╋╋╋╋╋┗┛╋┗┛`); - console.log(` `); - console.log(`\u001b[33m`, `――――――――――――――――――――――――――――――――――――――――――――`); - console.log(` `); - console.log(`\u001b[33m`, `${supportbot.General.Name} | [${supportbot.SupportBot_Version}]`, `\u001b[32m`, `Connected to Discord`,); - console.log("\u001b[32m", "SupportBot created by Emerald Development "); - console.log(` `); - console.log(`\u001b[33m`, `――――――――――――――――― [Links] ――――――――――――――――――`); - console.log("\u001b[32m", "Discord: https://dsc.gg/emerald-dev"); - console.log("\u001b[32m", "Website: https://emeraldsrv.com"); - console.log("\u001b[32m", "Community: https://community.emeraldsrv.com"); - console.log("\u001b[32m", "Documentation: https://docs.emeraldsrv.com"); - console.log(` `); - console.log(`\u001b[33m`, `――――――――――――――――― [Invite] ――――――――――――――――――`); - console.log("\u001b[32m", `Invite URL: https://discord.com/api/oauth2/authorize?client_id=${client.user.id}&permissions=8&scope=bot%20applications.commands`); - console.log(` `); - console.log(`\u001b[33m`, `――――――――――――――――― [Config Check] ――――――――――――――――――`); - console.log("\u001b[32m", "Config initialization..."); + // Extract bot details safely + const botName = supportbot?.General?.Name || "SupportBot"; + const botVersion = supportbot.SupportBot_Version; + + console.log("\n╔═══════════════════════════════════════════════════════════════╗"); + console.log("║ System Information ║"); + console.log("╠═══════════════════════════════════════════════════════════════╣"); + console.log(`║ Bot Name : ${botName.padEnd(45)} ║`); + console.log(`║ Version : ${botVersion.padEnd(45)} ║`); + console.log(`║ Node.js : ${process.version.padEnd(45)} ║`); + console.log(`║ Discord.js : v${Discord.version.padEnd(44)} ║`); + console.log("╚═══════════════════════════════════════════════════════════════╝\n"); + + // Links section + console.log(gradient.summer("┏━━━━━━━━━━━━━━━━━━━ Important Links ━━━━━━━━━━━━━━━━━━━┓")); + console.log(gradient.summer("┃ ┃")); + console.log(gradient.summer("┃ ") + "📚 Docs : " + gradient.cristal("https://emerald-services.gitbook.io/ ┃")); + console.log(gradient.summer("┃ ") + "🤝 Discord : " + gradient.cristal("https://dsc.gg/emerald-dev ┃")); + console.log(gradient.summer("┃ ┃")); + console.log(gradient.summer("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n")); + + // Bot Invite + const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${client.user.id}&permissions=8&scope=bot%20applications.commands`; + console.log(gradient.teen("╔════════════════════════ Bot Invite ════════════════════════╗")); + console.log(gradient.teen("║ ║")); + console.log(gradient.teen("║ ") + gradient.pastel(inviteUrl.padEnd(56)) + gradient.teen("║")); + console.log(gradient.teen("║ ║")); + console.log(gradient.teen("╚════════════════════════════════════════════════════════════╝\n")); +}; + +const checkConfigs = async (client) => { + const guild = client.guilds.cache.first(); + if (!guild) return; + + console.log(gradient.morning("┏━━━━━━━━━━━━━━━━━━━━ Config Check ━━━━━━━━━━━━━━━━━━━━━┓")); const roles = [ - supportbot.Roles.StaffMember.Admin, - supportbot.Roles.StaffMember.Staff - ]; + supportbot?.Roles?.StaffMember?.Admin, + supportbot?.Roles?.StaffMember?.Staff, + supportbot?.Roles?.AutoRole?.Role + ].filter(Boolean); - if (supportbot.Roles.AutoRole.Role) roles.push(supportbot.Roles.AutoRole.Role); - - const channels = [ - supportbot.Suggestions.Channel, - supportbot.Ticket.Log.TicketLog, - supportbot.Ticket.Log.TranscriptLog, - supportbot.Ticket.TicketHome, - supportbot.Welcome.Channel, - supportbot.Leave.Channel, - supportbot.Translate.Log - ]; - //const categories = [supportbot.TicketCategory]; + const channels = [ + supportbot?.Suggestions?.Channel, + supportbot?.Ticket?.Log?.TicketLog, + supportbot?.Ticket?.Log?.TranscriptLog, + supportbot?.Ticket?.TicketHome, + supportbot?.Welcome?.Channel, + supportbot?.Leave?.Channel, + supportbot?.Translate?.Log + ].filter(Boolean); - if (!channels) { - console.log("\u001b[31m", `[MISSING CHANNEL]`, `\u001b[37;1m`, `${channels}`, "\u001b[31m", `channel not found. Please check your config file.`); - return; - } -// else { -// console.log(`\u001b[32m`, `[CHANNEL LOCATED]`, `\u001b[37;1m`, `${channels}`, `\u001b[32;1m`, `channel has been found.`); -// } + const [missingRoles, missingChannels] = await Promise.all([ + Promise.all(roles.map((role) => client.getRole(role, guild))), + Promise.all(channels.map((channel) => client.getChannel(channel, guild))) + ]); - const missingC = []; - const missingR = []; - const missingCat = []; - - for (let r of roles) { - const find = await getRole(r, client.guilds.cache.first()); - if (!find) missingR.push(r); - } - // for (let cat of categories) { - // const find = await getCategory(cat, client.guilds.cache.first()); - // if (!find) missingCat.push(cat); - // } + const missing = { + roles: roles.filter((_, i) => !missingRoles[i]), + channels: channels.filter((_, i) => !missingChannels[i]) + }; - const missingRoles = await Promise.all(roles.map(role => getRole(role, client.guilds.cache.first()))); - const missingChannels = await Promise.all(channels.map(channel => getChannel(channel, client.guilds.cache.first()))); - - if (missingRoles.some(role => !role)) { - console.log("\u001b[31m", `Missing roles in your server configuration: ${missingRoles.filter(role => !role).join(', ')}`); + if (missing.roles.length || missing.channels.length) { + console.log(gradient.morning("┃ ⚠️ - Configuration Issues Found: ┃")); + if (missing.roles.length) { + console.log(gradient.morning("┃ • Missing Roles: ") + gradient.fruit(missing.roles.join(", "))); + } + if (missing.channels.length) { + console.log(gradient.morning("┃ • Missing Channels: ") + gradient.fruit(missing.channels.join(", "))); + } + } else { + console.log(gradient.morning("┃ ✅ - All configurations validated successfully ┃")); } - if (missingChannels.some(channel => !channel)) { - console.log("\u001b[31m", `Missing channels in your server configuration: ${missingChannels.filter(channel => !channel).join(', ')}`); - } - - console.log("\u001b[32m", "Configs initialized, No problems were detected."); - console.log(` `); + console.log(gradient.morning("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n")); +}; +module.exports = new Event("clientReady", async (client) => { + setActivity(client, supportbot?.Activity?.Type, supportbot?.Activity?.Status); + await logStartup(client); + await checkConfigs(client); }); \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index 0899fec..801ae42 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Emerald Services +Copyright (c) 2025 Emerald Services Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Structures/Client.js b/Structures/Client.js index 812b7e0..e3ac602 100644 --- a/Structures/Client.js +++ b/Structures/Client.js @@ -1,12 +1,28 @@ const fs = require("fs"); +const path = require("path"); const Discord = require("discord.js"); const { GatewayIntentBits, Partials } = require('discord.js'); const yaml = require("js-yaml"); -const { Command, Event } = require('./Addon.js'); // Adjust the path as needed +const { Command, Event } = require('./Addon.js'); const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); + +function walk(dir, ext = ".js", fileList = []) { + const files = fs.readdirSync(dir); + for (const file of files) { + const full = `${dir}/${file}`; + const stat = fs.statSync(full); + if (stat.isDirectory()) { + walk(full, ext, fileList); + } else if (file.endsWith(ext)) { + fileList.push(full); + } + } + return fileList; +} + class Client extends Discord.Client { constructor() { super({ @@ -23,7 +39,7 @@ class Client extends Discord.Client { } async getChannel(channel, guild) { - if (!channel) return null; // Add check for undefined channel + if (!channel) return null; return guild.channels.cache.find( (c) => (c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) && @@ -32,14 +48,14 @@ class Client extends Discord.Client { } async getRole(role, guild) { - if (!role) return null; // Add check for undefined role + if (!role) return null; return guild.roles.cache.find( (r) => r.id === role || (r.name && r.name.toLowerCase() === role.toLowerCase()) ); } async getCategory(category, guild) { - if (!category) return null; // Add check for undefined category + if (!category) return null; return guild.channels.cache.find( (c) => c.type === Discord.ChannelType.GuildCategory && @@ -48,72 +64,75 @@ class Client extends Discord.Client { } async start(token) { - let tempCommandFiles = fs.readdirSync("./Commands").filter((file) => file.endsWith(".js")); + let tempCommandFiles = walk("./Commands"); if (cmdconfig.Suggestion.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "suggest.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "suggest.js"); + } + + if (cmdconfig.Suggestion.Enabled === false) { + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "suggestadmin.js"); } if (cmdconfig.Help.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "help.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "help.js"); } if (cmdconfig.Info.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "info.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "info.js"); } if (cmdconfig.OpenTicket.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "ticket.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "ticket.js"); } if (cmdconfig.CloseTicket.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "close.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "close.js"); } if (cmdconfig.Embed.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "embed.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "embed.js"); } if (cmdconfig.Translate.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "translate.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "translate.js"); } if (cmdconfig.UserInfo.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "userinfo.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "userinfo.js"); } if (cmdconfig.Ping.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "Ping.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "Ping.js"); } if (cmdconfig.AddUser.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "addUser.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "addUser.js"); } if (cmdconfig.RemoveUser.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "removeUser.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "removeUser.js"); } if (cmdconfig.ForceAddUser.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "forceaddUser.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "forceaddUser.js"); } if (cmdconfig.Profile.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "profile.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "profile.js"); } if (cmdconfig.Settings.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "settings.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "settings.js"); } if (cmdconfig.Mod.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "mod.js"); + tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "mod.js"); } const commandFiles = tempCommandFiles; - const commands = commandFiles.map((file) => require(`../Commands/${file}`)); + const commands = commandFiles.map((file) => require(`../${file}`)); - // Load main commands console.log(`\u001b[33m`, "――――――――――――――――――――――――――――――――――――――――――――"); commands.forEach((cmd) => { @@ -121,7 +140,6 @@ class Client extends Discord.Client { this.commands.set(cmd.name, cmd); }); - // Load addon commands and events console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬"); if (supportbot.General.Addons.Enabled) { @@ -133,7 +151,6 @@ class Client extends Discord.Client { return addon; }); - // Addons console.log(" "); console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬"); @@ -163,9 +180,7 @@ class Client extends Discord.Client { console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬"); } - // Register Slash Commands - - this.once("ready", async () => { + this.once("clientReady", async () => { await this.guilds.cache.first()?.commands.set(this.commands); console.log( "\u001b[32m", diff --git a/Structures/Database.js b/Structures/Database.js new file mode 100644 index 0000000..9b10848 --- /dev/null +++ b/Structures/Database.js @@ -0,0 +1,221 @@ +// SupportBot | Emerald Services +// Database Structure + +const Database = require("better-sqlite3"); +const path = require("path"); + +const db = new Database(path.join(__dirname, "../Data/supportbot.db")); + +db.prepare(` + CREATE TABLE IF NOT EXISTS suggestions ( + thread_id TEXT PRIMARY KEY, + author_id TEXT, + text TEXT, + status TEXT DEFAULT 'Created' + ) +`).run(); + +db.prepare(` + CREATE TABLE IF NOT EXISTS profiles ( + user_id TEXT PRIMARY KEY, + bio TEXT, + timezone TEXT, + clockedIn INTEGER DEFAULT 0 + ) +`).run(); + +db.prepare(` + CREATE TABLE IF NOT EXISTS tickets ( + ticket_id TEXT PRIMARY KEY, + user_id TEXT, + status TEXT DEFAULT 'open', + created_at INTEGER, + updated_at INTEGER, + subject TEXT, + description TEXT + ) +`).run(); + +try { + db.prepare("ALTER TABLE tickets ADD COLUMN voiceChannelId TEXT").run(); +} catch (e) { + if (!String(e).includes("duplicate column name")) { + throw e; + } +} + +db.prepare(` + CREATE TABLE IF NOT EXISTS ticket_panel ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id TEXT, + channel_id TEXT, + created_at INTEGER + ) +`).run(); + +db.prepare(` + CREATE TABLE IF NOT EXISTS ticket_users ( + ticket_id TEXT, + user_id TEXT, + PRIMARY KEY(ticket_id, user_id) + ) +`).run(); + +db.prepare(` + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY, + data TEXT + ) +`).run(); + +module.exports = { + addSuggestion(threadId, authorId, text) { + db.prepare(` + INSERT INTO suggestions (thread_id, author_id, text) + VALUES (@threadId, @authorId, @text) + ON CONFLICT(thread_id) DO NOTHING + `).run({ threadId, authorId, text }); + }, + + setSuggestionStatus(threadId, status) { + db.prepare(` + UPDATE suggestions SET status=@status WHERE thread_id=@threadId + `).run({ threadId, status }); + }, + + getSuggestion(threadId) { + return db.prepare(` + SELECT * FROM suggestions WHERE thread_id=@threadId + `).get({ threadId }); + }, + + // ----- Profiles ----- + getProfile(userId) { + const row = db.prepare(`SELECT * FROM profiles WHERE user_id=?`).get(userId); + if (!row) { + db.prepare(` + INSERT INTO profiles (user_id, bio, timezone, clockedIn) + VALUES (?, '', '', 0) + `).run(userId); + return { user_id: userId, bio: "", timezone: "", clockedIn: 0 }; + } + return row; + }, + + updateProfile(userId, data) { + const { bio = "", timezone = "", clockedIn = 0 } = data; + db.prepare(` + INSERT INTO profiles (user_id, bio, timezone, clockedIn) + VALUES (@userId, @bio, @timezone, @clockedIn) + ON CONFLICT(user_id) DO UPDATE SET + bio = excluded.bio, + timezone = excluded.timezone, + clockedIn = excluded.clockedIn + `).run({ userId, bio, timezone, clockedIn }); + }, + + setClockedIn(userId, value) { + db.prepare(`UPDATE profiles SET clockedIn=? WHERE user_id=?`) + .run(value ? 1 : 0, userId); + }, + + addTicket(ticket) { + const now = Date.now(); + db.prepare(` + INSERT INTO tickets (ticket_id, user_id, subject, description, created_at, updated_at) + VALUES (@ticket_id, @user_id, @subject, @description, @created_at, @updated_at) + ON CONFLICT(ticket_id) DO NOTHING + `).run({ + ticket_id: ticket.id, + user_id: ticket.user, + subject: ticket.reason || "", + description: ticket.description || "", + created_at: now, + updated_at: now + }); + }, + + updateTicketVoice(ticketId, voiceChannelId) { + db.prepare(` + UPDATE tickets SET voiceChannelId=@voiceChannelId, updated_at=@updatedAt WHERE ticket_id=@ticketId + `).run({ ticketId, voiceChannelId, updatedAt: Date.now() }); + }, + + getTicketVoice(ticketId) { + return db.prepare(`SELECT voiceChannelId FROM tickets WHERE ticket_id=?`).get(ticketId); + }, + + deleteTicketVoice(ticketId) { + db.prepare(` + UPDATE tickets SET voiceChannelId=NULL, updated_at=@updatedAt WHERE ticket_id=@ticketId + `).run({ ticketId, updatedAt: Date.now() }); + }, + + updateTicketStatus(ticketId, status) { + db.prepare(` + UPDATE tickets SET status=@status, updated_at=@updatedAt WHERE ticket_id=@ticketId + `).run({ status, updatedAt: Date.now(), ticketId }); + }, + + getTicket(ticketId) { + return db.prepare(` + SELECT * FROM tickets WHERE ticket_id=@ticketId + `).get({ ticketId }); + }, + + getAllTickets() { + return db.prepare(`SELECT * FROM tickets`).all(); + }, + + saveTicketPanel(messageId, channelId) { + db.prepare(` + INSERT INTO ticket_panel (message_id, channel_id, created_at) + VALUES (@messageId, @channelId, @createdAt) + `).run({ messageId, channelId, createdAt: Date.now() }); + }, + + getTicketPanel() { + return db.prepare(` + SELECT * FROM ticket_panel ORDER BY id DESC LIMIT 1 + `).get(); + }, + + addUserToTicket(ticketId, userId) { + db.prepare(` + INSERT INTO ticket_users (ticket_id, user_id) + VALUES (?, ?) + ON CONFLICT(ticket_id, user_id) DO NOTHING + `).run(ticketId, userId); + }, + + removeUserFromTicket(ticketId, userId) { + db.prepare(` + DELETE FROM ticket_users WHERE ticket_id=? AND user_id=? + `).run(ticketId, userId); + }, + + getTicketUsers(ticketId) { + return db.prepare(` + SELECT user_id FROM ticket_users WHERE ticket_id=? + `).all(ticketId).map(r => r.user_id); + }, + + getSettings() { + const row = db.prepare(`SELECT data FROM settings WHERE id=1`).get(); + if (!row) return {}; + try { + return JSON.parse(row.data); + } catch { + return {}; + } + }, + + saveSettings(obj) { + const data = JSON.stringify(obj); + db.prepare(` + INSERT INTO settings (id, data) + VALUES (1, @data) + ON CONFLICT(id) DO UPDATE SET data = excluded.data + `).run({ data }); + } +}; diff --git a/Structures/Event.js b/Structures/Event.js index d644b72..06363cb 100644 --- a/Structures/Event.js +++ b/Structures/Event.js @@ -1,5 +1,6 @@ // SupportBot | Emerald Services // Event Structure + class Event { constructor(event, run) { this.event = event; diff --git a/Structures/TicketID.js b/Structures/TicketID.js index 610a515..7fb941e 100644 --- a/Structures/TicketID.js +++ b/Structures/TicketID.js @@ -1,47 +1,30 @@ // SupportBot | Emerald Services // Ticket ID Structure -const fs = require("fs"); +const db = require("./Database") class TicketNumberID { static async pad() { - let num = await this.get(); - let n = num + ""; - await this.set(num + 1); - return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n; + let num = await this.get() + let n = num + "" + await this.set(num + 1) + return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n } static async get() { - let ticketnumberID = await JSON.parse( - fs.readFileSync("./Data/ticket-panel-id.json", "utf8") - ); - if (!ticketnumberID.id) { - return this.set(0); - } else { - return ticketnumberID.id; - } + const row = db.getTicketPanel() + if (!row) return await this.set(0) + return row.id } static async set(value) { - let ticketnumberID = await JSON.parse( - fs.readFileSync("./Data/ticket-panel-id.json", "utf8") - ); - - ticketnumberID.id = value; - fs.writeFileSync( - "./Data/ticket-panel-id.json", - JSON.stringify(ticketnumberID, null, 4), - (err) => { - if (err) console.error(err); - } - ); - - return value; + db.saveTicketPanel(`panel-${value}`, "none") + return value } static async reset() { - return await this.set(0); + return await this.set(0) } } -module.exports = TicketNumberID; +module.exports = TicketNumberID diff --git a/index.js b/index.js index 7343fe9..7690bda 100644 --- a/index.js +++ b/index.js @@ -8,9 +8,10 @@ // Installed with MIT License // // Discord Support: https://emeraldsrv.dev/discord -// Community Resources: https://emeraldsrv.dev/resources +// Community Resources: https://community.emeraldsrv.dev const fs = require("fs"); +const path = require("path"); const yaml = require("js-yaml"); const supportbot = yaml.load( @@ -24,13 +25,51 @@ const client = new Client({ client.start(supportbot.General.Token); - - - - - - - +// SupportBot - New Logging System + +const logTypes = ["Output", "Warn", "Error"]; + +if (!fs.existsSync("./Logs")) { + fs.mkdirSync("./Logs"); +} +logTypes.forEach((type) => { + const dir = `./Logs/${type}`; + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } +}); + +function logToFile(type, data) { + const date = new Date().toISOString().split("T")[0]; + const file = path.join(`./logs/${type}`, `${type}-${date}.log`); + fs.appendFileSync(file, `[${new Date().toISOString()}] ${data}\n`); +} + +const origLog = console.log; +console.log = (...args) => { + origLog(...args); + logToFile("output", args.join(" ")); +}; + +const origWarn = console.warn; +console.warn = (...args) => { + origWarn(...args); + logToFile("warn", args.join(" ")); +}; + +const origError = console.error; +console.error = (...args) => { + origError(...args); + logToFile("error", args.join(" ")); +}; + +process.on("unhandledRejection", (reason) => { + console.error("Unhandled Rejection:", reason); +}); + +process.on("uncaughtException", (err) => { + console.error("Uncaught Exception:", err); +}); diff --git a/package.json b/package.json index 9debb6c..eea17b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supportbot", - "version": "8.3", + "version": "8.4", "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": { @@ -18,10 +18,11 @@ }, "homepage": "https://community.emeraldsrv.dev", "dependencies": { - "@paypal/checkout-server-sdk": "^1.0.3", - "@vitalets/google-translate-api": "^8.0.0", "axios": "^1.7.2", + "better-sqlite3": "^12.2.0", "discord.js": "^14.13.0", + "figlet": "^1.9.3", + "gradient-string": "^3.0.0", "js": "^0.1.0", "js-yaml": "^4.1.0", "moment": "^2.29.3",