UNPKG

@getsolara/solara.js

Version:

A lightweight and modular Discord bot framework built on discord.js v14, with truly optional feature packages.

659 lines (617 loc) 40.8 kB
const { Client, Collection, GatewayIntentBits, Partials, REST, Routes, InteractionType, ChannelType, ApplicationCommandOptionType, Message, ActivityType } = require('discord.js'); const EventEmitter = require('events'); const path = require('path'); const fs = require('fs'); const CommandHandler = require('../handlers/CommandHandler'); const FunctionParser = require('../handlers/FunctionParser'); const { mapIntents, mapPartials, mapOptionType, mapChannelTypes, mapActivityType } = require('../utils/discordMappings'); const { DEFAULT_INTENTS, DEFAULT_PARTIALS, DEFAULT_PREFIX, COMMAND_TYPES, INTERACTION_COMMAND_TYPES, MESSAGE_COMMAND_TYPES } = require('../utils/constants'); const { loadFiles, requireUncached } = require('../utils/fileUtils'); class SolaraClient extends Client { constructor(options = {}) { const { solara = {}, intents: userIntents, partials: userPartials, ...djsOptions } = options; const solaraConfig = { prefix: DEFAULT_PREFIX, token: process.env.DISCORD_TOKEN, dbPath: 'solara_db.sqlite', loadBuiltinCoreFunctions: true, db: false, canvas: false, voice: false, replyToBots: false, triggerOnEdit: false, mobilePresence: false, verboseStartupLogging: true, allowSearchWithoutVoice: false, playlistImportLimit: 50, logWhileMaxIterationsReached: true, loopMaxIterations: 100, loopMinDelay: 100, repeatMaxIterations: 25, repeatMinDelay: 100, whileMaxIterations: 50, whileMinDelay: 100, ...solara }; let finalIntents; if (Array.isArray(userIntents) && userIntents.length > 0) { finalIntents = mapIntents(userIntents); } else { if (solaraConfig.verboseStartupLogging) { console.warn("Solara Core: No intents provided by user. Defaulting to Guilds, GuildMessages, MessageContent."); } finalIntents = [...DEFAULT_INTENTS].filter(intent => intent !== GatewayIntentBits.GuildVoiceStates); } if (!finalIntents.includes(GatewayIntentBits.Guilds)) { finalIntents.push(GatewayIntentBits.Guilds); } if (solaraConfig.voice && !finalIntents.includes(GatewayIntentBits.GuildVoiceStates)) { if (solaraConfig.verboseStartupLogging) { console.warn("Solara Core: Voice features enabled, adding missing 'GuildVoiceStates' intent."); } finalIntents.push(GatewayIntentBits.GuildVoiceStates); } if (!finalIntents.includes(GatewayIntentBits.GuildPresences) && (solaraConfig.status || solaraConfig.statuses)) { if (solaraConfig.verboseStartupLogging) { console.warn("Solara Core: Status functionality typically requires GuildPresences intent. Consider adding it."); } } if (solaraConfig.triggerOnEdit === true && !finalIntents.includes(GatewayIntentBits.MessageContent)) { console.warn("Solara Core: 'triggerOnEdit' is true but MessageContent intent is missing."); } let finalPartials; if (Array.isArray(userPartials) && userPartials.length > 0) { finalPartials = mapPartials(userPartials); } else { if (solaraConfig.verboseStartupLogging) { console.log("Solara Core: No partials provided by user. Defaulting to Channel, Message, GuildMember, User, Reaction."); } finalPartials = [...DEFAULT_PARTIALS]; } const clientOptions = { intents: finalIntents, partials: finalPartials, ...djsOptions }; if (solaraConfig.mobilePresence) { clientOptions.ws = { ...(clientOptions.ws || {}), properties: { ...(clientOptions.ws?.properties || {}), $browser: 'Discord Android' } }; if (solaraConfig.verboseStartupLogging) { console.log("Solara Core: Mobile presence enabled (Identify as Discord Android)."); } } super(clientOptions); this.solaraOptions = solaraConfig; this.commands = new Collection(); this.events = new Collection(); this.functions = new Collection(); this.variables = new Collection(); this.slashCommandsData = []; this.commandHandler = new CommandHandler(this); this.functionParser = new FunctionParser(this); this.statuses = []; this.currentStatusIndex = 0; this.statusUpdateInterval = null; this.statusChangeIntervalTime = 0; this.db = null; this.canvasInitialized = false; this.voiceInitialized = false; this.solaraVoiceConnections = null; this.solaraAudioPlayers = null; this.solaraVoiceQueues = null; this.solaraNowPlaying = null; this.solaraLoopModes = null; this.solaraVoiceVolumes = null; if (this.solaraOptions.db) { try { const solaraDbPackage = require('@getsolara/solara.db'); if (solaraDbPackage && typeof solaraDbPackage.initializeDatabaseFeatures === 'function') { this.db = solaraDbPackage.initializeDatabaseFeatures( this, { dbPath: this.solaraOptions.dbPath }, this.solaraOptions.verboseStartupLogging ); if (!this.db && this.solaraOptions.verboseStartupLogging) { console.warn("Solara Core: @getsolara/solara.db was found but failed to initialize database features properly."); } } else { console.warn("Solara Core: @getsolara/solara.db package found but is invalid. DB features unavailable."); } } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { console.warn("Solara Core: Database enabled, but '@getsolara/solara.db' not found. Install it or set 'solara.db: false'."); } else { console.error("Solara Core: Error loading or initializing @getsolara/solara.db:", error); } this.db = null; } } else { if (this.solaraOptions.verboseStartupLogging) { console.log("Solara Core: Database usage is disabled by configuration."); } } if (this.solaraOptions.canvas) { try { const solaraCanvasPackage = require('@getsolara/solara.canvas'); if (solaraCanvasPackage && typeof solaraCanvasPackage.initializeCanvasFeatures === 'function') { this.canvasInitialized = solaraCanvasPackage.initializeCanvasFeatures( this, {}, this.solaraOptions.verboseStartupLogging ); if (!this.canvasInitialized && this.solaraOptions.verboseStartupLogging) { console.warn("Solara Core: @getsolara/solara.canvas was found but failed to initialize canvas features properly."); } } else { console.warn("Solara Core: @getsolara/solara.canvas package found but is invalid. Canvas features unavailable."); } } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { console.warn("Solara Core: Canvas enabled, but '@getsolara/solara.canvas' not found. Install it or set 'solara.canvas: false'."); } else { console.error("Solara Core: Error loading or initializing @getsolara/solara.canvas:", error); } this.canvasInitialized = false; } } else { if (this.solaraOptions.verboseStartupLogging) { console.log("Solara Core: Canvas usage is disabled by configuration."); } } if (this.solaraOptions.voice) { try { const solaraVoicePackage = require('@getsolara/solara.voice'); if (solaraVoicePackage && typeof solaraVoicePackage.initializeVoiceFeatures === 'function') { this.voiceInitialized = solaraVoicePackage.initializeVoiceFeatures( this, {}, this.solaraOptions.verboseStartupLogging ); if (!this.voiceInitialized && this.solaraOptions.verboseStartupLogging) { console.warn("Solara Core: @getsolara/solara.voice was found but failed to initialize voice features properly."); } } else { console.warn("Solara Core: @getsolara/solara.voice package found but is invalid. Voice features unavailable."); } } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { console.warn("Solara Core: Voice enabled, but '@getsolara/solara.voice' not found. Install it or set 'solara.voice: false'."); } else { console.error("Solara Core: Error loading or initializing @getsolara/solara.voice:", error); } this.voiceInitialized = false; } } else { if (this.solaraOptions.verboseStartupLogging) { console.log("Solara Core: Voice usage is disabled by configuration."); } } this.customEvents = new EventEmitter(); if (this.solaraOptions.loadBuiltinCoreFunctions) { this._loadBuiltinCoreFunctions(); } if (!this.solaraOptions.token) { console.warn("Solara Core: Bot token not found. Provide it via SolaraClient options or the login() method."); } } register(commandData) { if (Array.isArray(commandData)) { let registered = false; commandData.forEach(data => { if (this._registerSingle(data)) registered = true; }); return registered; } else if (typeof commandData === 'object' && commandData !== null) { return this._registerSingle(commandData); } else { console.warn(`Solara Core: Invalid data passed to register. Expected object or array.`); return false; } } _registerSingle(data) { if (data.event && typeof data.event === 'string' && data.code) { return this._registerEventHandler(data); } else if (data.name && data.code) { return this._registerCommand(data); } else { if (data) { console.warn(`Solara Core: Invalid command/event data. Requires 'name'/'event' and 'code'.`, data); } return false; } } _registerEventHandler(handlerData) { const eventName = handlerData.event; if (!this.events.has(eventName)) { this.events.set(eventName, []); } this.events.get(eventName).push(handlerData); return true; } _registerCommand(commandData) { const nameLower = commandData.name.toLowerCase(); const type = commandData.type || COMMAND_TYPES.BOTH; const key = (type === COMMAND_TYPES.BUTTON || type === COMMAND_TYPES.SELECT_MENU || type === COMMAND_TYPES.MODAL_SUBMIT) ? commandData.name : nameLower; if (this.commands.has(key) && this.solaraOptions.verboseStartupLogging) { console.warn(`Solara Core: Duplicate command/interaction key detected "${key}". Overwriting.`); } this.commands.set(key, { ...commandData, type }); if (MESSAGE_COMMAND_TYPES.includes(type)) { if (commandData.aliases && Array.isArray(commandData.aliases)) { commandData.aliases.forEach(alias => { const aliasLower = alias.toLowerCase(); if (aliasLower === nameLower) return; if (this.commands.has(aliasLower)) { const conflictingCmd = this.commands.get(aliasLower); if (conflictingCmd !== commandData && (!conflictingCmd._isAlias || conflictingCmd._aliasFor !== nameLower) && this.solaraOptions.verboseStartupLogging ) { console.warn(`Solara Core: Alias "${aliasLower}" for "${commandData.name}" conflicts with existing command/alias for "${conflictingCmd.name}". Overwriting.`); } } this.commands.set(aliasLower, { ...commandData, type, _isAlias: true, _aliasFor: nameLower }); }); } } if (INTERACTION_COMMAND_TYPES.includes(type) && commandData.description) { this._prepareSlashCommandData(commandData, nameLower); } else if (INTERACTION_COMMAND_TYPES.includes(type) && !commandData.description) { console.warn(`Solara Core: Command "${commandData.name}" is interaction-compatible but needs a 'description' for slash command registration.`); } return true; } _prepareSlashCommandData(commandData, nameLower) { const slashData = { name: nameLower, description: commandData.description, options: [] }; if (commandData.options && Array.isArray(commandData.options)) { slashData.options = commandData.options.map(opt => { if (!opt.name || !opt.description || !opt.type) { console.warn(`Solara Core Slash Prep: Option for "${nameLower}" missing name, description, or type. Skipping.`, opt); return null; } const optionType = mapOptionType(opt.type); const processedOption = { name: opt.name.toLowerCase(), description: opt.description, type: optionType, required: opt.required === true }; if (opt.choices && Array.isArray(opt.choices)) { processedOption.choices = opt.choices.map(choice => { if (typeof choice === 'object' && choice.name && choice.value !== undefined) return { name: choice.name, value: choice.value }; if (typeof choice === 'string' && optionType === ApplicationCommandOptionType.String) return { name: choice, value: choice }; if (typeof choice === 'number' && (optionType === ApplicationCommandOptionType.Number || optionType === ApplicationCommandOptionType.Integer)) return { name: String(choice), value: choice }; console.warn(`Solara Core Slash Prep: Invalid choice for option "${opt.name}" in "${nameLower}". Skipping.`, choice); return null; }).filter(c => c !== null); if (processedOption.choices.length === 0) delete processedOption.choices; } if (opt.channel_types && Array.isArray(opt.channel_types) && optionType === ApplicationCommandOptionType.Channel) { processedOption.channel_types = mapChannelTypes(opt.channel_types); if (processedOption.channel_types.length === 0) delete processedOption.channel_types; } if (opt.min_value !== undefined && (optionType === ApplicationCommandOptionType.Integer || optionType === ApplicationCommandOptionType.Number)) processedOption.min_value = opt.min_value; if (opt.max_value !== undefined && (optionType === ApplicationCommandOptionType.Integer || optionType === ApplicationCommandOptionType.Number)) processedOption.max_value = opt.max_value; if (opt.min_length !== undefined && optionType === ApplicationCommandOptionType.String) processedOption.min_length = opt.min_length; if (opt.max_length !== undefined && optionType === ApplicationCommandOptionType.String) processedOption.max_length = opt.max_length; if (opt.autocomplete !== undefined && [ApplicationCommandOptionType.String, ApplicationCommandOptionType.Integer, ApplicationCommandOptionType.Number].includes(optionType)) { processedOption.autocomplete = !!opt.autocomplete; if (processedOption.autocomplete && processedOption.choices) { console.warn(`Solara Core Slash Prep: Option "${opt.name}" in "${nameLower}" has autocomplete=true and choices. Choices ignored by Discord.`); delete processedOption.choices; } } return processedOption; }).filter(opt => opt !== null); } const existingIndex = this.slashCommandsData.findIndex(cmd => cmd.name === nameLower); if (existingIndex !== -1) this.slashCommandsData[existingIndex] = slashData; else this.slashCommandsData.push(slashData); } async registerSlashCommands() { const token = this.token || this.solaraOptions.token; const clientId = this.user?.id; if (!token) { console.warn("Solara Core: Cannot register slash commands - Bot token missing."); return; } if (!clientId) { console.warn("Solara Core: Cannot register slash commands - Client ID unavailable (Bot not ready?). Retrying on 'ready'."); this.once('ready', () => this.registerSlashCommands()); return; } if (this.slashCommandsData.length === 0) { if (this.solaraOptions.verboseStartupLogging) console.log("Solara Core: No slash commands prepared to register."); return; } const rest = new REST({ version: '10' }).setToken(token); try { if (this.solaraOptions.verboseStartupLogging) console.log(`Solara Core: Refreshing ${this.slashCommandsData.length} application (/) commands globally.`); const data = await rest.put(Routes.applicationCommands(clientId), { body: this.slashCommandsData }); console.log(`Solara Core: Successfully reloaded ${Array.isArray(data) ? data.length : 'unknown number of'} application (/) commands.`); } catch (error) { console.error("Solara Core: Failed to register application commands:", error.response?.data || error.message || error); } } loadCommands(dirPath, clearExisting = true) { const absolutePath = path.resolve(dirPath); if (!fs.existsSync(absolutePath)) { console.error(`Solara Core: Commands directory not found: ${absolutePath}`); return; } if (clearExisting) { if (this.solaraOptions.verboseStartupLogging) console.log("Solara Core: Clearing existing commands, events, and slash command data."); this.commands.clear(); this.events.clear(); this.slashCommandsData = []; } const commandFiles = loadFiles(absolutePath, '.js'); if (this.solaraOptions.verboseStartupLogging) console.log(`Solara Core: Loading ${commandFiles.length} command/event files from ${absolutePath}...`); let loadedCount = 0; let registeredCount = 0; for (const filePath of commandFiles) { let commandData = null; try { commandData = requireUncached(filePath); } catch (error) { console.error(`Solara Core: Critical error requiring command file ${path.basename(filePath)}. Skipping. Error: ${error.message}`); continue; } if (commandData) { loadedCount++; if (this.register(commandData)) registeredCount++; else if (this.solaraOptions.verboseStartupLogging) console.warn(`Solara Core: File ${path.basename(filePath)} loaded but contained invalid data or failed registration.`); } } console.log(`Solara Core: Processed ${loadedCount} command/event files, successfully registered ${registeredCount}. Prepared ${this.slashCommandsData.length} slash commands.`); } addFunction(funcData) { if (!funcData || typeof funcData.name !== 'string' || typeof funcData.execute !== 'function') { console.error(`Solara Core: Invalid function data provided. Requires 'name' (string) and 'execute' (function):`, funcData); return false; } let funcName = funcData.name; if (!funcName.startsWith('$')) { funcName = `$${funcName}`; if (this.solaraOptions.verboseStartupLogging) console.warn(`Solara Core: Function name "${funcData.name}" auto-prefixed to "${funcName}".`); } const nameLower = funcName.toLowerCase(); if (this.functions.has(nameLower) && this.solaraOptions.verboseStartupLogging) { console.warn(`Solara Core: Overwriting existing function "${nameLower}" (New source: ${funcData.name}, Path: ${funcData._filePath || 'unknown'}).`); } funcData._filePath = funcData._filePath || 'unknown'; this.functions.set(nameLower, { ...funcData, name: funcName }); return true; } loadFunctions(dirPath) { const absolutePath = path.resolve(dirPath); if (this.solaraOptions.verboseStartupLogging) console.log(`Solara Core: Loading user's custom functions from ${absolutePath}...`); if (!fs.existsSync(absolutePath)) { if (this.solaraOptions.verboseStartupLogging) console.warn(`Solara Core: User's custom functions directory not found: ${absolutePath}`); return; } const functionFiles = loadFiles(absolutePath, '.js'); let loadedCount = 0; for (const filePath of functionFiles) { let funcData = null; try { funcData = requireUncached(filePath); funcData._filePath = filePath; if (funcData && this.addFunction(funcData)) loadedCount++; else if (funcData && this.solaraOptions.verboseStartupLogging) console.warn(`Solara Core: User function ${path.basename(filePath)} loaded but failed registration.`); } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { const missingModule = error.message.split("'")[1] || "unknown"; console.warn(`Solara Core: Skipping user function ${path.basename(filePath)} due to missing dependency: '${missingModule}'.`); } else { console.error(`Solara Core: Error loading user function ${path.basename(filePath)}:`, error); } } } console.log(`Solara Core: Loaded ${loadedCount} user's custom functions.`); } _loadBuiltinCoreFunctions() { const funcDir = path.join(__dirname, '..', 'functions'); if (this.solaraOptions.verboseStartupLogging) { console.log(`Solara Core: Loading built-in core functions from ${funcDir}...`); } if (!fs.existsSync(funcDir)) { if (this.solaraOptions.verboseStartupLogging) console.warn(`Solara Core: Built-in core functions directory not found: ${funcDir}`); return; } const functionFiles = loadFiles(funcDir, '.js'); let loadedCount = 0; for (const filePath of functionFiles) { try { const funcData = require(filePath); funcData._filePath = filePath; const nameLower = funcData.name?.toLowerCase(); if (nameLower && !this.functions.has(nameLower)) { if(this.addFunction(funcData)) loadedCount++; } } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { const missingModuleMatch = error.message.match(/'([^']+)'/); const missingModule = missingModuleMatch ? missingModuleMatch[1] : "unknown"; console.warn(`Solara Core: Skipping built-in core function ${path.basename(filePath)} due to its own missing dependency: '${missingModule}'. This is unusual for a core function.`); } else { console.error(`Solara Core: Critical error loading built-in core function ${path.basename(filePath)}:`, error.message); } } } console.log(`Solara Core: Loaded ${loadedCount} built-in core functions.`); } status(statusData) { if (this.statusUpdateInterval) { clearInterval(this.statusUpdateInterval); this.statusUpdateInterval = null; } if (!statusData) { this.statuses = []; if (this.isReady() && this.user) this.user.setPresence({ activities: [], status: 'online', afk: false }); else this.once('ready', () => { this.user?.setPresence({ activities: [], status: 'online', afk: false }); }); if (this.solaraOptions.verboseStartupLogging) console.log("Solara Core: Status cleared."); return; } const rawStatusInput = Array.isArray(statusData) ? statusData : [statusData]; this.statuses = rawStatusInput.map(s => { if (!s || typeof s.name !== 'string' || typeof s.type !== 'string') { console.warn("Solara Status: Invalid status object (missing name/type). Skipping:", s); return null; } let finalPresenceStatus = 'online'; let finalAfk = s.afk === true; if (s.status && ['online', 'idle', 'dnd', 'invisible'].includes(s.status.toLowerCase())) { finalPresenceStatus = s.status.toLowerCase(); if (finalPresenceStatus === 'idle') finalAfk = true; } else if (s.afk === true) finalPresenceStatus = 'idle'; return { name: s.name, type: s.type.toUpperCase(), url: s.url, presenceStatus: finalPresenceStatus, afk: finalAfk, shardId: typeof s.shard === 'number' ? s.shard : undefined }; }).filter(s => s !== null); if (this.statuses.length === 0) { if (rawStatusInput.length > 0) console.warn("Solara Status: All provided status objects were invalid. Status not changed."); return; } this.statusChangeIntervalTime = 0; if (rawStatusInput.length > 0 && rawStatusInput[0]?.time > 0) this.statusChangeIntervalTime = rawStatusInput[0].time * 1000; else if (this.statuses.length > 1) this.statusChangeIntervalTime = 12000; this.currentStatusIndex = 0; const applyStatuses = () => { this._applyNextStatus(); if (this.statuses.length > 1 && this.statusChangeIntervalTime > 0) { this.statusUpdateInterval = setInterval(() => this._applyNextStatus(), this.statusChangeIntervalTime); } }; if (this.isReady()) applyStatuses(); else this.once('ready', applyStatuses); } _applyNextStatus() { if (!this.user || this.statuses.length === 0) return; const statusConfig = this.statuses[this.currentStatusIndex]; if (!statusConfig) { this.currentStatusIndex = (this.currentStatusIndex + 1) % this.statuses.length; return; } const activityType = mapActivityType(statusConfig.type); if (activityType === undefined) { console.warn(`Solara Status: Unknown activity type "${statusConfig.type}" for "${statusConfig.name}". Skipping.`); if (this.statuses.length > 0) this.currentStatusIndex = (this.currentStatusIndex + 1) % this.statuses.length; return; } const presenceData = { activities: [{ name: statusConfig.name, type: activityType, ...(activityType === ActivityType.Streaming && statusConfig.url ? { url: statusConfig.url } : {}) }], status: statusConfig.presenceStatus, afk: statusConfig.afk }; if (statusConfig.shardId !== undefined) presenceData.shardId = statusConfig.shardId; try { this.user.setPresence(presenceData); if (this.statuses.length > 0) this.currentStatusIndex = (this.currentStatusIndex + 1) % this.statuses.length; } catch (error) { console.error("Solara Status: Failed to set presence:", error); } } async login(token) { const botToken = token || this.solaraOptions.token; if (!botToken) throw new Error("Solara Core: Bot token missing."); this.solaraOptions.token = botToken; this._setupEventListeners(); console.log("Solara Core: Logging in..."); try { return await super.login(botToken); } catch (error) { console.error("Solara Core: Login failed:", error); throw error; } } _setupEventListeners() { this.once('ready', async () => { console.log(`Solara Core: Logged in as ${this.user?.tag}`); if (this.solaraOptions.db) { if (this.db) console.log("Solara Core: Database features (via @getsolara/solara.db) are active."); else console.warn("Solara Core: Database was configured, but @getsolara/solara.db failed to initialize or is missing."); } else if (this.solaraOptions.verboseStartupLogging) { console.log("Solara Core: Database features not configured for use."); } if (this.solaraOptions.canvas) { if (this.canvasInitialized) console.log("Solara Core: Canvas features (via @getsolara/solara.canvas) are active."); else console.warn("Solara Core: Canvas was configured, but @getsolara/solara.canvas failed to initialize or is missing."); } else if (this.solaraOptions.verboseStartupLogging) { console.log("Solara Core: Canvas features not configured for use."); } if (this.solaraOptions.voice) { if (this.voiceInitialized) console.log("Solara Core: Voice features (via @getsolara/solara.voice) are active."); else console.warn("Solara Core: Voice was configured, but @getsolara/solara.voice failed to initialize or is missing."); } else if (this.solaraOptions.verboseStartupLogging) { console.log("Solara Core: Voice features not configured for use."); } await this.registerSlashCommands(); if (this.solaraOptions.verboseStartupLogging && this.events.size > 0) { console.log(`Solara Core: Attaching ${this.events.size} types of event handlers...`); } let attachedCount = 0; for (const [eventName, handlers] of this.events) { this.on(eventName, async (...eventArgs) => { const context = eventArgs[0]; if (eventName === 'messageCreate' && context instanceof Message && (!this.solaraOptions.replyToBots && context.author?.bot)) return; for (const handlerData of handlers) { try { await this.commandHandler.executeCommand(handlerData, context, eventArgs.slice(1), eventName); } catch (error) { console.error(`Solara Core Event Dispatch: Uncaught error in handler for "${eventName}" (Handler: ${handlerData.name || 'unnamed'}):`, error); } } }); attachedCount += handlers.length; } if (attachedCount > 0) console.log(`Solara Core: Attached ${attachedCount} listeners across ${this.events.size} event types.`); else if (this.solaraOptions.verboseStartupLogging) console.log(`Solara Core: No user-defined event handlers found to attach.`); this.customEvents.emit('solaraReady', this); }); this.on('messageCreate', async (message) => { if ((!this.solaraOptions.replyToBots && message.author?.bot) || !message.guild) return; let effectivePrefix = null; if (typeof this.solaraOptions.prefix === 'function') { effectivePrefix = await this.solaraOptions.prefix(message); } else if (Array.isArray(this.solaraOptions.prefix)) { const lowerContent = message.content.toLowerCase(); for (const p of this.solaraOptions.prefix) { if (lowerContent.startsWith(String(p).toLowerCase())) { effectivePrefix = String(p); break; } } } else { effectivePrefix = this.solaraOptions.prefix; } if (effectivePrefix === null || effectivePrefix === undefined || !message.content.toLowerCase().startsWith(String(effectivePrefix).toLowerCase())) return; const args = message.content.slice(String(effectivePrefix).length).trim().split(/ +/); const commandName = args.shift()?.toLowerCase(); if (!commandName) return; const command = this.commands.get(commandName); if (command && MESSAGE_COMMAND_TYPES.includes(command.type)) { try { await this.commandHandler.executeCommand(command, message, args); } catch (error) { console.error(`Solara Core Message Handler: Error executing "${command.name}":`, error); } } }); if (this.solaraOptions.triggerOnEdit) { this.on('messageUpdate', async (oldMessage, newMessage) => { if (newMessage.partial) { try { await newMessage.fetch(); } catch (e) { if(this.solaraOptions.verboseStartupLogging) console.warn(`Solara Core MessageEdit: Could not fetch partial new message: ${e.message}`); return; }} if (oldMessage.partial && this.solaraOptions.verboseStartupLogging) { console.warn(`Solara Core MessageEdit: Old message was partial, content comparison might be iffy.`); } if ((!this.solaraOptions.replyToBots && newMessage.author?.bot) || !newMessage.guild || !newMessage.content) return; if (!oldMessage.partial && newMessage.content === oldMessage.content) return; const message = newMessage; let effectivePrefix = null; if (typeof this.solaraOptions.prefix === 'function') { effectivePrefix = await this.solaraOptions.prefix(message); } else if (Array.isArray(this.solaraOptions.prefix)) { const lowerContent = message.content.toLowerCase(); for (const p of this.solaraOptions.prefix) { if (lowerContent.startsWith(String(p).toLowerCase())) { effectivePrefix = String(p); break; } } } else { effectivePrefix = this.solaraOptions.prefix; } if (effectivePrefix === null || effectivePrefix === undefined || !message.content.toLowerCase().startsWith(String(effectivePrefix).toLowerCase())) return; const args = message.content.slice(String(effectivePrefix).length).trim().split(/ +/); const commandName = args.shift()?.toLowerCase(); if (!commandName) return; const command = this.commands.get(commandName); if (command && MESSAGE_COMMAND_TYPES.includes(command.type)) { try { await this.commandHandler.executeCommand(command, message, args); } catch (error) { console.error(`Solara Core MessageEdit Handler: Error executing "${command.name}" from edit:`, error); } } }); } this.on('interactionCreate', async (interaction) => { let commandIdentifier; let command; let expectedTypes = []; let isAutocomplete = false; const AUTHOR_ONLY_INTERACTION_PREFIX = "solara_ao_"; if (interaction.isChatInputCommand()) { commandIdentifier = interaction.commandName.toLowerCase(); expectedTypes = INTERACTION_COMMAND_TYPES; } else if (interaction.isButton()) { const rcid = interaction.customId; if (rcid.startsWith(AUTHOR_ONLY_INTERACTION_PREFIX)) { const p = rcid.substring(AUTHOR_ONLY_INTERACTION_PREFIX.length).split('_'); const eid = p.shift(); const oid = p.join('_'); if (interaction.user.id !== eid) { try { await interaction.reply({ content: "⚠️ Not authorized for this button.", ephemeral: true }); } catch (e) {} return; } commandIdentifier = oid; } else { commandIdentifier = rcid; } expectedTypes = [COMMAND_TYPES.BUTTON]; } else if (interaction.isAnySelectMenu()) { const rcid = interaction.customId; if (rcid.startsWith(AUTHOR_ONLY_INTERACTION_PREFIX)) { const p = rcid.substring(AUTHOR_ONLY_INTERACTION_PREFIX.length).split('_'); const eid = p.shift(); const oid = p.join('_'); if (interaction.user.id !== eid) { try { await interaction.reply({ content: "⚠️ Not authorized for this menu.", ephemeral: true }); } catch (e) {} return; } commandIdentifier = oid; } else { commandIdentifier = rcid; } expectedTypes = [COMMAND_TYPES.SELECT_MENU]; } else if (interaction.isModalSubmit()) { commandIdentifier = interaction.customId; expectedTypes = [COMMAND_TYPES.MODAL_SUBMIT]; } else if (interaction.isAutocomplete()) { commandIdentifier = interaction.commandName.toLowerCase(); expectedTypes = INTERACTION_COMMAND_TYPES; isAutocomplete = true; } else { if (this.solaraOptions.verboseStartupLogging) console.warn(`Solara Core: Unhandled interaction type: ${interaction.type}`); return; } if (!commandIdentifier && !isAutocomplete) { if (this.solaraOptions.verboseStartupLogging) console.warn(`Solara Core Interaction Handler: No command identifier for ${InteractionType[interaction.type]}. Raw: ${interaction.customId || interaction.commandName}`); if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) { try { await interaction.reply({ content: `⚠️ Interaction component misconfigured.`, ephemeral: true }).catch(()=>{}); } catch {} } return; } command = this.commands.get(commandIdentifier); if (isAutocomplete) { if (command?.options?.some(o => o.name === interaction.options.getFocused(true).name && o.autocomplete)) { try { await this.commandHandler.executeCommand(command, interaction, [], null, true); } catch (e) { console.error(`Solara Core Autocomplete: Error for "${commandIdentifier}":`, e); } } else { if (this.solaraOptions.verboseStartupLogging) console.warn(`Solara Core Autocomplete: No command/option for "${commandIdentifier}".`); try { await interaction.respond([]); } catch {} } return; } if (command && expectedTypes.includes(command.type)) { try { await this.commandHandler.executeCommand(command, interaction, []); } catch (e) { console.error(`Solara Core Interaction Handler: Error for ${InteractionType[interaction.type]} "${commandIdentifier}" cmd "${command.name}":`, e); } } else if (commandIdentifier) { if (this.solaraOptions.verboseStartupLogging) { if (!command) console.warn(`Solara Core Interaction: No command for ${InteractionType[interaction.type]} ID/Name "${commandIdentifier}".`); else console.warn(`Solara Core Interaction: ${InteractionType[interaction.type]} for "${commandIdentifier}" cmd "${command.name}" type (${command.type}) mismatch: ${expectedTypes.join('/')}.`); } if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) { try { await interaction.reply({ content: `⚠️ Interaction component (${commandIdentifier}) outdated/misconfigured.`, ephemeral: true }).catch(()=>{}); } catch {} } } }); this.on('error', (err) => console.error("Solara Core Discord Client Error:", err)); this.on('warn', (info) => { if (this.solaraOptions.verboseStartupLogging || String(info).includes("eprecate")) { console.warn("Solara Core Discord Client Warning:", info); }}); } } module.exports = SolaraClient;