diff --git a/API/server.js b/API/server.js new file mode 100644 index 0000000..edb4b1d --- /dev/null +++ b/API/server.js @@ -0,0 +1,360 @@ +const express = require('express'); +const cors = require('cors'); +const fs = require('fs'); +const os = require('os'); +const yaml = require('js-yaml'); +const YAML = require('yaml'); +const db = require('../Structures/Database.js'); + +class APIServer { + constructor(client) { + this.client = client; + this.app = express(); + + try { + const apiCfg = yaml.load(fs.readFileSync('./Configs/api.yml', 'utf8')).API; + this.config = apiCfg; + } catch (e) { + console.error('[API] Failed to load api.yml, defaulting to disabled.'); + this.config = { Enabled: false }; + } + + if (!this.config.Enabled) return; + + this.app.use(cors()); + this.app.use(express.json()); + + this.app.use((req, res, next) => { + const authHeader = req.headers.authorization; + if (!authHeader || authHeader !== `Bearer ${this.config.SecretKey}`) { + return res.status(401).json({ error: 'Unauthorized. Invalid Secret Key.' }); + } + next(); + }); + + this.setupRoutes(); + } + + start(port) { + const listenPort = port || this.config.Port || 3000; + this.app.listen(listenPort, () => { + console.log(`[API] Server running on port ${listenPort}`); + }); + } + + setupRoutes() { + this.app.get('/api/stats', async (req, res) => { + try { + if (!this.client.user) { + return res.json({ success: false, error: 'Bot is still starting up.' }); + } + + let userCount = 0; + this.client.guilds.cache.forEach(guild => { + userCount += guild.memberCount; + }); + + const allTickets = db.getAllTickets() || []; + const openTickets = allTickets.filter(t => t.open).length; + const closedTickets = allTickets.filter(t => !t.open).length; + + const totalMem = os.totalmem(); + const freeMem = os.freemem(); + const usedMem = totalMem - freeMem; + const ramPercent = Math.round((usedMem / totalMem) * 100); + const cpuLoad = os.loadavg()[0].toFixed(2); + + res.json({ + success: true, + data: { + bot: { + username: this.client.user.username, + id: this.client.user.id, + avatar: this.client.user.displayAvatarURL(), + version: require('../package.json').version, + ping: this.client.ws.ping, + uptime: process.uptime(), + }, + servers: this.client.guilds.cache.size, + users: userCount, + tickets: { + total: allTickets.length, + open: openTickets, + closed: closedTickets + }, + hosting: { + ram_used: (usedMem / 1024 / 1024 / 1024).toFixed(2) + ' GB', + ram_total: (totalMem / 1024 / 1024 / 1024).toFixed(2) + ' GB', + ram_percent: ramPercent, + cpu_load: cpuLoad, + uptime: Math.floor(os.uptime() / 86400) + ' Days' + } + } + }); + } catch (err) { + console.error('[API] Error fetching stats:', err); + res.status(500).json({ success: false, error: 'Internal Server Error' }); + } + }); + + this.app.get('/api/configs/:file', (req, res) => { + const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai']; + const file = req.params.file; + if (!validFiles.includes(file)) return res.status(400).json({ success: false, error: 'Invalid file' }); + try { + const configData = fs.readFileSync(`./Configs/${file}.yml`, 'utf8'); + res.json({ success: true, data: configData }); + } catch (err) { + res.status(500).json({ success: false, error: 'Failed to read config' }); + } + }); + + this.app.put('/api/configs/raw', (req, res) => { + const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai']; + const file = req.body.filename; + const content = req.body.content; + if (!validFiles.includes(file)) return res.status(400).json({ success: false, error: 'Invalid file' }); + try { + fs.writeFileSync(`./Configs/${file}.yml`, content); + res.json({ success: true }); + } catch (err) { + res.status(500).json({ success: false, error: 'Failed to write config' }); + } + }); + + this.app.get('/api/configs/json/:file', (req, res) => { + const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai']; + const file = req.params.file; + if (!validFiles.includes(file)) return res.status(400).json({ success: false, error: 'Invalid file' }); + try { + const configData = yaml.load(fs.readFileSync(`./Configs/${file}.yml`, 'utf8')); + res.json({ success: true, data: configData }); + } catch (err) { + res.status(500).json({ success: false, error: 'Failed to parse config' }); + } + }); + + this.app.post('/api/configs/update-fields/multi', (req, res) => { + const updates = req.body; + if (!updates || typeof updates !== 'object') return res.status(400).json({ success: false, error: 'Invalid updates' }); + try { + for (const [keyPath, value] of Object.entries(updates)) { + const [fileName, path] = keyPath.split(':'); + const fileContent = fs.readFileSync(`./Configs/${fileName}.yml`, 'utf8'); + const doc = YAML.parseDocument(fileContent); + const keys = path.split('.'); + doc.setIn(keys, value); + fs.writeFileSync(`./Configs/${fileName}.yml`, doc.toString()); + } + res.json({ success: true }); + } catch (err) { + console.error('[API] Update failed:', err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + this.app.post('/api/addons/install', async (req, res) => { + const { url, filename } = req.body; + if (!url || !filename) return res.status(400).json({ success: false, error: 'Missing url or filename' }); + + try { + const axios = require('axios'); + const response = await axios({ + method: 'get', + url: url, + responseType: 'stream' + }); + + // Ensure Addons directory exists + if (!fs.existsSync('./Addons')) { + fs.mkdirSync('./Addons'); + } + + const writer = fs.createWriteStream(`./Addons/${filename}`); + response.data.pipe(writer); + + writer.on('finish', () => { + res.json({ success: true, message: `Addon ${filename} installed successfully.` }); + }); + + writer.on('error', (err) => { + console.error('[API] Download failed:', err); + res.status(500).json({ success: false, error: 'Failed to write addon file' }); + }); + + } catch (err) { + console.error('[API] Addon install failed:', err); + res.status(500).json({ success: false, error: 'Failed to download addon' }); + } + }); + + this.app.post('/api/addons/push', (req, res) => { + const { filename, content } = req.body; + if (!filename || !content) return res.status(400).json({ success: false, error: 'Missing filename or content' }); + + const sanitizedFilename = filename.replace(/[^a-z0-9_.-]/gi, '_'); + + try { + if (!fs.existsSync('./Addons')) { + fs.mkdirSync('./Addons'); + } + fs.writeFileSync(`./Addons/${sanitizedFilename}`, content); + res.json({ success: true, message: `Addon ${sanitizedFilename} deployed successfully.` }); + } catch (err) { + console.error('[API] Push failed:', err); + res.status(500).json({ success: false, error: 'Failed to write addon file' }); + } + }); + + // --- Nexus Updater API --- + this.app.post('/api/system/update', async (req, res) => { + const { url, version } = req.body; + if (!url) return res.status(400).json({ success: false, error: 'Missing update URL' }); + + try { + const axios = require('axios'); + const { execSync } = require('child_process'); + const path = require('path'); + + console.log(`[Updater] Starting update to ${version || 'latest'}...`); + + // 1. Download + const tempZip = `./update_${Date.now()}.zip`; + const response = await axios({ method: 'get', url: url, responseType: 'stream' }); + const writer = fs.createWriteStream(tempZip); + response.data.pipe(writer); + + await new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }); + + // 2. Extract to temp dir + const tempDir = `./temp_update_${Date.now()}`; + if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir); + + const isWindows = process.platform === 'win32'; + try { + if (isWindows) { + execSync(`powershell -Command "Expand-Archive -Path '${tempZip}' -DestinationPath '${tempDir}' -Force"`); + } else { + execSync(`unzip -o "${tempZip}" -d "${tempDir}"`); + } + } catch (e) { + throw new Error(`Extraction failed. Ensure ${isWindows ? 'PowerShell' : 'unzip'} is installed. ${e.message}`); + } + + // Identify inner directory (sometimes zips contain a root folder) + let sourcePath = tempDir; + const items = fs.readdirSync(tempDir); + if (items.length === 1 && fs.statSync(path.join(tempDir, items[0])).isDirectory()) { + sourcePath = path.join(tempDir, items[0]); + } + + // 3. Smart Config Merge + const configFiles = ['supportbot', 'commands', 'messages', 'ticket-panel', 'supportbot-ai']; + configFiles.forEach(cf => { + const currentPath = `./Configs/${cf}.yml`; + const newPath = path.join(sourcePath, 'Configs', `${cf}.yml`); + + if (fs.existsSync(currentPath) && fs.existsSync(newPath)) { + const currentCfg = yaml.load(fs.readFileSync(currentPath, 'utf8')); + const newCfg = yaml.load(fs.readFileSync(newPath, 'utf8')); + + const merge = (target, source) => { + for (const key of Object.keys(source)) { + if (source[key] instanceof Object && !Array.isArray(source[key]) && target[key]) { + merge(target[key], source[key]); + } else if (target[key] === undefined) { + target[key] = source[key]; + console.log(`[Updater] Added missing key: ${key} to ${cf}.yml`); + } + } + }; + merge(currentCfg, newCfg); + fs.writeFileSync(currentPath, YAML.stringify(currentCfg)); + } + }); + + // 4. Overwrite Core Folders + const coreFolders = ['API', 'Commands', 'Events', 'Structures']; + coreFolders.forEach(folder => { + const src = path.join(sourcePath, folder); + if (fs.existsSync(src)) { + if (isWindows) { + execSync(`xcopy "${src}" ".\\${folder}" /E /I /Y`); + } else { + execSync(`cp -R "${src}/"* "./${folder}/"`); + } + } + }); + + // 5. Update package.json (version only) + const newPkgPath = path.join(sourcePath, 'package.json'); + if (fs.existsSync(newPkgPath)) { + const currentPkg = JSON.parse(fs.readFileSync('./package.json', 'utf8')); + const newPkg = JSON.parse(fs.readFileSync(newPkgPath, 'utf8')); + currentPkg.version = newPkg.version; + fs.writeFileSync('./package.json', JSON.stringify(currentPkg, null, 2)); + } + + fs.unlinkSync(tempZip); + if (isWindows) { + execSync(`rmdir /s /q "${tempDir}"`); + } else { + execSync(`rm -rf "${tempDir}"`); + } + + res.json({ success: true, message: 'Update installed successfully. Bot is restarting...' }); + + // Reboot + setTimeout(() => process.exit(0), 1000); + + } catch (err) { + console.error('[Updater] Update failed:', err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + // --- Transcript API --- + this.app.get('/api/system/transcripts', (req, res) => { + const transcriptDir = './Data/Transcripts'; + try { + if (!fs.existsSync(transcriptDir)) { + return res.json({ success: true, data: [] }); + } + const files = fs.readdirSync(transcriptDir); + const transcripts = files + .filter(f => f.endsWith('-transcript.html')) + .map(f => { + const stats = fs.statSync(`${transcriptDir}/${f}`); + return { + id: f.replace('-transcript.html', ''), + filename: f, + createdAt: stats.mtime + }; + }) + .sort((a, b) => b.createdAt - a.createdAt); + res.json({ success: true, data: transcripts }); + } catch (err) { + res.status(500).json({ success: false, error: 'Failed to list transcripts' }); + } + }); + + this.app.get('/api/system/transcripts/:id', (req, res) => { + const id = req.params.id.replace(/[^0-9]/g, ''); // Sanitize ID + const filePath = `./Data/Transcripts/${id}-transcript.html`; + try { + if (!fs.existsSync(filePath)) { + return res.status(404).json({ success: false, error: 'Transcript not found' }); + } + const content = fs.readFileSync(filePath, 'utf8'); + res.json({ success: true, data: content }); + } catch (err) { + res.status(500).json({ success: false, error: 'Failed to read transcript' }); + } + }); + } +} + +module.exports = APIServer; diff --git a/Commands/General/ai.js b/Commands/General/ai.js new file mode 100644 index 0000000..08cbffa --- /dev/null +++ b/Commands/General/ai.js @@ -0,0 +1,122 @@ +// SupportBot | Emerald Services +// AI Command (Consolidated) + +const fs = require("fs"); +const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, MessageFlags } = require("discord.js"); +const yaml = require("js-yaml"); + +const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); +const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const supportbotai = yaml.load(fs.readFileSync("./Configs/supportbot-ai.yml", "utf8")); + +const Command = require("../../Structures/Command.js"); +const AIDatabase = require("../../Structures/AIDatabase.js"); + +module.exports = new Command({ + name: cmdconfig.AI?.Command || "ai", + description: cmdconfig.AI?.Description || "Manage SupportBot AI settings and memory.", + type: ApplicationCommandType.ChatInput, + options: [ + { + name: cmdconfig.AI?.Train?.Command || "train", + description: cmdconfig.AI?.Train?.Description || "Train the AI by adding knowledge to its database.", + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + name: "content", + description: "The text/knowledge you want the bot to learn", + type: ApplicationCommandOptionType.String, + required: true, + }, + ], + }, + { + name: cmdconfig.AI?.Knowledge?.Command || "knowledge", + description: cmdconfig.AI?.Knowledge?.Description || "Manage the AI knowledge base.", + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: cmdconfig.AI?.Knowledge?.Clear?.Command || "clear", + description: cmdconfig.AI?.Knowledge?.Clear?.Description || "Clear all trained knowledge from the AI.", + type: ApplicationCommandOptionType.Subcommand, + }, + ], + }, + { + name: cmdconfig.AI?.Memory?.Command || "memory", + description: cmdconfig.AI?.Memory?.Description || "Manage the AI conversational memory.", + type: ApplicationCommandOptionType.SubcommandGroup, + options: [ + { + name: cmdconfig.AI?.Memory?.Clear?.Command || "clear", + description: cmdconfig.AI?.Memory?.Clear?.Description || "Clear recent conversation memory for this channel.", + type: ApplicationCommandOptionType.Subcommand, + }, + ], + }, + ], + permissions: cmdconfig.AI?.Permission || ["Administrator"], + + async run(interaction) { + if (!supportbotai.Enabled) { + return interaction.reply({ + content: "SupportBot AI is currently disabled.", + flags: MessageFlags.Ephemeral, + }); + } + + const subcommandGroup = interaction.options.getSubcommandGroup(false); + const subcommand = interaction.options.getSubcommand(false); + + try { + if (subcommand === "train" || subcommand === cmdconfig.AI?.Train?.Command) { + const content = interaction.options.getString("content"); + + AIDatabase.addKnowledge({ + sourceType: "user_command", + sourceName: interaction.user.tag, + title: "Trained Knowledge", + content: content, + }); + + const SuccessEmbed = new EmbedBuilder() + .setDescription(`✅ **Knowledge Added!**\n\nThe AI has learned the following information:\n\`\`\`${content}\`\`\``) + .setColor(supportbot.Embed.Colours.Success); + + return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral }); + } + + if (subcommandGroup === (cmdconfig.AI?.Knowledge?.Command || "knowledge")) { + if (subcommand === (cmdconfig.AI?.Knowledge?.Clear?.Command || "clear")) { + AIDatabase.clearKnowledge(); + + const SuccessEmbed = new EmbedBuilder() + .setDescription(`✅ **Knowledge Cleared!**\n\nAll manually trained knowledge has been wiped from the database.`) + .setColor(supportbot.Embed.Colours.Success); + + return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral }); + } + } + + if (subcommandGroup === (cmdconfig.AI?.Memory?.Command || "memory")) { + if (subcommand === (cmdconfig.AI?.Memory?.Clear?.Command || "clear")) { + AIDatabase.clearConversations(interaction.channel.id); + + const SuccessEmbed = new EmbedBuilder() + .setDescription(`✅ **Memory Cleared!**\n\nThe AI's recent conversation memory in this channel has been wiped. It will no longer remember previous messages sent here.`) + .setColor(supportbot.Embed.Colours.Success); + + return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral }); + } + } + + } catch (error) { + console.error("AI Command Error:", error); + const ErrorEmbed = new EmbedBuilder() + .setDescription(`❌ **Error:** Failed to process the AI command.`) + .setColor(supportbot.Embed.Colours.Error); + + return interaction.reply({ embeds: [ErrorEmbed], flags: MessageFlags.Ephemeral }); + } + }, +}); diff --git a/Commands/Tickets/manageticket.js b/Commands/Tickets/manageticket.js index 6b04fd6..4be8c57 100644 --- a/Commands/Tickets/manageticket.js +++ b/Commands/Tickets/manageticket.js @@ -201,7 +201,7 @@ module.exports = new Command({ 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)) + (!supportbot.Roles?.Mod?.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id)) ) { return interaction.reply({ embeds: [NoPerms], @@ -270,7 +270,7 @@ module.exports = new Command({ await interaction.channel.setParent(targetCategory.id); if (oldRoleId && oldRoleId !== targetRoleId) { - await interaction.channel.permissionOverwrites.delete(oldRoleId).catch(() => {}); + await interaction.channel.permissionOverwrites.delete(oldRoleId).catch(() => { }); } if (targetRoleId) { @@ -298,7 +298,7 @@ module.exports = new Command({ ); if (interaction.channel.name !== newChannelName) { - await interaction.channel.setName(newChannelName).catch(() => {}); + await interaction.channel.setName(newChannelName).catch(() => { }); } const deptEmoji = departmentConfig.Emoji || "🎫"; @@ -354,7 +354,7 @@ module.exports = new Command({ const newChannelName = buildTicketChannelName(ticketNumber, level, departmentConfig); if (interaction.channel.name !== newChannelName) { - await interaction.channel.setName(newChannelName).catch(() => {}); + await interaction.channel.setName(newChannelName).catch(() => { }); } const priorityEmoji = priorityConfig.Emoji || "🟡"; @@ -448,7 +448,7 @@ module.exports = new Command({ ) .setColor(supportbot.Embed.Colours.General); - await userToAdd.send({ embeds: [addedToTicketEmbed] }).catch(() => {}); + await userToAdd.send({ embeds: [addedToTicketEmbed] }).catch(() => { }); } } catch (err) { console.error("Error adding user to the ticket:", err); @@ -517,7 +517,7 @@ module.exports = new Command({ ) .setColor(supportbot.Embed.Colours.General); - await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => {}); + await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => { }); } } catch (err) { console.error("Error removing user from the ticket:", err); @@ -633,7 +633,7 @@ module.exports = new Command({ flags: MessageFlags.Ephemeral, }); - await interaction.channel.delete().catch(() => {}); + await interaction.channel.delete().catch(() => { }); } catch (err) { console.error("Error closing ticket:", err); diff --git a/Commands/Tickets/ticket.js b/Commands/Tickets/ticket.js index 11ca0b6..8b0952f 100644 --- a/Commands/Tickets/ticket.js +++ b/Commands/Tickets/ticket.js @@ -595,8 +595,12 @@ module.exports = new Command({ async run(interaction) { try { + if (!interaction.deferred && !interaction.replied) { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch(() => {}); + } + if (await isBlacklisted(interaction.user.id)) { - return interaction.reply({ + return interaction.editReply({ content: msgconfig.Ticket.Blacklisted, flags: MessageFlags.Ephemeral, }); @@ -620,7 +624,7 @@ module.exports = new Command({ (t) => t.user === interaction.user.id && t.open, ).length >= supportbot.Ticket.TicketsPerUser ) { - return interaction.reply({ + return interaction.editReply({ embeds: [ { title: "Too Many Tickets!", @@ -668,7 +672,7 @@ module.exports = new Command({ (ticketChannel) => ticketChannel.name === ticketChannelName, ) ) { - return await interaction.reply({ + return await interaction.editReply({ embeds: [TicketExists], flags: MessageFlags.Ephemeral, }); @@ -691,7 +695,7 @@ module.exports = new Command({ : null; if (!Staff || !Admin) { - return interaction.reply({ + return interaction.editReply({ content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", flags: MessageFlags.Ephemeral, @@ -731,7 +735,7 @@ module.exports = new Command({ ); if (!category) { - return interaction.reply({ + return interaction.editReply({ content: "The ticket category does not exist!", flags: MessageFlags.Ephemeral, }); @@ -758,7 +762,7 @@ module.exports = new Command({ if (secondaryCategory) { await ticketChannel.setParent(secondaryCategory.id); } else { - return interaction.reply({ + return interaction.editReply({ content: "The secondary ticket category does not exist!", flags: MessageFlags.Ephemeral, }); @@ -844,7 +848,7 @@ module.exports = new Command({ ) .setColor(supportbot.Embed.Colours.General); - await interaction.reply({ + await interaction.editReply({ embeds: [CreatedTicket], flags: MessageFlags.Ephemeral, }); @@ -1050,7 +1054,7 @@ module.exports = new Command({ console.error("Error in run method:", error); if (!interaction.replied && !interaction.deferred) { - return interaction.reply({ + return interaction.editReply({ content: "An error occurred while creating the ticket.", flags: MessageFlags.Ephemeral, }); diff --git a/Commands/Tickets/ticketpanel.js b/Commands/Tickets/ticketpanel.js index dd9b002..1fab59f 100644 --- a/Commands/Tickets/ticketpanel.js +++ b/Commands/Tickets/ticketpanel.js @@ -14,6 +14,8 @@ const { SeparatorBuilder, SectionBuilder, StringSelectMenuBuilder, + ThumbnailBuilder, + EmbedBuilder, } = require("discord.js"); const yaml = require("js-yaml"); @@ -92,7 +94,7 @@ module.exports = new Command({ try { const oldMessage = await channel.messages.fetch(panelRow.message_id); if (oldMessage) { - await oldMessage.delete().catch(() => {}); + await oldMessage.delete().catch(() => { }); } } catch (err) { console.log(`[TICKET PANEL] Previous panel message not found, creating a new one.`); @@ -139,9 +141,9 @@ module.exports = new Command({ .setCustomId(panelconfig.DepartmentMenu?.CustomId || "ticketdepartmentselect") .setPlaceholder( panelconfig.DepartmentMenu?.Placeholder || - departmentSystem.Placeholder || - panelconfig.Button?.Text || - "Choose a ticket department", + departmentSystem.Placeholder || + panelconfig.Button?.Text || + "Choose a ticket department", ) .addOptions(departmentOptions); } else { @@ -172,6 +174,8 @@ module.exports = new Command({ panelconfig.Style?.Title || "## SupportBot Ticket Creation", ); + const rawTitle = (panelconfig.Style?.Title || "SupportBot Ticket Creation").replace(/^#+\s*/, ""); + const panelDesc = new TextDisplayBuilder().setContent( panelconfig.Style?.Description || "Click on the button below to create a support ticket.", ); @@ -252,6 +256,49 @@ module.exports = new Command({ panelContainer.addActionRowComponents(new ActionRowBuilder().addComponents(openerComponent)); } + if (layout === "4") { + // Create a classic Discord Embed for Layout 4 + const oldEmbed = new EmbedBuilder() + .setTitle(rawTitle) + .setDescription(panelconfig.Style?.Description || "Click on the button below to create a support ticket."); + + if (panelconfig.Style?.Color) { + oldEmbed.setColor(panelconfig.Style.Color); + } + + if (showImage && panelconfig.Style?.Image) { + oldEmbed.setThumbnail(panelconfig.Style.Image); + } + + try { + const message = await channel.send({ + embeds: [oldEmbed], + components: [new ActionRowBuilder().addComponents(openerComponent)], + }); + + db.saveTicketPanel(message.id, channel.id); + + return interaction.reply({ + content: shouldUseDepartments + ? getPanelMessage( + "PanelSentWithDepartments", + "Ticket panel message with department selection has been sent!", + ) + : getPanelMessage("PanelSent", "Ticket panel message has been sent!"), + flags: MessageFlags.Ephemeral, + }); + } catch (e) { + console.log("Error sending message:", e); + return interaction.reply({ + content: getPanelMessage( + "SendError", + "An error occurred while sending the ticket panel message.", + ), + flags: MessageFlags.Ephemeral, + }); + } + } + if (showImage && layout === "1") { const panelImage = new MediaGalleryBuilder().addItems([ { @@ -275,9 +322,9 @@ module.exports = new Command({ return interaction.reply({ content: shouldUseDepartments ? getPanelMessage( - "PanelSentWithDepartments", - "Ticket panel message with department selection has been sent!", - ) + "PanelSentWithDepartments", + "Ticket panel message with department selection has been sent!", + ) : getPanelMessage("PanelSent", "Ticket panel message has been sent!"), flags: MessageFlags.Ephemeral, }); diff --git a/Configs/api.yml b/Configs/api.yml new file mode 100644 index 0000000..af83968 --- /dev/null +++ b/Configs/api.yml @@ -0,0 +1,8 @@ +# ------------------------------- +# API Server Configuration +# ------------------------------- + +API: + Enabled: true + Port: 3000 # PORT NUMBER FOR YOUR BOT + SecretKey: "PUT_RANDOM_STRING_HERE" # DO NOT SHARE THIS KEY WITH ANYONE \ No newline at end of file diff --git a/Configs/commands.yml b/Configs/commands.yml index 76154db..766e647 100644 --- a/Configs/commands.yml +++ b/Configs/commands.yml @@ -149,6 +149,30 @@ Settings: Permission: ["Administrator"] Enabled: true +AI: + Command: "ai" + Description: "Manage SupportBot AI settings and memory." + Permission: ["Administrator"] + Enabled: true + + Train: + Command: "train" + Description: "Train the AI by adding knowledge to its database." + + Knowledge: + Command: "knowledge" + Description: "Manage the AI knowledge base." + Clear: + Command: "clear" + Description: "Clear all trained knowledge from the AI." + + Memory: + Command: "memory" + Description: "Manage the AI conversational memory." + Clear: + Command: "clear" + Description: "Clear recent conversation memory for this channel." + # --------------------------------------- # Command Description Configuration # --------------------------------------- diff --git a/Configs/supportbot-ai.yml b/Configs/supportbot-ai.yml index dc2e889..da9534a 100644 --- a/Configs/supportbot-ai.yml +++ b/Configs/supportbot-ai.yml @@ -8,12 +8,13 @@ Enabled: true General: - Provider: "openai" - Model: "gpt-5.4" - Model_API_Key: "AI_API_KEY" + Provider: "claude" + Model: "claude-sonnet-4-6" + Model_API_Key: "MODEL_API_KEY" # Providers Available: # "openai" - Use OpenAI's GPT models. Example: "gpt-5.4" "gpt-5-mini" # "groq" - Use Groq's GroqAI models. Example: "meta-llama/llama-4-scout-17b-16e-instruct" + # "claude" - Use Anthropic's Claude models. Example: "claude-sonnet-4-6" Tokens: 1200 @@ -34,7 +35,7 @@ Features: SuggestTickets: true # Should SupportBot AI suggest creating a ticket if the user needs more help? Channels: - AIChannel: "CHANNEL_ID" + AIChannel: "1469028791312715902" Embed: Color: "#45b463" diff --git a/Configs/supportbot.yml b/Configs/supportbot.yml index 15f9739..21f77d1 100644 --- a/Configs/supportbot.yml +++ b/Configs/supportbot.yml @@ -12,7 +12,7 @@ # # SupportBot Configuration (supportbot.yml) -SupportBot_Version: "v26.0.0" +SupportBot_Version: "v26.1 DEV-2" # ------------------------- # General @@ -154,7 +154,7 @@ Ticket: DMOnRemove: true # DM the user when they are removed from the ticket DMTranscript: true # DM the user a transcript of the ticket when it is closed - TicketHome: "CHANNEL_ID" + TicketHome: "1469028381638000725" # What is TicketHome? # - Channel where the Ticket creation panel is sent. @@ -237,13 +237,14 @@ Ticket: # ------------------------- DepartmentSystem: - Enabled: false + Enabled: true Placeholder: "Choose a ticket department" DefaultDepartment: "general" Departments: general: Name: "General Support" + Description: "Open a general support ticket" Emoji: "🎫" Category: "CATEGORY_ID" Role: "ROLE_ID" @@ -255,6 +256,7 @@ Ticket: purchase: Name: "Purchase Support" + Description: "Get help with store purchases" Emoji: "💳" Category: "CATEGORY_ID" Role: "ROLE_ID" @@ -267,6 +269,7 @@ Ticket: reports: Name: "Player Report" + Description: "Report a player for breaking the rules" Emoji: "🚨" Category: "CATEGORY_ID" Role: "ROLE_ID" @@ -279,6 +282,7 @@ Ticket: appeals: Name: "Ban Appeals" + Description: "Appeal a server punishment" Emoji: "⚖️" Category: "CATEGORY_ID" Role: "ROLE_ID" diff --git a/Configs/ticket-panel.yml b/Configs/ticket-panel.yml index 27eb938..9817200 100644 --- a/Configs/ticket-panel.yml +++ b/Configs/ticket-panel.yml @@ -24,12 +24,13 @@ Style: Description: "Click on the button to file a `Ticket` and our team will try their best to assist you in the best way we can!" Image: "https://i.imgur.com/dejQvkg.png" Color: "#45b463" - Layout: "3" + Layout: "4" # Available Layout Styles # "1" - Style 1 - Title & Text, Button at the top -- Image at the bottom # "2" - Style 2 - Title & Text, Button at the bottom -- Image at the top # "3" - Style 3 - The classic look - Title & Text at the top -- Image in the middle -- Button at the bottom +# "4" - Style 4 - Classic Embed (Title & Text, Image on right, Button at bottom) Button: Text: "Create Ticket" diff --git a/Events/Models/claude.js b/Events/Models/claude.js new file mode 100644 index 0000000..2b38198 --- /dev/null +++ b/Events/Models/claude.js @@ -0,0 +1,84 @@ +// Emerald Services | SupportBot +// Claude integration for SupportBot AI + +const fs = require("fs"); +const yaml = require("js-yaml"); +const axios = require("axios"); + +const supportbotai = yaml.load( + fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"), +); + +function extractTextFromMessage(message) { + if (!message) return ""; + + if (typeof message === "string") { + return message.trim(); + } + + if (message.text) { + return message.text.trim(); + } + + if (Array.isArray(message)) { + return message + .map((part) => { + if (typeof part === "string") return part; + if (part?.type === "text" && typeof part.text === "string") { + return part.text; + } + return ""; + }) + .join("") + .trim(); + } + + return ""; +} + +async function chat({ messages, temperature, maxTokens }) { + const model = supportbotai.General.Model; + const apiKey = supportbotai.General.Model_API_Key; + + const systemMessages = messages.filter((m) => m.role === "system"); + const systemPrompt = systemMessages.map((m) => m.content).join("\n\n"); + + const userAssistantMessages = messages.filter((m) => m.role !== "system"); + + const response = await axios.post( + "https://api.anthropic.com/v1/messages", + { + model, + system: systemPrompt || undefined, + messages: userAssistantMessages, + temperature: temperature || supportbotai.General.Temperature || 0.4, + max_tokens: maxTokens || supportbotai.General.Tokens || 800, + }, + { + headers: { + "x-api-key": apiKey, + "anthropic-version": "2023-06-01", + "content-type": "application/json", + }, + } + ); + + const data = response.data; + const content = extractTextFromMessage(data.content); + + if (!content) { + console.error("Claude raw response:", JSON.stringify(data, null, 2)); + throw new Error( + `Claude returned empty response. stop_reason=${data?.stop_reason || "unknown"}`, + ); + } + + return { + content, + raw: data, + }; +} + +module.exports = { + chat, +}; diff --git a/Events/messageCreate.js b/Events/messageCreate.js index 55a8d8c..35506b1 100644 --- a/Events/messageCreate.js +++ b/Events/messageCreate.js @@ -16,6 +16,7 @@ const db = require("../Structures/Database.js"); const openai = require("./Models/openai.js"); const groq = require("./Models/groq.js"); +const claude = require("./Models/claude.js"); const PASTEBIN_API_KEY = supportbotai.General.PastebinAPI_Key; const PASTEBIN_API_URL = supportbotai.General.PastebinAPI_URL; @@ -57,6 +58,9 @@ function getProvider() { case "groq": return groq; + case "claude": + return claude; + default: throw new Error(`Unsupported AI provider: ${provider}`); } @@ -103,6 +107,9 @@ function buildSystemPrompt(message) { sections.push(...supportbotai.CustomInstructions); } + sections.push( + `I am currently powered by the ${supportbotai.General.Provider || "Unknown"} provider, running the ${supportbotai.General.Model || "Unknown"} model.`, + ); sections.push( `The current guild is: ${message.guild?.name || "Unknown Guild"}`, ); @@ -131,24 +138,24 @@ function buildConversationFromMemory(currentMessage) { const recentMessages = typeof AIDatabase.getRecentMessagesByScope === "function" ? AIDatabase.getRecentMessagesByScope({ - guildId: currentMessage.guild?.id || null, - channelId: currentMessage.channel?.id || null, - limit: maxRecentMessages, - scope: memoryScope, - }) + guildId: currentMessage.guild?.id || null, + channelId: currentMessage.channel?.id || null, + limit: maxRecentMessages, + scope: memoryScope, + }) : AIDatabase.getRecentMessages( - currentMessage.channel?.id, - maxRecentMessages, - ); + currentMessage.channel?.id, + maxRecentMessages, + ); const latestSummary = memoryConfig.StoreSummaries !== false ? typeof AIDatabase.getLatestSummaryByScope === "function" ? AIDatabase.getLatestSummaryByScope({ - guildId: currentMessage.guild?.id || null, - channelId: currentMessage.channel?.id || null, - scope: memoryScope, - }) + guildId: currentMessage.guild?.id || null, + channelId: currentMessage.channel?.id || null, + scope: memoryScope, + }) : AIDatabase.getLatestSummary(currentMessage.channel?.id) : null; @@ -156,29 +163,33 @@ function buildConversationFromMemory(currentMessage) { memoryConfig.StoreFacts !== false ? typeof AIDatabase.getFactsByScope === "function" ? AIDatabase.getFactsByScope({ - guildId: currentMessage.guild?.id || null, - channelId: currentMessage.channel?.id || null, - scope: memoryScope, - }) + guildId: currentMessage.guild?.id || null, + channelId: currentMessage.channel?.id || null, + scope: memoryScope, + }) : AIDatabase.getFacts(currentMessage.channel?.id) : []; const userFacts = memoryConfig.StoreFacts !== false && - typeof AIDatabase.getUserFactsByScope === "function" + typeof AIDatabase.getUserFactsByScope === "function" ? AIDatabase.getUserFactsByScope({ - guildId: currentMessage.guild?.id || null, - userId: currentMessage.author?.id || null, - scope: memoryScope, - }) + guildId: currentMessage.guild?.id || null, + userId: currentMessage.author?.id || null, + scope: memoryScope, + }) : memoryConfig.StoreFacts !== false && - typeof AIDatabase.getUserFacts === "function" + typeof AIDatabase.getUserFacts === "function" ? AIDatabase.getUserFacts( - currentMessage.guild?.id, - currentMessage.author?.id, - ) + currentMessage.guild?.id, + currentMessage.author?.id, + ) : []; + const knowledgeBase = typeof AIDatabase.getKnowledge === "function" + ? AIDatabase.getKnowledge(25) + : []; + if (latestSummary?.summary) { conversation.push({ role: "system", @@ -212,6 +223,17 @@ function buildConversationFromMemory(currentMessage) { }); } + if (knowledgeBase.length > 0) { + const knowledgeText = knowledgeBase + .map((k) => `- ${k.content}`) + .join("\n"); + + conversation.push({ + role: "system", + content: `Server Knowledge Base (use this to answer questions):\n${knowledgeText}`, + }); + } + for (const msg of recentMessages) { conversation.push({ role: msg.role === "assistant" ? "assistant" : "user", @@ -431,7 +453,7 @@ module.exports = new Event("messageCreate", async (client, message) => { } const typingInterval = setInterval(() => { - message.channel.sendTyping().catch(() => {}); + message.channel.sendTyping().catch(() => { }); }, 5000); try { @@ -475,7 +497,7 @@ module.exports = new Event("messageCreate", async (client, message) => { const embed = new Discord.EmbedBuilder() .setDescription( supportbotai.Messages?.ErrorResponse || - "I couldn't generate a response.", + "I couldn't generate a response.", ) .setColor(supportbotai.Embed.Color); @@ -530,16 +552,16 @@ module.exports = new Event("messageCreate", async (client, message) => { ) { const hintMessage = await message.channel.send( supportbotai.Messages?.SuggestTicketText || - "Need more help? React with 🎫 to open a ticket.", + "Need more help? React with 🎫 to open a ticket.", ); await hintMessage .react(supportbotai.Messages?.SuggestTicketReaction || "🎫") - .catch(() => {}); + .catch(() => { }); setTimeout( () => { - hintMessage.delete().catch(() => {}); + hintMessage.delete().catch(() => { }); }, 5 * 60 * 1000, ); @@ -552,11 +574,11 @@ module.exports = new Event("messageCreate", async (client, message) => { const embed = new Discord.EmbedBuilder() .setDescription( supportbotai.Messages?.AIError || - supportbotai.Messages?.ErrorResponse || - "There was an error contacting the AI provider.", + supportbotai.Messages?.ErrorResponse || + "There was an error contacting the AI provider.", ) .setColor(supportbot.Embed.Colours.Error); - message.reply({ embeds: [embed] }).catch(() => {}); + message.reply({ embeds: [embed] }).catch(() => { }); } }); \ No newline at end of file diff --git a/Structures/AIDatabase.js b/Structures/AIDatabase.js index 9d3dc0a..0a672a1 100644 --- a/Structures/AIDatabase.js +++ b/Structures/AIDatabase.js @@ -426,4 +426,13 @@ module.exports = { ) .all(limit); }, + + clearKnowledge() { + db.prepare(`DELETE FROM ai_knowledge`).run(); + }, + + clearConversations(channelId) { + if (!channelId) return; + db.prepare(`DELETE FROM ai_conversations WHERE channel_id = ?`).run(channelId); + }, }; \ No newline at end of file diff --git a/Structures/Client.js b/Structures/Client.js index 4d0a6b3..57ff112 100644 --- a/Structures/Client.js +++ b/Structures/Client.js @@ -119,28 +119,39 @@ class Client extends Discord.Client { if (supportbot.General.Addons.Enabled) { const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js")); - const addons = addonFiles.map((file) => { - const addon = require(`../Addons/${file}`); - addon.name = file.split(".")[0]; - return addon; - }); - section("Addons"); - addons.forEach((addon) => { - console.log(`${c.green}✓${c.reset} ${c.white}${addon.name}${c.reset}`); + addonFiles.forEach((file) => { + let addon = require(`../Addons/${file}`); + + // Handle single or multiple addons per file + const addonArray = Array.isArray(addon) ? addon : [addon]; + + addonArray.forEach((item, index) => { + // If name is missing, use filename (handling index for arrays) + if (!item.name) { + item.name = index === 0 ? file.split(".")[0] : `${file.split(".")[0]}_${index}`; + } - if (addon instanceof Command) { - this.commands.set(addon.name, addon); - } + console.log(`${c.green}✓${c.reset} ${c.white}${item.name}${c.reset}`); - if (addon.events && Array.isArray(addon.events)) { - addon.events.forEach((event) => { - if (event instanceof Event) { - this.on(event.event, (...args) => event.run(this, ...args)); - } - }); - } + if (item instanceof Command) { + this.commands.set(item.name, item); + } + + if (item.events && Array.isArray(item.events)) { + item.events.forEach((event) => { + if (event instanceof Event) { + this.on(event.event, (...args) => event.run(this, ...args)); + } + }); + } + + // If it's a direct Event instance + if (item instanceof Event) { + this.on(item.event, (...args) => item.run(this, ...args)); + } + }); }); } diff --git a/index.js b/index.js index ed1edba..90ea158 100644 --- a/index.js +++ b/index.js @@ -23,8 +23,15 @@ const client = new Client({ intents: ['Guilds', 'GuildMembers', 'GuildMessages', 'MessageContent'] }); +const APIServer = require("./API/server.js"); + client.start(supportbot.General.Token); +client.once('ready', () => { + const api = new APIServer(client); + api.start(); +}); + // SupportBot - New Logging System const logTypes = ["Output", "Warn", "Error"]; diff --git a/package.json b/package.json index 2e53aa9..0ff95c3 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "@vitalets/google-translate-api": "^9.2.1", "axios": "^1.7.2", "better-sqlite3": "^12.2.0", + "cors": "^2.8.6", "discord.js": "^14.13.0", + "express": "^5.2.1", "figlet": "^1.9.3", "google-translate-api": "^2.3.0", "gradient-string": "^3.0.0", @@ -29,6 +31,8 @@ "js-yaml": "^4.1.0", "moment": "^2.29.3", "ms": "^2.1.3", - "openai": "^4.50.0" + "openai": "^4.50.0", + "systeminformation": "^5.31.5", + "yaml": "^2.8.3" } }