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>
This commit is contained in:
megaproxy 2025-07-17 20:06:32 +00:00
parent a3ed25f8dd
commit 8552887b6c
9 changed files with 1536 additions and 30 deletions

View file

@ -32,8 +32,20 @@ class IRCBot {
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() {
@ -129,12 +141,17 @@ class IRCBot {
const commandLine = msg.message.substring(this.config.commandPrefix.length);
const [commandName, ...args] = commandLine.split(' ');
// Handle built-in admin commands first
// Handle built-in admin commands first (keeping legacy support)
if (commandName === 'reloadplugins' && msg.nick === 'megasconed') {
this.say(replyTo, '🔄 Reloading all plugins...');
this.say(replyTo, '🔄 Reloading all plugins... (use !reload command next time)');
const pluginCount = this.plugins.size;
this.reloadAllPlugins();
this.say(replyTo, `✅ Reloaded ${pluginCount} plugins. New commands: ${Array.from(this.commands.keys()).join(', ')}`);
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;
}
@ -163,6 +180,14 @@ class IRCBot {
}
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 {
@ -335,32 +360,64 @@ module.exports = {
}
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') {
plugin.cleanup(this);
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') {
plugin.cleanup(this);
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)}`);
}
});
@ -368,7 +425,9 @@ module.exports = {
this.commands.clear();
// Reload all plugins
console.log('🔄 Reloading all plugins...');
this.loadPlugins();
console.log('✅ Plugin reload complete');
}
// Helper methods for plugins
@ -404,6 +463,113 @@ module.exports = {
}
});
}
// 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