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:
parent
a3ed25f8dd
commit
8552887b6c
9 changed files with 1536 additions and 30 deletions
178
botmain.js
178
botmain.js
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue