From fd65d0a9a70e90b0f7269825913a5b65e1767767 Mon Sep 17 00:00:00 2001 From: 1tzEmerald Date: Wed, 3 Jul 2024 13:46:57 +0100 Subject: [PATCH] 8.1-BETA --- Addons/addon.js | 26 ++ Commands/genImage.js | 79 ++++ Commands/help.js | 31 +- Commands/profile.js | 323 +++++++++++++ Commands/ticket.js | 649 ++++++++++++++++---------- Commands/ticketstats.js | 56 --- Configs/messages.yml | 3 + Configs/supportbot-ai.yml | 6 +- Configs/supportbot.yml | 10 +- Data/Profiles.json | 7 + Data/Profiles/294146279456636929.json | 5 + Data/Profiles/419821884431925260.json | 5 + Data/Profiles/682708736359399524.json | 5 + Data/StaffData.json | 3 + Data/SuggestionData.json | 17 +- Data/TicketData.json | 181 ++++++- Data/ticket-panel-id.json | 4 +- Events/interactionCreate.js | 47 ++ Events/messageCreate.js | 180 ++++--- LICENSE.md | 2 +- Structures/Client.js | 37 +- 21 files changed, 1252 insertions(+), 424 deletions(-) create mode 100644 Addons/addon.js create mode 100644 Commands/genImage.js create mode 100644 Commands/profile.js delete mode 100644 Commands/ticketstats.js create mode 100644 Data/Profiles.json create mode 100644 Data/Profiles/294146279456636929.json create mode 100644 Data/Profiles/419821884431925260.json create mode 100644 Data/Profiles/682708736359399524.json create mode 100644 Data/StaffData.json diff --git a/Addons/addon.js b/Addons/addon.js new file mode 100644 index 0000000..7609fd7 --- /dev/null +++ b/Addons/addon.js @@ -0,0 +1,26 @@ +const Discord = require("discord.js"); +const fs = require("fs"); +const yaml = require("js-yaml"); +const { Command } = require('../Structures/Addon.js'); // Adjust the path as needed +const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); + +module.exports = new Command({ + name: "addon", + description: "This is a sample addon!", + options: [], + permissions: ["Administrator"], + + async run(interaction) { + let disableCommand = true; + + const PingEmbed = new Discord.EmbedBuilder() + .setDescription( + `This is a sample addon.\n\nPong! \`${interaction.client.ws.ping} ms\`` + ) + .setColor(supportbot.Embed.Colours.General); + + interaction.reply({ + embeds: [PingEmbed], + }); + }, + }); \ No newline at end of file diff --git a/Commands/genImage.js b/Commands/genImage.js new file mode 100644 index 0000000..de8e94c --- /dev/null +++ b/Commands/genImage.js @@ -0,0 +1,79 @@ +const fs = require("fs"); +const Discord = require("discord.js"); +const yaml = require("js-yaml"); +const generateImage = require('../Structures/generateImage.js'); + +const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); +const Command = require("../Structures/Command.js"); + +module.exports = new Command({ + name: "image", + description: "Create an image through SupportBot AI", + type: Discord.ApplicationCommandType.ChatInput, + options: [ + { + name: "prompt", + description: "The prompt to generate an image from", + type: Discord.ApplicationCommandOptionType.String, + required: true, + }, + { + name: "negativeprompt", + description: "The negative prompt to exclude from the image", + type: Discord.ApplicationCommandOptionType.String, + required: false, + }, + { + name: "cfgscale", + description: "The cfg scale for the image generation", + type: Discord.ApplicationCommandOptionType.Number, + required: false, + } + ], + permissions: "SendMessages", + + async run(interaction) { + const prompt = interaction.options.getString("prompt"); + const negativePrompt = interaction.options.getString("negativeprompt") || ""; + const cfgScale = interaction.options.getNumber("cfgscale") || 7.5; // Default value + + try { + const imageUrl = await generateImage(prompt, negativePrompt, cfgScale); + + const ImageEmbed = new Discord.EmbedBuilder() + .setTitle("Generated Image") + .setDescription(`Here is the image generated for your prompt: "${prompt}"`) + .setImage(imageUrl) + .setColor(supportbot.Embed.Colours.General) + .setFooter({ + text: supportbot.Embed.Footer, + iconURL: interaction.user.displayAvatarURL(), + }); + + if (!interaction.replied) { + await interaction.reply({ + ephemeral: true, + embeds: [ImageEmbed], + }); + } else { + await interaction.followUp({ + ephemeral: true, + embeds: [ImageEmbed], + }); + } + } catch (error) { + console.error('Error generating image:', error); + if (!interaction.replied) { + await interaction.reply({ + ephemeral: true, + content: `An error occurred while generating the image: ${error.message}. Please try again later.`, + }); + } else { + await interaction.followUp({ + ephemeral: true, + content: `An error occurred while generating the image: ${error.message}. Please try again later.`, + }); + } + } + }, +}); diff --git a/Commands/help.js b/Commands/help.js index edea11b..7e0f48a 100644 --- a/Commands/help.js +++ b/Commands/help.js @@ -1,17 +1,9 @@ -// SupportBot | Emerald Services -// Help Command - const fs = require("fs"); const Discord = require("discord.js"); const yaml = require("js-yaml"); -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8") -); - -const cmdconfig = yaml.load( - fs.readFileSync("./Configs/commands.yml", "utf8") -); +const 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"); @@ -32,11 +24,17 @@ module.exports = new Command({ ); } - // Fetch all commands from the client's commands collection + // Separate regular commands and addon commands let botCommands = ""; + let addonCommands = ""; + interaction.client.commands.forEach((command) => { if (command.permissions) { - botCommands += `\`${command.name}\` `; + if (command.addon) { + addonCommands += `\`${command.name}\` `; + } else { + botCommands += `\`${command.name}\` `; + } } }); @@ -51,15 +49,6 @@ module.exports = new Command({ inline: false, }); - // Conditionally add Addons section - if (supportbot.General.Addons.Enabled) { - HelpEmbed1.addFields({ - name: "🛠️ Addons\n", - value: "No addons available.", - inline: false, - }); - } - HelpEmbed1.setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL(), diff --git a/Commands/profile.js b/Commands/profile.js new file mode 100644 index 0000000..582d042 --- /dev/null +++ b/Commands/profile.js @@ -0,0 +1,323 @@ +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(); + +async function getRole(roleName, guild) { + return guild.roles.cache.find(role => role.name === roleName); +} + +function updateClockedInStatus(profileEmbed, clockedIn) { + profileEmbed.data.fields = profileEmbed.data.fields.map(field => { + if (field.name === 'Clocked In Status') { + field.value = clockedIn ? '✅ Clocked In' : '❌ Clocked Out'; + } + return field; + }); + return profileEmbed; +} + +module.exports = new Command({ + name: 'profile', + description: 'Show user profile', + type: ApplicationCommandType.ChatInput, + permissions: ["SendMessages"], + options: [ + { + name: 'user', + description: 'The user to view the profile of', + type: ApplicationCommandOptionType.User, + required: false + } + ], + + async run(interaction) { + await interaction.deferReply({ ephemeral: true }); // Acknowledge the interaction immediately + + 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 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/ticket.js b/Commands/ticket.js index ccb5c0b..7514a2e 100644 --- a/Commands/ticket.js +++ b/Commands/ticket.js @@ -8,7 +8,7 @@ const { ButtonBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, - GuildMemberRoleManager, + ButtonStyle } = require("discord.js"); const yaml = require("js-yaml"); @@ -20,6 +20,191 @@ const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); const Command = require("../Structures/Command.js"); const TicketNumberID = require("../Structures/TicketID.js"); +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); + } + } + return clockedInUsers; +} + +async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts = 0) { + console.log(`Assigning ticket attempt ${attempts}`); + + try { + const clockedInUsers = await getClockedInUsers(interaction.guild); + console.log(`Clocked in users: ${Array.from(clockedInUsers).join(', ')}`); + + if (clockedInUsers.size === 0 || attempts >= 2) { + console.log("No clocked in users or max attempts reached"); + const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel); + if (claimChannel) { + const claimEmbed = new EmbedBuilder() + .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle || 'Default Claim Title') + .setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage || 'Default Claim Message') + .setColor(supportbot.Embed.Colours.General); + + const claimButton = new ButtonBuilder() + .setCustomId(`claimticket-${ticketChannel.id}`) + .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || 'Claim Ticket') + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(claimButton); + + const claimMessage = await claimChannel.send({ + content: `<@&${Staff.id}> <@&${Admin.id}>`, + embeds: [claimEmbed], + components: [row], + }); + + 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 => { + console.log(`Ticket claimed by user ${i.user.id}`); + await ticketChannel.send({ content: `<@${i.user.id}> has claimed the ticket.` }); + }); + + collector.on('end', async collected => { + if (collected.size === 0) { + console.log("No one claimed the ticket, notifying admins"); + await claimMessage.delete(); + + 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)], + }); + } + }); + + return null; + } else { + console.error("Claim channel not found"); + return null; + } + } + + const clockedInArray = Array.from(clockedInUsers); + const assignedUserId = clockedInArray[Math.floor(Math.random() * clockedInArray.length)]; + const assignedUser = await interaction.guild.members.fetch(assignedUserId); + console.log(`Assigned user: ${assignedUserId}`); + + const claimEmbed = new EmbedBuilder() + .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle || 'Default Claim Title') + .setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage.replace('%user%', interaction.user.id) || 'Default Claim Message') + .setColor(supportbot.Embed.Colours.General); + + const claimButton = new ButtonBuilder() + .setCustomId(`claimticket-${ticketChannel.id}`) + .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); + if (!claimChannel) { + console.error("Claim channel not found"); + return null; + } + + const claimMessage = await claimChannel.send({ + content: `${assignedUser}`, + embeds: [claimEmbed], + components: [row], + }); + + const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.user.id === assignedUserId; + const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 }); + + collector.on('collect', async i => { + console.log(`Ticket claimed by user ${i.user.id}`); + await ticketChannel.send({ content: `<@${assignedUserId}>` }); + }); + + collector.on('end', async collected => { + if (collected.size === 0) { + console.log("No one claimed the ticket, attempting reassignment"); + 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)], + }); + + // Add a claim button for any staff member to click + const claimEmbed = new EmbedBuilder() + .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle) + .setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage) + .setColor(supportbot.Embed.Colours.General); + + const claimButton = new ButtonBuilder() + .setCustomId(`claimticket-${ticketChannel.id}`) + .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle) + .setEmoji(supportbot.Ticket.ClaimTickets.ButtonEmoji) + .setStyle(ButtonStyle.Primary); + + const row = new ActionRowBuilder().addComponents(claimButton); + + const claimMessage = await claimChannel.send({ + content: `<@&${Staff.id}> <@&${Admin.id}>`, + embeds: [claimEmbed], + components: [row], + }); + + 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 => { + console.log(`Ticket ${ticketChannel.id} claimed by user ${i.user.id}`); + await ticketChannel.member.add(i.user.id); + }); + + collector.on('end', async collected => { + if (collected.size === 0) { + console.log(`No one claimed the ticket (${ticketChannel.id}), notifying all staff`); + await claimMessage.delete(); + + await claimChannel.send({ + content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`, + }); + } + }); + } else { + const ReassigningEmbed = new EmbedBuilder() + .setTitle(msgconfig.Ticket.ClaimTickets.ReassigningTitle) + .setDescription(msgconfig.Ticket.ClaimTickets.ReassigningMessage.replace('%user%', assignedUserId)) + .setColor(supportbot.Embed.Colours.General); + + await claimChannel.send({ embeds: [ReassigningEmbed], components: [] }); + await assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts + 1); + } + } + }); + + return assignedUserId; + } catch (error) { + console.error("Error in assignTicketToUser function:", error); + if (error instanceof CombinedError) { + console.error("CombinedError details:", error.errors); + } + return null; + } +} + + + + + module.exports = new Command({ name: cmdconfig.OpenTicket.Command, description: cmdconfig.OpenTicket.Description, @@ -34,275 +219,233 @@ module.exports = new Command({ permissions: cmdconfig.OpenTicket.Permission, async run(interaction) { - let department = interaction.customId?.split("-")[1] || null; - let TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); - const { getRole, getChannel } = interaction.client; - let User = interaction.guild.members.cache.get(interaction.user.id); + try { + let department = interaction.customId?.split("-")[1] || null; + let TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); + const { getRole, getChannel } = interaction.client; + let User = interaction.guild.members.cache.get(interaction.user.id); - if ( - supportbot.Ticket.TicketsPerUser && - TicketData.tickets.filter((t) => t.user === interaction.user.id && t.open).length >= supportbot.Ticket.TicketsPerUser - ) { - return interaction.reply({ - embeds: [ - { - title: "Too Many Tickets!", - description: `You can't have more than ${supportbot.Ticket.TicketsPerUser} open tickets!`, - color: supportbot.Embed.Colours.Warn, - }, - ], - ephemeral: true, - }); - } - - if (User.roles.cache.has(supportbot.Roles.ModRoles.Blacklisted)) { - return interaction.reply({ - content: msgconfig.Ticket.Blacklisted, - ephemeral: true, - }); - } - - if (User.roles.cache.has(supportbot.Roles.ModRoles.Muted)) { - return interaction.reply({ - content: msgconfig.Ticket.Muted, - ephemeral: true, - }); - } - - let ticketNumberID = await TicketNumberID.pad(); - const TicketSubject = interaction.options?.getString("reason") || msgconfig.Ticket.InvalidSubject; - - const TicketExists = new EmbedBuilder() - .setTitle("Ticket Exists!") - .setDescription(msgconfig.Ticket.TicketExists); - - if ( - await interaction.guild.channels.cache.find( - (ticketChannel) => ticketChannel.name === `${supportbot.Ticket.Channel}${ticketNumberID}` - ) - ) { - return await interaction.reply({ - embeds: [TicketExists], - ephemeral: true, - }); - } - - 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.reply({ - content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", - ephemeral: true, - }); - - const TicketCategory = interaction.guild.channels.cache.find( - (c) => c.name === supportbot.Ticket.TicketCategory || c.id === supportbot.Ticket.TicketCategory - ); - - const channel = await getChannel(supportbot.Ticket.TicketCategory, interaction.guild); - const ticketChannel = await channel.threads.create({ - name: `${supportbot.Ticket.Channel}${ticketNumberID}`, - type: ChannelType.PrivateThread, - parent: TicketCategory, - autoArchiveDuration: 60, - reason: 'support ticket', - }); - - if (supportbot.Ticket.ClaimTickets.Enabled === false) { - const InvStaff = await ticketChannel.send(`<@&${Staff.id}> <@&${Admin.id}>`) - - setTimeout(() => { - InvStaff.delete(); - }, 2000) - } - - // TICKET CLAIMING SYSTEM [START] - - if (supportbot.Ticket.ClaimTickets.Enabled) { - const InvStaff1 = await ticketChannel.send(`<@&${Admin.id}>`) - - setTimeout(() => { - InvStaff1.delete(); - }, 2000) - - const claimChannel = await getChannel( - supportbot.Ticket.ClaimTickets.Channel, - interaction.guild - ); - - const claimEmbed = new EmbedBuilder() - .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle) - .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) - .setStyle(supportbot.Ticket.ClaimTickets.Button); - - const row = new ActionRowBuilder().addComponents(claimButton); - - await claimChannel.send({ - content: `<@&${Staff.id}>`, - embeds: [claimEmbed], - components: [row] - }); - - } - - // TICKET CLAIMING SYSTEM [END] - - if (!department) { - await ticketChannel.members.add(interaction.user.id); - } - - TicketData.tickets.push({ - id: ticketChannel.id, - name: ticketChannel.name, - user: interaction.user.id, - number: ticketNumberID, - reason: TicketSubject, - open: true, - subUsers: [], - createdAt: new Date().toISOString(), - claimedAt: null, // Will be updated when claimed - }); - - fs.writeFileSync( - "./Data/TicketData.json", - JSON.stringify(TicketData, null, 4), - (err) => { - if (err) console.error(err); + if ( + supportbot.Ticket.TicketsPerUser && + TicketData.tickets.filter((t) => t.user === interaction.user.id && t.open).length >= supportbot.Ticket.TicketsPerUser + ) { + return interaction.reply({ + embeds: [ + { + title: "Too Many Tickets!", + description: `You can't have more than ${supportbot.Ticket.TicketsPerUser} open tickets!`, + color: supportbot.Embed.Colours.Warn, + }, + ], + ephemeral: true, + }); } - ); - const CreatedTicket = new EmbedBuilder() - .setDescription( - msgconfig.Ticket.TicketCreatedAlert.replace(/%ticketauthor%/g, interaction.user.id) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username) - ) - .setColor(supportbot.Embed.Colours.General); - await interaction.reply({ embeds: [CreatedTicket], ephemeral: true }); + if (User.roles.cache.has(supportbot.Roles.ModRoles.Blacklisted)) { + return interaction.reply({ + content: msgconfig.Ticket.Blacklisted, + ephemeral: true, + }); + } - if (supportbot.Ticket.AllowMentions) { - await ticketChannel.send(`${interaction.user}`); - } + if (User.roles.cache.has(supportbot.Roles.ModRoles.Muted)) { + return interaction.reply({ + content: msgconfig.Ticket.Muted, + ephemeral: true, + }); + } - const TicketMessage = new EmbedBuilder() - .setAuthor({ - name: msgconfig.Ticket.TicketAuthorTitle.replace(/%ticketauthor%/g, interaction.user.id) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username), - iconURL: interaction.user.displayAvatarURL(), - }) - .setTitle( - msgconfig.Ticket.TicketTitle.replace(/%ticketauthor%/g, interaction.user.id) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username) - ) - .setDescription( - msgconfig.Ticket.TicketMessage.replace(/%ticketauthor%/g, interaction.user.id) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username) - ) - .setColor(supportbot.Embed.Colours.General); + let ticketNumberID = await TicketNumberID.pad(); + const TicketSubject = interaction.options?.getString("reason") || msgconfig.Ticket.InvalidSubject; - const SelectMenus = new StringSelectMenuBuilder() - .setCustomId("ticketcontrolpanel") - .setPlaceholder("Ticket Control Panel") - .addOptions( - new StringSelectMenuOptionBuilder() - .setLabel("Add User") - .setDescription("Add a user to the ticket.") - .setEmoji(supportbot.SelectMenus.Tickets.AddUserEmoji) - .setValue("ticketadduser"), - new StringSelectMenuOptionBuilder() - .setLabel("Remove User") - .setDescription("Remove a user from the ticket.") - .setEmoji(supportbot.SelectMenus.Tickets.RemoveUserEmoji) - .setValue("ticketremoveuser"), - new StringSelectMenuOptionBuilder() - .setLabel("Close") - .setDescription("Close the ticket.") - .setEmoji(supportbot.SelectMenus.Tickets.CloseEmoji) - .setValue("ticketclose"), - new StringSelectMenuOptionBuilder() - .setLabel("Enable Invites") - .setDescription("Enable invites.") - .setEmoji(supportbot.SelectMenus.Tickets.EnableInvitesEmoji) - .setValue("enableinvites"), - new StringSelectMenuOptionBuilder() - .setLabel("Disable Invites") - .setDescription("Disable invites.") - .setEmoji(supportbot.SelectMenus.Tickets.DisableInvitesEmoji) - .setValue("disableinvites"), - ); + const TicketExists = new EmbedBuilder() + .setTitle("Ticket Exists!") + .setDescription(msgconfig.Ticket.TicketExists); - const row2 = new ActionRowBuilder().addComponents(SelectMenus); + if ( + await interaction.guild.channels.cache.find( + (ticketChannel) => ticketChannel.name === `${supportbot.Ticket.Channel}${ticketNumberID}` + ) + ) { + return await interaction.reply({ + embeds: [TicketExists], + ephemeral: true, + }); + } - if (!department) { - try { - let buttons = supportbot.Departments.map((x) => - new ButtonBuilder() - .setCustomId("Department" + supportbot.Departments.indexOf(x)) - .setLabel(x.title) - .setStyle(x.color) - .setEmoji(x.emoji) - ); - - if (supportbot.Ticket.AllowMentions) { - await ticketChannel.send("<@&" + supportbot.Ticket.RoleMention + ">"); - } - - const row = new ActionRowBuilder().addComponents(buttons); - const m = await ticketChannel.send({ - embeds: [TicketMessage], - components: [row2], + 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.reply({ + content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", + ephemeral: true, }); - try { - const filter = (i) => i.user.id === interaction.user.id; - await m.awaitMessageComponent({ - filter, - max: 1, - componentType: "BUTTON", - time: supportbot.Ticket.Timeout * 60000, - }); - } catch (e) { - if (e.code === "COLLECTOR_ERROR") { - try { - let ticket = TicketData.tickets.findIndex((t) => t.id === ticketChannel.id); - TicketData.tickets[ticket].open = false; - fs.writeFileSync( - "./Data/TicketData.json", - JSON.stringify(TicketData, null, 4), - (err) => { - if (err) console.error(err); - } - ); - interaction.user.send( - "Your ticket has timed out. Please open a new one, and select a department." - ); - } catch (error) { - console.error("An error occurred while handling collector:", error); - } - return await ticketChannel.delete(); - } + const TicketCategory = interaction.guild.channels.cache.find( + (c) => c.name === supportbot.Ticket.TicketCategory || c.id === supportbot.Ticket.TicketCategory + ); + + const channel = await getChannel(supportbot.Ticket.TicketCategory, interaction.guild); + const ticketChannel = await channel.threads.create({ + name: `${supportbot.Ticket.Channel}${ticketNumberID}`, + type: ChannelType.PrivateThread, + parent: TicketCategory, + autoArchiveDuration: 60, + reason: 'support ticket', + }); + + // 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) { + const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel); + await claimChannel.send(`<@&${Staff.id}> <@&${Admin.id}>`); } - } catch (error) { - console.error("Error sending message:", error); } - } else { + + // Add user to the ticket channel + if (!department) { + await ticketChannel.members.add(interaction.user.id); + } + + // Save ticket data + TicketData.tickets.push({ + id: ticketChannel.id, + name: ticketChannel.name, + user: interaction.user.id, + number: ticketNumberID, + reason: TicketSubject, + open: true, + subUsers: [], + createdAt: new Date().toISOString(), + claimedAt: null, + }); + + 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) + .replace(/%ticketid%/g, ticketChannel.id) + .replace(/%ticketusername%/g, interaction.user.username) + ) + .setColor(supportbot.Embed.Colours.General); + await interaction.reply({ embeds: [CreatedTicket], ephemeral: true }); + + // Create the ticket message + const TicketMessage = new EmbedBuilder() + .setAuthor({ + name: msgconfig.Ticket.TicketAuthorTitle.replace(/%ticketauthor%/g, interaction.user.id) + .replace(/%ticketid%/g, ticketChannel.id) + .replace(/%ticketusername%/g, interaction.user.username), + iconURL: interaction.user.displayAvatarURL(), + }) + .setTitle( + msgconfig.Ticket.TicketTitle.replace(/%ticketauthor%/g, interaction.user.id) + .replace(/%ticketid%/g, ticketChannel.id) + .replace(/%ticketusername%/g, interaction.user.username) + ) + .setDescription( + msgconfig.Ticket.TicketMessage.replace(/%ticketauthor%/g, interaction.user.id) + .replace(/%ticketid%/g, ticketChannel.id) + .replace(/%ticketusername%/g, interaction.user.username) + ) + .setColor(supportbot.Embed.Colours.General); + + // Create the control panel + const SelectMenus = new StringSelectMenuBuilder() + .setCustomId("ticketcontrolpanel") + .setPlaceholder("Ticket Control Panel") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Add User") + .setDescription("Add a user to the ticket.") + .setEmoji(supportbot.SelectMenus.Tickets.AddUserEmoji) + .setValue("ticketadduser"), + new StringSelectMenuOptionBuilder() + .setLabel("Remove User") + .setDescription("Remove a user from the ticket.") + .setEmoji(supportbot.SelectMenus.Tickets.RemoveUserEmoji) + .setValue("ticketremoveuser"), + new StringSelectMenuOptionBuilder() + .setLabel("Close") + .setDescription("Close the ticket.") + .setEmoji(supportbot.SelectMenus.Tickets.CloseEmoji) + .setValue("ticketclose"), + new StringSelectMenuOptionBuilder() + .setLabel("Enable Invites") + .setDescription("Enable invites.") + .setEmoji(supportbot.SelectMenus.Tickets.EnableInvitesEmoji) + .setValue("enableinvites"), + new StringSelectMenuOptionBuilder() + .setLabel("Disable Invites") + .setDescription("Disable invites.") + .setEmoji(supportbot.SelectMenus.Tickets.DisableInvitesEmoji) + .setValue("disableinvites"), + ); + + const row2 = new ActionRowBuilder().addComponents(SelectMenus); + + // Send the ticket message and control panel await ticketChannel.send({ embeds: [TicketMessage], components: [row2], }); + if (!department) { + try { + let buttons = supportbot.Departments.map((x) => + new ButtonBuilder() + .setCustomId("Department" + supportbot.Departments.indexOf(x)) + .setLabel(x.title) + .setStyle(x.color) + .setEmoji(x.emoji) + ); + + const row = new ActionRowBuilder().addComponents(buttons); + + try { + const filter = (i) => i.user.id === interaction.user.id; + await m.awaitMessageComponent({ + filter, + max: 1, + componentType: "BUTTON", + time: supportbot.Ticket.Timeout * 60000, + }); + } catch (e) { + if (e.code === "COLLECTOR_ERROR") { + try { + let ticket = TicketData.tickets.findIndex((t) => t.id === ticketChannel.id); + TicketData.tickets[ticket].open = false; + fs.writeFileSync( + "./Data/TicketData.json", + JSON.stringify(TicketData, null, 4), + (err) => { + if (err) console.error(err); + } + ); + interaction.user.send( + "Your ticket has timed out. Please open a new one, and select a department." + ); + } catch (error) {return assignedUserId; + console.error("An error occurred while handling collector:", error); + } + return await ticketChannel.delete(); + } + } + } catch (error) { + console.error("Error sending message:", error); + } + } + } catch (error) { + console.error("Error in run method:", error); } - }, }); diff --git a/Commands/ticketstats.js b/Commands/ticketstats.js deleted file mode 100644 index 1afea81..0000000 --- a/Commands/ticketstats.js +++ /dev/null @@ -1,56 +0,0 @@ -const { EmbedBuilder, ApplicationCommandType } = 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"); - -module.exports = new Command({ - name: cmdconfig.TicketStats.Command, - description: cmdconfig.TicketStats.Description, - type: ApplicationCommandType.ChatInput, - permissions: ["SendMessages"], // Adjust permissions as needed - - async run(interaction) { - - if (supportbot.Ticket.ClaimTickets.Enabled) { - - const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); - const userId = interaction.user.id; - - const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === userId); - 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); // Convert to minutes - - const statsEmbed = new EmbedBuilder() - .setTitle(msgconfig.TicketStats.Title) - .setDescription(`> Ticket stats for <@${interaction.user.id}>`) - .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 } - ); - - await interaction.reply({ embeds: [statsEmbed], ephemeral: true }); - } - - } -}); diff --git a/Configs/messages.yml b/Configs/messages.yml index ce83f5a..d7fb80d 100644 --- a/Configs/messages.yml +++ b/Configs/messages.yml @@ -98,6 +98,9 @@ Ticket: NoPermsToClaimTitle: "Error, no permissions!" NoPermsToClaim: "> :x: **Err!** You do not permissions to claim this ticket." + ReassigningTitle: "Reassigning!..." + ReassigningMessage: "> <@%user%> did not claim the ticket. **Reassigning...**" + TicketStats: OpenTickets: "✅ Open Tickets" TotalTickets: "🎫 Total Tickets" diff --git a/Configs/supportbot-ai.yml b/Configs/supportbot-ai.yml index e3ce7d6..0dd7cc3 100644 --- a/Configs/supportbot-ai.yml +++ b/Configs/supportbot-ai.yml @@ -17,16 +17,16 @@ General: # Max tokens you can set is 400 # For more information about tokens, go to https://openai.com/api/pricing/ - OpenAI_Key: "OPEN_AI_API_KEY" + OpenAI_Key: "OPEN AI KEY" # To create an OpenAI API key, go to https://platform.openai.com/api-keys - PastebinAPI_Key: "PASTEBIN_API_KEY" + PastebinAPI_Key: "PASTEBIN KEY" # You can get your Pastebin API Key from https://pastebin.com PastebinAPI_URL: "https://pastebin.com/api/api_post.php" Channels: - AIChannel: "0000000000000000000" + AIChannel: "1250190338954498048" # This is the channel where AI will respond in without needing to mention the bot. Embed: diff --git a/Configs/supportbot.yml b/Configs/supportbot.yml index 71747da..092c0f1 100644 --- a/Configs/supportbot.yml +++ b/Configs/supportbot.yml @@ -11,7 +11,7 @@ # Official Documentation: https://github.com/Emerald-Services/SupportBot/wiki # Third-Party Documentation: COMING SOON -SupportBot_Version: "v8.0" +SupportBot_Version: "v8.0.1" # Do not change the version number # ------------------------- @@ -20,11 +20,11 @@ SupportBot_Version: "v8.0" General: Name: "SupportBot" - Token: "BOT_TOKEN_HERE" + Token: "BOT TOKEN HERE" # To get your Discord Bot Token, Go to https://discord.com/developers/applications Addons: - Enabled: false + Enabled: true # ------------------------- # Activity @@ -155,7 +155,11 @@ Ticket: Channel: "1249723473757077624" # Channel name or Channel ID + ClaimAcceptTime: 120000 + # How long the assigned user has to accept the ticket before it gets re-assigned to someone else. + ButtonTitle: "Claim Ticket" + ButtonEmoji: "🎫" Button: "1" # Available Button Styles diff --git a/Data/Profiles.json b/Data/Profiles.json new file mode 100644 index 0000000..667cd6b --- /dev/null +++ b/Data/Profiles.json @@ -0,0 +1,7 @@ +{ + "419821884431925260": { + "bio": "Hi", + "timezone": "", + "isClockedIn": false + } +} \ No newline at end of file diff --git a/Data/Profiles/294146279456636929.json b/Data/Profiles/294146279456636929.json new file mode 100644 index 0000000..90585c3 --- /dev/null +++ b/Data/Profiles/294146279456636929.json @@ -0,0 +1,5 @@ +{ + "bio": "", + "timezone": "", + "clockedIn": false +} \ No newline at end of file diff --git a/Data/Profiles/419821884431925260.json b/Data/Profiles/419821884431925260.json new file mode 100644 index 0000000..b096324 --- /dev/null +++ b/Data/Profiles/419821884431925260.json @@ -0,0 +1,5 @@ +{ + "clockedIn": true, + "bio": "Hi", + "timezone": "GMT" +} \ No newline at end of file diff --git a/Data/Profiles/682708736359399524.json b/Data/Profiles/682708736359399524.json new file mode 100644 index 0000000..76a6181 --- /dev/null +++ b/Data/Profiles/682708736359399524.json @@ -0,0 +1,5 @@ +{ + "bio": "This is the most W bio", + "timezone": "cst", + "clockedIn": false +} \ No newline at end of file diff --git a/Data/StaffData.json b/Data/StaffData.json new file mode 100644 index 0000000..66e39a4 --- /dev/null +++ b/Data/StaffData.json @@ -0,0 +1,3 @@ +[ + "419821884431925260" +] \ No newline at end of file diff --git a/Data/SuggestionData.json b/Data/SuggestionData.json index 9e26dfe..b550ff7 100644 --- a/Data/SuggestionData.json +++ b/Data/SuggestionData.json @@ -1 +1,16 @@ -{} \ No newline at end of file +{ + "1250864227460186240": { + "suggestion": "hi", + "author": "419821884431925260", + "upvotes": [ + "419821884431925260" + ], + "downvotes": [] + }, + "1250900161958248533": { + "suggestion": "blob", + "author": "419821884431925260", + "upvotes": [], + "downvotes": [] + } +} \ No newline at end of file diff --git a/Data/TicketData.json b/Data/TicketData.json index 987b7f9..9f923fb 100644 --- a/Data/TicketData.json +++ b/Data/TicketData.json @@ -1,5 +1,184 @@ { "tickets": [ - {} + {}, + { + "id": "1253862843053510687", + "name": "ticket-0021", + "user": "419821884431925260", + "number": "0021", + "reason": "> No Reason Provided!", + "open": true, + "subUsers": [], + "createdAt": "2024-06-22T00:03:18.227Z", + "claimedAt": null + }, + { + "id": "1253863588012232727", + "name": "ticket-0022", + "user": "419821884431925260", + "number": "0022", + "reason": "> No Reason Provided!", + "open": true, + "subUsers": [], + "createdAt": "2024-06-22T00:06:16.126Z", + "claimedAt": "2024-06-22T00:09:12.693Z", + "claimedBy": "419821884431925260" + }, + { + "id": "1255499438483574847", + "name": "ticket-0023", + "user": "419821884431925260", + "number": "0023", + "reason": "> No Reason Provided!", + "open": true, + "subUsers": [], + "createdAt": "2024-06-26T12:26:35.729Z", + "claimedAt": null + }, + { + "id": "1255500796355678229", + "name": "ticket-0024", + "user": "419821884431925260", + "number": "0024", + "reason": "> No Reason Provided!", + "open": true, + "subUsers": [], + "createdAt": "2024-06-26T12:31:56.888Z", + "claimedAt": "2024-06-26T12:32:41.240Z", + "claimedBy": "419821884431925260" + }, + { + "id": "1255501873386094603", + "name": "ticket-0025", + "user": "419821884431925260", + "number": "0025", + "reason": "> No Reason Provided!", + "open": false, + "subUsers": [], + "createdAt": "2024-06-26T12:36:13.810Z", + "claimedAt": null + }, + { + "id": "1255508653906198709", + "name": "ticket-0026", + "user": "419821884431925260", + "number": "0026", + "reason": "> No Reason Provided!", + "open": true, + "subUsers": [], + "createdAt": "2024-06-26T13:03:10.321Z", + "claimedAt": null + }, + { + "id": "1256299016023572572", + "name": "ticket-0027", + "user": "419821884431925260", + "number": "0027", + "reason": "> No Reason Provided!", + "open": false, + "subUsers": [], + "createdAt": "2024-06-28T17:23:50.113Z", + "claimedAt": null + }, + { + "id": "1256300956824633426", + "name": "ticket-0028", + "user": "419821884431925260", + "number": "0028", + "reason": "> No Reason Provided!", + "open": false, + "subUsers": [], + "createdAt": "2024-06-28T17:31:31.454Z", + "claimedAt": null + }, + { + "id": "1256302271461986507", + "name": "ticket-0029", + "user": "419821884431925260", + "number": "0029", + "reason": "> No Reason Provided!", + "open": false, + "subUsers": [], + "createdAt": "2024-06-28T17:36:44.673Z", + "claimedAt": null + }, + { + "id": "1256303116505317457", + "name": "ticket-0030", + "user": "419821884431925260", + "number": "0030", + "reason": "> No Reason Provided!", + "open": true, + "subUsers": [], + "createdAt": "2024-06-28T17:40:06.292Z", + "claimedAt": null + }, + { + "id": "1257775687562166304", + "name": "ticket-0031", + "user": "419821884431925260", + "number": "0031", + "reason": "> No Reason Provided!", + "open": false, + "subUsers": [], + "createdAt": "2024-07-02T19:11:33.760Z", + "claimedAt": null + }, + { + "id": "1257776136843427841", + "name": "ticket-0032", + "user": "419821884431925260", + "number": "0032", + "reason": "> No Reason Provided!", + "open": false, + "subUsers": [], + "createdAt": "2024-07-02T19:13:20.641Z", + "claimedAt": null + }, + { + "id": "1257777774483996795", + "name": "ticket-0033", + "user": "419821884431925260", + "number": "0033", + "reason": "> No Reason Provided!", + "open": false, + "subUsers": [], + "createdAt": "2024-07-02T19:19:51.116Z", + "claimedAt": null + }, + { + "id": "1257778630600167535", + "name": "ticket-0034", + "user": "419821884431925260", + "number": "0034", + "reason": "> No Reason Provided!", + "open": false, + "subUsers": [], + "createdAt": "2024-07-02T19:23:15.328Z", + "claimedAt": null + }, + { + "id": "1257780259374366841", + "name": "ticket-0035", + "user": "419821884431925260", + "number": "0035", + "reason": "> No Reason Provided!", + "open": false, + "subUsers": [], + "createdAt": "2024-07-02T19:29:43.607Z", + "claimedAt": null + }, + { + "id": "1257782171389657180", + "name": "ticket-0036", + "user": "419821884431925260", + "number": "0036", + "reason": "> No Reason Provided!", + "open": true, + "subUsers": [], + "createdAt": "2024-07-02T19:37:19.467Z", + "claimedAt": "2024-07-02T19:43:12.873Z", + "claimedBy": "419821884431925260" + } ] } \ No newline at end of file diff --git a/Data/ticket-panel-id.json b/Data/ticket-panel-id.json index b0b4882..805b116 100644 --- a/Data/ticket-panel-id.json +++ b/Data/ticket-panel-id.json @@ -1,4 +1,4 @@ { - "id": 0, - "TicketPanelID": "0000000000000000000" + "id": 37, + "TicketPanelID": "1250863985335472312" } \ No newline at end of file diff --git a/Events/interactionCreate.js b/Events/interactionCreate.js index 9f64240..c636649 100644 --- a/Events/interactionCreate.js +++ b/Events/interactionCreate.js @@ -274,5 +274,52 @@ 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`; + let profileData; + if (fs.existsSync(profilePath)) { + profileData = JSON.parse(fs.readFileSync(profilePath, "utf8")); + } else { + profileData = { + bio: "", + timezone: "", + clockedIn: false + }; + } + + try { + const newBio = interaction.fields.getTextInputValue('bio'); + const newTimezone = interaction.fields.getTextInputValue('timezone'); + + profileData.bio = newBio; + profileData.timezone = newTimezone; + + fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2)); + + const updatedProfileEmbed = new Discord.EmbedBuilder() + .setTitle(`${interaction.user.username}'s Profile`) + .setColor(supportbot.Embed.Colours.General) + .setThumbnail(interaction.user.displayAvatarURL()) + .addFields( + { name: 'Bio', value: newBio || 'No bio set.', inline: false }, + { name: 'Timezone', value: newTimezone || 'No timezone set.', inline: true } + ); + + if (profileData.clockedIn !== undefined) { + updatedProfileEmbed.addFields({ + name: 'Clocked In Status', + value: profileData.clockedIn ? '✅ Clocked In' : '❌ Clocked Out', + inline: true + }); + } + + await interaction.reply({ embeds: [updatedProfileEmbed], ephemeral: true }); + } catch (error) { + console.error("Failed to update profile data: ", error); + await interaction.reply({ content: "There was an error while updating your profile. Please try again later.", ephemeral: true }); + } + } }); diff --git a/Events/messageCreate.js b/Events/messageCreate.js index 537efdd..b1999a1 100644 --- a/Events/messageCreate.js +++ b/Events/messageCreate.js @@ -18,6 +18,7 @@ const supportbotai = yaml.load(fs.readFileSync("./Configs/supportbot-ai.yml")); 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")); + // Validate configuration values function validateConfig(config, keys) { keys.forEach(key => { @@ -68,99 +69,150 @@ module.exports = new Event("messageCreate", async (client, message) => { if (message.author.bot || !message.guild) return; if (!CHANNELS.includes(message.channelId) && !message.mentions.users.has(client.user.id)) return; -if (supportbotai.Enabled) { + if (supportbotai.Enabled) { + const sendTypingInterval = setInterval(() => { + message.channel.sendTyping(); + }, 5000); - const sendTypingInterval = setInterval(() => { - message.channel.sendTyping(); - }, 5000); + let conversation = [{ + role: "system", + content: `SupportBot is your friendly neighbourhood support bot. Current date and time: ${new Date().toLocaleString()}. + The bot uses a modular command handler where commands are loaded dynamically from the './Commands' directory. Each command has a name, description, and execute method. + + The main Client class structure: + - Commands are loaded from the './Commands/' directory. + - Configs are loaded from the './Configs/' directory if enabled. + - Events are loaded from the './Events/' directory. + - Addons are loaded from the './Addons/' directory if enabled. + + Example of a command structure: + { + module.exports = new Command({ + name: cmdconfig.Ping.Command, + description: cmdconfig.Ping.Description, + options: [], + permissions: cmdconfig.Ping.Permission, + )} + } - let conversation = [{ - role: "system", - content: "SupportBot is your friendly neighbourhood support bot." - }]; + You can find more information about the commands and configuration files in the 'Configs' and 'Commands' directories. - let prevMessages = await message.channel.messages.fetch({ limit: 10 }); - prevMessages = prevMessages.reverse().filter(msg => !msg.author.bot || msg.author.id === client.user.id); + All commands are loaded into the bot at startup from ./Events/ready.js and ./Client.js. - prevMessages.forEach((msg) => { - const username = msg.author.username.replace(/\s+/g, '_').replace(/[^\w\s]/gi, ''); + If someone asks you to do something, you can use the 'assist' command to have the bot respond, For example: "Can I speak to a support agent?", You would respond by opening a ticket which is the /Commands/ticket.js file or you can tell them to create a ticket in the channel defined as supportbot.Ticket. - conversation.push({ - role: msg.author.id === client.user.id ? "assistant" : "user", - name: username, - content: msg.content, - }); - }); + You can find the source code for the bot at https://github.com/EmeraldServices/SupportBot. - try { - const response = await openai.chat.completions.create({ - model: supportbotai.General.Model, - messages: conversation, - max_tokens: supportbotai.General.Tokens, + You then need to understand how the bot works. Interactions are handled by the ./Events/interactionCreate.js file. + + Commands are filtered based on configuration settings and loaded into the bot at startup. + + Xiled Gaming Network (XGN) is a gaming community that typically offers various services and activities for gamers. + + Xiled Gaming Network (XGN) has 4 divisions, Team Xiled International (TXI), The XiledOnes (TXO), Dealers of Death (DOD), Psychotic Solutions (PS) + Xiled Gaming Network (XGN) can be found at https://officialxgn.gg/handbook/, This is where you can the code of conduct, policies and other important information for members. + You can join XGN with the discord - discord.gg/xgn + You can apply to join the XGN Clan - join.officialxgn.gg + + You can find the founders of XGN under the history section of the handbook. https://officialxgn.gg/handbook/. + ` + }]; + + let prevMessages = await message.channel.messages.fetch({ limit: 10 }); + prevMessages = prevMessages.reverse().filter(msg => !msg.author.bot || msg.author.id === client.user.id); + + prevMessages.forEach((msg) => { + const username = msg.author.username.replace(/\s+/g, '_').replace(/[^\w\s]/gi, ''); + + conversation.push({ + role: msg.author.id === client.user.id ? "assistant" : "user", + name: username, + content: msg.content, + }); }); - clearInterval(sendTypingInterval); + try { + const response = await openai.chat.completions.create({ + model: supportbotai.General.Model, + messages: conversation, + max_tokens: supportbotai.General.Tokens, + }); - if (response.choices && response.choices.length > 0) { - const replyContent = response.choices[0].message.content; + clearInterval(sendTypingInterval); - const SupportBotAI = new Discord.EmbedBuilder() - .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) - .setDescription(replyContent) - .setColor(supportbotai.Embed.Color) - .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) - .setTimestamp(); + if (response.choices && response.choices.length > 0) { + const replyContent = response.choices[0].message.content; - if (replyContent.length > 2000) { - const pasteLink = await createPaste(replyContent); + if (replyContent.toLowerCase().startsWith("generate image:")) { + const prompt = replyContent.substring(15).trim(); + const imageUrl = await generateImage(prompt); - const SupportBotAIPastebin = new Discord.EmbedBuilder() + const SupportBotAIImage = new Discord.EmbedBuilder() + .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) + .setDescription(`Here is the image generated for your prompt: "${prompt}"`) + .setImage(imageUrl) + .setColor(supportbotai.Embed.Color) + .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) + .setTimestamp(); + + message.reply({ embeds: [SupportBotAIImage] }); + } else { + const SupportBotAI = new Discord.EmbedBuilder() + .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) + .setDescription(replyContent) + .setColor(supportbotai.Embed.Color) + .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) + .setTimestamp(); + + if (replyContent.length > 2000) { + const pasteLink = await createPaste(replyContent); + + const SupportBotAIPastebin = new Discord.EmbedBuilder() + .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) + .setDescription(supportbotai.Messages.MessageTooLong.replace("{pastebin}", pasteLink)) + .setColor(supportbotai.Embed.Color) + .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) + .setTimestamp(); + + message.reply({ embeds: [SupportBotAIPastebin] }); + } else { + message.reply({ embeds: [SupportBotAI] }); + } + } + } else { + const SupportBotAIErrorResponse = new Discord.EmbedBuilder() .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) - .setDescription(supportbotai.Messages.MessageTooLong.replace("{pastebin}", pasteLink)) + .setDescription(supportbotai.Messages.ErrorResponse) .setColor(supportbotai.Embed.Color) .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) .setTimestamp(); - message.reply({ embeds: [SupportBotAIPastebin] }); - } else { - message.reply({ embeds: [SupportBotAI] }); + message.reply({ embeds: [SupportBotAIErrorResponse], ephemeral: true }); } - } else { + } catch (error) { + clearInterval(sendTypingInterval); + const SupportBotAIErrorResponse = new Discord.EmbedBuilder() .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) - .setDescription(supportbotai.Messages.ErrorResponse) - .setColor(supportbotai.Embed.Color) + .setDescription(supportbotai.Messages.OpenAIError) + .setColor(supportbot.Embed.Colours.Error) .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) .setTimestamp(); + console.error('OpenAI Error:', error); message.reply({ embeds: [SupportBotAIErrorResponse], ephemeral: true }); } - } catch (error) { - clearInterval(sendTypingInterval); + } - const SupportBotAIErrorResponse = new Discord.EmbedBuilder() + if (!supportbotai.Enabled) { + const AIDisabled = new Discord.EmbedBuilder() .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) - .setDescription(supportbotai.Messages.OpenAIError) + .setDescription(supportbotai.Messages.AIDisabled) .setColor(supportbot.Embed.Colours.Error) .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) .setTimestamp(); - console.error('OpenAI Error:', error); - message.reply({ embeds: [SupportBotAIErrorResponse], ephemeral: true }); + message.reply({ embeds: [AIDisabled], ephemeral: true }); + return; } - -} - -if (supportbotai.Enabled === false) { - const AIDisabled = new Discord.EmbedBuilder() - .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) - .setDescription(supportbotai.Messages.AIDisabled) - .setColor(supportbot.Embed.Colours.Error) - .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) - .setTimestamp(); - - message.reply({ embeds: [AIDisabled], ephemeral: true }) - return; -} - -}); \ No newline at end of file +}); diff --git a/LICENSE.md b/LICENSE.md index 0a0b305..0899fec 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 Emerald Services +Copyright (c) 2024 Emerald Services Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Structures/Client.js b/Structures/Client.js index 350d80d..4afb632 100644 --- a/Structures/Client.js +++ b/Structures/Client.js @@ -1,12 +1,8 @@ -// C:\Users\ollie\Documents\GitHub\SupportBot\Structures\Client.js - -// SupportBot | Emerald Services -// Client Structure - const fs = require("fs"); 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 supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); @@ -89,18 +85,14 @@ class Client extends Discord.Client { tempCommandFiles = tempCommandFiles.filter(item => item !== "Ping.js"); } - if (cmdconfig.Ping.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "Ping.js"); - } - if (supportbot.Ticket.ClaimTickets.Enabled === false) { - tempCommandFiles = tempCommandFiles.filter(item => item !== "ticketstats.js"); + tempCommandFiles = tempCommandFiles.filter(item => item !== "ticketstats.js"); } const commandFiles = tempCommandFiles; const commands = commandFiles.map((file) => require(`../Commands/${file}`)); - // Commands + // Load main commands console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬"); commands.forEach((cmd) => { @@ -108,13 +100,14 @@ class Client extends Discord.Client { this.commands.set(cmd.name, cmd); }); + // Load addon commands and events console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬"); if (supportbot.General.Addons.Enabled) { const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js")); const addons = addonFiles.map((file) => { - let addon = require(`../Addons/${file}`); + const addon = require(`../Addons/${file}`); addon.name = file.split(".")[0]; return addon; }); @@ -132,18 +125,24 @@ class Client extends Discord.Client { `\u001b[32;1m`, "Loaded" ); - addon.commands?.forEach((command) => { - this.commands.set(command.name, command); - }); - addon.events?.forEach((event) => { - this.on(event.event, (...args) => event.run(this, ...args)); - }); + + if (addon instanceof Command) { + this.commands.set(addon.name, addon); + } + + if (addon.events && Array.isArray(addon.events)) { + addon.events.forEach((event) => { + if (event instanceof Event) { + this.on(event.event, (...args) => event.run(this, ...args)); + } + }); + } }); console.log(`\u001b[34;1m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬"); } - // Slash Commands + // Register Slash Commands this.once("ready", async () => { await this.guilds.cache.first()?.commands.set(this.commands);