const Discord = require("discord.js"); const axios = require("axios"); const supportbotai = require("../Structures/ConfigStore").supportbotAi; const supportbot = require("../Structures/ConfigStore").supportbot; const Event = require("../Structures/Event.js"); const AIDatabase = require("../Structures/AIDatabase.js"); 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; const CHANNELS = [supportbotai.Channels.AIChannel].filter(Boolean); async function createPaste(content) { try { if (!PASTEBIN_API_KEY || !PASTEBIN_API_URL) return null; const response = await axios.post( PASTEBIN_API_URL, new URLSearchParams({ api_dev_key: PASTEBIN_API_KEY, api_option: "paste", api_paste_code: content, api_paste_private: 1, api_paste_expire_date: "1D", api_paste_format: "text", }), ); return response.data; } catch (error) { console.error("Pastebin Error:", error); return null; } } function getProvider() { const provider = String( supportbotai.General.Provider || "openai", ).toLowerCase(); switch (provider) { case "openai": return openai; case "groq": return groq; case "claude": return claude; default: throw new Error(`Unsupported AI provider: ${provider}`); } } function formatTicketQuestionAnswers(questionAnswers) { if (!Array.isArray(questionAnswers) || questionAnswers.length === 0) { return "No ticket question answers were saved."; } return questionAnswers .map((item, index) => { const question = item?.question || `Question ${index + 1}`; const answer = item?.answer || "No answer provided"; return `${index + 1}. ${question}\nAnswer: ${answer}`; }) .join("\n\n"); } function buildTicketContext(ticket, message) { if (!ticket) return ""; return ` Ticket Information: Ticket ID: ${ticket.ticket_id || ticket.id || "Unknown"} Subject: ${ticket.subject || ticket.reason || "No subject provided"} Department: ${ticket.department || "Not set"} Priority: ${ticket.priority || "Not set"} Created By: ${ticket.user_id || ticket.user || "Unknown"} Current User: ${message?.author?.tag || "Unknown"} (${message?.author?.id || "Unknown"}) Ticket Question Answers: ${formatTicketQuestionAnswers(ticket.questionAnswers)} Use this information when helping the user with their ticket. Do not invent missing ticket details. `; } function buildSystemPrompt(message) { const sections = []; if (Array.isArray(supportbotai.CustomInstructions)) { 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"}`, ); sections.push( `The current channel is: #${message.channel?.name || "unknown-channel"}`, ); return sections.join("\n- ").replace(/^/, "- "); } function buildConversationFromMemory(currentMessage) { const memoryConfig = supportbotai.Memory || {}; const memoryEnabled = memoryConfig.Enabled !== false; const maxRecentMessages = memoryConfig.MaxRecentMessages || 15; const memoryScope = String(memoryConfig.Scope || "guild").toLowerCase(); const conversation = [ { role: "system", content: buildSystemPrompt(currentMessage), }, ]; if (!memoryEnabled) return conversation; const recentMessages = typeof AIDatabase.getRecentMessagesByScope === "function" ? AIDatabase.getRecentMessagesByScope({ guildId: currentMessage.guild?.id || null, channelId: currentMessage.channel?.id || null, limit: maxRecentMessages, scope: memoryScope, }) : AIDatabase.getRecentMessages( 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, }) : AIDatabase.getLatestSummary(currentMessage.channel?.id) : null; const facts = memoryConfig.StoreFacts !== false ? typeof AIDatabase.getFactsByScope === "function" ? AIDatabase.getFactsByScope({ 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" ? AIDatabase.getUserFactsByScope({ guildId: currentMessage.guild?.id || null, userId: currentMessage.author?.id || null, scope: memoryScope, }) : memoryConfig.StoreFacts !== false && typeof AIDatabase.getUserFacts === "function" ? AIDatabase.getUserFacts( currentMessage.guild?.id, currentMessage.author?.id, ) : []; const knowledgeBase = typeof AIDatabase.getKnowledge === "function" ? AIDatabase.getKnowledge(25) : []; if (latestSummary?.summary) { conversation.push({ role: "system", content: `Shared memory summary (${memoryScope} scope):\n${latestSummary.summary}`, }); } if (facts.length > 0) { const factText = facts .map( (fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`, ) .join("\n"); conversation.push({ role: "system", content: `Known shared facts (${memoryScope} scope):\n${factText}`, }); } if (userFacts.length > 0) { const userFactText = userFacts .map( (fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`, ) .join("\n"); conversation.push({ role: "system", content: `Known facts about this user across chats:\n${userFactText}`, }); } 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", content: msg.content, }); } return conversation; } function getTicketContextForMessage(message) { if (!message?.channel) return null; if (typeof db.getTicketByAIChannel === "function") { const aiTicket = db.getTicketByAIChannel(message.channel.id); if (aiTicket) return aiTicket; } if (typeof db.getTicket === "function") { const directTicket = db.getTicket(message.channel.id); if (directTicket) return directTicket; } if (message.channel.isThread() && typeof db.getTicket === "function") { const parentTicket = db.getTicket(message.channel.parentId); if (parentTicket) return parentTicket; } return null; } function getAIMessageType() { return String(supportbotai.Style?.MessageType || "embed").toLowerCase(); } function shouldSuggestTicket(message, ticket) { if (supportbotai.Features?.SuggestTickets !== true) return false; if (ticket) return false; if (message.channel.isThread()) return false; return CHANNELS.includes(message.channel.id); } function getSuggestTicketText() { return ( supportbotai.Messages?.SuggestTicketText || "-# Need more help? React with 🎫 to open a ticket." ); } async function sendAIResponse({ client, message, replyContent, suggestTicket = false, }) { const messageType = getAIMessageType(); const ticketHint = suggestTicket ? `\n\n${getSuggestTicketText()}` : ""; if (messageType === "text") { return message.reply({ content: `${replyContent}${ticketHint}`, }); } if (messageType === "components_v2") { const { ContainerBuilder, TextDisplayBuilder, SeparatorBuilder, MessageFlags, } = Discord; const container = new ContainerBuilder(); if (supportbot.Embed?.Colours?.General) { container.setAccentColor( parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16), ); } const title = new TextDisplayBuilder().setContent( `**${supportbotai.General?.Name || "SupportBot AI"}**`, ); const body = new TextDisplayBuilder().setContent(replyContent); container.addTextDisplayComponents(title); container.addSeparatorComponents(new SeparatorBuilder().setDivider(true)); container.addTextDisplayComponents(body); if (suggestTicket) { container.addSeparatorComponents(new SeparatorBuilder().setDivider(true)); container.addTextDisplayComponents( new TextDisplayBuilder().setContent(getSuggestTicketText()), ); } return message.reply({ flags: MessageFlags.IsComponentsV2, components: [container], }); } const embed = new Discord.EmbedBuilder() .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL(), }) .setDescription(`${replyContent}${ticketHint}`) .setColor(supportbotai.Embed.Color) .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL(), }) .setTimestamp(); return message.reply({ embeds: [embed] }); } function rememberUserFacts(message) { if (!message?.guild || !message?.author || !message?.content) return; const content = message.content.trim(); const nameMatch = content.match(/^my name is\s+(.+)$/i); if (nameMatch && typeof AIDatabase.saveUserFact === "function") { AIDatabase.saveUserFact({ guildId: message.guild.id, userId: message.author.id, factType: "profile", factKey: "name", factValue: nameMatch[1].trim(), }); } const nicknameMatch = content.match(/^i(?:'| a)?m\s+(.+)$/i); if ( nicknameMatch && nicknameMatch[1] && nicknameMatch[1].length <= 32 && typeof AIDatabase.saveUserFact === "function" ) { AIDatabase.saveUserFact({ guildId: message.guild.id, userId: message.author.id, factType: "profile", factKey: "display_name", factValue: nicknameMatch[1].trim(), }); } const usesMatch = content.match(/^i use\s+(.+)$/i); if (usesMatch && typeof AIDatabase.saveUserFact === "function") { AIDatabase.saveUserFact({ guildId: message.guild.id, userId: message.author.id, factType: "preference", factKey: "uses", factValue: usesMatch[1].trim(), }); } } module.exports = new Event("messageCreate", async (client, message) => { if (message.guild && typeof db.getTicket === "function") { const activeTicket = db.getTicket(message.channel.id); if (activeTicket && global.apiServer) { global.apiServer.broadcast("ticket_message", { ticketId: message.channel.id, message: { id: message.id, author: { id: message.author.id, username: message.author.username, globalName: message.author.globalName || message.author.username, avatar: message.author.displayAvatarURL({ extension: "png", size: 64 }), bot: message.author.bot }, content: (() => { if (message.content && message.content.trim()) return message.content; if (Array.isArray(message.components) && message.components.length > 0) { const texts = []; for (const comp of message.components) { const data = typeof comp.toJSON === "function" ? comp.toJSON() : comp; if (data.content) texts.push(data.content); if (Array.isArray(data.components)) { for (const child of data.components) { const childData = typeof child.toJSON === "function" ? child.toJSON() : child; if (childData.content) texts.push(childData.content); if (childData.label && (childData.type === 10 || childData.type === "TEXT_DISPLAY")) { texts.push(childData.label); } } } } if (texts.length > 0) return texts.join("\n"); } return message.content || ""; })(), containerColor: (() => { if (Array.isArray(message.components)) { for (const comp of message.components) { const data = typeof comp.toJSON === "function" ? comp.toJSON() : comp; if (data.accent_color) { return `#${data.accent_color.toString(16).padStart(6, "0")}`; } } } return null; })(), embeds: (message.embeds || []).map(e => ({ title: e.title || null, description: e.description || null, color: e.color ? `#${e.color.toString(16).padStart(6, "0")}` : "#5865F2", author: e.author ? { name: e.author.name, iconURL: e.author.iconURL } : null, fields: (e.fields || []).map(f => ({ name: f.name, value: f.value, inline: f.inline })), footer: e.footer ? { text: e.footer.text, iconURL: e.footer.iconURL } : null, thumbnail: e.thumbnail?.url ? { url: e.thumbnail.url } : null, image: e.image?.url ? { url: e.image.url } : null, timestamp: e.timestamp || null })), components: (message.components || []).map(c => { const data = typeof c.toJSON === "function" ? c.toJSON() : c; return { type: data.type, components: (data.components || []).map(sub => { const subData = typeof sub.toJSON === "function" ? sub.toJSON() : sub; return { type: subData.type, label: subData.label || subData.content || null, style: subData.style || null, url: subData.url || null, customId: subData.custom_id || subData.customId || null, emoji: subData.emoji ? (subData.emoji.name || subData.emoji.id) : null }; }) }; }), attachments: Array.from((message.attachments || []).values()).map(a => ({ id: a.id, name: a.name, url: a.url, contentType: a.contentType })), createdAt: message.createdTimestamp } }); } } if (message.author.bot || !message.guild) return; let shouldRespond = CHANNELS.includes(message.channelId) || message.mentions.users.has(client.user.id); if (!shouldRespond && message.channel.isThread()) { let ticket = null; if (typeof db.getTicketByAIChannel === "function") { ticket = db.getTicketByAIChannel(message.channel.id); } if (!ticket && typeof db.getTicket === "function") { ticket = db.getTicket(message.channel.id); } if ( !ticket && message.channel.parentId && typeof db.getTicket === "function" ) { ticket = db.getTicket(message.channel.parentId); } if ( ticket && ticket.aiModeEnabled && ticket.aiChannelId === message.channel.id ) { shouldRespond = true; } } if (!shouldRespond) return; if (!supportbotai.Enabled) { const embed = new Discord.EmbedBuilder() .setAuthor({ name: supportbotai.General.Name, iconURL: client.user.displayAvatarURL(), }) .setDescription( supportbotai.Messages?.AIDisabled || "AI is currently disabled.", ) .setColor(supportbot.Embed.Colours.Error) .setFooter({ text: supportbotai.Embed.Footer, iconURL: message.author.displayAvatarURL(), }); return message.reply({ embeds: [embed] }); } const typingInterval = setInterval(() => { message.channel.sendTyping().catch(() => { }); }, 5000); try { const memoryConfig = supportbotai.Memory || {}; const memoryEnabled = memoryConfig.Enabled !== false; if (memoryEnabled) { AIDatabase.addMessage({ guildId: message.guild.id, channelId: message.channel.id, userId: message.author.id, role: "user", content: message.content, }); rememberUserFacts(message); } const ticket = getTicketContextForMessage(message); const conversation = buildConversationFromMemory(message); if (ticket) { conversation.push({ role: "system", content: buildTicketContext(ticket, message), }); } const provider = getProvider(); const response = await provider.chat({ messages: conversation, temperature: supportbotai.General.Temperature || 0.4, maxTokens: supportbotai.General.Tokens || 800, }); clearInterval(typingInterval); if (!response?.content) { const embed = new Discord.EmbedBuilder() .setDescription( supportbotai.Messages?.ErrorResponse || "I couldn't generate a response.", ) .setColor(supportbotai.Embed.Color); return message.reply({ embeds: [embed] }); } const replyContent = response.content; if (memoryEnabled) { AIDatabase.addMessage({ guildId: message.guild.id, channelId: message.channel.id, userId: client.user.id, role: "assistant", content: replyContent, }); } if (replyContent.length > 4000) { const pasteLink = await createPaste(replyContent); if (pasteLink) { const embed = new Discord.EmbedBuilder() .setDescription( ( supportbotai.Messages?.MessageTooLong || "My response was too long, so I uploaded it here: {pastebin}" ).replace("{pastebin}", pasteLink), ) .setColor(supportbotai.Embed.Color); return message.reply({ embeds: [embed] }); } return message.reply({ content: replyContent.slice(0, 1900), }); } const suggestTicket = shouldSuggestTicket(message, ticket); const sentMessage = await sendAIResponse({ client, message, replyContent, suggestTicket: false, }); if ( shouldSuggestTicket(message, ticket) && supportbotai.Features?.SuggestTickets === true ) { const hintMessage = await message.channel.send( supportbotai.Messages?.SuggestTicketText || "Need more help? React with 🎫 to open a ticket.", ); await hintMessage .react(supportbotai.Messages?.SuggestTicketReaction || "🎫") .catch(() => { }); setTimeout( () => { hintMessage.delete().catch(() => { }); }, 5 * 60 * 1000, ); } } catch (error) { clearInterval(typingInterval); console.error("AI Error:", error); const embed = new Discord.EmbedBuilder() .setDescription( supportbotai.Messages?.AIError || supportbotai.Messages?.ErrorResponse || "There was an error contacting the AI provider.", ) .setColor(supportbot.Embed.Colours.Error); message.reply({ embeds: [embed] }).catch(() => { }); } });