From a05d65099bb36426c704c80e41f24d70e629f005 Mon Sep 17 00:00:00 2001 From: 1tzEmerald Date: Sat, 20 Sep 2025 23:09:22 +0100 Subject: [PATCH] 8.4 DEV 1 Changed JSON to SQLlite Added Suggestion Forums Added /suggestadmin --- Commands/{ => General}/help.js | 4 +- Commands/{ => General}/info.js | 2 +- Commands/{ => General}/ping.js | 2 +- Commands/General/profile.js | 296 +++++++++++++++++++++++ Commands/{ => General}/translate.js | 4 +- Commands/Suggestions/suggest.js | 147 ++++++++++++ Commands/Suggestions/suggestadmin.js | 183 ++++++++++++++ Commands/Tickets/addUser.js | 119 +++++++++ Commands/{ => Tickets}/close.js | 60 ++--- Commands/{ => Tickets}/forceaddUser.js | 65 ++--- Commands/Tickets/removeUser.js | 108 +++++++++ Commands/{ => Tickets}/ticket.js | 165 ++++++------- Commands/Tickets/ticketpanel.js | 113 +++++++++ Commands/{ => Utils}/embed.js | 25 +- Commands/{ => Utils}/mod.js | 20 +- Commands/{ => Utils}/settings.js | 82 +++---- Commands/addUser.js | 217 ----------------- Commands/profile.js | 320 ------------------------- Commands/removeUser.js | 134 ----------- Commands/suggest.js | 179 -------------- Commands/ticketpanel.js | 120 ---------- Configs/commands.yml | 6 + Configs/supportbot.yml | 18 +- Data/Profiles.json | 1 - Data/Profiles/000000000000000.json | 29 --- Data/Profiles/0000000000000000.json | 5 - Data/StaffData.json | 1 - Data/SuggestionData.json | 1 - Data/TicketData.json | 5 - Data/profiles/0000000000000000.json | 5 - Data/settings.json | 1 - Data/supportbot.db | Bin 0 -> 57344 bytes Data/ticket-panel-id.json | 4 - Events/interactionCreate.js | 297 ++++++++--------------- Events/ready.js | 6 +- Structures/Client.js | 68 +++--- Structures/Database.js | 227 ++++++++++++++++++ Structures/Event.js | 1 + Structures/TicketID.js | 41 +--- package.json | 3 + 40 files changed, 1562 insertions(+), 1522 deletions(-) rename Commands/{ => General}/help.js (94%) rename Commands/{ => General}/info.js (95%) rename Commands/{ => General}/ping.js (93%) create mode 100644 Commands/General/profile.js rename Commands/{ => General}/translate.js (97%) create mode 100644 Commands/Suggestions/suggest.js create mode 100644 Commands/Suggestions/suggestadmin.js create mode 100644 Commands/Tickets/addUser.js rename Commands/{ => Tickets}/close.js (89%) rename Commands/{ => Tickets}/forceaddUser.js (64%) create mode 100644 Commands/Tickets/removeUser.js rename Commands/{ => Tickets}/ticket.js (81%) create mode 100644 Commands/Tickets/ticketpanel.js rename Commands/{ => Utils}/embed.js (95%) rename Commands/{ => Utils}/mod.js (95%) rename Commands/{ => Utils}/settings.js (77%) delete mode 100644 Commands/addUser.js delete mode 100644 Commands/profile.js delete mode 100644 Commands/removeUser.js delete mode 100644 Commands/suggest.js delete mode 100644 Commands/ticketpanel.js delete mode 100644 Data/Profiles.json delete mode 100644 Data/Profiles/000000000000000.json delete mode 100644 Data/Profiles/0000000000000000.json delete mode 100644 Data/StaffData.json delete mode 100644 Data/SuggestionData.json delete mode 100644 Data/TicketData.json delete mode 100644 Data/profiles/0000000000000000.json delete mode 100644 Data/settings.json create mode 100644 Data/supportbot.db delete mode 100644 Data/ticket-panel-id.json create mode 100644 Structures/Database.js 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/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 89% rename from Commands/close.js rename to Commands/Tickets/close.js index ef853cf..b56ef40 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; 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 81% rename from Commands/ticket.js rename to Commands/Tickets/ticket.js index ed84267..e023d89 100644 --- a/Commands/ticket.js +++ b/Commands/Tickets/ticket.js @@ -10,16 +10,19 @@ const { StringSelectMenuOptionBuilder, ButtonStyle, MessageCollector, + MessageFlags, } = 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 +43,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 +59,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 +78,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 +94,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 +115,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 +126,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 +157,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 +189,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 +231,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 +259,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 +273,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 +297,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 +323,7 @@ module.exports = new Command({ color: supportbot.Embed.Colours.Warn, }, ], - ephemeral: true, + flags: MessageFlags.Ephemeral }); } @@ -326,7 +341,7 @@ module.exports = new Command({ ) { return await interaction.reply({ embeds: [TicketExists], - ephemeral: true, + flags: MessageFlags.Ephemeral }); } @@ -335,7 +350,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 +376,7 @@ module.exports = new Command({ if (!category) { return interaction.reply({ content: "The ticket category does not exist!", - ephemeral: true, + flags: MessageFlags.Ephemeral }); } @@ -372,7 +387,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 +397,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 +431,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 +443,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,9 +453,8 @@ 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) @@ -486,7 +481,6 @@ module.exports = new Command({ ); } - // Create the control panel const SelectMenus = new StringSelectMenuBuilder() .setCustomId("ticketcontrolpanel") .setPlaceholder("Ticket Control Panel") @@ -542,6 +536,13 @@ module.exports = new Command({ components: [row2], }); + 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..ba3659f --- /dev/null +++ b/Commands/Tickets/ticketpanel.js @@ -0,0 +1,113 @@ +const fs = require("fs"); +const { + EmbedBuilder, + ApplicationCommandType, + ActionRowBuilder, + ButtonBuilder, + MessageFlags, +} = 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) {} + } + + 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], + }); + + 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 c01b584..0000000 --- a/Commands/suggest.js +++ /dev/null @@ -1,179 +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 mode = supportbot.Suggestions.Mode || "Standard"; - - let suggestion = interaction.options.getString("suggestion"); - - if (mode === "Forum") { - const forumChannel = await interaction.guild.channels.fetch(supportbot.Suggestions.ForumChannel).catch(() => null); - - const NoChannel = new Discord.EmbedBuilder() - .setTitle("Missing Forum Channel!") - .setDescription(msgconfig.Error.MissingChannel) - .setColor(supportbot.Embed.Colours.Error); - - if (!forumChannel || forumChannel.type !== Discord.ChannelType.GuildForum) { - return interaction.reply({ embeds: [NoChannel], ephemeral: true }); - } - - const SuggestForumEmbed = 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 UpvoteForumButton = new Discord.ButtonBuilder() - .setCustomId("upvote") - .setEmoji(supportbot.Suggestions.UpvoteEmoji) - .setStyle(supportbot.Suggestions.Buttons.Upvote); - - const DownvoteForumButton = new Discord.ButtonBuilder() - .setCustomId("downvote") - .setEmoji(supportbot.Suggestions.DownvoteEmoji) - .setStyle(supportbot.Suggestions.Buttons.Downvote); - - const RemoveVoteForumButton = new Discord.ButtonBuilder() - .setCustomId("removevote") - .setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title) - .setStyle(supportbot.Suggestions.Buttons.RemoveVote); - - const row = new Discord.ActionRowBuilder().addComponents(UpvoteForumButton, DownvoteForumButton, RemoveVoteForumButton); - - const post = await forumChannel.threads.create({ - name: suggestion.slice(0, 100), - message: { - embeds: [SuggestForumEmbed], - components: [row], - }, - reason: "New suggestion submitted", - autoArchiveDuration: 1440, - }); - - suggestions[post.id] = { - suggestion: suggestion, - author: interaction.user.id, - upvotes: [], - downvotes: [] - }; - - fs.writeFileSync("./Data/SuggestionData.json", JSON.stringify(suggestions, null, 2)); - - const successEmbed = new Discord.EmbedBuilder() - .setTitle(msgconfig.Suggestions.Sent_Title) - .setDescription(msgconfig.Suggestions.Sent) - .addFields({ name: "Posted in forum:", value: `<#${forumChannel.id}>` }) - .setColor(supportbot.Embed.Colours.Success); - - return interaction.reply({ embeds: [successEmbed], ephemeral: true }); - } - - const suggestChannel = await getChannel(supportbot.Suggestions.Channel, interaction.guild); - - const NoChannel = new Discord.EmbedBuilder() - .setTitle("Missing Channel!") - .setDescription(msgconfig.Error.MissingChannel) - .setColor(supportbot.Embed.Colours.Error); - - if (!suggestChannel) return interaction.reply({ embeds: [NoChannel] }); - - 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); - - 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/supportbot.yml b/Configs/supportbot.yml index bbcc6e0..d19a3e1 100644 --- a/Configs/supportbot.yml +++ b/Configs/supportbot.yml @@ -21,7 +21,7 @@ SupportBot_Version: "v8.4-DEV-1" General: Name: "SupportBot" - Token: "DISCORD_BOT_TOKEN" + Token: "BOT_TOKEN" Addons: Enabled: true @@ -95,7 +95,7 @@ Leave: # ---------------------------- Suggestions: - Mode: "Forum" # or Forum + Mode: "Channel" # Channel or Forum ForumChannel: "1402655496477081671" Channel: "1295788900094316706" @@ -129,13 +129,19 @@ Suggestions: # 3 - Green # 4 - Red + Tags: + Created: "1418937323118137435" + Considering: "1418937237156139130" + Solved: "1418937168029679697" + Denied: "1418937185469464647" + # ------------------------- # Tickets # ------------------------- VoiceTickets: Name: "%username%'s VC" - Category: "1297539636159315999" + Category: "1419062819688550460" Ticket: TicketType: "channels" @@ -151,9 +157,9 @@ Ticket: # - Channel where ticket threads are made. (only if ticket threads is enabled above.) # For TicketType: channels - TicketChannelsCategory: "1295789095070728253" - TicketChannelsCategory2: "1295789210040537088" # This will be used if the first category has reached the max channel limit. - TicketArchiveCategory: "1295789286322343946" + TicketChannelsCategory: "1418988408193155112" + TicketChannelsCategory2: "1418988456167870615" # This will be used if the first category has reached the max channel limit. + TicketArchiveCategory: "1418988434600759348" # --- Channel: "ticket-" # The prefix for ticket channel names 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/Profiles/000000000000000.json b/Data/Profiles/000000000000000.json deleted file mode 100644 index 2e52c6b..0000000 --- a/Data/Profiles/000000000000000.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "bio": "", - "timezone": "", - "clockedIn": true, - "workingHours": 0, - "lastClockInTime": null, - "schedule": { - "monday": { - "start": "09:00", - "end": "17:00" - }, - "tuesday": { - "start": "09:00", - "end": "17:00" - }, - "wednesday": { - "start": "09:00", - "end": "17:00" - }, - "thursday": { - "start": "09:00", - "end": "17:00" - }, - "friday": { - "start": "09:00", - "end": "17:00" - } - } -} \ 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/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/supportbot.db b/Data/supportbot.db new file mode 100644 index 0000000000000000000000000000000000000000..672dfb3ac04f0ed8dce1369084747577974d1679 GIT binary patch literal 57344 zcmeI*Z)_W99S88cI8I_acjva$UKdq(DxtC3wfFxI3<LSFbee;@ycOrGaPA0Ex*9?QL!{Z_7;y>Fw{ z5r+T-AOHafKmY>&e}RkT^l))(jQv8nS3fi7^xBQHPR|V=h7O&mR%WWDnacggs--Yk zn#h)D3SQOPw)dhmQ+;Tr^uUS9TxT&_B3LGfyS;jE$@N!^);(N3 zQaN>OrnI-a;B@u|i!~OVde5OC({bj!!g^H+!-s(*zTwO;GD&#hfK2=+InFgJzE z!cCU(HvZ2)7%TCev+S}PcL8VGU2nsrslP?`+Vjq*x*aDTYs__N4{c9&qMl!h_MD>Eq6Y^z+YF*I z)uiZIdgTB+T->vVy|^dX5SC_V9qIwyj_dZH4kT(8n-Ny7*P`xaN8NCJiWM4 zVwH0q4foOgSpWBe7h9{@-a|p}-Rsp*8yG51?%Al#Vr8pIu@@=&O*-QJHs!p#ymyBQ z0uX=z1Rwwb2tWV=5P$##AOL}_DvU$-3yB|4#`oF~V^!a#I7F$NEK5Gdz%q}ft@KSv;O#><7!MkyTSGTecQRvTBOEe6_R7}~_Ek!c*^?$K?{}7SA)ZX>|{RW;Ex3{~AQ?amwDrnH6Zj#+!wc4ytge2}NOmPfM`I)O&n6vZ#_ zEhp5%;%)!qQ3-W2_6Nf$AkkD)rW1w4Ol}LgAp3i{pkAV( z=(n9Pt3S=KFGSE*-ilI%uJp=58orFjf> zTlgs>d%gQVtRi)uGue!fvX2Wk2i-IjNwOqaluVV{>edsuYbrOt{~WbK_VKsp=iA*! zT3*#JSf&}mQzx+u$rDx$`J=-@<@Q8Unyl)gYDto=ZC1J0&$FJC^wyD2oU9z7Tjk`D zsdPeJqv9&2AyPwXlD0{4U!8uKa$R}lpUq{D-ClN^ZgXldnM^YjOQF84SdzF&|8A|e z|3S%qoqoC1YSLEQq95CxOa;mA32I5j(KSt^Ml>{SGsvHO;CGa4_0YAZvrL8aZim?* zRWV4V(9Vf^G3}_Rben;F+4%^idS>iv{`NaP5Mj!+qR;v+LaT{4pw(cm6-m)A9eGg;#_Zh0DU1gmc1ap(2!p zZT#E(ReqI!p1(v3V1fVyAOHafKmY;|fB*y_009X6F9PA|>RZSC^8I*X;7;hr69aL4 zEFK?P<6`mnVA*Ito*3rJt;LfAFWG)PF%*>P#}mU>BmKA(A6*%a#pB~sLu>Km2uivi zPYi|(_T!0hjp+10eJmg`yf9EmDH(s5Ejf7Meg2>4zrxU`|9>nzEqqa!7fuNu62=9N ze}k?Ec$t5WW@3T>1Rwwb2tWV=5P$##AOHafyu$)TKF#djnfAXK@?J^p(AWLZRSmno z{<9(%pP_^^c3!`~I2xZJhcmWcJyFcXXGqbE%JpJ4K0^#=U~Ie&h6ug`rqn598N9eYKE|#kDa0`oa$igRyv6=hl_$-zcPF z@vzFRS0{f|7>LEg8n;&M?@DZ%QIxcQm`@k&Xmr^KU7(;@bm@d_X@;rNMK^T8fgy^L zA<-oTmsfx29sje!Um3b#f&c^{009U<00Izz00bZa0SG`~O9H+}>KmY;|fB*y_009U< z00I!W69gvM6n$v~eWiey{BMG)Dl4+0shVWyl1g90A$sTkar}QL^a<1v0uX=z1Rwwb z2tWV=5P$##ww^$FP?3WF_W$pW|D*H&Th9PBf&c^{009U<00Izz00bZa0SG`K68IN6 Cw3&YZ literal 0 HcmV?d00001 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/interactionCreate.js b/Events/interactionCreate.js index 1849074..2346eeb 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) => { @@ -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,31 +87,24 @@ 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, }); } - 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, @@ -146,137 +133,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!", + ephemeral: true, + }); + } + + 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 +243,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 +252,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { }); } } - if (Admin && Admin.id) { await interaction.channel.permissionOverwrites.edit(Admin.id, { ViewChannel: true, @@ -304,59 +259,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,38 +326,29 @@ 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, }); - - 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, @@ -422,11 +356,9 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { } } 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, @@ -436,36 +368,28 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { const errorEmbed = new Discord.EmbedBuilder() .setDescription('⚠️ This button is not linked to a valid voice channel.') .setColor(supportbot.Embed.Colours.Warn); - await interaction.reply({ embeds: [errorEmbed], ephemeral: true, }); } } - - if (interaction.isButton()) { if (interaction.customId === "unarchiveTicket") { - 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 +397,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 +406,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 +415,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { }); } } - if (Admin && Admin.id) { await interaction.channel.permissionOverwrites.edit(Admin.id, { ViewChannel: true, @@ -501,7 +422,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 +429,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 +452,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { .setRequired(true) ) ); - await interaction.showModal(modal); } else { try { @@ -566,7 +482,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { .setRequired(true) ) ); - await interaction.showModal(modal); } else { try { @@ -589,7 +504,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { }); } - 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 +516,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 +527,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] @@ -777,7 +679,6 @@ module.exports = new Event("interactionCreate", async (client, interaction) => { }); } - // 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`; diff --git a/Events/ready.js b/Events/ready.js index 169a74e..b4d575d 100644 --- a/Events/ready.js +++ b/Events/ready.js @@ -60,9 +60,7 @@ const logStartup = async (client) => { // Links section console.log(gradient.summer("┏━━━━━━━━━━━━━━━━━━━ Important Links ━━━━━━━━━━━━━━━━━━━┓")); console.log(gradient.summer("┃ ┃")); - console.log(gradient.summer("┃ ") + "🌐 Website : " + gradient.cristal("https://emeraldsrv.com ┃")); - console.log(gradient.summer("┃ ") + "📚 Docs : " + gradient.cristal("https://docs.emeraldsrv.com ┃")); - console.log(gradient.summer("┃ ") + "💬 Community : " + gradient.cristal("https://community.emeraldsrv.com ┃")); + 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")); @@ -123,7 +121,7 @@ const checkConfigs = async (client) => { console.log(gradient.morning("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n")); }; -module.exports = new Event("ready", async (client) => { +module.exports = new Event("clientReady", async (client) => { setActivity(client, supportbot?.Activity?.Type, supportbot?.Activity?.Status); await logStartup(client); await checkConfigs(client); diff --git a/Structures/Client.js b/Structures/Client.js index 94ca573..a1388fe 100644 --- a/Structures/Client.js +++ b/Structures/Client.js @@ -1,12 +1,27 @@ 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 +38,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 +47,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 +63,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 +139,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 +150,6 @@ class Client extends Discord.Client { return addon; }); - // Addons console.log(" "); console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬"); @@ -163,8 +179,6 @@ class Client extends Discord.Client { console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬"); } - // Register Slash Commands - this.once("ready", async () => { await this.guilds.cache.first()?.commands.set(this.commands); console.log( @@ -196,4 +210,4 @@ class Client extends Discord.Client { } } -module.exports = Client; \ No newline at end of file +module.exports = Client; diff --git a/Structures/Database.js b/Structures/Database.js new file mode 100644 index 0000000..7134056 --- /dev/null +++ b/Structures/Database.js @@ -0,0 +1,227 @@ +// SupportBot | Emerald Services +// Database Structure + +const Database = require("better-sqlite3"); +const path = require("path"); + +const db = new Database(path.join(__dirname, "../Data/supportbot.db")); + +// ----- Core tables ----- +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(); + +/* ---- NEW table for global bot settings ---- */ +db.prepare(` + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY, + data TEXT + ) +`).run(); + +module.exports = { + // ----- Suggestions ----- + 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); + }, + + // ----- Tickets ----- + 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(); + }, + + // ----- Ticket participants ----- + 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); + }, + + // ----- Global Settings ----- + 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/package.json b/package.json index 9debb6c..af2d866 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ "@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",