// bot.js - Main IRC Bot with Plugin System (Debug Version) // ================================ // BOT CONFIGURATION - EDIT HERE // ================================ const config = { server: 'irc.libera.chat', // IRC server address port: 6667, // IRC server port (6667 for plain, 6697 for SSL) nick: 'cancerbot', // Bot's nickname channels: ['#bakedbeans'], // Channels to join (add more like: ['#channel1', '#channel2']) commandPrefix: '!', // Command prefix (e.g., !help, !ping) pluginsDir: './plugins' // Directory containing plugin files }; const net = require('net'); const fs = require('fs'); const path = require('path'); class IRCBot { constructor(config) { this.config = { server: config.server || 'irc.libera.chat', port: config.port || 6667, nick: config.nick || 'MyBot', channels: config.channels || ['#test'], commandPrefix: config.commandPrefix || '!', pluginsDir: config.pluginsDir || './plugins' }; this.socket = null; this.plugins = new Map(); this.commands = new Map(); this.connected = false; console.log('🔧 Bot configuration:', this.config); this.loadPlugins(); } connect() { console.log(`Connecting to ${this.config.server}:${this.config.port}`); this.socket = net.createConnection(this.config.port, this.config.server); this.socket.on('connect', () => { console.log('Connected to IRC server'); this.send(`NICK ${this.config.nick}`); this.send(`USER ${this.config.nick} 0 * :${this.config.nick}`); }); this.socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (line.trim()) { this.handleMessage(line); } }); }); this.socket.on('error', (err) => { console.error('Socket error:', err); }); this.socket.on('close', () => { console.log('Connection closed'); this.connected = false; // Reconnect after 5 seconds setTimeout(() => this.connect(), 5000); }); } send(message) { if (this.socket && this.socket.writable) { console.log('→', message); this.socket.write(message + '\r\n'); } } handleMessage(line) { console.log('←', line); // Handle PING if (line.startsWith('PING')) { this.send('PONG ' + line.substring(5)); return; } // Handle successful connection if (line.includes('001')) { this.connected = true; console.log('Successfully connected and registered'); // Join channels this.config.channels.forEach(channel => { this.send(`JOIN ${channel}`); }); return; } // Parse IRC message const parsed = this.parseMessage(line); if (parsed) { this.handleParsedMessage(parsed); } } parseMessage(line) { // Basic IRC message parsing const match = line.match(/^:([^!]+)!([^@]+)@([^\s]+)\s+(\w+)\s+([^:]+):(.*)$/); if (match) { return { nick: match[1], user: match[2], host: match[3], command: match[4], target: match[5].trim(), message: match[6] }; } return null; } handleParsedMessage(msg) { // Handle PRIVMSG (channel messages and private messages) if (msg.command === 'PRIVMSG') { const isChannel = msg.target.startsWith('#'); const replyTo = isChannel ? msg.target : msg.nick; // Check if it's a command if (msg.message.startsWith(this.config.commandPrefix)) { const commandLine = msg.message.substring(this.config.commandPrefix.length); const [commandName, ...args] = commandLine.split(' '); // Handle built-in admin commands first if (commandName === 'reloadplugins' && msg.nick === 'megasconed') { this.say(replyTo, '🔄 Reloading all plugins...'); const pluginCount = this.plugins.size; this.reloadAllPlugins(); this.say(replyTo, `✅ Reloaded ${pluginCount} plugins. New commands: ${Array.from(this.commands.keys()).join(', ')}`); return; } this.executeCommand(commandName, { nick: msg.nick, user: msg.user, host: msg.host, channel: isChannel ? msg.target : null, replyTo: replyTo, args: args, fullMessage: msg.message }); } // Emit message event for plugins this.emit('message', { nick: msg.nick, user: msg.user, host: msg.host, target: msg.target, message: msg.message, isChannel: isChannel, replyTo: replyTo }); } } executeCommand(commandName, context) { const command = this.commands.get(commandName); if (command) { try { command.execute(context, this); } catch (error) { console.error(`Error executing command ${commandName}:`, error); this.say(context.replyTo, `Error executing command: ${error.message}`); } } else { console.log(`Command not found: ${commandName}`); } } loadPlugins() { console.log(`🔍 Looking for plugins in: ${this.config.pluginsDir}`); console.log(`🔍 Full path: ${path.resolve(this.config.pluginsDir)}`); if (!fs.existsSync(this.config.pluginsDir)) { console.log(`📁 Creating plugins directory: ${this.config.pluginsDir}`); fs.mkdirSync(this.config.pluginsDir, { recursive: true }); // Create a basic example plugin this.createExamplePlugin(); return; } console.log(`📁 Plugins directory exists, scanning for .js files...`); let pluginFiles; try { pluginFiles = fs.readdirSync(this.config.pluginsDir) .filter(file => file.endsWith('.js')); console.log(`📋 Found files:`, pluginFiles); } catch (error) { console.error(`❌ Error reading plugins directory:`, error); return; } if (pluginFiles.length === 0) { console.log(`âš ī¸ No .js files found in plugins directory`); console.log(`💡 Creating example plugin...`); this.createExamplePlugin(); return; } pluginFiles.forEach(file => { console.log(`🔄 Loading plugin: ${file}`); this.loadPlugin(file); }); console.log(`✅ Loaded ${pluginFiles.length} plugins`); console.log(`🎮 Available commands:`, Array.from(this.commands.keys())); } createExamplePlugin() { const examplePlugin = `// plugins/basic.js - Basic commands plugin module.exports = { init(bot) { console.log('Basic plugin initialized'); }, cleanup(bot) { console.log('Basic plugin cleaned up'); }, commands: [ { name: 'ping', description: 'Responds with pong', execute(context, bot) { bot.say(context.replyTo, \`\${context.nick}: pong!\`); } }, { name: 'help', description: 'Shows available commands', execute(context, bot) { const commands = Array.from(bot.commands.keys()); bot.say(context.replyTo, \`Available commands: \${commands.join(', ')}\`); } }, { name: 'time', description: 'Shows current time', execute(context, bot) { const now = new Date().toLocaleString(); bot.say(context.replyTo, \`Current time: \${now}\`); } } ] };`; const pluginPath = path.join(this.config.pluginsDir, 'basic.js'); try { fs.writeFileSync(pluginPath, examplePlugin); console.log(`✅ Created example plugin: ${pluginPath}`); // Give filesystem a moment to settle setTimeout(() => { console.log(`🔄 Loading newly created plugin...`); this.loadPlugin('basic.js'); }, 100); } catch (writeError) { console.error(`❌ Error creating example plugin:`, writeError); } } loadPlugin(filename) { const filePath = path.join(this.config.pluginsDir, filename); console.log(`🔄 Attempting to load: ${filePath}`); try { // Check if file exists if (!fs.existsSync(filePath)) { console.error(`❌ Plugin file not found: ${filePath}`); return; } // Get absolute path for require.resolve const absolutePath = path.resolve(filePath); console.log(`📍 Absolute path: ${absolutePath}`); // Clear require cache to allow reloading (only if already cached) try { const fullPath = require.resolve(absolutePath); console.log(`đŸ—‘ī¸ Clearing cache for: ${fullPath}`); delete require.cache[fullPath]; } catch (resolveError) { console.log(`â„šī¸ File not in cache yet: ${absolutePath}`); } // Require the plugin console.log(`đŸ“Ĩ Requiring plugin: ${absolutePath}`); const plugin = require(absolutePath); console.log(`📋 Plugin object:`, Object.keys(plugin)); // Initialize plugin if (typeof plugin.init === 'function') { console.log(`🚀 Initializing plugin: ${filename}`); plugin.init(this); } else { console.log(`âš ī¸ Plugin ${filename} has no init function`); } // Register commands if (plugin.commands && Array.isArray(plugin.commands)) { console.log(`📝 Registering ${plugin.commands.length} commands from ${filename}`); plugin.commands.forEach(command => { if (command.name && typeof command.execute === 'function') { this.commands.set(command.name, command); console.log(`✅ Registered command: ${command.name}`); } else { console.error(`❌ Invalid command in ${filename}:`, command); } }); } else { console.log(`âš ī¸ Plugin ${filename} has no commands array`); } this.plugins.set(filename, plugin); console.log(`✅ Successfully loaded plugin: ${filename}`); } catch (error) { console.error(`❌ Error loading plugin ${filename}:`, error.message); console.error(`📍 Stack trace:`, error.stack); } } reloadPlugin(filename) { const plugin = this.plugins.get(filename); if (plugin) { // Unregister old commands if (plugin.commands) { plugin.commands.forEach(command => { this.commands.delete(command.name); }); } // Call cleanup if available if (typeof plugin.cleanup === 'function') { plugin.cleanup(this); } this.plugins.delete(filename); } // Load the plugin again this.loadPlugin(filename); } reloadAllPlugins() { // Clear all plugins and commands this.plugins.forEach((plugin, filename) => { if (typeof plugin.cleanup === 'function') { plugin.cleanup(this); } }); this.plugins.clear(); this.commands.clear(); // Reload all plugins this.loadPlugins(); } // Helper methods for plugins say(target, message) { this.send(`PRIVMSG ${target} :${message}`); } action(target, message) { this.send(`PRIVMSG ${target} :\x01ACTION ${message}\x01`); } notice(target, message) { this.send(`NOTICE ${target} :${message}`); } join(channel) { this.send(`JOIN ${channel}`); } part(channel, reason = '') { this.send(`PART ${channel} :${reason}`); } // Simple event system for plugins emit(event, data) { this.plugins.forEach(plugin => { if (typeof plugin[`on${event.charAt(0).toUpperCase() + event.slice(1)}`] === 'function') { try { plugin[`on${event.charAt(0).toUpperCase() + event.slice(1)}`](data, this); } catch (error) { console.error(`Error in plugin event handler:`, error); } } }); } } // Create and start bot const bot = new IRCBot(config); bot.connect(); // Handle graceful shutdown process.on('SIGINT', () => { console.log('\nShutting down bot...'); if (bot.socket) { bot.send('QUIT :Bot shutting down'); bot.socket.end(); } process.exit(0); }); module.exports = IRCBot;