├── Addons └── addon.js ├── Commands ├── addUser.js ├── close.js ├── embed.js ├── forceaddUser.js ├── help.js ├── info.js ├── mod.js ├── ping.js ├── profile.js ├── removeUser.js ├── settings.js ├── suggest.js ├── ticket.js ├── ticketpanel.js └── translate.js ├── Configs ├── commands.yml ├── messages.yml ├── supportbot-ai.yml ├── supportbot.yml └── ticket-panel.yml ├── Data ├── BlacklistedUsers.json ├── Profiles.json ├── StaffData.json ├── SuggestionData.json ├── SupportBot-AI.json ├── TicketData.json ├── profiles │ └── 0000000000000000.json ├── settings.json └── ticket-panel-id.json ├── Events ├── guildCreate.js ├── guildDelete.js ├── guildMemberAdd.js ├── guildMemberRemove.js ├── interactionCreate.js ├── messageCreate.js ├── messageDelete.js ├── messageUpdate.js └── ready.js ├── LICENSE.md ├── README.md ├── SECURITY.md ├── Structures ├── Addon.js ├── Client.js ├── Command.js ├── Event.js └── TicketID.js ├── index.js └── package.json /Addons/addon.js: -------------------------------------------------------------------------------- 1 | const Discord = require("discord.js"); 2 | const fs = require("fs"); 3 | const yaml = require("js-yaml"); 4 | const { Command } = require('../Structures/Addon.js'); // Adjust the path as needed 5 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 6 | 7 | module.exports = new Command({ 8 | name: "addon", 9 | description: "This is a sample addon!", 10 | options: [], 11 | permissions: ["Administrator"], 12 | 13 | async run(interaction) { 14 | let disableCommand = true; 15 | 16 | const PingEmbed = new Discord.EmbedBuilder() 17 | .setDescription( 18 | `This is a sample addon.\n\nPong! \`${interaction.client.ws.ping} ms\`` 19 | ) 20 | .setColor(supportbot.Embed.Colours.General); 21 | 22 | interaction.reply({ 23 | embeds: [PingEmbed], 24 | }); 25 | }, 26 | }); -------------------------------------------------------------------------------- /Commands/addUser.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { 3 | EmbedBuilder, 4 | ApplicationCommandOptionType, 5 | ApplicationCommandType, 6 | ActionRowBuilder, 7 | ButtonBuilder, 8 | ButtonStyle, 9 | ChannelType, 10 | } = require("discord.js"); 11 | const yaml = require("js-yaml"); 12 | 13 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 14 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 15 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 16 | 17 | const Command = require("../Structures/Command.js"); 18 | 19 | module.exports = new Command({ 20 | name: cmdconfig.AddUser.Command, 21 | description: cmdconfig.AddUser.Description, 22 | type: ApplicationCommandType.ChatInput, 23 | options: [ 24 | { 25 | name: "user", 26 | description: "The user to add", 27 | type: ApplicationCommandOptionType.User, 28 | required: true, 29 | }, 30 | ], 31 | permissions: cmdconfig.AddUser.Permission, 32 | 33 | async run(interaction) { 34 | const userToAdd = interaction.options.getUser("user"); 35 | const ticketChannel = interaction.channel; 36 | const ticketDataPath = "./Data/TicketData.json"; 37 | 38 | if ( 39 | (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || 40 | (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText) 41 | ) { 42 | const onlyInTicket = new EmbedBuilder() 43 | .setTitle(msgconfig.AddUser.NotInTicket_Title) 44 | .setDescription(msgconfig.AddUser.NotInTicket_Description) 45 | .setColor(supportbot.Embed.Colours.Error); 46 | 47 | return interaction.reply({ 48 | embeds: [onlyInTicket], 49 | ephemeral: true, 50 | }); 51 | } 52 | 53 | const ticketInviteEmbed = new EmbedBuilder() 54 | .setTitle(msgconfig.AddUser.Invite_Title) 55 | .setDescription(msgconfig.AddUser.Invite_Description.replace("%username%", userToAdd.username)) 56 | .setColor(supportbot.Embed.Colours.General); 57 | 58 | await interaction.reply({ 59 | embeds: [ticketInviteEmbed], 60 | ephemeral: true, 61 | }); 62 | 63 | try { 64 | const dmEmbed = new EmbedBuilder() 65 | .setTitle(msgconfig.AddUser.DM_Title) 66 | .setDescription(msgconfig.AddUser.DM_Description.replace("%channelname%", ticketChannel.name)) 67 | .setColor(supportbot.Embed.Colours.General); 68 | 69 | const acceptButton = new ButtonBuilder() 70 | .setCustomId("accept_invite") 71 | .setLabel(msgconfig.AddUser.Accept_Label) 72 | .setEmoji("✅") 73 | .setStyle(ButtonStyle.Primary); 74 | 75 | const rejectButton = new ButtonBuilder() 76 | .setCustomId("reject_invite") 77 | .setLabel(msgconfig.AddUser.Reject_Label) 78 | .setEmoji("❌") 79 | .setStyle(ButtonStyle.Secondary); 80 | 81 | const row = new ActionRowBuilder().addComponents(acceptButton, rejectButton); 82 | 83 | const dmMessage = await userToAdd.send({ 84 | embeds: [dmEmbed], 85 | components: [row], 86 | }); 87 | 88 | const filter = (i) => i.user.id === userToAdd.id; 89 | const collector = dmMessage.createMessageComponentCollector({ 90 | filter, 91 | time: 60000, 92 | }); 93 | 94 | collector.on("collect", async (i) => { 95 | if (i.customId === "accept_invite") { 96 | try { 97 | // Add user to thread or channel based on TicketType 98 | if (supportbot.Ticket.TicketType === "threads") { 99 | await ticketChannel.members.add(userToAdd.id); // For threads 100 | } else if (supportbot.Ticket.TicketType === "channels") { 101 | await ticketChannel.permissionOverwrites.create(userToAdd.id, { 102 | ViewChannel: true, 103 | SendMessages: true, 104 | ReadMessageHistory: true, 105 | }); // For channels 106 | } 107 | 108 | const ticketData = JSON.parse(fs.readFileSync(ticketDataPath, "utf8")); 109 | const ticketIndex = ticketData.tickets.findIndex((t) => t.id === ticketChannel.id); 110 | if (ticketIndex !== -1) { 111 | ticketData.tickets[ticketIndex].subUsers.push(userToAdd.id); 112 | fs.writeFileSync(ticketDataPath, JSON.stringify(ticketData, null, 4)); 113 | } 114 | 115 | const acceptedEmbed = new EmbedBuilder() 116 | .setTitle(msgconfig.AddUser.Accepted_Title) 117 | .setDescription(msgconfig.AddUser.Accepted_Message) 118 | .setColor(supportbot.Embed.Colours.Success); 119 | 120 | await i.update({ 121 | embeds: [acceptedEmbed], 122 | components: [], 123 | }); 124 | 125 | // Send the added user ticket message in the ticket channel 126 | 127 | const addeduserTicket = new EmbedBuilder() 128 | .setTitle(msgconfig.AddUser.Added_Title) 129 | .setDescription(msgconfig.AddUser.Added_Description.replace('%username%', userToAdd.username)) 130 | .setColor(supportbot.Embed.Colours.General); 131 | 132 | await ticketChannel.send({ 133 | embeds: [addeduserTicket], 134 | }); 135 | 136 | 137 | const addedToTicketEmbed = new EmbedBuilder() 138 | .setTitle(msgconfig.AddUser.AddedToTicket_Title) 139 | .setDescription(msgconfig.AddUser.AddedToTicket_Description.replace( 140 | "%channel_link%", 141 | `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})` 142 | )) 143 | .setColor(supportbot.Embed.Colours.General); 144 | 145 | const ticketLinkButton = new ButtonBuilder() 146 | .setLabel(msgconfig.AddUser.ViewTicket_Label) 147 | .setURL(`https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id}`) 148 | .setStyle(ButtonStyle.Link); 149 | 150 | await userToAdd.send({ 151 | embeds: [addedToTicketEmbed], 152 | components: [new ActionRowBuilder().addComponents(ticketLinkButton)], 153 | }); 154 | 155 | } catch (err) { 156 | console.error("Error adding user to the ticket:", err); 157 | 158 | const errorEmbed = new EmbedBuilder() 159 | .setTitle(msgconfig.AddUser.Error_Title) 160 | .setDescription(msgconfig.AddUser.Error_Adding) 161 | .setColor(supportbot.Embed.Colours.Error); 162 | 163 | await i.update({ 164 | embeds: [errorEmbed], 165 | components: [], 166 | }); 167 | } 168 | } else { 169 | const declinedEmbed = new EmbedBuilder() 170 | .setTitle(msgconfig.AddUser.Declined_Title) 171 | .setDescription(msgconfig.AddUser.Declined_Message) 172 | .setColor(supportbot.Embed.Colours.Error); 173 | 174 | await i.update({ 175 | embeds: [declinedEmbed], 176 | components: [], 177 | }); 178 | } 179 | }); 180 | 181 | collector.on("end", (collected) => { 182 | if (collected.size === 0) { 183 | userToAdd.send({ 184 | embeds: [ 185 | new EmbedBuilder() 186 | .setTitle(msgconfig.AddUser.NoResponse_Title) 187 | .setDescription(msgconfig.AddUser.NoResponse_Message) 188 | .setColor(supportbot.Embed.Colours.Warning), 189 | ], 190 | }); 191 | } 192 | }); 193 | } catch (error) { 194 | if (error.code === 50007) { 195 | const errorEmbed = new EmbedBuilder() 196 | .setTitle(msgconfig.AddUser.DM_Error_Title) 197 | .setDescription(msgconfig.AddUser.DM_Error_Description.replace("%username%", userToAdd.username)) 198 | .setColor(supportbot.Embed.Colours.Error); 199 | 200 | await ticketChannel.send({ 201 | embeds: [errorEmbed], 202 | }); 203 | } else { 204 | console.error("Error sending DM:", error); 205 | 206 | const unexpectedErrorEmbed = new EmbedBuilder() 207 | .setTitle(msgconfig.AddUser.Unexpected_Error_Title) 208 | .setDescription(msgconfig.AddUser.Unexpected_Error_Description.replace("%error%", error.message)) 209 | .setColor(supportbot.Embed.Colours.Error); 210 | 211 | await ticketChannel.send({ 212 | embeds: [unexpectedErrorEmbed], 213 | }); 214 | } 215 | } 216 | }, 217 | }); 218 | -------------------------------------------------------------------------------- /Commands/close.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Discord = require("discord.js"); 3 | const yaml = require("js-yaml"); 4 | 5 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 6 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 7 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 8 | 9 | const Command = require("../Structures/Command.js"); 10 | 11 | module.exports = new Command({ 12 | name: cmdconfig.CloseTicket.Command, 13 | description: cmdconfig.CloseTicket.Description, 14 | type: Discord.ApplicationCommandType.ChatInput, 15 | options: [ 16 | { 17 | name: "reason", 18 | description: "Ticket Close Reason", 19 | type: Discord.ApplicationCommandOptionType.String, 20 | } 21 | ], 22 | permissions: cmdconfig.CloseTicket.Permission, 23 | 24 | async run(interaction) { 25 | const { getRole, getChannel } = interaction.client; 26 | 27 | if (supportbot.Ticket.Close.StaffOnly) { 28 | let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); 29 | let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); 30 | 31 | if (!SupportStaff || !Admin) { 32 | return interaction.reply("Some roles seem to be missing! Please check for errors when starting the bot."); 33 | } 34 | 35 | const NoPerms = new Discord.EmbedBuilder() 36 | .setTitle("Invalid Permissions!") 37 | .setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``) 38 | .setColor(supportbot.Embed.Colours.Warn); 39 | 40 | if (!interaction.member.roles.cache.has(SupportStaff.id) && !interaction.member.roles.cache.has(Admin.id)) { 41 | return interaction.reply({ embeds: [NoPerms] }); 42 | } 43 | } 44 | 45 | const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread; 46 | if ( 47 | (supportbot.Ticket.TicketType === "threads" && !isThread) || 48 | (supportbot.Ticket.TicketType === "channels" && interaction.channel.type !== Discord.ChannelType.GuildText) 49 | ) { 50 | const NotTicketChannel = new Discord.EmbedBuilder() 51 | .setTitle("Invalid Channel!") 52 | .setDescription(`This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`) 53 | .setColor(supportbot.Embed.Colours.Warn); 54 | 55 | return interaction.reply({ embeds: [NotTicketChannel], ephemeral: true }); 56 | } 57 | 58 | await interaction.deferReply(); 59 | 60 | let tickets; 61 | try { 62 | tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); 63 | } catch (err) { 64 | console.error("Error reading ticket data file:", err); 65 | return interaction.followUp({ content: "There was an error loading ticket data." }); 66 | } 67 | 68 | let TicketData = tickets.tickets.findIndex((t) => t.id === interaction.channel.id); 69 | let ticket = tickets.tickets[TicketData]; 70 | 71 | if (TicketData === -1) { 72 | const Exists = new Discord.EmbedBuilder() 73 | .setTitle("No Ticket Found!") 74 | .setDescription(msgconfig.Error.NoValidTicket) 75 | .setColor(supportbot.Embed.Colours.Warn); 76 | return interaction.followUp({ embeds: [Exists] }); 77 | } 78 | 79 | let reason = interaction.options?.getString("reason") || "No Reason Provided."; 80 | const ticketUserId = ticket.user; 81 | 82 | if (supportbot.Ticket.ReviewSystem.Enabled) { 83 | const rating = await collectReviewRating(interaction, ticketUserId); 84 | const comment = await collectReviewComment(interaction, ticketUserId); 85 | 86 | const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild); 87 | const reviewer = supportbot.Ticket.ClaimTickets ? `<@${ticket.claimedBy}>` : null; 88 | 89 | const reviewEmbed = new Discord.EmbedBuilder() 90 | .setTitle(msgconfig.ReviewSystem.ReviewEmbed.Title) 91 | .addFields( 92 | { 93 | name: msgconfig.ReviewSystem.ReviewEmbed.RatingTitle, 94 | value: `${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(rating)}`, 95 | inline: false 96 | }, 97 | { 98 | name: msgconfig.ReviewSystem.ReviewEmbed.CommentTitle, 99 | value: `\`\`\`${comment}\`\`\``, 100 | inline: false 101 | } 102 | ) 103 | .setColor(msgconfig.ReviewSystem.ReviewEmbed.Color); 104 | 105 | if (supportbot.Ticket.ClaimTickets.Enabled) { 106 | reviewEmbed.setDescription( 107 | `**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${reviewer}\n**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** <@${ticketUserId}>` || `N/A` 108 | ); 109 | } else { 110 | reviewEmbed.setDescription( 111 | `**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** <@${ticketUserId}>` 112 | ); 113 | } 114 | 115 | if (reviewChannel) { 116 | await reviewChannel.send({ embeds: [reviewEmbed] }); 117 | } 118 | 119 | if (!interaction.replied && !interaction.deferred) { 120 | await interaction.followUp({ embeds: [reviewEmbed] }); 121 | } 122 | 123 | await handleCloseTicket(interaction, reason, ticket, TicketData); 124 | } else { 125 | await handleCloseTicket(interaction, reason, ticket, TicketData); 126 | } 127 | }, 128 | }); 129 | 130 | async function collectReviewRating(interaction, ticketUserId) { 131 | const reviewPrompt = new Discord.EmbedBuilder() 132 | .setTitle(msgconfig.ReviewSystem.Rate.Title) 133 | .setDescription(`${msgconfig.ReviewSystem.Rate.Description}`) 134 | .setColor(supportbot.Embed.Colours.General); 135 | 136 | const stars = new Discord.ActionRowBuilder().addComponents( 137 | new Discord.StringSelectMenuBuilder() 138 | .setCustomId("starRating") 139 | .setPlaceholder("Select a rating") 140 | .addOptions([ 141 | { label: msgconfig.ReviewSystem.Stars.One, value: "1" }, 142 | { label: msgconfig.ReviewSystem.Stars.Two, value: "2" }, 143 | { label: msgconfig.ReviewSystem.Stars.Three, value: "3" }, 144 | { label: msgconfig.ReviewSystem.Stars.Four, value: "4" }, 145 | { label: msgconfig.ReviewSystem.Stars.Five, value: "5" }, 146 | ]) 147 | ); 148 | 149 | await interaction.followUp({ 150 | content: `<@${ticketUserId}>`, 151 | embeds: [reviewPrompt], 152 | components: [stars], 153 | ephemeral: true, 154 | }); 155 | 156 | const starRating = await interaction.channel.awaitMessageComponent({ 157 | componentType: Discord.ComponentType.StringSelect, 158 | filter: (i) => i.customId === "starRating" && i.user.id === ticketUserId, 159 | }); 160 | await starRating.deferUpdate(); 161 | return starRating.values[0]; 162 | } 163 | 164 | async function collectReviewComment(interaction, ticketUserId) { 165 | const commentPrompt = new Discord.EmbedBuilder() 166 | .setTitle(msgconfig.ReviewSystem.Comment.Title) 167 | .setDescription(`${msgconfig.ReviewSystem.Comment.Description}`) 168 | .setColor(supportbot.Embed.Colours.General); 169 | 170 | await interaction.followUp({ 171 | content: `<@${ticketUserId}>`, 172 | embeds: [commentPrompt], 173 | ephemeral: true, 174 | }); 175 | 176 | const filter = (response) => response.author.id === ticketUserId; 177 | 178 | const commentCollection = await interaction.channel.awaitMessages({ filter, max: 1 }); 179 | const comment = commentCollection.first()?.content; 180 | return comment && comment.toLowerCase() !== "no" ? comment : "No Comment Provided"; 181 | } 182 | async function handleCloseTicket(interaction, reason, ticket, TicketData) { 183 | const { getChannel } = interaction.client; 184 | 185 | let tickets; 186 | try { 187 | tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); 188 | } catch (err) { 189 | console.error("Error reading ticket data:", err); 190 | return interaction.followUp("Failed to load ticket data."); 191 | } 192 | 193 | let tUser = interaction.client.users.cache.get(ticket.user); 194 | let transcriptChannel = await getChannel(supportbot.Ticket.Log.TicketDataLog, interaction.guild); 195 | 196 | if (!transcriptChannel) { 197 | console.log("Transcript channel missing or inaccessible"); 198 | return interaction.followUp("Error: Transcript log channel is missing or bot lacks permission."); 199 | } 200 | 201 | try { 202 | // Fetch all messages from the channel 203 | let allMessages = []; 204 | let lastId = null; 205 | 206 | while (true) { 207 | const options = { limit: 100 }; 208 | if (lastId) { 209 | options.before = lastId; 210 | } 211 | 212 | const messages = await interaction.channel.messages.fetch(options); 213 | if (messages.size === 0) break; 214 | 215 | allMessages = [...allMessages, ...messages.values()]; 216 | lastId = messages.last().id; 217 | 218 | if (messages.size < 100) break; 219 | } 220 | 221 | // Sort messages by timestamp 222 | allMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp); 223 | 224 | // Create transcript data 225 | const transcriptData = allMessages.map(msg => ({ 226 | content: msg.content || "No content", 227 | username: msg.author.username, 228 | userId: msg.author.id, 229 | avatar: msg.author.displayAvatarURL(), 230 | timestamp: msg.createdAt.toISOString(), 231 | attachments: Array.from(msg.attachments.values()).map(att => ({ 232 | url: att.url, 233 | name: att.name 234 | })), 235 | embeds: msg.embeds.map(embed => ({ 236 | title: embed.title, 237 | description: embed.description, 238 | fields: embed.fields 239 | })) 240 | })); 241 | 242 | // Update ticket status 243 | tickets.tickets[TicketData].open = false; 244 | tickets.tickets[TicketData].messages = transcriptData; 245 | fs.writeFileSync("./Data/TicketData.json", JSON.stringify(tickets, null, 4)); 246 | 247 | const transcriptEmbed = new Discord.EmbedBuilder() 248 | .setTitle(msgconfig.TicketLog.Title) 249 | .setColor(msgconfig.TicketLog.Colour) 250 | .setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() }) 251 | .setDescription( 252 | `> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` + 253 | `> **User:** ${tUser?.tag || "Unknown User"} (\`${tUser?.id || ticket.user}\`)\n` + 254 | `> **Closed by:** <@${interaction.user.id}>\n` + 255 | `> **Message Count:** ${transcriptData.length}` 256 | ) 257 | .addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: true }); 258 | 259 | const html = createTranscriptHTML( 260 | { 261 | id: interaction.channel.id, 262 | messages: transcriptData, 263 | name: interaction.channel.name 264 | }, 265 | reason 266 | ); 267 | 268 | const fileName = `${interaction.channel.id}-transcript.html`; 269 | fs.writeFileSync(`./Data/Transcripts/${fileName}`, html); 270 | 271 | await transcriptChannel.send({ 272 | embeds: [transcriptEmbed], 273 | files: [{ 274 | attachment: `./Data/Transcripts/${fileName}`, 275 | name: `SPOILER_${fileName}`, 276 | spoiler: true 277 | }] 278 | }); 279 | 280 | if (interaction.channel.type === Discord.ChannelType.GuildText || 281 | interaction.channel.type === Discord.ChannelType.PrivateThread) { 282 | await interaction.channel.delete(); 283 | } 284 | } catch (err) { 285 | console.error("Error handling ticket close:", err); 286 | interaction.followUp("An error occurred while closing the ticket."); 287 | } 288 | } 289 | 290 | function parseMarkdown(content) { 291 | // Convert **bold** text to tags 292 | content = content.replace(/\*\*(.*?)\*\*/g, '$1'); 293 | 294 | // Convert _italic_ text to tags 295 | content = content.replace(/_(.*?)_/g, '$1'); 296 | 297 | // Convert inline `code` to tags 298 | content = content.replace(/`(.*?)`/g, '$1'); 299 | 300 | // Convert [text](link) to tags 301 | content = content.replace(/\[(.*?)\]\((.*?)\)/g, '$1'); 302 | 303 | // Convert multiline code blocks ```code``` to
 tags
304 |   content = content.replace(/```(.*?)```/gs, '
$1
'); 305 | 306 | return content; 307 | } 308 | 309 | function createTranscriptHTML(ticket, reason) { 310 | return ` 311 | 312 | 313 | 314 | 315 | 316 | 317 | 378 | 379 | 380 |
381 |
382 |

Ticket Transcript

383 |

Channel: ${ticket.name}

384 |

Ticket ID: ${ticket.id}

385 |

Message Count: ${ticket.messages.length}

386 |

Close Reason: ${reason}

387 |
388 | 389 |
390 | ${ticket.messages.map(msg => ` 391 |
392 |
393 | Avatar 394 | ${msg.username} 395 | ${new Date(msg.timestamp).toLocaleString()} 396 |
397 |
398 | ${parseMarkdown(msg.content)} 399 | 400 | ${msg.embeds.map(embed => ` 401 |
402 | ${embed.title ? `
${embed.title}
` : ''} 403 | ${embed.description ? `
${embed.description}
` : ''} 404 | ${embed.fields.map(field => ` 405 |
406 | ${field.name}: 407 |
${field.value}
408 |
409 | `).join('')} 410 |
411 | `).join('')} 412 | 413 | ${msg.attachments.map(att => ` 414 | 415 | 📎 ${att.name} 416 | 417 | `).join('')} 418 |
419 |
420 | `).join('')} 421 |
422 |
423 | 424 | 425 | `; 426 | } 427 | -------------------------------------------------------------------------------- /Commands/embed.js: -------------------------------------------------------------------------------- 1 | const { 2 | ApplicationCommandType, 3 | ApplicationCommandOptionType, 4 | ActionRowBuilder, 5 | ButtonBuilder, 6 | EmbedBuilder, 7 | StringSelectMenuBuilder, 8 | ModalBuilder, 9 | TextInputBuilder, 10 | TextInputStyle, 11 | ButtonStyle, 12 | ComponentType 13 | } = require('discord.js'); 14 | const Command = require('../Structures/Command.js'); 15 | const fs = require('fs'); 16 | const yaml = require('js-yaml'); 17 | 18 | const supportbot = yaml.load(fs.readFileSync('./Configs/supportbot.yml', 'utf8')); 19 | const cmdconfig = yaml.load(fs.readFileSync('./Configs/commands.yml', 'utf8')); 20 | const msgconfig = yaml.load(fs.readFileSync('./Configs/messages.yml', 'utf8')); 21 | 22 | // Helper function to validate and convert hex color 23 | function parseColor(color) { 24 | if (!color) return 0x5865F2; 25 | if (typeof color === 'number') return color; 26 | if (typeof color !== 'string') return 0x5865F2; 27 | color = color.replace('#', ''); 28 | if (/^[0-9A-F]{6}$/i.test(color)) { 29 | return parseInt(color, 16); 30 | } 31 | return 0x5865F2; 32 | } 33 | 34 | module.exports = new Command({ 35 | name: cmdconfig.Embed.Command, 36 | description: cmdconfig.Embed.Description, 37 | type: ApplicationCommandType.ChatInput, 38 | options: [ 39 | { 40 | name: 'channel', 41 | description: 'Select channel to send the message', 42 | type: ApplicationCommandOptionType.Channel, 43 | required: true 44 | } 45 | ], 46 | permissions: cmdconfig.Embed.Permission, 47 | 48 | async run(interaction) { 49 | // Permission check 50 | const { getRole } = interaction.client; 51 | let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); 52 | let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); 53 | 54 | if (!SupportStaff || !Admin) { 55 | return interaction.reply('Some roles seem to be missing!\nPlease check for errors when starting the bot.'); 56 | } 57 | 58 | if (!interaction.member.roles.cache.has(SupportStaff.id) && 59 | !interaction.member.roles.cache.has(Admin.id)) { 60 | const NoPerms = new EmbedBuilder() 61 | .setTitle('Invalid Permissions!') 62 | .setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``) 63 | .setColor(0xFF0000); 64 | return interaction.reply({ embeds: [NoPerms], ephemeral: true }); 65 | } 66 | 67 | const channel = interaction.options.getChannel('channel'); 68 | 69 | // Initial message state 70 | const messageData = { 71 | type: 'embed', 72 | title: 'New Embed', 73 | description: 'Click the buttons below to edit this embed', 74 | color: 0x5865F2, 75 | fields: [], 76 | footer: null, 77 | thumbnail: null, 78 | image: null, 79 | timestamp: false 80 | }; 81 | 82 | // Create initial embed 83 | function createEmbed() { 84 | const embed = new EmbedBuilder() 85 | .setTitle(messageData.title) 86 | .setDescription(messageData.description) 87 | .setColor(messageData.color); 88 | 89 | if (messageData.fields.length > 0) { 90 | embed.addFields(messageData.fields); 91 | } 92 | if (messageData.footer) { 93 | embed.setFooter({ text: messageData.footer }); 94 | } 95 | if (messageData.thumbnail) { 96 | embed.setThumbnail(messageData.thumbnail); 97 | } 98 | if (messageData.image) { 99 | embed.setImage(messageData.image); 100 | } 101 | if (messageData.timestamp) { 102 | embed.setTimestamp(); 103 | } 104 | 105 | return embed; 106 | } 107 | 108 | // Create type selector 109 | const typeRow = new ActionRowBuilder() 110 | .addComponents( 111 | new StringSelectMenuBuilder() 112 | .setCustomId('message_type') 113 | .setPlaceholder('Select message type') 114 | .addOptions([ 115 | { 116 | label: 'Embed Message', 117 | description: 'Send as an embedded message', 118 | value: 'embed' 119 | }, 120 | { 121 | label: 'Text Message', 122 | description: 'Send as a plain text message', 123 | value: 'text' 124 | } 125 | ]) 126 | ); 127 | 128 | // Create button rows function 129 | function createButtons(messageType) { 130 | const buttons = []; 131 | 132 | if (messageType === 'embed') { 133 | // Only show these buttons for embed mode 134 | const row1 = new ActionRowBuilder() 135 | .addComponents( 136 | new ButtonBuilder() 137 | .setCustomId('edit_title') 138 | .setLabel('Edit Title') 139 | .setStyle(ButtonStyle.Secondary), 140 | new ButtonBuilder() 141 | .setCustomId('edit_description') 142 | .setLabel('Edit Description') 143 | .setStyle(ButtonStyle.Secondary), 144 | new ButtonBuilder() 145 | .setCustomId('edit_color') 146 | .setLabel('Edit Color') 147 | .setStyle(ButtonStyle.Secondary), 148 | new ButtonBuilder() 149 | .setCustomId('add_field') 150 | .setLabel('Add Field') 151 | .setStyle(ButtonStyle.Secondary) 152 | ); 153 | 154 | const row2 = new ActionRowBuilder() 155 | .addComponents( 156 | new ButtonBuilder() 157 | .setCustomId('edit_footer') 158 | .setLabel('Edit Footer') 159 | .setStyle(ButtonStyle.Secondary), 160 | new ButtonBuilder() 161 | .setCustomId('edit_image') 162 | .setLabel('Add Image') 163 | .setStyle(ButtonStyle.Secondary), 164 | new ButtonBuilder() 165 | .setCustomId('edit_thumbnail') 166 | .setLabel('Add Thumbnail') 167 | .setStyle(ButtonStyle.Secondary), 168 | new ButtonBuilder() 169 | .setCustomId('toggle_timestamp') 170 | .setLabel('Toggle Timestamp') 171 | .setStyle(ButtonStyle.Secondary) 172 | ); 173 | 174 | buttons.push(row1, row2); 175 | } else { 176 | // Text mode only shows edit text button 177 | const textRow = new ActionRowBuilder() 178 | .addComponents( 179 | new ButtonBuilder() 180 | .setCustomId('edit_description') 181 | .setLabel('Edit Text') 182 | .setStyle(ButtonStyle.Secondary) 183 | ); 184 | 185 | buttons.push(textRow); 186 | } 187 | 188 | // Send button always shows 189 | const sendRow = new ActionRowBuilder() 190 | .addComponents( 191 | new ButtonBuilder() 192 | .setCustomId('send_message') 193 | .setLabel('Send') 194 | .setStyle(ButtonStyle.Success) 195 | ); 196 | 197 | buttons.push(sendRow); 198 | return buttons; 199 | } 200 | 201 | // Function to update the message preview 202 | async function updatePreview(i) { 203 | if (messageData.type === 'embed') { 204 | await i.update({ 205 | content: 'Customize your message:', 206 | embeds: [createEmbed()], 207 | components: [typeRow, ...createButtons('embed')] 208 | }); 209 | } else { 210 | // For text mode, just show the text directly 211 | await i.update({ 212 | content: messageData.description, 213 | embeds: [], 214 | components: [typeRow, ...createButtons('text')] 215 | }); 216 | } 217 | } 218 | 219 | // Send initial message 220 | const message = await interaction.reply({ 221 | content: messageData.type === 'text' ? messageData.description : 'Customize your message:', 222 | components: [typeRow, ...createButtons(messageData.type)], 223 | embeds: messageData.type === 'embed' ? [createEmbed()] : [], 224 | ephemeral: true 225 | }); 226 | 227 | // Create collector for interactions 228 | const collector = message.createMessageComponentCollector({ 229 | time: 900000 230 | }); 231 | 232 | collector.on('collect', async (i) => { 233 | if (i.user.id !== interaction.user.id) { 234 | return i.reply({ 235 | content: 'You cannot edit this message.', 236 | ephemeral: true 237 | }); 238 | } 239 | 240 | switch (i.customId) { 241 | case 'message_type': 242 | messageData.type = i.values[0]; 243 | await updatePreview(i); 244 | break; 245 | 246 | case 'edit_description': 247 | const descModal = new ModalBuilder() 248 | .setCustomId('desc_modal') 249 | .setTitle(messageData.type === 'embed' ? 'Edit Description' : 'Edit Text'); 250 | 251 | descModal.addComponents( 252 | new ActionRowBuilder() 253 | .addComponents( 254 | new TextInputBuilder() 255 | .setCustomId('desc_input') 256 | .setLabel(messageData.type === 'embed' ? 'Description' : 'Message Text') 257 | .setStyle(TextInputStyle.Paragraph) 258 | .setValue(messageData.description) 259 | .setMaxLength(4000) 260 | ) 261 | ); 262 | 263 | await i.showModal(descModal); 264 | break; 265 | 266 | case 'edit_title': 267 | const titleModal = new ModalBuilder() 268 | .setCustomId('title_modal') 269 | .setTitle('Edit Title'); 270 | 271 | titleModal.addComponents( 272 | new ActionRowBuilder() 273 | .addComponents( 274 | new TextInputBuilder() 275 | .setCustomId('title_input') 276 | .setLabel('Title') 277 | .setStyle(TextInputStyle.Short) 278 | .setValue(messageData.title) 279 | .setMaxLength(256) 280 | ) 281 | ); 282 | 283 | await i.showModal(titleModal); 284 | break; 285 | 286 | case 'edit_color': 287 | const colorModal = new ModalBuilder() 288 | .setCustomId('color_modal') 289 | .setTitle('Edit Color'); 290 | 291 | colorModal.addComponents( 292 | new ActionRowBuilder() 293 | .addComponents( 294 | new TextInputBuilder() 295 | .setCustomId('color_input') 296 | .setLabel('Color (HEX)') 297 | .setStyle(TextInputStyle.Short) 298 | .setPlaceholder('#FF0000') 299 | .setMinLength(6) 300 | .setMaxLength(7) 301 | .setValue(messageData.color.toString(16).padStart(6, '0')) 302 | ) 303 | ); 304 | 305 | await i.showModal(colorModal); 306 | break; 307 | 308 | case 'add_field': 309 | const fieldModal = new ModalBuilder() 310 | .setCustomId('field_modal') 311 | .setTitle('Add Field'); 312 | 313 | fieldModal.addComponents( 314 | new ActionRowBuilder() 315 | .addComponents( 316 | new TextInputBuilder() 317 | .setCustomId('field_name') 318 | .setLabel('Field Name') 319 | .setStyle(TextInputStyle.Short) 320 | .setMaxLength(256) 321 | ), 322 | new ActionRowBuilder() 323 | .addComponents( 324 | new TextInputBuilder() 325 | .setCustomId('field_value') 326 | .setLabel('Field Value') 327 | .setStyle(TextInputStyle.Paragraph) 328 | .setMaxLength(1024) 329 | ), 330 | new ActionRowBuilder() 331 | .addComponents( 332 | new TextInputBuilder() 333 | .setCustomId('field_inline') 334 | .setLabel('Inline? (true/false)') 335 | .setStyle(TextInputStyle.Short) 336 | .setValue('false') 337 | .setPlaceholder('true or false') 338 | ) 339 | ); 340 | 341 | await i.showModal(fieldModal); 342 | break; 343 | 344 | case 'edit_footer': 345 | const footerModal = new ModalBuilder() 346 | .setCustomId('footer_modal') 347 | .setTitle('Edit Footer'); 348 | 349 | footerModal.addComponents( 350 | new ActionRowBuilder() 351 | .addComponents( 352 | new TextInputBuilder() 353 | .setCustomId('footer_input') 354 | .setLabel('Footer Text') 355 | .setStyle(TextInputStyle.Short) 356 | .setValue(messageData.footer || '') 357 | .setMaxLength(2048) 358 | ) 359 | ); 360 | 361 | await i.showModal(footerModal); 362 | break; 363 | 364 | case 'edit_image': 365 | const imageModal = new ModalBuilder() 366 | .setCustomId('image_modal') 367 | .setTitle('Add Image'); 368 | 369 | imageModal.addComponents( 370 | new ActionRowBuilder() 371 | .addComponents( 372 | new TextInputBuilder() 373 | .setCustomId('image_input') 374 | .setLabel('Image URL') 375 | .setStyle(TextInputStyle.Short) 376 | .setValue(messageData.image || '') 377 | .setPlaceholder('https://example.com/image.png') 378 | ) 379 | ); 380 | 381 | await i.showModal(imageModal); 382 | break; 383 | 384 | case 'edit_thumbnail': 385 | const thumbnailModal = new ModalBuilder() 386 | .setCustomId('thumbnail_modal') 387 | .setTitle('Add Thumbnail'); 388 | 389 | thumbnailModal.addComponents( 390 | new ActionRowBuilder() 391 | .addComponents( 392 | new TextInputBuilder() 393 | .setCustomId('thumbnail_input') 394 | .setLabel('Thumbnail URL') 395 | .setStyle(TextInputStyle.Short) 396 | .setValue(messageData.thumbnail || '') 397 | .setPlaceholder('https://example.com/thumbnail.png') 398 | ) 399 | ); 400 | 401 | await i.showModal(thumbnailModal); 402 | break; 403 | 404 | case 'toggle_timestamp': 405 | messageData.timestamp = !messageData.timestamp; 406 | await updatePreview(i); 407 | break; 408 | 409 | case 'send_message': 410 | try { 411 | if (messageData.type === 'embed') { 412 | await channel.send({ embeds: [createEmbed()] }); 413 | } else { 414 | await channel.send(messageData.description); 415 | } 416 | await i.update({ 417 | content: 'Message sent successfully!', 418 | components: [], 419 | embeds: [] 420 | }); 421 | collector.stop(); 422 | } catch (error) { 423 | await i.reply({ 424 | content: 'Failed to send message. Please check channel permissions.', 425 | ephemeral: true 426 | }); 427 | } 428 | break; 429 | } 430 | }); 431 | 432 | // Handle modal submissions 433 | interaction.client.on('interactionCreate', async (modal) => { 434 | if (!modal.isModalSubmit()) return; 435 | if (modal.user.id !== interaction.user.id) return; 436 | 437 | switch (modal.customId) { 438 | case 'title_modal': 439 | messageData.title = modal.fields.getTextInputValue('title_input'); 440 | break; 441 | case 'desc_modal': 442 | messageData.description = modal.fields.getTextInputValue('desc_input'); 443 | break; 444 | case 'color_modal': 445 | messageData.color = parseColor(modal.fields.getTextInputValue('color_input')); 446 | break; 447 | case 'field_modal': 448 | const inlineValue = modal.fields.getTextInputValue('field_inline').toLowerCase(); 449 | messageData.fields.push({ 450 | name: modal.fields.getTextInputValue('field_name'), 451 | value: modal.fields.getTextInputValue('field_value'), 452 | inline: inlineValue === 'true' 453 | }); 454 | break; 455 | case 'footer_modal': 456 | messageData.footer = modal.fields.getTextInputValue('footer_input'); 457 | break; 458 | case 'image_modal': 459 | messageData.image = modal.fields.getTextInputValue('image_input'); 460 | break; 461 | case 'thumbnail_modal': 462 | messageData.thumbnail = modal.fields.getTextInputValue('thumbnail_input'); 463 | break; 464 | default: 465 | return; 466 | } 467 | 468 | await updatePreview(modal); 469 | }); 470 | 471 | collector.on('end', () => { 472 | interaction.editReply({ 473 | content: 'Message editor timed out.', 474 | components: [], 475 | embeds: [] 476 | }).catch(() => {}); 477 | }); 478 | } 479 | }); -------------------------------------------------------------------------------- /Commands/forceaddUser.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { 3 | EmbedBuilder, 4 | ApplicationCommandOptionType, 5 | ApplicationCommandType, 6 | ChannelType, 7 | } = require("discord.js"); 8 | const yaml = require("js-yaml"); 9 | 10 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 11 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 12 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 13 | 14 | const Command = require("../Structures/Command.js"); 15 | 16 | module.exports = new Command({ 17 | name: cmdconfig.ForceAddUser.Command, 18 | description: cmdconfig.ForceAddUser.Description, 19 | type: ApplicationCommandType.ChatInput, 20 | options: [ 21 | { 22 | name: "user", 23 | description: "The user to forcefully add", 24 | type: ApplicationCommandOptionType.User, 25 | required: true, 26 | }, 27 | ], 28 | permissions: cmdconfig.ForceAddUser.Permission, 29 | 30 | async run(interaction) { 31 | const userToAdd = interaction.options.getUser("user"); 32 | const ticketChannel = interaction.channel; 33 | const ticketDataPath = "./Data/TicketData.json"; 34 | 35 | if ( 36 | (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || 37 | (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText) 38 | ) { 39 | const onlyInTicket = new EmbedBuilder() 40 | .setTitle(msgconfig.ForceAddUser.NotInTicket_Title) 41 | .setDescription(msgconfig.ForceAddUser.NotInTicket_Description) 42 | .setColor(supportbot.Embed.Colours.Error); 43 | 44 | return interaction.reply({ 45 | embeds: [onlyInTicket], 46 | ephemeral: true, 47 | }); 48 | } 49 | 50 | try { 51 | 52 | if (supportbot.Ticket.TicketType === "threads") { 53 | try { 54 | // Explicitly ensure the bot is in the thread 55 | const botMember = await ticketChannel.members.fetch(interaction.client.user.id).catch(() => null); 56 | if (!botMember) { 57 | await ticketChannel.join(); 58 | } 59 | 60 | // Delay to ensure API has processed the bot's membership 61 | await new Promise(res => setTimeout(res, 1000)); 62 | 63 | // Attempt to add the user to the thread 64 | await ticketChannel.members.add(userToAdd.id); 65 | 66 | console.log("User successfully added to the thread."); 67 | } catch (err) { 68 | console.error("Error adding user to the thread:", err.message); 69 | const errorEmbed = new EmbedBuilder() 70 | .setTitle(msgconfig.ForceAddUser.Error_Title) 71 | .setDescription(`An error occurred: ${err.message}`) 72 | .setColor(supportbot.Embed.Colours.Error); 73 | 74 | return interaction.reply({ 75 | embeds: [errorEmbed], 76 | ephemeral: true, 77 | }); 78 | } 79 | 80 | } else if (supportbot.Ticket.TicketType === "channels") { 81 | await ticketChannel.permissionOverwrites.create(userToAdd.id, { 82 | ViewChannel: true, 83 | SendMessages: true, 84 | ReadMessageHistory: true, 85 | }); 86 | } 87 | 88 | const ticketData = JSON.parse(fs.readFileSync(ticketDataPath, "utf8")); 89 | const ticketIndex = ticketData.tickets.findIndex((t) => t.id === ticketChannel.id); 90 | if (ticketIndex !== -1) { 91 | ticketData.tickets[ticketIndex].subUsers.push(userToAdd.id); 92 | fs.writeFileSync(ticketDataPath, JSON.stringify(ticketData, null, 4)); 93 | } 94 | 95 | const addedEmbed = new EmbedBuilder() 96 | .setTitle(msgconfig.ForceAddUser.Added_Title) 97 | .setDescription( 98 | msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username) 99 | ) 100 | .setColor(supportbot.Embed.Colours.Success); 101 | 102 | await interaction.reply({ 103 | embeds: [addedEmbed], 104 | ephemeral: true, 105 | }); 106 | 107 | const addedToTicketEmbed = new EmbedBuilder() 108 | .setTitle(msgconfig.ForceAddUser.AddedToTicket_Title) 109 | .setDescription( 110 | msgconfig.ForceAddUser.AddedToTicket_Description.replace( 111 | "%channel_link%", 112 | `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})` 113 | ) 114 | ) 115 | .setColor(supportbot.Embed.Colours.General); 116 | 117 | await userToAdd.send({ 118 | embeds: [addedToTicketEmbed], 119 | }); 120 | } catch (err) { 121 | console.error("Error adding user to the ticket:", err); 122 | 123 | const errorEmbed = new EmbedBuilder() 124 | .setTitle(msgconfig.ForceAddUser.Error_Title) 125 | .setDescription(msgconfig.ForceAddUser.Error_Adding) 126 | .setColor(supportbot.Embed.Colours.Error); 127 | 128 | await interaction.reply({ 129 | embeds: [errorEmbed], 130 | ephemeral: true, 131 | }); 132 | } 133 | }, 134 | }); 135 | -------------------------------------------------------------------------------- /Commands/help.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Discord = require("discord.js"); 3 | const yaml = require("js-yaml"); 4 | 5 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 6 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 7 | 8 | const Command = require("../Structures/Command.js"); 9 | 10 | module.exports = new Command({ 11 | name: cmdconfig.Help.Command, 12 | description: cmdconfig.Help.Description, 13 | type: Discord.ApplicationCommandType.ChatInput, 14 | options: [], 15 | permissions: cmdconfig.Help.Permission, 16 | 17 | async run(interaction) { 18 | const { getRole } = interaction.client; 19 | let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); 20 | let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); 21 | if (!SupportStaff || !Admin) { 22 | return interaction.reply( 23 | "Some roles seem to be missing!\nPlease check for errors when starting the bot." 24 | ); 25 | } 26 | 27 | // Separate regular commands and addon commands 28 | let botCommands = ""; 29 | let addonCommands = ""; 30 | 31 | interaction.client.commands.forEach((command) => { 32 | if (command.permissions) { 33 | if (command.addon) { 34 | addonCommands += `\`${command.name}\` `; 35 | } else { 36 | botCommands += `\`${command.name}\` `; 37 | } 38 | } 39 | }); 40 | 41 | // Create embed 42 | const HelpEmbed1 = new Discord.EmbedBuilder() 43 | .setTitle(supportbot.General.Name + " Commands") 44 | .setThumbnail(interaction.user.displayAvatarURL()) 45 | .setColor(supportbot.Embed.Colours.General) 46 | .addFields({ 47 | name: "🖥️ Commands\n", 48 | value: botCommands || "No commands available.", 49 | inline: false, 50 | }); 51 | 52 | HelpEmbed1.setFooter({ 53 | text: supportbot.Embed.Footer, 54 | iconURL: interaction.user.displayAvatarURL(), 55 | }); 56 | 57 | interaction.reply({ 58 | ephemeral: true, 59 | embeds: [HelpEmbed1], 60 | }); 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /Commands/info.js: -------------------------------------------------------------------------------- 1 | // SupportBot | Emerald Services 2 | // Info Command 3 | 4 | const fs = require("fs"); 5 | 6 | const Discord = require("discord.js"); 7 | const yaml = require("js-yaml"); 8 | 9 | const supportbot = yaml.load( 10 | fs.readFileSync("./Configs/supportbot.yml", "utf8") 11 | ); 12 | 13 | const cmdconfig = yaml.load( 14 | fs.readFileSync("./Configs/commands.yml", "utf8") 15 | ); 16 | 17 | const msgconfig = yaml.load( 18 | fs.readFileSync("./Configs/messages.yml", "utf8") 19 | ); 20 | 21 | const Command = require("../Structures/Command.js"); 22 | 23 | module.exports = new Command({ 24 | name: cmdconfig.Info.Command, 25 | description: cmdconfig.Info.Description, 26 | options: [], 27 | permissions: cmdconfig.Info.Permission, 28 | 29 | async run(interaction) { 30 | let disableCommand = true; 31 | 32 | const InfoButton = new Discord.ButtonBuilder() 33 | .setLabel(msgconfig.Info.Button) 34 | .setURL(msgconfig.Info.URL) 35 | .setStyle("Link"); 36 | 37 | const inforow = new Discord.ActionRowBuilder().addComponents(InfoButton); 38 | 39 | const InfoEmbed = new Discord.EmbedBuilder() 40 | .setURL(msgconfig.Info.URL) 41 | .setTitle(msgconfig.Info.Title) 42 | .setDescription(msgconfig.Info.Description) 43 | .setColor(msgconfig.Info.Colour) 44 | 45 | interaction.reply({ 46 | embeds: [InfoEmbed], 47 | components: [inforow], 48 | }); 49 | 50 | }, 51 | }); -------------------------------------------------------------------------------- /Commands/mod.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType } = require("discord.js"); 3 | const yaml = require("js-yaml"); 4 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 5 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 6 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 7 | 8 | const Command = require("../Structures/Command.js"); 9 | 10 | // Function to chunk array into smaller arrays 11 | function chunkArray(arr, size) { 12 | const chunks = []; 13 | for (let i = 0; i < arr.length; i += size) { 14 | chunks.push(arr.slice(i, i + size)); 15 | } 16 | return chunks; 17 | } 18 | 19 | module.exports = new Command({ 20 | name: cmdconfig.Mod.Command, 21 | description: cmdconfig.Mod.Command, 22 | type: ApplicationCommandType.ChatInput, 23 | options: [ 24 | { 25 | name: cmdconfig.Mod.TicketBlacklist.Command, 26 | description: cmdconfig.Mod.TicketBlacklist.Description, 27 | type: ApplicationCommandOptionType.SubcommandGroup, 28 | options: [ 29 | { 30 | name: cmdconfig.Mod.TicketBlacklist.Add.Command, 31 | description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the ticket blacklist", 32 | type: ApplicationCommandOptionType.Subcommand, 33 | options: [ 34 | { 35 | name: 'user', 36 | description: msgconfig.Mod.TicketBlacklist.Add.UserDescription || "User to blacklist", 37 | type: ApplicationCommandOptionType.User, 38 | required: true 39 | }, 40 | { 41 | name: 'reason', 42 | description: msgconfig.Mod.TicketBlacklist.Add.ReasonDescription || "Reason for blacklisting", 43 | type: ApplicationCommandOptionType.String, 44 | required: false 45 | } 46 | ] 47 | }, 48 | { 49 | name: cmdconfig.Mod.TicketBlacklist.Remove.Command, 50 | description: msgconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the ticket blacklist", 51 | type: ApplicationCommandOptionType.Subcommand, 52 | options: [ 53 | { 54 | name: 'user', 55 | description: msgconfig.Mod.TicketBlacklist.Remove.UserDescription || "User to remove from blacklist", 56 | type: ApplicationCommandOptionType.User, 57 | required: true 58 | }, 59 | { 60 | name: 'reason', 61 | description: msgconfig.Mod.TicketBlacklist.Remove.ReasonDescription || "Reason for removing from blacklist", 62 | type: ApplicationCommandOptionType.String, 63 | required: false 64 | } 65 | ] 66 | }, 67 | { 68 | name: "view", 69 | description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users", 70 | type: ApplicationCommandOptionType.Subcommand, 71 | } 72 | ] 73 | } 74 | ], 75 | permissions: cmdconfig.Mod.Permission, 76 | 77 | async run(interaction) { 78 | const { getRole } = interaction.client; 79 | 80 | let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); 81 | let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); 82 | let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild); 83 | 84 | if (!SupportStaff || !Admin || !Moderator) { 85 | const missingRolesEmbed = new EmbedBuilder() 86 | .setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!") 87 | .setColor(supportbot.Embed.Colours.Warn); 88 | return interaction.reply({ embeds: [missingRolesEmbed] }); 89 | } 90 | 91 | const NoPerms = new EmbedBuilder() 92 | .setDescription(msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!") 93 | .setColor(supportbot.Embed.Colours.Warn); 94 | 95 | if (!interaction.member.roles.cache.has(Admin.id) && !interaction.member.roles.cache.has(Moderator.id) && (!supportbot.Mod.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))) { 96 | return interaction.reply({ embeds: [NoPerms] }); 97 | } 98 | 99 | const subcommand = interaction.options.getSubcommand(false); 100 | const user = interaction.options.getUser('user'); 101 | const reason = interaction.options.getString('reason') || "No reason provided"; 102 | 103 | try { 104 | let blacklistedUsers; 105 | try { 106 | blacklistedUsers = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8")).blacklistedUsers; 107 | if (!Array.isArray(blacklistedUsers)) { 108 | blacklistedUsers = []; 109 | } 110 | } catch (error) { 111 | blacklistedUsers = []; 112 | } 113 | 114 | if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) { 115 | if (blacklistedUsers.includes(user.id)) { 116 | const alreadyBlacklistedEmbed = new EmbedBuilder() 117 | .setDescription(msgconfig.Mod.TicketBlacklist.Add.AlreadyBlacklisted.replace("{userTag}", user.tag) || ":x: User is already blacklisted.") 118 | .setColor(supportbot.Embed.Colours.Warn); 119 | return interaction.reply({ embeds: [alreadyBlacklistedEmbed], ephemeral: true }); 120 | } 121 | 122 | blacklistedUsers.push(user.id); 123 | fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4)); 124 | 125 | const successEmbed = new EmbedBuilder() 126 | .setDescription(msgconfig.Mod.TicketBlacklist.Add.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been blacklisted.") 127 | .setColor(supportbot.Embed.Colours.Success); 128 | 129 | const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog); 130 | if (blacklistChannel) { 131 | const blacklistLogEmbed = new EmbedBuilder() 132 | .setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log") 133 | .setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success) 134 | .setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() }) 135 | .setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`) 136 | .addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false }) 137 | .addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false }); 138 | 139 | blacklistChannel.send({ embeds: [blacklistLogEmbed] }); 140 | } 141 | 142 | return interaction.reply({ 143 | embeds: [successEmbed], 144 | ephemeral: true 145 | }); 146 | 147 | } else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) { 148 | if (!blacklistedUsers.includes(user.id)) { 149 | const notBlacklistedEmbed = new EmbedBuilder() 150 | .setDescription(msgconfig.Mod.TicketBlacklist.Remove.NotBlacklisted.replace("{userTag}", user.tag) || ":x: User is not blacklisted.") 151 | .setColor(supportbot.Embed.Colours.Warn); 152 | return interaction.reply({ embeds: [notBlacklistedEmbed], ephemeral: true }); 153 | } 154 | 155 | blacklistedUsers = blacklistedUsers.filter(id => id !== user.id); 156 | fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4)); 157 | 158 | const removedEmbed = new EmbedBuilder() 159 | .setDescription(msgconfig.Mod.TicketBlacklist.Remove.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been removed from the blacklist.") 160 | .setColor(supportbot.Embed.Colours.Success); 161 | 162 | const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog); 163 | if (blacklistChannel) { 164 | const blacklistLogEmbed = new EmbedBuilder() 165 | .setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log") 166 | .setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success) 167 | .setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() }) 168 | .setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`) 169 | .addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false }) 170 | .addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false }); 171 | 172 | blacklistChannel.send({ embeds: [blacklistLogEmbed] }); 173 | } 174 | 175 | return interaction.reply({ embeds: [removedEmbed], ephemeral: true }); 176 | 177 | } else if (subcommand === "view") { 178 | if (blacklistedUsers.length === 0) { 179 | const noBlacklistedUsersEmbed = new EmbedBuilder() 180 | .setDescription(msgconfig.Mod.TicketBlacklist.View.NoBlacklistedUsers || "No users are blacklisted.") 181 | .setColor(supportbot.Embed.Colours.Success); 182 | return interaction.reply({ embeds: [noBlacklistedUsersEmbed], ephemeral: true }); 183 | } 184 | 185 | const chunkedUsers = chunkArray(blacklistedUsers, 5); 186 | let currentPage = 0; 187 | 188 | const createEmbed = (page) => { 189 | return new EmbedBuilder() 190 | .setTitle(msgconfig.Mod.TicketBlacklist.View.EmbedTitle || "Blacklisted Users") 191 | .setDescription(chunkedUsers[page].map(id => `<@${id}>`).join("\n")) 192 | .setColor(supportbot.Embed.Colours.Success) 193 | .setFooter({ text: `Page ${page + 1} of ${chunkedUsers.length}` }); 194 | }; 195 | 196 | const buttons = new ActionRowBuilder() 197 | .addComponents( 198 | new ButtonBuilder() 199 | .setCustomId('previous') 200 | .setLabel('Previous') 201 | .setStyle(ButtonStyle.Secondary) 202 | .setDisabled(currentPage === 0), 203 | new ButtonBuilder() 204 | .setCustomId('next') 205 | .setLabel('Next') 206 | .setStyle(ButtonStyle.Secondary) 207 | .setDisabled(currentPage === chunkedUsers.length - 1) 208 | ); 209 | 210 | const response = await interaction.reply({ 211 | embeds: [createEmbed(currentPage)], 212 | components: chunkedUsers.length > 1 ? [buttons] : [], 213 | ephemeral: true 214 | }); 215 | 216 | if (chunkedUsers.length <= 1) return; 217 | 218 | const collector = response.createMessageComponentCollector({ 219 | componentType: ComponentType.Button, 220 | time: 60000 221 | }); 222 | 223 | collector.on('collect', async (i) => { 224 | if (i.user.id !== interaction.user.id) { 225 | return i.reply({ 226 | content: 'You cannot use these buttons.', 227 | ephemeral: true 228 | }); 229 | } 230 | 231 | if (i.customId === 'previous' && currentPage > 0) { 232 | currentPage--; 233 | } else if (i.customId === 'next' && currentPage < chunkedUsers.length - 1) { 234 | currentPage++; 235 | } 236 | 237 | buttons.components[0].setDisabled(currentPage === 0); 238 | buttons.components[1].setDisabled(currentPage === chunkedUsers.length - 1); 239 | 240 | await i.update({ 241 | embeds: [createEmbed(currentPage)], 242 | components: [buttons] 243 | }); 244 | }); 245 | 246 | collector.on('end', () => { 247 | buttons.components.forEach(button => button.setDisabled(true)); 248 | interaction.editReply({ 249 | components: [buttons] 250 | }).catch(() => {}); 251 | }); 252 | } 253 | } catch (error) { 254 | console.error("Error in mod command:", error); 255 | const errorEmbed = new EmbedBuilder() 256 | .setDescription(msgconfig.Error.ActionFailed || "An error occurred while processing your request.") 257 | .setColor(supportbot.Embed.Colours.Warn); 258 | interaction.reply({ embeds: [errorEmbed], ephemeral: true }); 259 | } 260 | } 261 | }); -------------------------------------------------------------------------------- /Commands/ping.js: -------------------------------------------------------------------------------- 1 | // SupportBot | Emerald Services 2 | // Ping Command 3 | 4 | const fs = require("fs"); 5 | 6 | const Discord = require("discord.js"); 7 | const yaml = require("js-yaml"); 8 | const supportbot = yaml.load( 9 | fs.readFileSync("./Configs/supportbot.yml", "utf8") 10 | ); 11 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 12 | 13 | const Command = require("../Structures/Command.js"); 14 | 15 | module.exports = new Command({ 16 | name: cmdconfig.Ping.Command, 17 | description: cmdconfig.Ping.Description, 18 | options: [], 19 | permissions: cmdconfig.Ping.Permission, 20 | 21 | async run(interaction) { 22 | let disableCommand = true; 23 | 24 | const PingEmbed = new Discord.EmbedBuilder() 25 | .setDescription( 26 | `:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\`` 27 | ) 28 | .setColor(supportbot.Embed.Colours.General); 29 | 30 | interaction.reply({ 31 | embeds: [PingEmbed], 32 | }); 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /Commands/profile.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ApplicationCommandType, ActionRowBuilder, ButtonBuilder, ButtonStyle, ApplicationCommandOptionType } = require("discord.js"); 2 | const fs = require("fs"); 3 | const yaml = require("js-yaml"); 4 | 5 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 6 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 7 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 8 | 9 | const Command = require("../Structures/Command.js"); 10 | 11 | const clockedInUsers = new Set(); 12 | 13 | function updateClockedInStatus(profileEmbed, clockedIn) { 14 | profileEmbed.data.fields = profileEmbed.data.fields.map(field => { 15 | if (field.name === 'Clocked In Status') { 16 | field.value = clockedIn ? '✅ Clocked In' : '❌ Clocked Out'; 17 | } 18 | return field; 19 | }); 20 | return profileEmbed; 21 | } 22 | 23 | module.exports = new Command({ 24 | name: cmdconfig.Profile.Command, 25 | description: cmdconfig.Profile.Description, 26 | type: ApplicationCommandType.ChatInput, 27 | permissions: cmdconfig.Profile.Permission, 28 | options: [ 29 | { 30 | name: 'user', 31 | description: 'The user to view the profile of', 32 | type: ApplicationCommandOptionType.User, 33 | required: false 34 | } 35 | ], 36 | 37 | async run(interaction) { 38 | await interaction.deferReply({ ephemeral: true }); 39 | 40 | const userOption = interaction.options.getUser('user'); 41 | const viewingUser = userOption || interaction.user; 42 | const viewingUserId = viewingUser.id; 43 | const interactionUserId = interaction.user.id; 44 | 45 | const user = interaction.guild.members.cache.get(viewingUserId); 46 | const { getRole, getChannel } = interaction.client; 47 | 48 | const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); 49 | const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); 50 | 51 | if (!Staff || !Admin) { 52 | return interaction.editReply({ 53 | content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", 54 | ephemeral: true, 55 | }); 56 | } 57 | 58 | const isStaff = user.roles.cache.has(Staff.id) || user.roles.cache.has(Admin.id); 59 | const isOwnProfile = interactionUserId === viewingUserId; 60 | 61 | const profilePath = `./Data/Profiles/${viewingUserId}.json`; 62 | let profileData; 63 | 64 | if (fs.existsSync(profilePath)) { 65 | profileData = JSON.parse(fs.readFileSync(profilePath, "utf8")); 66 | } else { 67 | profileData = { 68 | bio: "", 69 | timezone: "", 70 | clockedIn: false 71 | }; 72 | fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2)); 73 | } 74 | 75 | const { bio, timezone, clockedIn } = profileData; 76 | 77 | let profileEmbed = new EmbedBuilder() 78 | .setTitle(`${viewingUser.username}'s Profile`) 79 | .setColor(supportbot.Embed.Colours.General) 80 | .setThumbnail(viewingUser.displayAvatarURL()) 81 | .addFields( 82 | { name: 'Bio', value: bio || 'No bio set.', inline: false }, 83 | { name: 'Timezone', value: timezone || 'No timezone set.', inline: true } 84 | ); 85 | 86 | if (isStaff) { 87 | profileEmbed.addFields({ name: 'Clocked In Status', value: clockedIn ? '✅ Clocked In' : '❌ Clocked Out', inline: true }); 88 | } 89 | 90 | const buttonRow = new ActionRowBuilder(); 91 | if (isOwnProfile) { 92 | buttonRow.addComponents( 93 | new ButtonBuilder() 94 | .setCustomId('editProfile') 95 | .setLabel('Edit Profile') 96 | .setStyle(ButtonStyle.Secondary) 97 | ); 98 | 99 | if (isStaff) { 100 | buttonRow.addComponents( 101 | new ButtonBuilder() 102 | .setCustomId('clockInOut') 103 | .setLabel(clockedIn ? 'Clock Out' : 'Clock In') 104 | .setStyle(ButtonStyle.Secondary) 105 | ); 106 | } 107 | } 108 | 109 | if (isStaff && supportbot.Ticket.ClaimTickets.Enabled) { 110 | buttonRow.addComponents( 111 | new ButtonBuilder() 112 | .setCustomId('viewTicketStats') 113 | .setLabel('View Ticket Stats') 114 | .setStyle(ButtonStyle.Secondary) 115 | ); 116 | } 117 | 118 | await interaction.editReply({ embeds: [profileEmbed], components: buttonRow.components.length > 0 ? [buttonRow] : [] }); 119 | 120 | const filter = i => ( 121 | ['editProfile', 'clockInOut', 'viewTicketStats', 'showOpenTickets', 'backToStats', 'backToProfile'].includes(i.customId) 122 | && i.user.id === interactionUserId 123 | ); 124 | 125 | const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 }); 126 | 127 | collector.on('collect', async i => { 128 | if (i.customId === 'editProfile') { 129 | const modal = new ModalBuilder() 130 | .setCustomId('editProfileModal') 131 | .setTitle('Edit Profile') 132 | .addComponents( 133 | new ActionRowBuilder().addComponents( 134 | new TextInputBuilder() 135 | .setCustomId('bio') 136 | .setLabel('Bio') 137 | .setStyle(TextInputStyle.Paragraph) 138 | .setPlaceholder('Enter your bio') 139 | .setValue(String(bio)) 140 | .setRequired(false) 141 | ), 142 | new ActionRowBuilder().addComponents( 143 | new TextInputBuilder() 144 | .setCustomId('timezone') 145 | .setLabel('Timezone') 146 | .setStyle(TextInputStyle.Short) 147 | .setPlaceholder('Enter your timezone') 148 | .setValue(String(timezone)) 149 | .setRequired(false) 150 | ) 151 | ); 152 | 153 | await i.showModal(modal); 154 | } 155 | 156 | if (i.customId === 'clockInOut') { 157 | profileData.clockedIn = !profileData.clockedIn; 158 | fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2)); 159 | profileEmbed = updateClockedInStatus(profileEmbed, profileData.clockedIn); 160 | 161 | if (profileData.clockedIn) { 162 | clockedInUsers.add(viewingUserId); 163 | } else { 164 | clockedInUsers.delete(viewingUserId); 165 | } 166 | 167 | const updatedButtonRow = new ActionRowBuilder() 168 | .addComponents( 169 | new ButtonBuilder() 170 | .setCustomId('editProfile') 171 | .setLabel('Edit Profile') 172 | .setStyle(ButtonStyle.Secondary) 173 | ); 174 | 175 | if (isStaff) { 176 | updatedButtonRow.addComponents( 177 | new ButtonBuilder() 178 | .setCustomId('clockInOut') 179 | .setLabel(profileData.clockedIn ? 'Clock Out' : 'Clock In') 180 | .setStyle(ButtonStyle.Secondary) 181 | ); 182 | 183 | if (supportbot.Ticket.ClaimTickets.Enabled) { 184 | updatedButtonRow.addComponents( 185 | new ButtonBuilder() 186 | .setCustomId('viewTicketStats') 187 | .setLabel('View Ticket Stats') 188 | .setStyle(ButtonStyle.Secondary) 189 | ); 190 | } 191 | } 192 | 193 | await i.update({ embeds: [profileEmbed], components: [updatedButtonRow] }); 194 | 195 | let clockedinout = new EmbedBuilder() 196 | .setTitle(profileData.clockedIn ? 'Clocked In!' : 'Clocked Out!') 197 | .setDescription(profileData.clockedIn ? 'You have successfully clocked in.' : 'You have successfully clocked out.') 198 | .setColor(supportbot.Embed.Colours.General) 199 | 200 | await interaction.followUp({ embeds: [clockedinout], ephemeral: true }); 201 | } 202 | 203 | if (i.customId === 'viewTicketStats') { 204 | const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); 205 | const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId); 206 | const ticketsOpen = ticketsClaimed.filter(t => t.open); 207 | const totalTickets = ticketsClaimed.length; 208 | 209 | let totalResponseTime = 0; 210 | let responseCount = 0; 211 | 212 | ticketsClaimed.forEach(ticket => { 213 | if (ticket.claimedAt && ticket.createdAt) { 214 | const responseTime = new Date(ticket.claimedAt) - new Date(ticket.createdAt); 215 | totalResponseTime += responseTime; 216 | responseCount++; 217 | } 218 | }); 219 | 220 | const averageResponseTime = responseCount ? totalResponseTime / responseCount : 0; 221 | const averageResponseTimeMinutes = Math.round(averageResponseTime / 60000); 222 | 223 | const statsEmbed = new EmbedBuilder() 224 | .setTitle(msgconfig.TicketStats.Title) 225 | .setDescription(`> Ticket stats for <@${viewingUserId}>`) 226 | .setColor(supportbot.Embed.Colours.General) 227 | .addFields( 228 | { name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true }, 229 | { name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true }, 230 | { name: msgconfig.TicketStats.ResponseTime, value: `${averageResponseTimeMinutes} minutes`, inline: false } 231 | ); 232 | 233 | const statsButtonRow = new ActionRowBuilder() 234 | .addComponents( 235 | new ButtonBuilder() 236 | .setCustomId('showOpenTickets') 237 | .setLabel('Show Open Tickets') 238 | .setStyle(ButtonStyle.Secondary), 239 | new ButtonBuilder() 240 | .setCustomId('backToProfile') 241 | .setLabel('Back to Profile') 242 | .setStyle(ButtonStyle.Secondary) 243 | ); 244 | 245 | await i.update({ embeds: [statsEmbed], components: [statsButtonRow] }); 246 | } 247 | 248 | if (i.customId === 'showOpenTickets') { 249 | const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); 250 | const ticketsOpen = TicketData.tickets.filter(t => t.claimedBy === viewingUserId && t.open); 251 | 252 | const openTicketsEmbed = new EmbedBuilder() 253 | .setTitle('Active Tickets') 254 | .setDescription(ticketsOpen.map(t => `<#${t.id}>`).join('\n') || 'No active tickets.') 255 | .setColor(supportbot.Embed.Colours.General); 256 | 257 | const openTicketsButtonRow = new ActionRowBuilder() 258 | .addComponents( 259 | new ButtonBuilder() 260 | .setCustomId('backToStats') 261 | .setLabel('Back to Stats') 262 | .setStyle(ButtonStyle.Secondary) 263 | ); 264 | 265 | await i.update({ embeds: [openTicketsEmbed], components: [openTicketsButtonRow] }); 266 | } 267 | 268 | if (i.customId === 'backToStats') { 269 | const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); 270 | const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId); 271 | const ticketsOpen = ticketsClaimed.filter(t => t.open); 272 | const totalTickets = ticketsClaimed.length; 273 | 274 | let totalResponseTime = 0; 275 | let responseCount = 0; 276 | 277 | ticketsClaimed.forEach(ticket => { 278 | if (ticket.claimedAt && ticket.createdAt) { 279 | const responseTime = new Date(ticket.claimedAt) - new Date(ticket.createdAt); 280 | totalResponseTime += responseTime; 281 | responseCount++; 282 | } 283 | }); 284 | 285 | const averageResponseTime = responseCount ? totalResponseTime / responseCount : 0; 286 | const averageResponseTimeMinutes = Math.round(averageResponseTime / 60000); 287 | 288 | const statsEmbed = new EmbedBuilder() 289 | .setTitle(msgconfig.TicketStats.Title) 290 | .setDescription(`> Ticket stats for <@${viewingUserId}>`) 291 | .setColor(supportbot.Embed.Colours.General) 292 | .addFields( 293 | { name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true }, 294 | { name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true }, 295 | { name: msgconfig.TicketStats.ResponseTime, value: `${averageResponseTimeMinutes} minutes`, inline: false } 296 | ); 297 | 298 | const statsButtonRow = new ActionRowBuilder() 299 | .addComponents( 300 | new ButtonBuilder() 301 | .setCustomId('showOpenTickets') 302 | .setLabel('Show Open Tickets') 303 | .setStyle(ButtonStyle.Secondary), 304 | new ButtonBuilder() 305 | .setCustomId('backToProfile') 306 | .setLabel('Back to Profile') 307 | .setStyle(ButtonStyle.Secondary) 308 | ); 309 | 310 | await i.update({ embeds: [statsEmbed], components: [statsButtonRow] }); 311 | } 312 | 313 | if (i.customId === 'backToProfile') { 314 | await i.update({ embeds: [profileEmbed], components: buttonRow.components.length > 0 ? [buttonRow] : [] }); 315 | } 316 | }); 317 | } 318 | }); 319 | 320 | module.exports.clockedInUsers = clockedInUsers; 321 | -------------------------------------------------------------------------------- /Commands/removeUser.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { 3 | EmbedBuilder, 4 | ApplicationCommandOptionType, 5 | ApplicationCommandType, 6 | ChannelType, 7 | ActionRowBuilder, 8 | ButtonBuilder, 9 | ButtonStyle, 10 | } = require("discord.js"); 11 | const yaml = require("js-yaml"); 12 | 13 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 14 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 15 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 16 | 17 | const Command = require("../Structures/Command.js"); 18 | 19 | module.exports = new Command({ 20 | name: cmdconfig.RemoveUser.Command, 21 | description: cmdconfig.RemoveUser.Description, 22 | type: ApplicationCommandType.ChatInput, 23 | options: [ 24 | { 25 | name: "user", 26 | description: "The user to remove", 27 | type: ApplicationCommandOptionType.User, 28 | required: true, 29 | }, 30 | ], 31 | permissions: cmdconfig.RemoveUser.Permission, 32 | 33 | async run(interaction) { 34 | 35 | const { getRole } = interaction.client; 36 | let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); 37 | let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); 38 | if (!SupportStaff || !Admin) { 39 | return interaction.reply( 40 | "Some roles seem to be missing!\nPlease check for errors when starting the bot." 41 | ); 42 | } 43 | 44 | const NoPerms = new EmbedBuilder() 45 | .setTitle("Invalid Permissions!") 46 | .setDescription( 47 | `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\`` 48 | ) 49 | .setColor(supportbot.Embed.Colours.Warn); 50 | 51 | if ( 52 | !interaction.member.roles.cache.has(SupportStaff.id) && 53 | !interaction.member.roles.cache.has(Admin.id) 54 | ) { 55 | return interaction.reply({ embeds: [NoPerms] }); 56 | } 57 | 58 | const userToRemove = interaction.options.getUser("user"); 59 | const ticketChannel = interaction.channel; 60 | const ticketDataPath = "./Data/TicketData.json"; 61 | 62 | // Check if the channel is a ticket thread or text channel based on TicketType 63 | if ( 64 | (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || 65 | (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText) 66 | ) { 67 | const onlyInTicket = new EmbedBuilder() 68 | .setTitle(msgconfig.RemoveUser.NotInTicket_Title) 69 | .setDescription(msgconfig.RemoveUser.NotInTicket_Description) 70 | .setColor(supportbot.Embed.Colours.Error); 71 | 72 | return interaction.reply({ 73 | embeds: [onlyInTicket], 74 | ephemeral: true, 75 | }); 76 | } 77 | 78 | try { 79 | // Remove user from thread or channel based on TicketType 80 | if (supportbot.Ticket.TicketType === "threads") { 81 | await ticketChannel.members.remove(userToRemove.id); // For threads 82 | } else if (supportbot.Ticket.TicketType === "channels") { 83 | await ticketChannel.permissionOverwrites.delete(userToRemove.id); // For channels 84 | } 85 | 86 | // Update the ticket data 87 | const ticketData = JSON.parse(fs.readFileSync(ticketDataPath, "utf8")); 88 | const ticketIndex = ticketData.tickets.findIndex((t) => t.id === ticketChannel.id); 89 | if (ticketIndex !== -1) { 90 | const subUsers = ticketData.tickets[ticketIndex].subUsers; 91 | const userIndex = subUsers.indexOf(userToRemove.id); 92 | if (userIndex !== -1) { 93 | subUsers.splice(userIndex, 1); 94 | fs.writeFileSync(ticketDataPath, JSON.stringify(ticketData, null, 4)); 95 | } 96 | } 97 | 98 | const removedEmbed = new EmbedBuilder() 99 | .setTitle(msgconfig.RemoveUser.Removed_Title) 100 | .setDescription(msgconfig.RemoveUser.Removed_Message) 101 | .setColor(supportbot.Embed.Colours.Success); 102 | 103 | await interaction.reply({ 104 | embeds: [removedEmbed], 105 | ephemeral: true, 106 | }); 107 | 108 | const removedFromTicketEmbed = new EmbedBuilder() 109 | .setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title) 110 | .setDescription(msgconfig.RemoveUser.RemovedFromTicket_Description.replace( 111 | "%channel_link%", 112 | `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})` 113 | )) 114 | .setColor(supportbot.Embed.Colours.General); 115 | 116 | await userToRemove.send({ 117 | embeds: [removedFromTicketEmbed], 118 | }); 119 | 120 | } catch (err) { 121 | console.error("Error removing user from the ticket:", err); 122 | 123 | const errorEmbed = new EmbedBuilder() 124 | .setTitle(msgconfig.RemoveUser.Error_Title) 125 | .setDescription(msgconfig.RemoveUser.Error_Removing) 126 | .setColor(supportbot.Embed.Colours.Error); 127 | 128 | await interaction.reply({ 129 | embeds: [errorEmbed], 130 | ephemeral: true, 131 | }); 132 | } 133 | }, 134 | }); 135 | -------------------------------------------------------------------------------- /Commands/settings.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('../Structures/Addon.js'); 2 | const Discord = require("discord.js"); 3 | const fs = require("fs"); 4 | const yaml = require("js-yaml"); 5 | const path = require("path"); 6 | 7 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 8 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 9 | const settingsFilePath = path.resolve(__dirname, '../Data/settings.json'); 10 | 11 | function loadSettings() { 12 | try { 13 | if (fs.existsSync(settingsFilePath)) { 14 | const data = fs.readFileSync(settingsFilePath, 'utf8'); 15 | return JSON.parse(data); 16 | } else { 17 | return {}; 18 | } 19 | } catch (error) { 20 | console.error("Error reading settings file:", error); 21 | return {}; 22 | } 23 | } 24 | 25 | function saveSettings(settings) { 26 | try { 27 | fs.writeFileSync(settingsFilePath, JSON.stringify(settings, null, 2), 'utf8'); 28 | } catch (error) { 29 | console.error("Error writing settings file:", error); 30 | } 31 | } 32 | 33 | module.exports = new Command({ 34 | name: cmdconfig.Settings.Command, 35 | description: cmdconfig.Settings.Description, 36 | permissions: cmdconfig.Settings.Permission, 37 | options: [ 38 | { 39 | name: 'setstatus', 40 | type: Discord.ApplicationCommandOptionType.Subcommand, 41 | description: 'Set the bot status', 42 | options: [ 43 | { 44 | name: 'status', 45 | type: Discord.ApplicationCommandOptionType.String, 46 | description: 'Choose the bot status', 47 | required: true, 48 | choices: [ 49 | { name: 'Online 🟢', value: 'online' }, 50 | { name: 'Idle 🌙', value: 'idle' }, 51 | { name: 'Do Not Disturb ⛔', value: 'dnd' }, 52 | { name: 'Invisible 👻', value: 'invisible' } 53 | ], 54 | }, 55 | ], 56 | }, 57 | { 58 | name: 'setactivity', 59 | type: Discord.ApplicationCommandOptionType.Subcommand, 60 | description: 'Set the bot activity', 61 | options: [ 62 | { 63 | name: 'type', 64 | type: Discord.ApplicationCommandOptionType.String, 65 | description: 'Choose the activity type', 66 | required: true, 67 | choices: [ 68 | { name: 'Playing 🎮', value: 'PLAYING' }, 69 | { name: 'Watching 👀', value: 'WATCHING' }, 70 | { name: 'Listening 🎧', value: 'LISTENING' }, 71 | { name: 'Competing 🏆', value: 'COMPETING' } 72 | ], 73 | }, 74 | { 75 | name: 'message', 76 | type: Discord.ApplicationCommandOptionType.String, 77 | description: 'The activity message', 78 | required: true, 79 | }, 80 | ], 81 | }, 82 | { 83 | name: 'streammode', 84 | type: Discord.ApplicationCommandOptionType.Subcommand, 85 | description: 'Set the bot streaming status', 86 | options: [ 87 | { 88 | name: 'toggle', 89 | type: Discord.ApplicationCommandOptionType.String, 90 | description: 'Turn streaming mode on or off', 91 | required: true, 92 | choices: [ 93 | { name: 'On', value: 'on' }, 94 | { name: 'Off', value: 'off' } 95 | ], 96 | }, 97 | { 98 | name: 'twitchurl', 99 | type: Discord.ApplicationCommandOptionType.String, 100 | description: 'Twitch stream URL (Required if turning on)', 101 | required: false, 102 | }, 103 | ], 104 | }, 105 | { 106 | name: 'setnickname', 107 | type: Discord.ApplicationCommandOptionType.Subcommand, 108 | description: 'Set the bot nickname', 109 | options: [ 110 | { 111 | name: 'nickname', 112 | type: Discord.ApplicationCommandOptionType.String, 113 | description: 'The new nickname for the bot', 114 | required: true, 115 | }, 116 | ], 117 | }, 118 | ], 119 | 120 | async run(interaction) { 121 | const subcommand = interaction.options.getSubcommand(); 122 | const settings = loadSettings(); 123 | 124 | if (subcommand === 'setstatus') { 125 | const status = interaction.options.getString('status'); 126 | 127 | try { 128 | interaction.client.user.setPresence({ 129 | status: status, 130 | activities: interaction.client.user.presence.activities || [], 131 | }); 132 | 133 | settings.status = status; 134 | saveSettings(settings); 135 | 136 | const embed = new Discord.EmbedBuilder() 137 | .setDescription(`Status set to **${status.charAt(0).toUpperCase() + status.slice(1)}**`) 138 | .setColor(supportbot.Embed.Colours.General); 139 | 140 | return interaction.reply({ embeds: [embed], ephemeral: true }); 141 | } catch (error) { 142 | return interaction.reply({ content: `Failed to set status: ${error.message}`, ephemeral: true }); 143 | } 144 | } 145 | 146 | if (subcommand === 'setactivity') { 147 | const activityType = interaction.options.getString('type'); 148 | const activityMessage = interaction.options.getString('message'); 149 | 150 | try { 151 | interaction.client.user.setPresence({ 152 | activities: [{ name: activityMessage, type: Discord.ActivityType[activityType] }], 153 | status: interaction.client.user.presence.status || 'online', 154 | }); 155 | 156 | settings.activity = { type: activityType, message: activityMessage }; 157 | saveSettings(settings); 158 | 159 | const embed = new Discord.EmbedBuilder() 160 | .setDescription(`Activity set to **${activityType.toLowerCase()} ${activityMessage}**`) 161 | .setColor(supportbot.Embed.Colours.General); 162 | 163 | return interaction.reply({ embeds: [embed], ephemeral: true }); 164 | } catch (error) { 165 | return interaction.reply({ content: `Failed to set activity: ${error.message}`, ephemeral: true }); 166 | } 167 | } else if (subcommand === 'streammode') { 168 | const toggle = interaction.options.getString('toggle'); 169 | const twitchUrl = interaction.options.getString('twitchurl'); 170 | 171 | if (toggle === 'on') { 172 | if (!twitchUrl) { 173 | return interaction.reply({ content: 'Please provide a Twitch URL to stream.', ephemeral: true }); 174 | } 175 | 176 | try { 177 | interaction.client.user.setPresence({ 178 | activities: [{ name: 'Streaming', type: Discord.ActivityType.Streaming, url: twitchUrl }], 179 | status: 'online', 180 | }); 181 | 182 | settings.streaming = { active: true, url: twitchUrl }; 183 | saveSettings(settings); 184 | 185 | const embed = new Discord.EmbedBuilder() 186 | .setDescription(`**Streaming mode enabled!**\nStreaming: [Twitch](${twitchUrl})`) 187 | .setColor(supportbot.Embed.Colours.General); 188 | 189 | return interaction.reply({ embeds: [embed], ephemeral: true }); 190 | } catch (error) { 191 | return interaction.reply({ content: `Failed to enable streaming mode: ${error.message}`, ephemeral: true }); 192 | } 193 | } else if (toggle === 'off') { 194 | try { 195 | interaction.client.user.setPresence({ 196 | activities: interaction.client.user.presence.activities.filter(a => a.type !== Discord.ActivityType.Streaming), 197 | status: settings.status || 'online', 198 | }); 199 | 200 | settings.streaming = { active: false }; 201 | saveSettings(settings); 202 | 203 | const embed = new Discord.EmbedBuilder() 204 | .setDescription(`**Streaming mode disabled!**`) 205 | .setColor(supportbot.Embed.Colours.General); 206 | 207 | return interaction.reply({ embeds: [embed], ephemeral: true }); 208 | } catch (error) { 209 | return interaction.reply({ content: `Failed to disable streaming mode: ${error.message}`, ephemeral: true }); 210 | } 211 | } 212 | } else if (subcommand === 'setnickname') { 213 | const nickname = interaction.options.getString('nickname'); 214 | 215 | try { 216 | const guild = interaction.guild; 217 | const botMember = guild.members.cache.get(interaction.client.user.id); 218 | await botMember.setNickname(nickname); 219 | 220 | settings.nickname = nickname; 221 | saveSettings(settings); 222 | 223 | const embed = new Discord.EmbedBuilder() 224 | .setDescription(`Bot nickname set to **${nickname}**`) 225 | .setColor(supportbot.Embed.Colours.General); 226 | 227 | return interaction.reply({ embeds: [embed], ephemeral: true }); 228 | } catch (error) { 229 | return interaction.reply({ content: `Failed to set nickname: ${error.message}`, ephemeral: true }); 230 | } 231 | } else { 232 | return interaction.reply({ content: 'Invalid command usage.', ephemeral: true }); 233 | } 234 | }, 235 | }); -------------------------------------------------------------------------------- /Commands/suggest.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Discord = require("discord.js"); 3 | const yaml = require("js-yaml"); 4 | 5 | // Load configurations 6 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 7 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 8 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 9 | 10 | const Command = require("../Structures/Command.js"); 11 | 12 | // Ensure suggestions file exists and is correctly loaded 13 | let suggestions; 14 | try { 15 | suggestions = require("../Data/SuggestionData.json"); 16 | } catch (error) { 17 | suggestions = {}; 18 | } 19 | 20 | module.exports = new Command({ 21 | name: cmdconfig.Suggestion.Command, 22 | description: cmdconfig.Suggestion.Description, 23 | type: Discord.ApplicationCommandType.ChatInput, 24 | options: [ 25 | { 26 | name: "suggestion", 27 | description: "Create a Suggestion", 28 | type: Discord.ApplicationCommandOptionType.String, 29 | required: true, 30 | }, 31 | ], 32 | permissions: cmdconfig.Suggestion.Permission, 33 | 34 | async run(interaction) { 35 | const { getChannel } = interaction.client; 36 | 37 | const suggestChannel = await getChannel( 38 | supportbot.Suggestions.Channel, 39 | interaction.guild 40 | ); 41 | 42 | const NoChannel = new Discord.EmbedBuilder() 43 | .setTitle("Missing Channel!") 44 | .setDescription(msgconfig.Error.InvalidChannel) 45 | .setColor(supportbot.Embed.Colours.Error); 46 | 47 | if (!suggestChannel) return interaction.reply({ embeds: [NoChannel] }); 48 | 49 | let suggestion = interaction.options.getString("suggestion"); 50 | 51 | const SuggestEmbed = new Discord.EmbedBuilder() 52 | .addFields( 53 | { name: "Suggestion", value: suggestion, inline: true }, 54 | { name: "From", value: `<@${interaction.user.id}>` }, 55 | { name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, value: "0", inline: true }, 56 | { name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, value: "0", inline: true } 57 | ) 58 | .setThumbnail(interaction.user.displayAvatarURL()) 59 | .setFooter({ 60 | text: supportbot.Embed.Footer, 61 | iconURL: interaction.user.displayAvatarURL(), 62 | }) 63 | .setColor(supportbot.Embed.Colours.General); 64 | 65 | const UpvoteButton = new Discord.ButtonBuilder() 66 | .setCustomId("upvote") 67 | .setEmoji(supportbot.Suggestions.UpvoteEmoji) 68 | .setStyle(supportbot.Suggestions.Buttons.Upvote); 69 | 70 | const DownvoteButton = new Discord.ButtonBuilder() 71 | .setCustomId("downvote") 72 | .setEmoji(supportbot.Suggestions.DownvoteEmoji) 73 | .setStyle(supportbot.Suggestions.Buttons.Downvote); 74 | 75 | const RemoveVoteButton = new Discord.ButtonBuilder() 76 | .setCustomId("removevote") 77 | .setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title) 78 | .setStyle(supportbot.Suggestions.Buttons.RemoveVote); // Red button 79 | 80 | const row = new Discord.ActionRowBuilder().addComponents(UpvoteButton, DownvoteButton, RemoveVoteButton); 81 | 82 | const suggestionMsg = await suggestChannel.send({ embeds: [SuggestEmbed], components: [row] }); 83 | 84 | suggestions[suggestionMsg.id] = { 85 | suggestion: suggestion, 86 | author: interaction.user.id, 87 | upvotes: [], 88 | downvotes: [] 89 | }; 90 | 91 | fs.writeFileSync("./Data/SuggestionData.json", JSON.stringify(suggestions, null, 2)); 92 | 93 | if (supportbot.Suggestions.Threads.Enabled) { 94 | await suggestionMsg.startThread({ 95 | name: supportbot.Suggestions.Threads.Title, 96 | autoArchiveDuration: 60, 97 | type: Discord.ChannelType.PublicThread, 98 | reason: supportbot.Suggestions.Threads.Reason, 99 | }); 100 | } 101 | 102 | const Submitted = new Discord.EmbedBuilder() 103 | .setTitle(`${msgconfig.Suggestions.Sent_Title}`) 104 | .setDescription(`${msgconfig.Suggestions.Sent}`) 105 | .addFields( 106 | { name: "Sent to:", value: `<#${suggestChannel.id}>` }, 107 | ) 108 | .setColor(supportbot.Embed.Colours.Success); 109 | 110 | await interaction.reply({ 111 | ephemeral: true, 112 | embeds: [Submitted] 113 | }); 114 | }, 115 | }); 116 | -------------------------------------------------------------------------------- /Commands/ticket.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { 3 | EmbedBuilder, 4 | ApplicationCommandOptionType, 5 | ApplicationCommandType, 6 | ChannelType, 7 | ActionRowBuilder, 8 | ButtonBuilder, 9 | StringSelectMenuBuilder, 10 | StringSelectMenuOptionBuilder, 11 | ButtonStyle, 12 | MessageCollector, 13 | } = require("discord.js"); 14 | const yaml = require("js-yaml"); 15 | 16 | const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8")); 17 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 18 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 19 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 20 | 21 | const Command = require("../Structures/Command.js"); 22 | const TicketNumberID = require("../Structures/TicketID.js"); 23 | 24 | async function isBlacklisted(userId) { 25 | let blacklistData; 26 | try { 27 | blacklistData = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8")); 28 | } catch (error) { 29 | console.error("Error reading BlacklistedUsers.json:", error); 30 | return false; 31 | } 32 | 33 | if (!blacklistData || !Array.isArray(blacklistData.blacklistedUsers)) { 34 | console.error("blacklistedUsers is not defined or is not an array"); 35 | return false; 36 | } 37 | 38 | return blacklistData.blacklistedUsers.includes(userId); 39 | } 40 | 41 | async function getClockedInUsers(guild) { 42 | const clockedInUsers = new Set(); 43 | 44 | const profilesDir = './Data/Profiles'; 45 | const profileFiles = fs.readdirSync(profilesDir); 46 | 47 | for (const file of profileFiles) { 48 | const profileData = JSON.parse(fs.readFileSync(`${profilesDir}/${file}`, "utf8")); 49 | if (profileData.clockedIn) { 50 | const userId = file.replace('.json', ''); 51 | const member = await guild.members.fetch(userId); 52 | if (member) clockedInUsers.add(member.id); 53 | } 54 | } 55 | return clockedInUsers; 56 | } 57 | 58 | async function askTicketQuestions(ticketChannel, interaction, questions) { 59 | const answers = []; 60 | let questionIndex = 0; 61 | 62 | const sendNextQuestion = async () => { 63 | if (questionIndex < questions.length) { 64 | 65 | const questionsEmbed = new EmbedBuilder() 66 | .setDescription(`**${questionIndex + 1}.** ${questions[questionIndex]}`) 67 | .setColor(supportbot.Embed.Colours.General) 68 | 69 | const questionMessage = await ticketChannel.send({ 70 | embeds: [questionsEmbed], 71 | }); 72 | 73 | const collector = new MessageCollector(ticketChannel, { 74 | filter: (m) => m.author.id === interaction.user.id, 75 | max: 1, 76 | time: 60000, 77 | }); 78 | 79 | collector.on("collect", async (message) => { 80 | answers.push({ question: questions[questionIndex], answer: message.content }); 81 | questionIndex++; 82 | await message.delete(); 83 | await questionMessage.delete(); 84 | sendNextQuestion(); 85 | }); 86 | 87 | } else { 88 | 89 | const summaryEmbed = new EmbedBuilder() 90 | .setTitle(msgconfig.Ticket.TicketQuestions.Details.Title) 91 | .setDescription(msgconfig.Ticket.TicketQuestions.Details.Description.replace('%user%', interaction.user.id)) 92 | .setColor(supportbot.Embed.Colours.General) 93 | .addFields( 94 | answers.map((a) => ({ 95 | name: `Q: ${a.question}`, 96 | value: `A: ${a.answer}`, 97 | inline: false, 98 | })) 99 | ) 100 | .setTimestamp(); 101 | 102 | await ticketChannel.send({ 103 | embeds: [summaryEmbed] 104 | }); 105 | } 106 | }; 107 | 108 | // Start asking the first question 109 | sendNextQuestion(); 110 | } 111 | 112 | async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts = 0) { 113 | 114 | try { 115 | const clockedInUsers = await getClockedInUsers(interaction.guild); 116 | 117 | if (clockedInUsers.size === 0 || attempts >= 2) { 118 | const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel); 119 | if (claimChannel) { 120 | const claimEmbed = new EmbedBuilder() 121 | .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle) 122 | .setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage) 123 | .setColor(supportbot.Embed.Colours.General); 124 | 125 | const claimButton = new ButtonBuilder() 126 | .setCustomId(`claimticket-${ticketChannel.id}`) 127 | .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || 'Claim Ticket') 128 | .setStyle(ButtonStyle.Primary); 129 | 130 | const row = new ActionRowBuilder().addComponents(claimButton); 131 | 132 | const claimMessage = await claimChannel.send({ 133 | content: `<@&${Staff.id}> <@&${Admin.id}>`, 134 | embeds: [claimEmbed], 135 | components: [row], 136 | }); 137 | 138 | const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.member.roles.cache.has(Staff.id); 139 | const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 }); 140 | 141 | collector.on('collect', async i => { 142 | await ticketChannel.send({ content: `<@${i.user.id}> has claimed the ticket.` }); 143 | }); 144 | 145 | collector.on('end', async collected => { 146 | if (collected.size === 0) { 147 | await claimMessage.delete(); 148 | 149 | await claimChannel.send({ 150 | content: `Ticket is still unclaimed. \nCC: [<@&${Staff.id}> <@&${Admin.id}]`, 151 | embeds: [new EmbedBuilder().setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`).setColor(supportbot.Embed.Colours.Warn)], 152 | }); 153 | } 154 | }); 155 | 156 | return null; 157 | } else { 158 | console.error("Claim channel not found"); 159 | return null; 160 | } 161 | } 162 | 163 | const clockedInArray = Array.from(clockedInUsers); 164 | const assignedUserId = clockedInArray[Math.floor(Math.random() * clockedInArray.length)]; 165 | const assignedUser = await interaction.guild.members.fetch(assignedUserId); 166 | 167 | const claimEmbed = new EmbedBuilder() 168 | .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle) 169 | .setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage.replace('%user%', interaction.user.id)) 170 | .setColor(supportbot.Embed.Colours.General); 171 | 172 | const claimButton = new ButtonBuilder() 173 | .setCustomId(`claimticket-${ticketChannel.id}`) 174 | .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || 'Claim Ticket') 175 | .setStyle(ButtonStyle.Primary); 176 | 177 | const row = new ActionRowBuilder().addComponents(claimButton); 178 | 179 | const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel); 180 | if (!claimChannel) { 181 | console.error("Claim channel not found"); 182 | return null; 183 | } 184 | 185 | const claimMessage = await claimChannel.send({ 186 | content: `${assignedUser}`, 187 | embeds: [claimEmbed], 188 | components: [row], 189 | }); 190 | 191 | const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.user.id === assignedUserId; 192 | const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 }); 193 | 194 | collector.on('collect', async i => { 195 | await ticketChannel.send({ content: `<@${assignedUserId}>` }).then(msg => setTimeout(() => msg.delete(), 5000)); 196 | }); 197 | 198 | collector.on('end', async collected => { 199 | if (collected.size === 0) { 200 | await claimMessage.delete(); 201 | 202 | if (attempts >= 1) { 203 | await claimChannel.send({ 204 | content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`, 205 | embeds: [new EmbedBuilder().setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`).setColor(supportbot.Embed.Colours.Warn)], 206 | }); 207 | 208 | const claimEmbed = new EmbedBuilder() 209 | .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle) 210 | .setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage) 211 | .setColor(supportbot.Embed.Colours.General); 212 | 213 | const claimButton = new ButtonBuilder() 214 | .setCustomId(`claimticket-${ticketChannel.id}`) 215 | .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle) 216 | .setEmoji(supportbot.Ticket.ClaimTickets.ButtonEmoji) 217 | .setStyle(ButtonStyle.Primary); 218 | 219 | const row = new ActionRowBuilder().addComponents(claimButton); 220 | 221 | const claimMessage = await claimChannel.send({ 222 | embeds: [claimEmbed], 223 | components: [row], 224 | }); 225 | 226 | const filter = i => i.customId === `claimticket-${ticketChannel.id}` && i.member.roles.cache.has(Staff.id); 227 | const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 }); 228 | 229 | collector.on('collect', async i => { 230 | console.log(`Ticket ${ticketChannel.id} claimed by user ${i.user.id}`); 231 | await ticketChannel.member.add(i.user.id); 232 | }); 233 | 234 | collector.on('end', async collected => { 235 | if (collected.size === 0) { 236 | console.log(`No one claimed the ticket (${ticketChannel.id}), notifying all staff`); 237 | await claimMessage.delete(); 238 | 239 | await claimChannel.send({ 240 | content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`, 241 | }); 242 | } 243 | }); 244 | } else { 245 | const ReassigningEmbed = new EmbedBuilder() 246 | .setTitle(msgconfig.Ticket.ClaimTickets.ReassigningTitle) 247 | .setDescription(msgconfig.Ticket.ClaimTickets.ReassigningMessage.replace('%user%', assignedUserId)) 248 | .setColor(supportbot.Embed.Colours.General); 249 | 250 | await claimChannel.send({ embeds: [ReassigningEmbed], components: [] }); 251 | await assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts + 1); 252 | } 253 | } 254 | }); 255 | 256 | return assignedUserId; 257 | } catch (error) { 258 | console.error("Error in assignTicketToUser function:", error); 259 | if (error instanceof CombinedError) { 260 | console.error("CombinedError details:", error.errors); 261 | } 262 | return null; 263 | } 264 | } 265 | 266 | module.exports = new Command({ 267 | name: cmdconfig.OpenTicket.Command, 268 | description: cmdconfig.OpenTicket.Description, 269 | type: ApplicationCommandType.ChatInput, 270 | options: [ 271 | { 272 | name: "reason", 273 | description: "Ticket Reason", 274 | type: ApplicationCommandOptionType.String, 275 | }, 276 | ], 277 | permissions: cmdconfig.OpenTicket.Permission, 278 | 279 | async run(interaction) { 280 | 281 | try { 282 | 283 | if (await isBlacklisted(interaction.user.id)) { 284 | return interaction.reply({ 285 | content: msgconfig.Ticket.Blacklisted, 286 | ephemeral: true, 287 | }); 288 | } 289 | 290 | let TicketReason = null; 291 | if (supportbot.Ticket.TicketReason) { 292 | TicketReason = interaction.reason || null; // Retrieve the reason from the interaction object 293 | } 294 | 295 | let TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); 296 | const { getRole, getChannel } = interaction.client; 297 | let User = interaction.guild.members.cache.get(interaction.user.id); 298 | 299 | if ( 300 | supportbot.Ticket.TicketsPerUser && 301 | TicketData.tickets.filter((t) => t.user === interaction.user.id && t.open).length >= supportbot.Ticket.TicketsPerUser 302 | ) { 303 | return interaction.reply({ 304 | embeds: [ 305 | { 306 | title: "Too Many Tickets!", 307 | description: `You can't have more than ${supportbot.Ticket.TicketsPerUser} open tickets!`, 308 | color: supportbot.Embed.Colours.Warn, 309 | }, 310 | ], 311 | ephemeral: true, 312 | }); 313 | } 314 | 315 | let ticketNumberID = await TicketNumberID.pad(); 316 | const TicketSubject = TicketReason || msgconfig.Ticket.InvalidSubject; 317 | 318 | const TicketExists = new EmbedBuilder() 319 | .setTitle("Ticket Exists!") 320 | .setDescription(msgconfig.Ticket.TicketExists); 321 | 322 | if ( 323 | await interaction.guild.channels.cache.find( 324 | (ticketChannel) => ticketChannel.name === `${supportbot.Ticket.Channel}${ticketNumberID}` 325 | ) 326 | ) { 327 | return await interaction.reply({ 328 | embeds: [TicketExists], 329 | ephemeral: true, 330 | }); 331 | } 332 | 333 | const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); 334 | const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); 335 | if (!Staff || !Admin) 336 | return interaction.reply({ 337 | content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", 338 | ephemeral: true, 339 | }); 340 | 341 | let ticketChannel; 342 | 343 | if (supportbot.Ticket.TicketType === "threads") { 344 | const TicketHome = interaction.guild.channels.cache.find( 345 | (c) => c.name === supportbot.Ticket.TicketHome || c.id === supportbot.Ticket.TicketHome 346 | ); 347 | 348 | const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild); 349 | ticketChannel = await channel.threads.create({ 350 | name: `${supportbot.Ticket.Channel}${ticketNumberID}`, 351 | type: ChannelType.PrivateThread, 352 | parent: TicketHome, 353 | autoArchiveDuration: 60, 354 | reason: 'support ticket', 355 | }); 356 | } else if (supportbot.Ticket.TicketType === "channels") { 357 | const category = interaction.guild.channels.cache.find( 358 | (c) => c.name === supportbot.Ticket.TicketChannelsCategory || c.id === supportbot.Ticket.TicketChannelsCategory 359 | ); 360 | 361 | if (!category) { 362 | return interaction.reply({ 363 | content: "The ticket category does not exist!", 364 | ephemeral: true, 365 | }); 366 | } 367 | 368 | ticketChannel = await interaction.guild.channels.create({ 369 | name: `${supportbot.Ticket.Channel}${ticketNumberID}`, 370 | type: ChannelType.GuildText, 371 | parent: category.id, 372 | reason: 'support ticket', 373 | }); 374 | 375 | // Move to secondary category if the primary one is full 376 | if (category.children.size >= 50) { 377 | const secondaryCategory = interaction.guild.channels.cache.find( 378 | (c) => c.name === supportbot.Ticket.TicketChannelsCategory2 || c.id === supportbot.Ticket.TicketChannelsCategory2 379 | ); 380 | 381 | if (secondaryCategory) { 382 | await ticketChannel.setParent(secondaryCategory.id); 383 | } else { 384 | return interaction.reply({ 385 | content: "The secondary ticket category does not exist!", 386 | ephemeral: true, 387 | }); 388 | } 389 | } 390 | } 391 | 392 | // Assign ticket to a user if claiming tickets is enabled 393 | if (supportbot.Ticket.ClaimTickets.Enabled) { 394 | const assignedUserId = await assignTicketToUser(ticketChannel, Staff, Admin, interaction); 395 | if (!assignedUserId) { 396 | const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel); 397 | } 398 | } 399 | 400 | if (supportbot.Ticket.TicketType === "threads") { 401 | await ticketChannel.members.add(interaction.user.id); 402 | 403 | } else if (supportbot.Ticket.TicketType === "channels") { 404 | 405 | await ticketChannel.permissionOverwrites.create(interaction.user.id, { 406 | ViewChannel: true, 407 | SendMessages: true, 408 | ReadMessageHistory: true, 409 | }); 410 | 411 | await ticketChannel.permissionOverwrites.create(Admin, { 412 | ViewChannel: true, 413 | SendMessages: true, 414 | ReadMessageHistory: true, 415 | }); 416 | 417 | await ticketChannel.permissionOverwrites.create(interaction.guild.roles.everyone, { 418 | ViewChannel: false, 419 | }); 420 | } 421 | 422 | // Ask questions to the user after the ticket is opened 423 | if (supportbot.Ticket.Questions.Enabled) { 424 | const questions = supportbot.Ticket.Questions.List || []; 425 | if (questions.length > 0) { 426 | await askTicketQuestions(ticketChannel, interaction, questions); 427 | } 428 | } 429 | 430 | // Save ticket data 431 | TicketData.tickets.push({ 432 | id: ticketChannel.id, 433 | name: ticketChannel.name, 434 | user: interaction.user.id, 435 | number: ticketNumberID, 436 | reason: TicketSubject, 437 | open: true, 438 | subUsers: [], 439 | createdAt: new Date().toISOString(), 440 | claimedAt: null, 441 | }); 442 | 443 | fs.writeFileSync( 444 | "./Data/TicketData.json", 445 | JSON.stringify(TicketData, null, 4), 446 | (err) => { 447 | if (err) console.error(err); 448 | } 449 | ); 450 | 451 | // Notify the user that the ticket was created 452 | const CreatedTicket = new EmbedBuilder() 453 | .setDescription( 454 | msgconfig.Ticket.TicketCreatedAlert.replace(/%ticketauthor%/g, interaction.user.id) 455 | .replace(/%ticketid%/g, ticketChannel.id) 456 | .replace(/%ticketusername%/g, interaction.user.username) 457 | .replace(/%ticketreason%/g, TicketSubject) 458 | ) 459 | .setColor(supportbot.Embed.Colours.General); 460 | await interaction.reply({ embeds: [CreatedTicket], ephemeral: true }); 461 | 462 | // Create the ticket message 463 | const TicketMessage = new EmbedBuilder() 464 | .setAuthor({ 465 | name: msgconfig.Ticket.TicketAuthorTitle.replace(/%ticketauthor%/g, interaction.user.id) 466 | .replace(/%ticketid%/g, ticketChannel.id) 467 | .replace(/%ticketusername%/g, interaction.user.username), 468 | iconURL: interaction.user.displayAvatarURL(), 469 | }) 470 | .setTitle( 471 | msgconfig.Ticket.TicketTitle.replace(/%ticketauthor%/g, interaction.user.id) 472 | .replace(/%ticketid%/g, ticketChannel.id) 473 | .replace(/%ticketusername%/g, interaction.user.username) 474 | ) 475 | .setDescription( 476 | msgconfig.Ticket.TicketMessage.replace(/%ticketauthor%/g, interaction.user.id) 477 | .replace(/%ticketid%/g, ticketChannel.id) 478 | .replace(/%ticketusername%/g, interaction.user.username) 479 | .replace(/%ticketreason%/g, TicketSubject) 480 | ) 481 | .setColor(supportbot.Embed.Colours.General); 482 | 483 | if (supportbot.Ticket.TicketReason && TicketReason) { 484 | TicketMessage.addFields( 485 | { name: "Reason", value: TicketReason, inline: false } 486 | ); 487 | } 488 | 489 | // Create the control panel 490 | const SelectMenus = new StringSelectMenuBuilder() 491 | .setCustomId("ticketcontrolpanel") 492 | .setPlaceholder("Ticket Control Panel") 493 | .addOptions( 494 | new StringSelectMenuOptionBuilder() 495 | .setLabel("Close") 496 | .setDescription("Close the ticket.") 497 | .setEmoji(supportbot.SelectMenus.Tickets.CloseEmoji) 498 | .setValue("ticketclose"), 499 | new StringSelectMenuOptionBuilder() 500 | .setLabel("Voice Channel") 501 | .setDescription("Create a support voice channel.") 502 | .setEmoji(supportbot.SelectMenus.Tickets.VCEmoji) 503 | .setValue("supportvc"), 504 | new StringSelectMenuOptionBuilder() 505 | .setLabel("Archive") 506 | .setDescription("Archive the ticket.") 507 | .setEmoji(supportbot.SelectMenus.Tickets.ArchiveEmoji) 508 | .setValue("archiveticket"), 509 | ...(supportbot.Ticket.TicketType === "threads" ? [ 510 | new StringSelectMenuOptionBuilder() 511 | .setLabel("Lock Ticket") 512 | .setDescription("Lock the ticket.") 513 | .setEmoji(supportbot.SelectMenus.Tickets.LockEmoji) 514 | .setValue("lockticket") 515 | ] : []), 516 | new StringSelectMenuOptionBuilder() 517 | .setLabel("Rename Ticket") 518 | .setDescription("Rename the ticket.") 519 | .setEmoji(supportbot.SelectMenus.Tickets.RenameEmoji) 520 | .setValue("renameticket") 521 | ); 522 | 523 | if (supportbot.Ticket.TicketType === "threads") { 524 | SelectMenus.addOptions( 525 | new StringSelectMenuOptionBuilder() 526 | .setLabel("Enable Invites") 527 | .setDescription("Enable invites.") 528 | .setEmoji(supportbot.SelectMenus.Tickets.EnableInvitesEmoji) 529 | .setValue("enableinvites"), 530 | new StringSelectMenuOptionBuilder() 531 | .setLabel("Disable Invites") 532 | .setDescription("Disable invites.") 533 | .setEmoji(supportbot.SelectMenus.Tickets.DisableInvitesEmoji) 534 | .setValue("disableinvites"), 535 | ); 536 | } 537 | 538 | const row2 = new ActionRowBuilder().addComponents(SelectMenus); 539 | 540 | await ticketChannel.send({ 541 | embeds: [TicketMessage], 542 | components: [row2], 543 | }); 544 | 545 | 546 | } catch (error) { 547 | console.error("Error in run method:", error); 548 | } 549 | }, 550 | }); 551 | -------------------------------------------------------------------------------- /Commands/ticketpanel.js: -------------------------------------------------------------------------------- 1 | // ticketpanel.js 2 | const fs = require("fs"); 3 | const { 4 | EmbedBuilder, 5 | ApplicationCommandType, 6 | ActionRowBuilder, 7 | ButtonBuilder, 8 | } = require("discord.js"); 9 | const yaml = require("js-yaml"); 10 | 11 | const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8")); 12 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 13 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 14 | 15 | const Command = require("../Structures/Command.js"); 16 | 17 | module.exports = new Command({ 18 | name: cmdconfig.TicketPanel.Command, 19 | description: cmdconfig.TicketPanel.Description, 20 | type: ApplicationCommandType.ChatInput, 21 | options: [], 22 | permissions: cmdconfig.TicketPanel.Permission, 23 | 24 | async run(interaction) { 25 | if (!panelconfig.Panel) { 26 | console.log( 27 | "\u001b[31m", 28 | `[TICKET PANEL]`, 29 | "\u001b[33m", 30 | "Ticket Panel is not setup, You can set this in", `\u001b[31m`, '/Configs/ticket-panel.yml', `\n `, 31 | ); 32 | return interaction.reply({ 33 | content: "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.", 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | const { getChannel } = interaction.client; 39 | 40 | const channel = await getChannel( 41 | supportbot.Ticket.TicketHome, 42 | interaction.guild 43 | ); 44 | 45 | if (!channel) { 46 | console.log(`[TICKET PANEL] ${channelName} channel not found. Please check your config file.`); 47 | return interaction.reply({ 48 | content: `${channelName} channel not found. Please check your config file.`, 49 | ephemeral: true, 50 | }); 51 | } 52 | 53 | const panelid = JSON.parse(fs.readFileSync("./Data/ticket-panel-id.json", "utf8")); 54 | 55 | try { 56 | await channel.messages.fetch(panelid.TicketPanelID); 57 | return interaction.reply({ 58 | content: "Ticket panel message already exists.", 59 | ephemeral: true, 60 | }); 61 | } catch (error) { 62 | let embed = new EmbedBuilder() 63 | .setTitle(panelconfig.PanelTitle) 64 | .setColor(panelconfig.PanelColour) 65 | .setFooter({ 66 | text: supportbot.Embed.Footer, 67 | iconURL: interaction.user.displayAvatarURL(), 68 | }); 69 | 70 | if (panelconfig.TicketPanel_Description) { 71 | embed.setDescription(panelconfig.PanelMessage); 72 | } 73 | 74 | if (panelconfig.TicketPanel_Thumbnail) { 75 | embed.setThumbnail(panelconfig.PanelThumbnail); 76 | } 77 | 78 | if (panelconfig.TicketPanel_Image) { 79 | embed.setImage(panelconfig.PanelImage); 80 | } 81 | 82 | const createTicketButton = new ButtonBuilder() 83 | .setCustomId("createticket") 84 | .setLabel(panelconfig.Button.Text) 85 | .setEmoji(panelconfig.Button.Emoji) 86 | .setStyle(panelconfig.Button.Color); 87 | 88 | let row = new ActionRowBuilder().addComponents(createTicketButton); 89 | 90 | try { 91 | const message = await channel.send({ 92 | embeds: [embed], 93 | components: [row], 94 | }); 95 | 96 | let data = { 97 | id: panelid.id, 98 | TicketPanelID: message.id, 99 | }; 100 | 101 | fs.writeFileSync( 102 | "./Data/ticket-panel-id.json", 103 | JSON.stringify(data), 104 | "utf8" 105 | ); 106 | 107 | return interaction.reply({ 108 | content: "Ticket panel message has been sent!", 109 | ephemeral: true, 110 | }); 111 | } catch (e) { 112 | console.log("Error sending message:", e); 113 | return interaction.reply({ 114 | content: "An error occurred while sending the ticket panel message.", 115 | ephemeral: true, 116 | }); 117 | } 118 | } 119 | }, 120 | }); -------------------------------------------------------------------------------- /Commands/translate.js: -------------------------------------------------------------------------------- 1 | // SupportBot | Emerald Services 2 | // Translate Command 3 | 4 | const fs = require("fs"); 5 | 6 | const Discord = require("discord.js"); 7 | const yaml = require("js-yaml"); 8 | 9 | const supportbot = yaml.load( 10 | fs.readFileSync("./Configs/supportbot.yml", "utf8") 11 | ); 12 | 13 | const cmdconfig = yaml.load( 14 | fs.readFileSync("./Configs/commands.yml", "utf8") 15 | ); 16 | 17 | const msgconfig = yaml.load( 18 | fs.readFileSync("./Configs/messages.yml", "utf8") 19 | ) 20 | 21 | const Command = require("../Structures/Command.js"); 22 | const translate = require("@vitalets/google-translate-api"); 23 | 24 | module.exports = new Command({ 25 | name: cmdconfig.Translate.Command, 26 | description: cmdconfig.Translate.Description, 27 | type: Discord.ApplicationCommandType.ChatInput, 28 | options: [ 29 | { 30 | type: Discord.ApplicationCommandOptionType.String, 31 | name: "language", 32 | description: "A 2 letter language code", 33 | required: true, 34 | }, 35 | { 36 | type: Discord.ApplicationCommandOptionType.String, 37 | name: "text", 38 | description: "The text to translate", 39 | required: true, 40 | }, // The input text for the translation 41 | ], 42 | permissions: cmdconfig.Translate.Permission, // The permission the user/role at least requires 43 | 44 | async run(interaction) { 45 | let disableCommand = true; 46 | 47 | const { getRole } = interaction.client; 48 | let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); 49 | let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); 50 | if (!SupportStaff || !Admin) 51 | 52 | return interaction.reply( 53 | "Some roles seem to be missing!\nPlease check for errors when starting the bot." 54 | ); 55 | 56 | const NoPerms = new Discord.EmbedBuilder() 57 | .setTitle("Invalid Permissions!") 58 | .setDescription( 59 | `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\`` 60 | ) 61 | .setColor(supportbot.Embed.Colours.Warn); 62 | 63 | if ( 64 | !interaction.member.roles.cache.has(SupportStaff.id) && 65 | !interaction.member.roles.cache.has(Admin.id) 66 | ) 67 | return interaction.reply({ embeds: [NoPerms] }); 68 | 69 | const { getChannel } = interaction.client; 70 | let lang = interaction.options.getString("language"); // Grab choice of language code by user 71 | let url = "https://www.science.co.il/language/Codes.php"; // URL to all available language codes 72 | let text = await interaction.options.getString("text"); // Grab the text to translate by the user 73 | let translatelog = await getChannel( 74 | supportbot.Translate.TranslateLog, 75 | interaction.guild 76 | ); // Grab the set logging channel for translations 77 | 78 | 79 | // Return message if user has provided an invalid language code 80 | const result = await translate(text, { to: lang }).catch(async (err) => { 81 | if (err && err.code == 400) { 82 | await interaction.reply({ 83 | embeds: [ 84 | new Discord.EmbedBuilder() 85 | .setColor(supportbot.Embed.Colours.Warn) 86 | .setTitle("Valid Language Codes") 87 | .setURL(url) 88 | .setDescription( 89 | "Click the title to see which 2 letter language codes are valid." 90 | ), 91 | ], 92 | ephemeral: true, 93 | }); 94 | } 95 | }); 96 | if (!result) return; 97 | 98 | // Embed containing language code, original text and translated text 99 | let transembed = new Discord.EmbedBuilder() 100 | .setAuthor({ name: interaction.user.tag, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) }) 101 | .setColor(supportbot.Embed.Colours.Success) 102 | .setDescription(`**Translation to ${lang}**`) 103 | .addFields( 104 | { name: "Original text", value: text }, 105 | { name: "Translated text", value: result.text }, 106 | ) 107 | .setFooter({ text: `Author: ${interaction.user.id}` }) 108 | .setTimestamp(); 109 | 110 | // Send embed [transembed] with translated message to channel 111 | await interaction.reply({ embeds: [transembed] }); 112 | 113 | // Send embed [transembed] to logging channel 114 | await translatelog.send({ embeds: [transembed] }); 115 | }, 116 | 117 | }) -------------------------------------------------------------------------------- /Configs/commands.yml: -------------------------------------------------------------------------------- 1 | # ___ _ ___ _ 2 | # / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ 3 | # \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | 4 | # <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| 5 | # |_| |_| 6 | # 7 | # SupportBot created by Emerald Services 8 | # Installed with MIT License 9 | # 10 | # Discord Support: https://discord.gg/emeraldsrv 11 | # Official Documentation: https://docs.emeraldsrv.com 12 | # Addons Marketplace: https://community.emeraldsrv.com 13 | # 14 | # Commands Configuration (commands.yml) 15 | 16 | Help: 17 | Command: "help" 18 | Description: "Get a list of all the commands." 19 | Permission: ["SendMessages"] 20 | Enabled: true 21 | 22 | Info: 23 | Command: "info" 24 | Description: "Get information about the **ServerName**." 25 | Permission: ["SendMessages"] 26 | Enabled: true 27 | 28 | TicketPanel: 29 | Command: "ticketpanel" 30 | Description: "Create the ticket creation panel" 31 | Permission: ["Administrator"] 32 | Enabled: true 33 | 34 | Profile: 35 | Command: "profile" 36 | Description: "Shows the profile for a discord member." 37 | Permission: ["SendMessages"] 38 | # Command is automatically disabled when the TicketClaims is disabled. 39 | 40 | OpenTicket: 41 | Command: "ticket" 42 | Description: "Create a support ticket." 43 | Permission: ["SendMessages"] 44 | Enabled: true 45 | 46 | AddUser: 47 | Command: "add" 48 | Description: "Add a user to a support ticket." 49 | Permission: ["SendMessages"] 50 | Enabled: true 51 | 52 | ForceAddUser: 53 | Command: "forceadd" 54 | Description: "Add a user to a support ticket by bypassing the invite system." 55 | Permission: ["SendMessages"] 56 | Enabled: true 57 | 58 | RemoveUser: 59 | Command: "remove" 60 | Description: "Remove a user from a support ticket." 61 | Permission: ["SendMessages"] 62 | Enabled: true 63 | 64 | CloseTicket: 65 | Command: "close" 66 | Description: "Close a support ticket" 67 | Permission: ["SendMessages"] 68 | Enabled: true 69 | 70 | Suggestion: 71 | Command: "suggest" 72 | Description: "Create a suggestion." 73 | Permission: ["SendMessages"] 74 | Enabled: true 75 | 76 | Embed: 77 | Command: "embed" 78 | Description: "Build a discord embed." 79 | Permission: ["SendMessages"] 80 | Enabled: true 81 | 82 | Translate: 83 | Command: "translate" 84 | Description: "Translate something to english" 85 | Permission: ["SendMessages"] 86 | Enabled: false 87 | 88 | UserInfo: 89 | Command: "userinfo" 90 | Description: "Get info about a user." 91 | Permission: ["SendMessages"] 92 | Enabled: true 93 | 94 | Ping: 95 | Command: "ping" 96 | Description: "Pong." 97 | Permission: ["SendMessages"] 98 | Enabled: true 99 | 100 | # --------------------------------------- 101 | # Bot Moderator Commands 102 | # --------------------------------------- 103 | 104 | Mod: 105 | Command: "mod" 106 | Description: "Execute a moderation action." 107 | Permission: ["BanMembers", "KickMembers"] 108 | Enabled: true 109 | 110 | TicketBlacklist: 111 | Command: "ticket-blacklist" 112 | Description: "Blacklist a user from creating Tickets." 113 | 114 | Add: 115 | Command: "add" 116 | Description: "Add a user to the blacklist." 117 | 118 | Remove: 119 | Command: "remove" 120 | Description: "Remove a user from the blacklist." 121 | 122 | View: 123 | Command: "view" 124 | Description: "View all blacklisted users." 125 | 126 | # --------------------------------------- 127 | # Bot Administrator Commands 128 | # --------------------------------------- 129 | 130 | Settings: 131 | Command: "settings" 132 | Description: "Update bot settings." 133 | Permission: ["Administrator"] 134 | Enabled: true 135 | 136 | # --------------------------------------- 137 | # Command Description Configuration 138 | # --------------------------------------- 139 | 140 | BlacklistDesc: "Blacklist a user from creating Tickets." 141 | TicketAddDesc: "Add a user to a support ticket." 142 | TicketRemoveDesc: "Remove a user from a support ticket." 143 | TranslateCommandDesc: "Translates the language provided, along with the abbreviation (2 char) long language code" 144 | OpenTicketDesc: "Open a new support ticket." 145 | CloseTicketDesc: "Close your support ticket." 146 | InfoCommandDesc: "Get information about the brand." 147 | PingCommandDesc: "Pong!" 148 | HelpCommandDesc: "Get a list of all the commands." 149 | EmbedCommandDesc: "Send a message as an embed." 150 | SuggestCommandDesc: "Suggest a feature for the server." 151 | UserInfoCommandDesc: "Get info about a user." 152 | -------------------------------------------------------------------------------- /Configs/messages.yml: -------------------------------------------------------------------------------- 1 | # ___ _ ___ _ 2 | # / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ 3 | # \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | 4 | # <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| 5 | # |_| |_| 6 | # 7 | # SupportBot created by Emerald Services 8 | # Installed with MIT License 9 | # 10 | # Discord Support: https://discord.gg/emeraldsrv 11 | # Official Documentation: https://docs.emeraldsrv.com 12 | # Addons Marketplace: https://community.emeraldsrv.com 13 | # 14 | # Messages Configuration (messages.yml) 15 | 16 | # ---------------------------- 17 | # Info Command 18 | # ---------------------------- 19 | 20 | Info: 21 | Title: "About Us" 22 | Description: "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand." 23 | Button: "Visit Site" 24 | URL: "https://docs.emeraldsrv.com" 25 | Colour: "#f74343" 26 | 27 | # ---------------------------- 28 | # Welcome/Leave Messages 29 | # ---------------------------- 30 | 31 | Welcome: 32 | Embed: 33 | Colour: "#f74343" 34 | Title: "Welcome to ServerName" 35 | Message: ":smile: %joined_user% has just landed!" 36 | Thumbnail: "BOT" 37 | 38 | ImageEnabled: false 39 | ImageURL: "https://animagelink.png/" 40 | 41 | Leave: 42 | Embed: 43 | Colour: "#f74343" 44 | Title: "Goodbye!" 45 | Message: ":slight_frown: %joined_user% has recently dropped out!" 46 | Thumbnail: "BOT" 47 | 48 | ImageEnabled: false 49 | ImageURL: "https://animagelink.png/" 50 | 51 | # ---------------------------- 52 | # Suggestion Messages 53 | # ---------------------------- 54 | 55 | Suggestions: 56 | Sent_Title: "Suggestion Sent!" 57 | Sent: ":white_check_mark: **You have successfully sent a suggestion.**" 58 | NoSelfVote: ":x: **You cannot vote on your own suggestion.**" 59 | RemoveVoteButton: "Remove Vote" 60 | 61 | # ---------------------------- 62 | # Ticket Messages 63 | # ---------------------------- 64 | 65 | Ticket: 66 | 67 | # Available Placeholders: 68 | 69 | # %ticketusername% - User who created the ticket. 70 | # <@%ticketauthor%> - Tags the user who created the ticket. 71 | # <#%ticketid%> - Mentions the ticket channel. 72 | 73 | TicketAuthorTitle: "%ticketusername%'s Ticket" 74 | TicketTitle: "Support Ticket" 75 | TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you." 76 | TicketThumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png" 77 | TicketCreatedAlert: ":ticket: Ticked Created: <#%ticketid%>" 78 | TicketExists: "❌ Ticket already exists" 79 | TicketClaim: ":white_check_mark: Ticket claimed by" #The user who claimed the ticket will be added to the end of this message 80 | TicketUnclaim: ":white_check_mark: Ticket unclaimed by" #The user who claimed the ticket will be added to the end of this message 81 | 82 | TicketArchived: ":white_check_mark: Ticket archived." 83 | TicketUnarchived: ":white_check_mark: Ticket unarchived." 84 | TicketLocked: ":lock: Ticket is locked." 85 | DisableInvites: "🔒 Invites have been disabled for this ticket." 86 | EnableInvites: "🔓 Invites have been enabled for this ticket." 87 | 88 | # Available Placeholders: 89 | 90 | # <@%user%> - Tags the user who was added to the ticket. 91 | 92 | Blacklisted: ":x: You are not allowed to open a ticket" 93 | Muted: ":x: You are `Muted` and therefore not allowed to open any tickets" 94 | InvalidSubject: "No Reason Provided!" 95 | 96 | ConfirmClose: "Please confirm closing the ticket by clicking the button below." 97 | CloseTimeout: "Ticket close confirmation timed out." 98 | 99 | RenamedSuccess: "🏷️ Ticket renamed successfully!" 100 | 101 | ClaimTickets: 102 | ClaimTitle: "Incoming Support Request!" 103 | ClaimMessage: "A new ticket has been created by <@!%user%>. Click the button below to claim this ticket." 104 | ClaimMessage_Edit: "<#%channel%> was claimed by <@!%user%>." 105 | 106 | ClaimedTitle: "Ticket Claimed!" 107 | Claimed: "You have successfully claimed this ticket. <#%channel%>" 108 | NoPermsToClaimTitle: "Error, no permissions!" 109 | NoPermsToClaim: ":x: **Err!** You do not permissions to claim this ticket." 110 | 111 | ReassigningTitle: "Reassigning!..." 112 | ReassigningMessage: "<@%user%> did not claim the ticket. **Reassigning...**" 113 | 114 | TicketQuestions: 115 | Details: 116 | Title: "Ticket Details" 117 | Description: "Questions by <@%user%>" # Use <@%user%> to display the username. 118 | 119 | # ---------------------------- 120 | # Review Messages 121 | # ---------------------------- 122 | 123 | ReviewSystem: 124 | Rate: 125 | Title: "Review us!" 126 | Description: "Please rate our support for this ticket (1-5 stars)." 127 | 128 | Stars: 129 | One: "⭐ 1 Star" 130 | Two: "⭐⭐ 2 Stars" 131 | Three: "⭐⭐⭐ 3 Stars" 132 | Four: "⭐⭐⭐⭐ 4 Stars" 133 | Five: "⭐⭐⭐⭐⭐ 5 Stars" 134 | 135 | Comment: 136 | Title: "Write a comment." 137 | Description: "Write a comment about your experience." 138 | 139 | ReviewEmbed: 140 | Title: "Ticket Review" 141 | CommentTitle: "Comment" 142 | RatingTitle: "Rating" 143 | ReviewedStaffTitle: "Reviewed Staff:" 144 | ReviewedByTitle: "Reviewed By:" 145 | ReviewEmoji: "⭐" 146 | Color: "#f7a943" 147 | 148 | # ---------------------------- 149 | # Profile Messages 150 | # ---------------------------- 151 | 152 | TicketStats: 153 | OpenTickets: "✅ Open Tickets" 154 | TotalTickets: "🎫 Total Tickets" 155 | ResponseTime: "⏱️ Average Response Time" 156 | 157 | Title: "Your Ticket Stats" 158 | 159 | # ---------------------------- 160 | # Mod Command Messages 161 | # ---------------------------- 162 | 163 | Mod: 164 | 165 | TicketBlacklist: 166 | Add: 167 | Description: "Add a user to the ticket blacklist" 168 | UserDescription: "User to blacklist" 169 | AlreadyBlacklisted: ":x: {userTag} is already blacklisted." 170 | Success: ":white_check_mark: {userTag} has been added to the blacklist. They can no longer use the ticket system." 171 | 172 | Remove: 173 | Description: "Remove a user from the ticket blacklist" 174 | UserDescription: "User to remove from blacklist" 175 | NotBlacklisted: ":x: {userTag} is not blacklisted." 176 | Success: ":white_check_mark: {userTag} has been removed from the blacklist. They can now use the ticket system again." 177 | 178 | View: 179 | Description: "View the list of all blacklisted users" 180 | NoBlacklistedUsers: ":information_source: There are currently no users on the blacklist." 181 | 182 | 183 | # ---------------------------- 184 | # Logging Messages 185 | # ---------------------------- 186 | 187 | TicketLog: 188 | Title: "Ticket Log" 189 | Colour: "#0cc559" 190 | 191 | TicketBlacklistLog: 192 | Title: "Ticket Blacklist Log" 193 | Colour: "#0cc559" 194 | 195 | # ---------------------------- 196 | # Misc Messages 197 | # ---------------------------- 198 | 199 | AddUser: 200 | NotInTicket_Title: "Ticket Invitation" 201 | NotInTicket_Description: "This command can only be used within a ticket thread." 202 | 203 | Invite_Title: "User Invited" 204 | Invite_Description: "%username% has been invited to join the ticket." 205 | 206 | DM_Title: "Ticket Invitation" 207 | DM_Description: "You have been invited to join the ticket %channelname%. Do you accept?" 208 | 209 | Accept_Label: "Accept" 210 | Reject_Label: "Reject" 211 | ViewTicket_Label: "Go to Ticket" 212 | 213 | Accepted_Title: "Invitation Accepted" 214 | Accepted_Message: "You have been added to the ticket." 215 | AddedToTicket_Title: "Added to Ticket" 216 | AddedToTicket_Description: "You have been added to the ticket %channel_link%." 217 | 218 | # Added Message for inside the ticket, this informs everyone inside the ticket that the user has been added. 219 | Added_Title: "User Added" 220 | Added_Description: "Successfully added **%username%** to the ticket." 221 | 222 | Declined_Title: "Invitation Declined" 223 | Declined_Message: "You have declined the invitation." 224 | 225 | NoResponse_Title: "No Response" 226 | NoResponse_Message: "You did not respond in time. Please contact staff if you wish to join the ticket." 227 | 228 | Error_Title: "Error" 229 | Error_Adding: "There was an error adding you to the ticket. Please try again." 230 | 231 | DM_Error_Title: "Error" 232 | DM_Error_Description: "Unable to send a DM to %username%. Please ensure your DMs are enabled." 233 | 234 | Unexpected_Error_Title: "Error" 235 | Unexpected_Error_Description: "An unexpected error occurred: %error%" 236 | 237 | RemoveUser: 238 | NotInTicket_Title: "Error" 239 | NotInTicket_Description: "This command can only be used in ticket threads." 240 | 241 | Removed_Title: "User Removed" 242 | Removed_Message: "The user has been successfully removed from the ticket." 243 | 244 | RemovedFromTicket_Title: "Removed from Ticket" 245 | RemovedFromTicket_Description: "You have been removed from the ticket channel %channel_link%." 246 | 247 | Error_Title: "Error" 248 | Error_Removing: "There was an error removing the user from the ticket." 249 | 250 | ForceAddUser: 251 | NotInTicket_Title: "Not a Ticket" 252 | NotInTicket_Description: "This command can only be used within a ticket thread." 253 | 254 | Added_Title: "User Added" 255 | Added_Description: "Successfully added %username% to the ticket." 256 | 257 | AddedToTicket_Title: "You have been added to a Ticket" 258 | AddedToTicket_Description: "You have been added to the ticket channel [%channel_link%]." 259 | 260 | Error_Title: "Error" 261 | Error_Adding: "There was an error adding the user to the ticket." 262 | 263 | Error: 264 | IncorrectPermissions: ":x: **Error!** You do not have the correct permissions to use this command." 265 | MissingChannel: ":x: **Error!** Required channel not found. Please ensure it is set up correctly!" 266 | UserNotFound: ":x: **Error!** Specified user not found. Please check if they are in this server." 267 | CommandNotAllowedHere: ":warning: This command cannot be used in this channel." 268 | EntryAlreadyExists: ":warning: Cannot complete the action, as the entry already exists." 269 | EntryDoesNotExist: ":warning: Cannot complete the action, as the entry does not exist." 270 | ActionFailed: ":x: **Error!** An error occurred while processing your request. Please try again later." 271 | 272 | 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /Configs/supportbot-ai.yml: -------------------------------------------------------------------------------- 1 | # ------------------------- 2 | # SupportBot AI 3 | # 4 | # In order to use the SupportBot AI, You need to setup an OpenAI Account. 5 | # this can be done at https://platform.openai.com/ 6 | # ------------------------- 7 | 8 | Enabled: true 9 | 10 | General: 11 | Name: "SupportBot AI" 12 | # What would you like to call your AI 13 | Model: "gpt-4o-mini" 14 | # You can see a list of OpenAI Models here: https://platform.openai.com/docs/models 15 | 16 | Tokens: 400 17 | # Max tokens you can set is 400 18 | # For more information about tokens, go to https://openai.com/api/pricing/ 19 | OpenAI_Key: "OPEN_AI_API_KEY" 20 | # To create an OpenAI API key, go to https://platform.openai.com/api-keys 21 | 22 | 23 | PastebinAPI_Key: "PASTEBIN_API_KEY" 24 | # You can get your Pastebin API Key from https://pastebin.com 25 | PastebinAPI_URL: "https://pastebin.com/api/api_post.php" 26 | 27 | Channels: 28 | AIChannel: "AI_BOT_CHANNEL" 29 | # This is the channel where AI will respond in without needing to mention the bot. 30 | 31 | Embed: 32 | Color: 0x0cc559 # 0x 33 | Footer: "SupportBot | Developed by Emerald Development" 34 | 35 | Messages: 36 | MessageTooLong: "The response was too long, so I've posted it here: {pastebin}" 37 | ErrorResponse: "I didn't receive a proper response from OpenAI. Please try again later." 38 | OpenAIError: "I'm having some trouble talking to SupportBot AI's Model, OpenAI. Please try again later." 39 | AIDisabled: "I am disabled, You have enable me in `supportbot-ai.yml`" -------------------------------------------------------------------------------- /Configs/supportbot.yml: -------------------------------------------------------------------------------- 1 | # ___ _ ___ _ 2 | # / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ 3 | # \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | 4 | # <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| 5 | # |_| |_| 6 | # 7 | # SupportBot created by Emerald Services 8 | # Installed with MIT License 9 | # 10 | # Discord Support: https://discord.gg/emeraldsrv 11 | # Official Documentation: https://docs.emeraldsrv.com 12 | # Addons Marketplace: https://community.emeraldsrv.com 13 | # 14 | # SupportBot Configuration (supportbot.yml) 15 | 16 | SupportBot_Version: "v8.3" 17 | 18 | # ------------------------- 19 | # General 20 | # ------------------------- 21 | 22 | General: 23 | Name: "SupportBot" 24 | Token: "BOT_TOKEN" 25 | 26 | Addons: 27 | Enabled: true 28 | 29 | # ------------------------- 30 | # Activity 31 | # ------------------------- 32 | 33 | Activity: 34 | Status: "SupportBot v8" 35 | Type: "Competing" 36 | StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME" 37 | 38 | # Available Types: 39 | # Playing 40 | # Watching 41 | # Listening 42 | # Competing 43 | # Streaming (URL required) 44 | 45 | # You can also update the activity via Discord using: /settings 46 | 47 | # ------------------------- 48 | # Embeds 49 | # ------------------------- 50 | 51 | Embed: 52 | Colours: 53 | General: 0x0cc559 54 | Success: 0x04b545 55 | Error: 0xd13030 56 | Warn: 0xfca117 57 | 58 | Footer: "SupportBot | Developed by Emerald Development" 59 | 60 | # Use 0x to change the colour of the embeds. 61 | 62 | # ------------------------- 63 | # Roles 64 | # ------------------------- 65 | 66 | Roles: 67 | AutoRole: 68 | Enabled: true 69 | Role: "ROLE_ID" 70 | 71 | StaffMember: 72 | Staff: "ROLE_ID" 73 | Admin: "ROLE_ID" 74 | Moderator: "ROLE_ID" 75 | 76 | Mod: 77 | AllowSupportStaff: false 78 | 79 | # Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml) 80 | 81 | # ---------------------------- 82 | # System Messages 83 | # ---------------------------- 84 | 85 | Welcome: 86 | Enabled: true 87 | Channel: "JOIN_CHANNEL_ID" 88 | 89 | Leave: 90 | Enabled: true 91 | Channel: "JOIN_CHANNEL_ID" 92 | 93 | # ---------------------------- 94 | # Suggestions 95 | # ---------------------------- 96 | 97 | Suggestions: 98 | Channel: "SUGGESTIONS_CHANNEL_ID" 99 | UpvoteEmoji: "✅" 100 | DownvoteEmoji: "❌" 101 | 102 | UpvoteTitle: "Yes" 103 | DownvoteTitle: "No" 104 | 105 | OwnSuggestion: true 106 | 107 | # If false this will not allow the user to vote for a suggestion they created 108 | # If true this will allow the user to vote for a suggestion they created 109 | 110 | ShowUsers: true 111 | 112 | Threads: 113 | Enabled: true 114 | Title: "Suggestion-releated Thread" 115 | Reason: "A thread releted to this suggestion" 116 | 117 | Buttons: 118 | Upvote: "2" 119 | Downvote: "2" 120 | RemoveVote: "4" 121 | RemoveVote_Title: "Remove Vote" 122 | 123 | # Available Button Styles 124 | # 1 - Blurple 125 | # 2 - Grey 126 | # 3 - Green 127 | # 4 - Red 128 | 129 | # ------------------------- 130 | # Tickets 131 | # ------------------------- 132 | 133 | VoiceTickets: 134 | Name: "%username%'s VC" 135 | Category: "VOICEE_CATEGORY_ID" 136 | 137 | Ticket: 138 | TicketType: "channels" 139 | 140 | # Available TicketTypes: 141 | # channels - Tickets will be created as channels in the TicketChannelsCategory 142 | # threads - Tickets will be created as threads through the "TicketHome" channel. 143 | 144 | TicketHome: "TICKETHOME_CHANNEL_ID" 145 | 146 | # What is TicketHome? 147 | # - Channel where the Ticket creation panel is sent. 148 | # - Channel where ticket threads are made. (only if ticket threads is enabled above.) 149 | 150 | # For TicketType: channels 151 | TicketChannelsCategory: "TICKETCHANNEL_CATEGORY_ID" 152 | TicketChannelsCategory2: "TICKETCHANNEL_BACKUP_CATEGORY_ID" # This will be used if the first category has reached the max channel limit. 153 | TicketArchiveCategory: "ARCHIVE_CATEGORY_ID" 154 | # --- 155 | 156 | Channel: "ticket-" # The prefix for ticket channel names 157 | TicketsPerUser: 100 158 | DMTranscripts: true 159 | TicketReason: true 160 | # If ticket reason is set to false, It will not prompt to create a reason 161 | 162 | TicketSubject: "description" 163 | 164 | # Available TicketSubjects: 165 | # "description" will display the ticket reason within the channel description 166 | # "embed" will display the ticket reason within the ticket embed 167 | 168 | Timeout: 5 169 | # Set to true to allow all staff to access the ticket 170 | 171 | Close: 172 | StaffOnly: true 173 | # Set to false is anyone is able to close a ticket. 174 | 175 | Confirmation_Button: "Confirm Close" 176 | Confirmation_Emoji: "🗑️" 177 | Confirmation_Style: "2" 178 | 179 | Title: "Are you sure you want to close this ticket?" 180 | 181 | # Available Button Styles: 182 | # 1 - Blurple 183 | # 2 - Grey 184 | # 3 - Green 185 | # 4 - Red 186 | 187 | Invites: 188 | StaffOnly: true 189 | 190 | ClaimTickets: 191 | Enabled: true 192 | Channel: "CLAIM_TICKETS_CHANNEL_ID" 193 | # Channel where the ticket assignments are sent for staff to claim. 194 | 195 | ClaimAcceptTime: 120000 196 | # How long the assigned user has to accept the ticket before it gets re-assigned to someone else. 197 | # 120000 = 2 Minutes 198 | # 180000 = 3 Minutes 199 | # 240000 = 4 Minutes 200 | # 300000 = 5 Minutes 201 | # Time in Mliseconds 202 | 203 | ButtonTitle: "Claim Ticket" 204 | ButtonEmoji: "🎫" 205 | Button: "1" 206 | 207 | # Available Button Styles 208 | # 1 - Blurple 209 | # 2 - Grey 210 | # 3 - Green 211 | # 4 - Red 212 | 213 | ReviewSystem: 214 | Enabled: true 215 | Channel: "REVIEW_CHANNEL_ID" 216 | UseModal: true 217 | 218 | Questions: 219 | Enabled: true # Set to false to disable ticket questions 220 | List: 221 | - "Please provide your account email?" 222 | - "What is the nature of your issue?" 223 | - "Have you tried resolving the issue yourself?" 224 | 225 | Log: 226 | TicketDataLog: "TICKET_DATA_LOG_ID" 227 | TickeDataTitle: "Ticket Data" 228 | TicketBlacklistLog: "TICKET_BLACKLIST_LOG_ID" 229 | 230 | # ------------------------- 231 | # Buttons 232 | # ------------------------- 233 | Buttons: 234 | General: 235 | Delete: "🗑️" 236 | DeleteStyle: "4" 237 | 238 | Voice: 239 | TicketDeleteText: "Delete Voice Channel" 240 | 241 | Tickets: 242 | ClaimEmoji: "🧑‍💻" 243 | ClaimStyle: "1" 244 | 245 | Unarchive: "Unarchive Ticket" 246 | Unarchive_Emoji: "♻️" 247 | Unarchive_Style: "2" 248 | 249 | Unlock: "Unlock Ticket" 250 | Unlock_Emoji: "🔓" 251 | Unlock_Style: "2" 252 | 253 | # Available Button Styles 254 | # 1 - Blurple 255 | # 2 - Grey 256 | # 3 - Green 257 | # 4 - Red 258 | 259 | # ------------------------- 260 | # Select Menus 261 | # ------------------------- 262 | 263 | SelectMenus: 264 | Tickets: 265 | PanelEmoji: "⚙️" 266 | 267 | CloseEmoji: "🗑️" 268 | ArchiveEmoji: "♻️" 269 | LockEmoji: "🔐" 270 | RenameEmoji: "🏷️" 271 | VCEmoji: "🔊" 272 | 273 | EnableInvitesEmoji: "📤" 274 | DisableInvitesEmoji: "📥" 275 | 276 | # ------------------------- 277 | # Logging 278 | # ------------------------- 279 | 280 | MessageDelete: 281 | Channel: "MESSAGE_DELETE_CHANNEL_ID" 282 | Colour: "#d92525" 283 | 284 | MessageUpdate: 285 | Channel: "MESSAGE_UPDATE_CHANNEL_ID" 286 | Colour: "#f2a024" 287 | 288 | Translate: 289 | TranslateLog: "TRANSLATE_LOG_ID" 290 | -------------------------------------------------------------------------------- /Configs/ticket-panel.yml: -------------------------------------------------------------------------------- 1 | # ___ _ ___ _ 2 | # / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ 3 | # \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | 4 | # <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| 5 | # |_| |_| 6 | # 7 | # SupportBot created by Emerald Services 8 | # Installed with MIT License 9 | # 10 | # Discord Support: https://discord.gg/emeraldsrv 11 | # Official Documentation: https://docs.emeraldsrv.com 12 | # Addons Marketplace: https://community.emeraldsrv.com 13 | # 14 | # Ticket Creation Panel Configuration (ticket-panel.yml) 15 | 16 | # ------------------------------- 17 | # Ticket Panel Configuration 18 | # ------------------------------- 19 | 20 | Panel: true 21 | 22 | # ---------------------------------------- 23 | # Ticket Panel Message Configuration #2b2d31 24 | # ---------------------------------------- 25 | 26 | PanelTitle: "SupportBot" 27 | PanelColour: "#0cc559" 28 | 29 | Button: 30 | Text: "Create Ticket" 31 | Emoji: "🎫" 32 | Color: "2" 33 | 34 | # Available Button Styles 35 | # 1 - Blurple 36 | # 2 - Grey 37 | # 3 - Red 38 | # 4 - Green 39 | 40 | TicketPanel_Description: true 41 | TicketPanel_Thumbnail: false 42 | TicketPanel_Image: true 43 | 44 | PanelMessage: "Click on the button to file a `Ticket` and our team will try their best to assist you in the best way we can!" 45 | PanelThumbnail: "https://i.imgur.com/ieeZejl.png" 46 | PanelImage: "https://i.imgur.com/dejQvkg.png" -------------------------------------------------------------------------------- /Data/BlacklistedUsers.json: -------------------------------------------------------------------------------- 1 | { 2 | "blacklistedUsers": [] 3 | } -------------------------------------------------------------------------------- /Data/Profiles.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Data/StaffData.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Data/SuggestionData.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Data/SupportBot-AI.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Data/TicketData.json: -------------------------------------------------------------------------------- 1 | { 2 | "tickets": [ 3 | {} 4 | ] 5 | } -------------------------------------------------------------------------------- /Data/profiles/0000000000000000.json: -------------------------------------------------------------------------------- 1 | { 2 | "bio": "", 3 | "timezone": "", 4 | "clockedIn": false 5 | } 6 | -------------------------------------------------------------------------------- /Data/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /Data/ticket-panel-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 0, 3 | "TicketPanelID": "000000000000000" 4 | } -------------------------------------------------------------------------------- /Events/guildCreate.js: -------------------------------------------------------------------------------- 1 | const Event = require("../Structures/Event.js"); 2 | module.exports = new Event("guildCreate", async (client) => { 3 | console.log( 4 | `\u001b[31m`, 5 | `${client.user.username} is not in the correct server set in your config. Please leave all other servers and restart the bot.` 6 | ); 7 | console.log(`\u001b[31m`, `${client.user.username} will now exit.`); 8 | return process.exit(1); 9 | }); 10 | -------------------------------------------------------------------------------- /Events/guildDelete.js: -------------------------------------------------------------------------------- 1 | const Event = require("../Structures/Event.js"); 2 | 3 | module.exports = new Event("guildDelete", async (client) => { 4 | console.log( 5 | `\u001b[31m`, 6 | `${client.user.username} is not in the correct server set in your config. Please join the server and restart the bot.` 7 | ); 8 | console.log(`\u001b[31m`, `${client.user.username} will now exit.`); 9 | return process.exit(1); 10 | }); 11 | -------------------------------------------------------------------------------- /Events/guildMemberAdd.js: -------------------------------------------------------------------------------- 1 | 2 | const Discord = require('discord.js'); 3 | const fs = require("fs"); 4 | const Event = require("../Structures/Event.js"); 5 | const yaml = require("js-yaml"); 6 | 7 | const supportbot = yaml.load( 8 | fs.readFileSync("./Configs/supportbot.yml", "utf8") 9 | ); 10 | 11 | const cmdconfig = yaml.load( 12 | fs.readFileSync("./Configs/commands.yml", "utf8") 13 | ); 14 | 15 | const msgconfig = yaml.load( 16 | fs.readFileSync("./Configs/messages.yml", "utf8") 17 | ); 18 | 19 | module.exports = new Event("guildMemberAdd", async (client, member, interaction, guild) => { 20 | 21 | if (supportbot.Welcome.Enabled) { 22 | const WelcomeChannel = member.guild.channels.cache.get(supportbot.Welcome.Channel) || 23 | member.guild.channels.cache.find(channel => channel.name === supportbot.Welcome.Channel); 24 | 25 | const WelcomeEmbed = new Discord.EmbedBuilder() 26 | .setColor(msgconfig.Welcome.Embed.Colour) 27 | .setTitle(msgconfig.Welcome.Embed.Title) 28 | .setDescription(msgconfig.Welcome.Embed.Message.replace(/%joined_user%/g, member.user)) 29 | .setTimestamp(); 30 | 31 | if (msgconfig.Welcome.Embed.Thumbnail === "BOT") { 32 | WelcomeEmbed.setThumbnail(client.user.displayAvatarURL()) 33 | } else if (msgconfig.Welcome.Embed.Thumbnail === "USER") { 34 | WelcomeEmbed.setThumbnail(member.user.displayAvatarURL()) 35 | } 36 | 37 | if (msgconfig.Welcome.Embed.ImageEnabled) { 38 | WelcomeEmbed.setImage(msgconfig.Welcome.Embed.ImageURL) 39 | } 40 | 41 | if (WelcomeChannel) { 42 | WelcomeChannel.send({ embeds: [WelcomeEmbed] }); 43 | } 44 | 45 | if (supportbot.Roles.AutoRole.Enabled) { 46 | const role = member.guild.roles.cache.get(supportbot.Roles.AutoRole.Role) || 47 | member.guild.roles.cache.find(role => role.name === supportbot.Roles.AutoRole.Role); 48 | if (role) { 49 | member.roles.add(role); 50 | } 51 | } 52 | 53 | console.log(`\u001b[32m`, `[+]`, `\u001b[33m`, `${member.user.username} joined the server!`); 54 | } 55 | 56 | }); -------------------------------------------------------------------------------- /Events/guildMemberRemove.js: -------------------------------------------------------------------------------- 1 | 2 | const Discord = require('discord.js'); 3 | const fs = require("fs"); 4 | const Event = require("../Structures/Event.js"); 5 | const yaml = require("js-yaml"); 6 | 7 | const supportbot = yaml.load( 8 | fs.readFileSync("./Configs/supportbot.yml", "utf8") 9 | ); 10 | 11 | const cmdconfig = yaml.load( 12 | fs.readFileSync("./Configs/commands.yml", "utf8") 13 | ); 14 | 15 | const msgconfig = yaml.load( 16 | fs.readFileSync("./Configs/messages.yml", "utf8") 17 | ); 18 | 19 | module.exports = new Event("guildMemberRemove", async (client, member, interaction, guild) => { 20 | 21 | if (supportbot.Leave.Enabled) { 22 | const LeaveChannel = member.guild.channels.cache.get(supportbot.Leave.Channel) || 23 | member.guild.channels.cache.find(channel => channel.name === supportbot.Leave.Channel); 24 | 25 | const LeaveEmbed = new Discord.EmbedBuilder() 26 | .setColor(msgconfig.Leave.Embed.Colour) 27 | .setTitle(msgconfig.Leave.Embed.Title) 28 | .setDescription(msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user)) 29 | .setTimestamp(); 30 | 31 | if (msgconfig.Leave.Embed.Thumbnail === "BOT") { 32 | LeaveEmbed.setThumbnail(client.user.displayAvatarURL()); 33 | } else if (msgconfig.Leave.Embed.Thumbnail === "USER") { 34 | LeaveEmbed.setThumbnail(member.user.displayAvatarURL()); 35 | } 36 | 37 | if (msgconfig.Leave.Embed.ImageEnabled) { 38 | LeaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL); 39 | } 40 | 41 | if (LeaveChannel) { 42 | LeaveChannel.send({ 43 | embeds: [LeaveEmbed] 44 | }); 45 | } 46 | 47 | console.log(`\u001b[31m`, `[-]`, `\u001b[33m`, `${member.user.username} left the server!`); 48 | } 49 | 50 | }); -------------------------------------------------------------------------------- /Events/messageCreate.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Discord = require("discord.js"); 3 | const yaml = require("js-yaml"); 4 | const axios = require("axios"); 5 | const { OpenAI } = require('openai'); 6 | 7 | // Load configuration files with validation 8 | function loadConfig(path) { 9 | try { 10 | return yaml.load(fs.readFileSync(path, "utf8")); 11 | } catch (error) { 12 | console.error(`Error loading config file at ${path}:`, error); 13 | throw error; 14 | } 15 | } 16 | 17 | const supportbotai = yaml.load(fs.readFileSync("./Configs/supportbot-ai.yml")); 18 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 19 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 20 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 21 | // Validate configuration values 22 | function validateConfig(config, keys) { 23 | keys.forEach(key => { 24 | if (!config[key]) { 25 | throw new Error(`Missing configuration key: ${key}`); 26 | } 27 | }); 28 | } 29 | 30 | const CHANNELS = [supportbotai.Channels.AIChannel]; 31 | 32 | // Initialize Discord client 33 | const client = new Discord.Client({ 34 | intents: ['Guilds', 'GuildMembers', 'GuildMessages'] 35 | }); 36 | 37 | // Load custom Event structure 38 | const Event = require("../Structures/Event.js"); 39 | 40 | // Initialize OpenAI client 41 | const openai = new OpenAI({ 42 | apiKey: supportbotai.General.OpenAI_Key, 43 | }); 44 | 45 | const PASTEBIN_API_KEY = supportbotai.General.PastebinAPI_Key; 46 | const PASTEBIN_API_URL = supportbotai.General.PastebinAPI_URL; 47 | 48 | // Function to create a paste on Pastebin 49 | async function createPaste(content) { 50 | try { 51 | const response = await axios.post(PASTEBIN_API_URL, new URLSearchParams({ 52 | api_dev_key: PASTEBIN_API_KEY, 53 | api_option: 'paste', 54 | api_paste_code: content, 55 | api_paste_private: 1, // 0 = public, 1 = unlisted, 2 = private 56 | api_paste_expire_date: '1D', // Expire in 1 day 57 | api_paste_format: 'text' 58 | })); 59 | return response.data; 60 | } catch (error) { 61 | console.error('Pastebin Error:', error); 62 | throw error; 63 | } 64 | } 65 | 66 | // Event handler for message creation 67 | module.exports = new Event("messageCreate", async (client, message) => { 68 | if (message.author.bot || !message.guild) return; 69 | if (!CHANNELS.includes(message.channelId) && !message.mentions.users.has(client.user.id)) return; 70 | 71 | if (supportbotai.Enabled) { 72 | 73 | const sendTypingInterval = setInterval(() => { 74 | message.channel.sendTyping(); 75 | }, 5000); 76 | 77 | let conversation = [{ 78 | role: "system", 79 | content: "SupportBot is your friendly neighbourhood support bot." 80 | }]; 81 | 82 | let prevMessages = await message.channel.messages.fetch({ limit: 10 }); 83 | prevMessages = prevMessages.reverse().filter(msg => !msg.author.bot || msg.author.id === client.user.id); 84 | 85 | prevMessages.forEach((msg) => { 86 | const username = msg.author.username.replace(/\s+/g, '_').replace(/[^\w\s]/gi, ''); 87 | 88 | conversation.push({ 89 | role: msg.author.id === client.user.id ? "assistant" : "user", 90 | name: username, 91 | content: msg.content, 92 | }); 93 | }); 94 | 95 | try { 96 | const response = await openai.chat.completions.create({ 97 | model: supportbotai.General.Model, 98 | messages: conversation, 99 | max_tokens: supportbotai.General.Tokens, 100 | }); 101 | 102 | clearInterval(sendTypingInterval); 103 | 104 | if (response.choices && response.choices.length > 0) { 105 | const replyContent = response.choices[0].message.content; 106 | 107 | const SupportBotAI = new Discord.EmbedBuilder() 108 | .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) 109 | .setDescription(replyContent) 110 | .setColor(supportbotai.Embed.Color) 111 | .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) 112 | .setTimestamp(); 113 | 114 | if (replyContent.length > 2000) { 115 | const pasteLink = await createPaste(replyContent); 116 | 117 | const SupportBotAIPastebin = new Discord.EmbedBuilder() 118 | .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) 119 | .setDescription(supportbotai.Messages.MessageTooLong.replace("{pastebin}", pasteLink)) 120 | .setColor(supportbotai.Embed.Color) 121 | .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) 122 | .setTimestamp(); 123 | 124 | message.reply({ embeds: [SupportBotAIPastebin] }); 125 | } else { 126 | message.reply({ embeds: [SupportBotAI] }); 127 | } 128 | } else { 129 | const SupportBotAIErrorResponse = new Discord.EmbedBuilder() 130 | .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) 131 | .setDescription(supportbotai.Messages.ErrorResponse) 132 | .setColor(supportbotai.Embed.Color) 133 | .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) 134 | .setTimestamp(); 135 | 136 | message.reply({ embeds: [SupportBotAIErrorResponse], ephemeral: true }); 137 | } 138 | } catch (error) { 139 | clearInterval(sendTypingInterval); 140 | 141 | const SupportBotAIErrorResponse = new Discord.EmbedBuilder() 142 | .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) 143 | .setDescription(supportbotai.Messages.OpenAIError) 144 | .setColor(supportbot.Embed.Colours.Error) 145 | .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) 146 | .setTimestamp(); 147 | 148 | console.error('OpenAI Error:', error); 149 | message.reply({ embeds: [SupportBotAIErrorResponse], ephemeral: true }); 150 | } 151 | 152 | } 153 | 154 | if (supportbotai.Enabled === false) { 155 | const AIDisabled = new Discord.EmbedBuilder() 156 | .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL() }) 157 | .setDescription(supportbotai.Messages.AIDisabled) 158 | .setColor(supportbot.Embed.Colours.Error) 159 | .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL() }) 160 | .setTimestamp(); 161 | 162 | message.reply({ embeds: [AIDisabled], ephemeral: true }) 163 | return; 164 | } 165 | 166 | }); -------------------------------------------------------------------------------- /Events/messageDelete.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require('discord.js'); 2 | const fs = require("fs"); 3 | const yaml = require("js-yaml"); 4 | const Event = require("../Structures/Event.js"); 5 | 6 | // Load the configuration files 7 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 8 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 9 | 10 | module.exports = new Event("messageDelete", async (client, message) => { 11 | 12 | // Check if the message is from a guild and not from a bot 13 | if (!message.guild || message.author.bot) return; 14 | 15 | // Get the delete log channel either by ID or by name 16 | const deleteLogChannel = message.guild.channels.cache.get(supportbot.MessageDelete.Channel) || 17 | message.guild.channels.cache.find(channel => channel.name === supportbot.MessageDelete.Channel); 18 | 19 | // Create the embed for the deleted message 20 | const deletedMessageEmbed = new EmbedBuilder() 21 | .setColor(supportbot.MessageDelete.Colour) 22 | .setTitle("Message Deleted") 23 | .setDescription(`> **Channel:** <#${message.channel.id}>\n> **Message ID** [${message.id}](https://discord.com/channels/${message.guild.id}/${message.channel.id}/${message.id})\n> **Message author:** ${message.author.tag} (${message.author.id})\n> **Message Crated:** `) 24 | .addFields( 25 | { name: 'Message', value: message.content || '*No content*', inline: false } 26 | ) 27 | .setThumbnail(message.author.displayAvatarURL()) 28 | .setTimestamp(); 29 | 30 | // If the message contains an attachment, add it to the embed 31 | if (message.attachments.size > 0) { 32 | const attachment = message.attachments.first(); 33 | deletedMessageEmbed.addFields({ name: 'Attachment', value: `[${attachment.name}](${attachment.proxyURL})` }); 34 | } 35 | 36 | // Send the embed to the delete log channel if the channel is found 37 | if (deleteLogChannel) { 38 | deleteLogChannel.send({ 39 | embeds: [deletedMessageEmbed] 40 | }); 41 | } 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /Events/messageUpdate.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require('discord.js'); 2 | const fs = require("fs"); 3 | const yaml = require("js-yaml"); 4 | const Event = require("../Structures/Event.js"); 5 | 6 | // Load the configuration files 7 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 8 | const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); 9 | 10 | module.exports = new Event("messageUpdate", async (client, oldMessage, newMessage) => { 11 | 12 | // Check if the message is from a guild and not from a bot 13 | if (!newMessage.guild || newMessage.author.bot) return; 14 | 15 | // Fetch old message if it's not cached (important for message content) 16 | if (!oldMessage.content && oldMessage.partial) { 17 | try { 18 | oldMessage = await oldMessage.fetch(); 19 | } catch (err) { 20 | console.error('Error fetching old message:', err); 21 | return; 22 | } 23 | } 24 | 25 | // Fallback if content is missing 26 | const oldMessageContent = oldMessage.content || (oldMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*'); 27 | const newMessageContent = newMessage.content || (newMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*'); 28 | 29 | // Get the update log channel either by ID or by name 30 | const updateLogChannel = newMessage.guild.channels.cache.get(supportbot.MessageUpdate.Channel) || 31 | newMessage.guild.channels.cache.find(channel => channel.name === supportbot.MessageUpdate.Channel); 32 | 33 | // Create the embed for the updated message 34 | const updatedMessageEmbed = new EmbedBuilder() 35 | .setColor(supportbot.MessageUpdate.Colour) 36 | .setTitle("Message Updated") 37 | .setDescription(`> **Channel:** <#${newMessage.channel.id}>\n> **Message ID:** [${newMessage.id}](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${newMessage.id})\n> **Message author:** ${newMessage.author.tag} (${newMessage.author.id})\n> **Message Created:** `) 38 | .addFields( 39 | { name: 'Old Message', value: oldMessageContent, inline: false }, 40 | { name: 'New Message', value: newMessageContent, inline: false } 41 | ) 42 | .setThumbnail(newMessage.author.displayAvatarURL()) 43 | .setTimestamp(); 44 | 45 | // If the new message contains an attachment, add it to the embed 46 | if (newMessage.attachments.size > 0) { 47 | const attachment = newMessage.attachments.first(); 48 | updatedMessageEmbed.addFields({ name: 'Attachment', value: `[${attachment.name}](${attachment.proxyURL})` }); 49 | } 50 | 51 | // Send the embed to the update log channel if the channel is found 52 | if (updateLogChannel) { 53 | updateLogChannel.send({ 54 | embeds: [updatedMessageEmbed] 55 | }); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /Events/ready.js: -------------------------------------------------------------------------------- 1 | // SupportBot | Emerald Services 2 | // Ready Event 3 | 4 | const fs = require("fs"); 5 | 6 | const Discord = require("discord.js"); 7 | const { Client, GatewayIntentBits, ActivityType } = require('discord.js'); 8 | const client = new Discord.Client({intents: 32767}) 9 | 10 | const yaml = require("js-yaml"); 11 | 12 | const supportbot = yaml.load( 13 | fs.readFileSync("./Configs/supportbot.yml", "utf8") 14 | ); 15 | const panelconfig = yaml.load( 16 | fs.readFileSync("./Configs/ticket-panel.yml", "utf8") 17 | ); 18 | 19 | const cmdconfig = yaml.load( 20 | fs.readFileSync("./Configs/commands.yml", "utf8") 21 | ); 22 | 23 | const msgconfig = yaml.load( 24 | fs.readFileSync("./Configs/messages.yml", "utf8") 25 | ); 26 | 27 | const Event = require("../Structures/Event.js"); 28 | 29 | let chan1 = client.channels.cache.get(supportbot.Ticket.TicketHome); 30 | 31 | module.exports = new Event("ready", async (client, interaction) => { 32 | const { getRole, getChannel, getCategory } = client; 33 | 34 | if (supportbot.Activity.Type === "Playing", "playing") { 35 | client.user.setPresence({ 36 | activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Playing }], 37 | status: supportbot.Activity.Type, 38 | }); 39 | } 40 | 41 | if (supportbot.Activity.Type === "Watching", "watching") { 42 | client.user.setPresence({ 43 | activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Watching }], 44 | status: supportbot.Activity.Type, 45 | }); 46 | } 47 | 48 | if (supportbot.Activity.Type === "Listening", "listening") { 49 | client.user.setPresence({ 50 | activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Listening }], 51 | status: supportbot.Activity.Type, 52 | }); 53 | } 54 | 55 | if (supportbot.Activity.Type === "Competing", "competing") { 56 | client.user.setPresence({ 57 | activities: [{ name: supportbot.Activity.Status, type: Discord.ActivityType.Competing }], 58 | status: supportbot.Activity.Type, 59 | }); 60 | } 61 | 62 | console.log(`\u001b[33m`, `――――――――――――――――――――――――――――――――――――――――――――`); 63 | console.log(` `); 64 | console.log(`\u001b[31m`, `┏━━━┓╋╋╋╋╋╋╋╋╋╋╋╋╋┏┓┏━━┓╋╋╋┏┓`); 65 | console.log(`\u001b[31m`, `┃┏━┓┃╋╋╋╋╋╋╋╋╋╋╋╋┏┛┗┫┏┓┃╋╋┏┛┗┓`); 66 | console.log(`\u001b[31m`, `┃┗━━┳┓┏┳━━┳━━┳━━┳┻┓┏┫┗┛┗┳━┻┓┏┛`); 67 | console.log(`\u001b[31m`, `┗━━┓┃┃┃┃┏┓┃┏┓┃┏┓┃┏┫┃┃┏━┓┃┏┓┃┃`); 68 | console.log(`\u001b[31m`, `┃┗━┛┃┗┛┃┗┛┃┗┛┃┗┛┃┃┃┗┫┗━┛┃┗┛┃┗┓`); 69 | console.log(`\u001b[31m`, `┗━━━┻━━┫┏━┫┏━┻━━┻┛┗━┻━━━┻━━┻━┛`); 70 | console.log(`\u001b[31m`, `┗╋╋╋╋╋╋╋┃┃╋┃┃`); 71 | console.log(`\u001b[31m`, `╋╋╋╋╋╋╋┗┛╋┗┛`); 72 | console.log(` `); 73 | console.log(`\u001b[33m`, `――――――――――――――――――――――――――――――――――――――――――――`); 74 | console.log(` `); 75 | console.log(`\u001b[33m`, `${supportbot.General.Name} | [${supportbot.SupportBot_Version}]`, `\u001b[32m`, `Connected to Discord`,); 76 | console.log("\u001b[32m", "SupportBot created by Emerald Development "); 77 | console.log(` `); 78 | console.log(`\u001b[33m`, `――――――――――――――――― [Links] ――――――――――――――――――`); 79 | console.log("\u001b[32m", "Discord: https://dsc.gg/emerald-dev"); 80 | console.log("\u001b[32m", "Website: https://emeraldsrv.com"); 81 | console.log("\u001b[32m", "Community: https://community.emeraldsrv.com"); 82 | console.log("\u001b[32m", "Documentation: https://docs.emeraldsrv.com"); 83 | console.log(` `); 84 | console.log(`\u001b[33m`, `――――――――――――――――― [Invite] ――――――――――――――――――`); 85 | console.log("\u001b[32m", `Invite URL: https://discord.com/api/oauth2/authorize?client_id=${client.user.id}&permissions=8&scope=bot%20applications.commands`); 86 | console.log(` `); 87 | console.log(`\u001b[33m`, `――――――――――――――――― [Config Check] ――――――――――――――――――`); 88 | console.log("\u001b[32m", "Config initialization..."); 89 | 90 | const roles = [ 91 | supportbot.Roles.StaffMember.Admin, 92 | supportbot.Roles.StaffMember.Staff 93 | ]; 94 | 95 | if (supportbot.Roles.AutoRole.Role) roles.push(supportbot.Roles.AutoRole.Role); 96 | 97 | const channels = [ 98 | supportbot.Suggestions.Channel, 99 | supportbot.Ticket.Log.TicketLog, 100 | supportbot.Ticket.Log.TranscriptLog, 101 | supportbot.Ticket.TicketHome, 102 | supportbot.Welcome.Channel, 103 | supportbot.Leave.Channel, 104 | supportbot.Translate.Log 105 | ]; 106 | //const categories = [supportbot.TicketCategory]; 107 | 108 | if (!channels) { 109 | console.log("\u001b[31m", `[MISSING CHANNEL]`, `\u001b[37;1m`, `${channels}`, "\u001b[31m", `channel not found. Please check your config file.`); 110 | return; 111 | } 112 | // else { 113 | // console.log(`\u001b[32m`, `[CHANNEL LOCATED]`, `\u001b[37;1m`, `${channels}`, `\u001b[32;1m`, `channel has been found.`); 114 | // } 115 | 116 | const missingC = []; 117 | const missingR = []; 118 | const missingCat = []; 119 | 120 | for (let r of roles) { 121 | const find = await getRole(r, client.guilds.cache.first()); 122 | if (!find) missingR.push(r); 123 | } 124 | // for (let cat of categories) { 125 | // const find = await getCategory(cat, client.guilds.cache.first()); 126 | // if (!find) missingCat.push(cat); 127 | // } 128 | 129 | const missingRoles = await Promise.all(roles.map(role => getRole(role, client.guilds.cache.first()))); 130 | const missingChannels = await Promise.all(channels.map(channel => getChannel(channel, client.guilds.cache.first()))); 131 | 132 | if (missingRoles.some(role => !role)) { 133 | console.log("\u001b[31m", `Missing roles in your server configuration: ${missingRoles.filter(role => !role).join(', ')}`); 134 | } 135 | 136 | if (missingChannels.some(channel => !channel)) { 137 | console.log("\u001b[31m", `Missing channels in your server configuration: ${missingChannels.filter(channel => !channel).join(', ')}`); 138 | } 139 | 140 | console.log("\u001b[32m", "Configs initialized, No problems were detected."); 141 | console.log(` `); 142 | 143 | }); -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Emerald Services 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 |
5 | SupportBot 6 |
7 |

8 | 9 |
10 | 11 | 12 | shield.png 13 | 14 | 15 | 16 | shield.png 17 | 18 | 19 | 20 | shield.png 21 | 22 | 23 | 24 | shield.png 25 | 26 | 27 | ## Supported Versions 28 | 29 | | SupportBot | Node.JS | Discord.JS | Supported | 30 | |------------|---------|------------|------------| 31 | | 8.x | 20.x | 14.x. | ✅ Yes 32 | | 7.7.3 | 16.x | 13.x. | ✅ Yes 33 | | 7.x | 16.x | 13.x. | ❌ No 34 | | 6.x | 14.x | 13.x. | ❌ No 35 | | 5.x | 12.x | 13.x. | ❌ No 36 | | 4.x | 11.x | 13.x. | ❌ No 37 | | Older | 10.x | 13.x. | ❌ No 38 | 39 |
40 | 41 |

42 | Website 43 | • 44 | Wiki 45 |
46 | About 47 | • 48 | Support 49 | • 50 | License 51 | • 52 | Third-Party 53 | • 54 | Contributors 55 |

56 | 57 | ## About 58 | 59 | SupportBot is an Open-Source discord bot built with [discord.js](https://github.com/discordjs/discord.js). It's primarily focused to help servers organise and manage their support system in a unique way with full customisation to match your brand. 60 | 61 | ## Support 62 | 63 | 64 | Discord 65 | 66 | 67 | 68 | ## License 69 | 70 | Released under the [MIT](https://opensource.org/licenses/MIT) license. 71 | 72 | 73 | ## Third-Party 74 | 75 | - [Official addons repository](https://github.com/Emerald-Services/Addons/) 76 | - [Syphers Addons](https://github.com/SypherRed/SB_Addons_Unofficial) 77 | - [BuiltByBit](https://builtbybit.com/supportbot) 78 | - [Polymart](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518) 79 | 80 | 81 | ## Contributors 82 | 83 | 84 | 85 | 86 | 87 | ## Hosting 88 | 89 | You can use our [Pterodactyl egg](https://github.com/Emerald-Services/Pterodactyl-Egg) to host it on your own panel. We currently don't offer hosting for you, altough we might add this in the future. 90 | If you don't know how to install pterodactyl, you could always host it on replit: 91 | 92 | [![Run on Repl.it](https://repl.it/badge/github/ItsNyoty/IKI-bot)]([https://repl.it/github/ItsNyoty/iki](https://repl.it/github/Emerald-Services/SupportBot 93 | ) 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 7.7.2 | :white_check_mark: | 11 | | 7.7.1 | :white_check_mark: | 12 | | < 4.0 | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | Use this section to tell people how to report a vulnerability. 17 | 18 | Tell them where to go, how often they can expect to get an update on a 19 | reported vulnerability, what to expect if the vulnerability is accepted or 20 | declined, etc. 21 | -------------------------------------------------------------------------------- /Structures/Addon.js: -------------------------------------------------------------------------------- 1 | 2 | // SupportBot | Emerald Services 3 | // Addon Structure 4 | class Command { 5 | constructor(options) { 6 | this.name = options.name; 7 | this.description = options.description; 8 | this.permissions = options.permissions; 9 | this.options = options.options || []; 10 | this.run = options.run; 11 | this.defaultPermission = true; 12 | } 13 | 14 | // Method to dynamically update options 15 | setOptions(options) { 16 | this.options = options; 17 | } 18 | } 19 | 20 | class Event { 21 | constructor(event, run) { 22 | this.event = event; 23 | this.run = run; 24 | } 25 | } 26 | 27 | module.exports = { Command, Event }; 28 | -------------------------------------------------------------------------------- /Structures/Client.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Discord = require("discord.js"); 3 | const { GatewayIntentBits, Partials } = require('discord.js'); 4 | const yaml = require("js-yaml"); 5 | const { Command, Event } = require('./Addon.js'); // Adjust the path as needed 6 | 7 | const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); 8 | const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); 9 | 10 | class Client extends Discord.Client { 11 | constructor() { 12 | super({ 13 | intents: [ 14 | GatewayIntentBits.Guilds, 15 | GatewayIntentBits.GuildMembers, 16 | GatewayIntentBits.GuildMessages, 17 | GatewayIntentBits.MessageContent 18 | ], 19 | partials: [Partials.Message, Partials.Channel] 20 | }); 21 | 22 | this.commands = new Discord.Collection(); 23 | } 24 | 25 | async getChannel(channel, guild) { 26 | if (!channel) return null; // Add check for undefined channel 27 | return guild.channels.cache.find( 28 | (c) => 29 | (c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) && 30 | (c.id === channel || (c.name && c.name.toLowerCase() === channel.toLowerCase())) 31 | ); 32 | } 33 | 34 | async getRole(role, guild) { 35 | if (!role) return null; // Add check for undefined role 36 | return guild.roles.cache.find( 37 | (r) => r.id === role || (r.name && r.name.toLowerCase() === role.toLowerCase()) 38 | ); 39 | } 40 | 41 | async getCategory(category, guild) { 42 | if (!category) return null; // Add check for undefined category 43 | return guild.channels.cache.find( 44 | (c) => 45 | c.type === Discord.ChannelType.GuildCategory && 46 | (c.id === category || (c.name && c.name.toLowerCase() === category.toLowerCase())) 47 | ); 48 | } 49 | 50 | async start(token) { 51 | let tempCommandFiles = fs.readdirSync("./Commands").filter((file) => file.endsWith(".js")); 52 | 53 | if (cmdconfig.Suggestion.Enabled === false) { 54 | tempCommandFiles = tempCommandFiles.filter(item => item !== "suggest.js"); 55 | } 56 | 57 | if (cmdconfig.Help.Enabled === false) { 58 | tempCommandFiles = tempCommandFiles.filter(item => item !== "help.js"); 59 | } 60 | 61 | if (cmdconfig.Info.Enabled === false) { 62 | tempCommandFiles = tempCommandFiles.filter(item => item !== "info.js"); 63 | } 64 | 65 | if (cmdconfig.OpenTicket.Enabled === false) { 66 | tempCommandFiles = tempCommandFiles.filter(item => item !== "ticket.js"); 67 | } 68 | 69 | if (cmdconfig.CloseTicket.Enabled === false) { 70 | tempCommandFiles = tempCommandFiles.filter(item => item !== "close.js"); 71 | } 72 | 73 | if (cmdconfig.Embed.Enabled === false) { 74 | tempCommandFiles = tempCommandFiles.filter(item => item !== "embed.js"); 75 | } 76 | 77 | if (cmdconfig.Translate.Enabled === false) { 78 | tempCommandFiles = tempCommandFiles.filter(item => item !== "translate.js"); 79 | } 80 | 81 | if (cmdconfig.UserInfo.Enabled === false) { 82 | tempCommandFiles = tempCommandFiles.filter(item => item !== "userinfo.js"); 83 | } 84 | 85 | if (cmdconfig.Ping.Enabled === false) { 86 | tempCommandFiles = tempCommandFiles.filter(item => item !== "Ping.js"); 87 | } 88 | 89 | if (cmdconfig.AddUser.Enabled === false) { 90 | tempCommandFiles = tempCommandFiles.filter(item => item !== "addUser.js"); 91 | } 92 | 93 | if (cmdconfig.RemoveUser.Enabled === false) { 94 | tempCommandFiles = tempCommandFiles.filter(item => item !== "removeUser.js"); 95 | } 96 | 97 | if (cmdconfig.ForceAddUser.Enabled === false) { 98 | tempCommandFiles = tempCommandFiles.filter(item => item !== "forceaddUser.js"); 99 | } 100 | 101 | if (cmdconfig.Profile.Enabled === false) { 102 | tempCommandFiles = tempCommandFiles.filter(item => item !== "profile.js"); 103 | } 104 | 105 | if (cmdconfig.Settings.Enabled === false) { 106 | tempCommandFiles = tempCommandFiles.filter(item => item !== "settings.js"); 107 | } 108 | 109 | if (cmdconfig.Mod.Enabled === false) { 110 | tempCommandFiles = tempCommandFiles.filter(item => item !== "mod.js"); 111 | } 112 | 113 | const commandFiles = tempCommandFiles; 114 | const commands = commandFiles.map((file) => require(`../Commands/${file}`)); 115 | 116 | // Load main commands 117 | console.log(`\u001b[33m`, "――――――――――――――――――――――――――――――――――――――――――――"); 118 | 119 | commands.forEach((cmd) => { 120 | console.log(`\u001b[32m`, `[CMD]`, `\u001b[37;1m`, `${cmd.name}`, `\u001b[32;1m`, "Loaded"); 121 | this.commands.set(cmd.name, cmd); 122 | }); 123 | 124 | // Load addon commands and events 125 | console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬"); 126 | 127 | if (supportbot.General.Addons.Enabled) { 128 | const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js")); 129 | 130 | const addons = addonFiles.map((file) => { 131 | const addon = require(`../Addons/${file}`); 132 | addon.name = file.split(".")[0]; 133 | return addon; 134 | }); 135 | 136 | // Addons 137 | console.log(" "); 138 | console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬"); 139 | 140 | addons.forEach((addon) => { 141 | console.log( 142 | `\u001b[32m`, 143 | `[ADDON]`, 144 | `\u001b[37;1m`, 145 | `${addon.name}`, 146 | `\u001b[32;1m`, 147 | "Loaded" 148 | ); 149 | 150 | if (addon instanceof Command) { 151 | this.commands.set(addon.name, addon); 152 | } 153 | 154 | if (addon.events && Array.isArray(addon.events)) { 155 | addon.events.forEach((event) => { 156 | if (event instanceof Event) { 157 | this.on(event.event, (...args) => event.run(this, ...args)); 158 | } 159 | }); 160 | } 161 | }); 162 | 163 | console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬"); 164 | } 165 | 166 | // Register Slash Commands 167 | 168 | this.once("ready", async () => { 169 | await this.guilds.cache.first()?.commands.set(this.commands); 170 | console.log( 171 | "\u001b[32m", 172 | `[SUCCESS]`, 173 | `\u001b[37;1m`, 174 | `Slash Commands Registered for ${this.guilds.cache.first().name}` 175 | ); 176 | }); 177 | 178 | console.log(" "); 179 | console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬"); 180 | 181 | fs.readdirSync("./Events").filter((file) => file.endsWith(".js")).forEach((file) => { 182 | const event = require(`../Events/${file}`); 183 | console.log(`\u001b[32m`, `[EVENT]`, `\u001b[37;1m`, `${event.event}`, `\u001b[32;1m`, "Loaded"); 184 | this.on(event.event, (...args) => event.run(this, ...args)); 185 | }); 186 | 187 | console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬"); 188 | if (process.argv[2] !== "test") { 189 | await this.login(token); 190 | } else { 191 | console.log( 192 | `\u001b[31;1m`, 193 | "RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start" 194 | ); 195 | } 196 | } 197 | } 198 | 199 | module.exports = Client; 200 | -------------------------------------------------------------------------------- /Structures/Command.js: -------------------------------------------------------------------------------- 1 | // SupportBot | Emerald Services 2 | // Command Structure 3 | 4 | class Command { 5 | constructor(options) { 6 | this.name = options.name; 7 | this.description = options.description; 8 | this.permissions = options.permissions || []; 9 | this.options = options.options || []; 10 | this.run = options.run; 11 | this.defaultPermission = true; 12 | } 13 | } 14 | module.exports = Command; 15 | -------------------------------------------------------------------------------- /Structures/Event.js: -------------------------------------------------------------------------------- 1 | // SupportBot | Emerald Services 2 | // Event Structure 3 | class Event { 4 | constructor(event, run) { 5 | this.event = event; 6 | this.run = run; 7 | } 8 | } 9 | 10 | module.exports = Event; 11 | -------------------------------------------------------------------------------- /Structures/TicketID.js: -------------------------------------------------------------------------------- 1 | // SupportBot | Emerald Services 2 | // Ticket ID Structure 3 | 4 | const fs = require("fs"); 5 | 6 | class TicketNumberID { 7 | static async pad() { 8 | let num = await this.get(); 9 | let n = num + ""; 10 | await this.set(num + 1); 11 | return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n; 12 | } 13 | 14 | static async get() { 15 | let ticketnumberID = await JSON.parse( 16 | fs.readFileSync("./Data/ticket-panel-id.json", "utf8") 17 | ); 18 | if (!ticketnumberID.id) { 19 | return this.set(0); 20 | } else { 21 | return ticketnumberID.id; 22 | } 23 | } 24 | 25 | static async set(value) { 26 | let ticketnumberID = await JSON.parse( 27 | fs.readFileSync("./Data/ticket-panel-id.json", "utf8") 28 | ); 29 | 30 | ticketnumberID.id = value; 31 | fs.writeFileSync( 32 | "./Data/ticket-panel-id.json", 33 | JSON.stringify(ticketnumberID, null, 4), 34 | (err) => { 35 | if (err) console.error(err); 36 | } 37 | ); 38 | 39 | return value; 40 | } 41 | 42 | static async reset() { 43 | return await this.set(0); 44 | } 45 | } 46 | 47 | module.exports = TicketNumberID; 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // ___ _ ___ _ 2 | // / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ 3 | // \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | 4 | // <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| 5 | // |_| |_| 6 | // 7 | // SupportBot created by Emerald Services 8 | // Installed with MIT License 9 | // 10 | // Discord Support: https://emeraldsrv.dev/discord 11 | // Community Resources: https://emeraldsrv.dev/resources 12 | 13 | const fs = require("fs"); 14 | 15 | const yaml = require("js-yaml"); 16 | const supportbot = yaml.load( 17 | fs.readFileSync("./Configs/supportbot.yml", "utf8") 18 | ); 19 | 20 | const Client = require("./Structures/Client.js"); 21 | const client = new Client({ 22 | intents: ['Guilds', 'GuildMembers', 'GuildMessages', 'MessageContent'] 23 | }); 24 | 25 | client.start(supportbot.General.Token); 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supportbot", 3 | "version": "8.3", 4 | "description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.", 5 | "main": "index.js", 6 | "scripts": { 7 | "setup": "npm i && node index.js", 8 | "start": "node index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Emerald-Services/SupportBot.git" 13 | }, 14 | "author": "Emerald Services", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/Emerald-Services/SupportBot/issues" 18 | }, 19 | "homepage": "https://community.emeraldsrv.dev", 20 | "dependencies": { 21 | "@paypal/checkout-server-sdk": "^1.0.3", 22 | "@vitalets/google-translate-api": "^8.0.0", 23 | "axios": "^1.7.2", 24 | "discord.js": "^14.13.0", 25 | "js": "^0.1.0", 26 | "js-yaml": "^4.1.0", 27 | "moment": "^2.29.3", 28 | "ms": "^2.1.3", 29 | "openai": "^4.50.0" 30 | } 31 | } 32 | --------------------------------------------------------------------------------