@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
JavaScript
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;