// 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; // Rate limiting system this.rateLimits = new Map(); // nick -> { commands: [], blocked: false, blockExpiry: null } this.rateLimitConfig = { maxCommands: 5, // Max commands per window windowMs: 30000, // 30 second window blockDurationMs: 60000, // 1 minute block cleanupInterval: 300000, // 5 minutes cleanup adminWhitelist: ['megasconed'] // Users exempt from rate limiting }; console.log('๐Ÿ”ง Bot configuration:', this.config); console.log('๐Ÿšฆ Rate limiting enabled:', this.rateLimitConfig); this.loadPlugins(); this.startRateLimitCleanup(); } 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 (keeping legacy support) if (commandName === 'reloadplugins' && msg.nick === 'megasconed') { this.say(replyTo, '๐Ÿ”„ Reloading all plugins... (use !reload command next time)'); const pluginCount = this.plugins.size; try { this.reloadAllPlugins(); this.say(replyTo, `โœ… Reloaded ${pluginCount} plugins. New commands: ${Array.from(this.commands.keys()).join(', ')}`); } catch (error) { console.error('โŒ Error during plugin reload:', error); this.say(replyTo, 'โŒ Error during plugin reload - check console for details.'); } 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) { // Check rate limit BEFORE executing command if (this.isRateLimited(context.nick, context.replyTo)) { return; // User is rate limited, ignore command } // Track this command this.trackCommand(context.nick); 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) { console.log(`๐Ÿ”„ Reloading plugin: ${filename}`); const plugin = this.plugins.get(filename); if (plugin) { // Unregister old commands if (plugin.commands) { plugin.commands.forEach(command => { this.commands.delete(command.name); console.log(`๐Ÿ—‘๏ธ Unregistered command: ${command.name}`); }); } // Call cleanup if available if (typeof plugin.cleanup === 'function') { try { plugin.cleanup(this); console.log(`๐Ÿงน Cleaned up plugin: ${filename}`); } catch (error) { console.error(`โŒ Error cleaning up plugin ${filename}:`, error); } } this.plugins.delete(filename); } // Clear require cache for this specific plugin const pluginPath = path.resolve(this.config.pluginsDir, filename); if (require.cache[pluginPath]) { delete require.cache[pluginPath]; console.log(`๐Ÿ—‘๏ธ Cleared cache for: ${filename}`); } // Load the plugin again this.loadPlugin(filename); } reloadAllPlugins() { console.log('๐Ÿ”„ Starting plugin reload process...'); // Clear all plugins and commands this.plugins.forEach((plugin, filename) => { if (typeof plugin.cleanup === 'function') { try { plugin.cleanup(this); console.log(`๐Ÿงน Cleaned up plugin: ${filename}`); } catch (error) { console.error(`โŒ Error cleaning up plugin ${filename}:`, error); } } }); // Clear require cache for all plugin files console.log('๐Ÿ—‘๏ธ Clearing require cache...'); const pluginDir = path.resolve(this.config.pluginsDir); Object.keys(require.cache).forEach(filepath => { if (filepath.startsWith(pluginDir)) { delete require.cache[filepath]; console.log(`๐Ÿ—‘๏ธ Cleared cache for: ${path.basename(filepath)}`); } }); this.plugins.clear(); this.commands.clear(); // Reload all plugins console.log('๐Ÿ”„ Reloading all plugins...'); this.loadPlugins(); console.log('โœ… Plugin reload complete'); } // 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); } } }); } // Rate limiting methods isRateLimited(nick, replyTo) { // Check if user is whitelisted (admin) if (this.rateLimitConfig.adminWhitelist.includes(nick)) { return false; } const now = Date.now(); const userLimits = this.rateLimits.get(nick); if (!userLimits) { // First time user, no limits return false; } // Check if user is currently blocked if (userLimits.blocked && now < userLimits.blockExpiry) { const remainingMs = userLimits.blockExpiry - now; const remainingSeconds = Math.ceil(remainingMs / 1000); console.log(`๐Ÿšซ Rate limited user ${nick} tried command, ${remainingSeconds}s remaining`); return true; } // If block expired, unblock user if (userLimits.blocked && now >= userLimits.blockExpiry) { userLimits.blocked = false; userLimits.blockExpiry = null; userLimits.commands = []; console.log(`โœ… Rate limit expired for ${nick}`); return false; } // Count recent commands in sliding window const windowStart = now - this.rateLimitConfig.windowMs; const recentCommands = userLimits.commands.filter(timestamp => timestamp > windowStart); // If user exceeds limit, block them if (recentCommands.length >= this.rateLimitConfig.maxCommands) { userLimits.blocked = true; userLimits.blockExpiry = now + this.rateLimitConfig.blockDurationMs; const blockMinutes = Math.ceil(this.rateLimitConfig.blockDurationMs / 60000); this.say(replyTo, `โš ๏ธ ${nick}: Rate limit exceeded! Please wait ${blockMinutes} minute(s) before using commands again.`); console.log(`๐Ÿšซ Rate limited ${nick} for ${blockMinutes} minute(s)`); return true; } return false; } trackCommand(nick) { // Don't track admin commands if (this.rateLimitConfig.adminWhitelist.includes(nick)) { return; } const now = Date.now(); if (!this.rateLimits.has(nick)) { this.rateLimits.set(nick, { commands: [], blocked: false, blockExpiry: null }); } const userLimits = this.rateLimits.get(nick); userLimits.commands.push(now); // Clean up old command timestamps (sliding window) const windowStart = now - this.rateLimitConfig.windowMs; userLimits.commands = userLimits.commands.filter(timestamp => timestamp > windowStart); } startRateLimitCleanup() { // Clean up old rate limit entries every 5 minutes setInterval(() => { const now = Date.now(); const cutoffTime = now - (this.rateLimitConfig.windowMs * 2); // Keep data for 2x window time let cleanedUsers = 0; for (const [nick, limits] of this.rateLimits.entries()) { // Remove users who haven't used commands recently and aren't blocked if (!limits.blocked && limits.commands.length === 0) { this.rateLimits.delete(nick); cleanedUsers++; } else if (!limits.blocked) { // Clean up old command timestamps const oldLength = limits.commands.length; limits.commands = limits.commands.filter(timestamp => timestamp > cutoffTime); // If no recent commands, remove user if (limits.commands.length === 0) { this.rateLimits.delete(nick); cleanedUsers++; } } } if (cleanedUsers > 0) { console.log(`๐Ÿงน Cleaned up ${cleanedUsers} old rate limit entries`); } }, this.rateLimitConfig.cleanupInterval); console.log(`๐Ÿงน Rate limit cleanup scheduled every ${this.rateLimitConfig.cleanupInterval / 1000}s`); } } // 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;