megabot/botmain.js
megaproxy 8552887b6c Add new IRC games and enhance bot functionality
- Add Rock Paper Scissors game with PvP and bot modes
  - Fixed syntax errors and improved game mechanics
  - PvP moves now require PM for secrecy

- Add Word Scramble game with difficulty levels
  - Multiple word categories and persistent scoring

- Enhance duck hunt with better statistics tracking
  - Separate points vs duck count tracking
  - Fixed migration logic issues

- Add core rate limiting system (5 commands/30s)
  - Admin whitelist for megasconed
  - Automatic cleanup and unblocking

- Improve reload functionality for hot-reloading plugins
- Add channel-specific commands (\!stopducks/\!startducks)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-17 20:06:32 +00:00

589 lines
No EOL
21 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;