Implement comprehensive rate limiting system and item spawn configuration
Major Features Added: - Complete token bucket rate limiting for IRC commands and web interface - Per-user rate tracking with category-based limits (Basic, Gameplay, Management, Admin, Web) - Admin commands for rate limit management (\!rate_stats, \!rate_user, \!rate_unban, \!rate_reset) - Automatic violation tracking and temporary bans with cleanup - Global item spawn multiplier system with 75% spawn rate reduction - Central admin configuration system (config.py) - One-command bot startup script (start_petbot.sh) Rate Limiting: - Token bucket algorithm with burst capacity and refill rates - Category limits: Basic (20/min), Gameplay (10/min), Management (5/min), Web (60/min) - Graceful violation handling with user-friendly error messages - Admin exemption and override capabilities - Background cleanup of old violations and expired bans Item Spawn System: - Added global_spawn_multiplier to config/items.json for easy adjustment - Reduced all individual spawn rates by 75% (multiplied by 0.25) - Admins can fine-tune both global multiplier and individual item rates - Game engine integration applies multiplier to all spawn calculations Infrastructure: - Single admin user configuration in config.py - Enhanced startup script with dependency management and verification - Updated documentation and help system with rate limiting guide - Comprehensive test suite for rate limiting functionality Security: - Rate limiting protects against command spam and abuse - IP-based tracking for web interface requests - Proper error handling and status codes (429 for rate limits) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f8ac661cd1
commit
915aa00bea
28 changed files with 5730 additions and 57 deletions
149
modules/admin.py
149
modules/admin.py
|
|
@ -1,21 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Admin commands module for PetBot"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path for config import
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from .base_module import BaseModule
|
||||
from config import ADMIN_USER # Import admin user from central config
|
||||
|
||||
# =============================================================================
|
||||
# ADMIN CONFIGURATION
|
||||
# =============================================================================
|
||||
# To change the admin user, edit config.py in the project root
|
||||
# Current admin user: {ADMIN_USER}
|
||||
# =============================================================================
|
||||
|
||||
class Admin(BaseModule):
|
||||
"""Handles admin-only commands like reload"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["reload"]
|
||||
return ["reload", "rate_stats", "rate_user", "rate_unban", "rate_reset"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "reload":
|
||||
await self.cmd_reload(channel, nickname)
|
||||
elif command == "rate_stats":
|
||||
await self.cmd_rate_stats(channel, nickname)
|
||||
elif command == "rate_user":
|
||||
await self.cmd_rate_user(channel, nickname, args)
|
||||
elif command == "rate_unban":
|
||||
await self.cmd_rate_unban(channel, nickname, args)
|
||||
elif command == "rate_reset":
|
||||
await self.cmd_rate_reset(channel, nickname, args)
|
||||
|
||||
async def cmd_reload(self, channel, nickname):
|
||||
"""Reload bot modules (megasconed only)"""
|
||||
if nickname.lower() != "megasconed":
|
||||
"""Reload bot modules (admin only)"""
|
||||
if not self.is_admin(nickname):
|
||||
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
|
||||
return
|
||||
|
||||
|
|
@ -27,4 +49,123 @@ class Admin(BaseModule):
|
|||
else:
|
||||
self.send_message(channel, f"{nickname}: ❌ Module reload failed!")
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: ❌ Reload error: {str(e)}")
|
||||
self.send_message(channel, f"{nickname}: ❌ Reload error: {str(e)}")
|
||||
|
||||
def is_admin(self, nickname):
|
||||
"""Check if user is admin"""
|
||||
return nickname.lower() == ADMIN_USER.lower()
|
||||
|
||||
async def cmd_rate_stats(self, channel, nickname):
|
||||
"""Show global rate limiting statistics"""
|
||||
if not self.is_admin(nickname):
|
||||
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
|
||||
return
|
||||
|
||||
if not self.bot.rate_limiter:
|
||||
self.send_message(channel, f"{nickname}: Rate limiter not available.")
|
||||
return
|
||||
|
||||
try:
|
||||
stats = self.bot.rate_limiter.get_global_stats()
|
||||
|
||||
response = f"{nickname}: 📊 Rate Limiter Stats:\n"
|
||||
response += f"• Status: {'Enabled' if stats['enabled'] else 'Disabled'}\n"
|
||||
response += f"• Requests this minute: {stats['requests_this_minute']}\n"
|
||||
response += f"• Active users: {stats['active_users']}\n"
|
||||
response += f"• Total requests: {stats['total_requests']}\n"
|
||||
response += f"• Blocked requests: {stats['blocked_requests']}\n"
|
||||
response += f"• Banned users: {stats['banned_users']}\n"
|
||||
response += f"• Tracked users: {stats['tracked_users']}\n"
|
||||
response += f"• Total violations: {stats['total_violations']}"
|
||||
|
||||
self.send_message(channel, response)
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: ❌ Error getting rate stats: {str(e)}")
|
||||
|
||||
async def cmd_rate_user(self, channel, nickname, args):
|
||||
"""Show rate limiting stats for a specific user"""
|
||||
if not self.is_admin(nickname):
|
||||
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
|
||||
return
|
||||
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Usage: !rate_user <username>")
|
||||
return
|
||||
|
||||
if not self.bot.rate_limiter:
|
||||
self.send_message(channel, f"{nickname}: Rate limiter not available.")
|
||||
return
|
||||
|
||||
try:
|
||||
target_user = args[0]
|
||||
stats = self.bot.rate_limiter.get_user_stats(target_user)
|
||||
|
||||
response = f"{nickname}: 👤 Rate Stats for {stats['user']}:\n"
|
||||
response += f"• Admin exemption: {'Yes' if stats['admin_exemption'] else 'No'}\n"
|
||||
response += f"• Currently banned: {'Yes' if stats['is_banned'] else 'No'}\n"
|
||||
if stats['ban_expires']:
|
||||
response += f"• Ban expires: {stats['ban_expires']}\n"
|
||||
response += f"• Total violations: {stats['violations']}\n"
|
||||
if stats['buckets']:
|
||||
response += f"• Available tokens: {stats['buckets']['tokens']}\n"
|
||||
response += f"• Last request: {stats['buckets']['last_request']}"
|
||||
else:
|
||||
response += "• No recent activity"
|
||||
|
||||
self.send_message(channel, response)
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: ❌ Error getting user stats: {str(e)}")
|
||||
|
||||
async def cmd_rate_unban(self, channel, nickname, args):
|
||||
"""Manually unban a user from rate limiting"""
|
||||
if not self.is_admin(nickname):
|
||||
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
|
||||
return
|
||||
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Usage: !rate_unban <username>")
|
||||
return
|
||||
|
||||
if not self.bot.rate_limiter:
|
||||
self.send_message(channel, f"{nickname}: Rate limiter not available.")
|
||||
return
|
||||
|
||||
try:
|
||||
target_user = args[0]
|
||||
success = self.bot.rate_limiter.unban_user(target_user)
|
||||
|
||||
if success:
|
||||
self.send_message(channel, f"{nickname}: ✅ User {target_user} has been unbanned.")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: ℹ️ User {target_user} was not banned.")
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: ❌ Error unbanning user: {str(e)}")
|
||||
|
||||
async def cmd_rate_reset(self, channel, nickname, args):
|
||||
"""Reset rate limiting violations for a user"""
|
||||
if not self.is_admin(nickname):
|
||||
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
|
||||
return
|
||||
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Usage: !rate_reset <username>")
|
||||
return
|
||||
|
||||
if not self.bot.rate_limiter:
|
||||
self.send_message(channel, f"{nickname}: Rate limiter not available.")
|
||||
return
|
||||
|
||||
try:
|
||||
target_user = args[0]
|
||||
success = self.bot.rate_limiter.reset_user_violations(target_user)
|
||||
|
||||
if success:
|
||||
self.send_message(channel, f"{nickname}: ✅ Violations reset for user {target_user}.")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: ℹ️ No violations found for user {target_user}.")
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: ❌ Error resetting violations: {str(e)}")
|
||||
256
modules/backup_commands.py
Normal file
256
modules/backup_commands.py
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
from modules.base_module import BaseModule
|
||||
from src.backup_manager import BackupManager, BackupScheduler
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BackupCommands(BaseModule):
|
||||
"""Module for database backup management commands."""
|
||||
|
||||
def __init__(self, bot, database):
|
||||
super().__init__(bot, database)
|
||||
self.backup_manager = BackupManager()
|
||||
self.scheduler = BackupScheduler(self.backup_manager)
|
||||
self.scheduler_task = None
|
||||
|
||||
# Setup logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Start the scheduler
|
||||
self._start_scheduler()
|
||||
|
||||
def _start_scheduler(self):
|
||||
"""Start the backup scheduler task."""
|
||||
if self.scheduler_task is None or self.scheduler_task.done():
|
||||
self.scheduler_task = asyncio.create_task(self.scheduler.start_scheduler())
|
||||
self.logger.info("Backup scheduler started")
|
||||
|
||||
def get_commands(self):
|
||||
"""Return list of available backup commands."""
|
||||
return [
|
||||
"backup", "restore", "backups", "backup_stats", "backup_cleanup"
|
||||
]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
"""Handle backup-related commands."""
|
||||
|
||||
# Check if user has admin privileges for backup commands
|
||||
if not await self._is_admin(nickname):
|
||||
self.send_message(channel, f"{nickname}: Backup commands require admin privileges.")
|
||||
return
|
||||
|
||||
if command == "backup":
|
||||
await self.cmd_backup(channel, nickname, args)
|
||||
elif command == "restore":
|
||||
await self.cmd_restore(channel, nickname, args)
|
||||
elif command == "backups":
|
||||
await self.cmd_list_backups(channel, nickname)
|
||||
elif command == "backup_stats":
|
||||
await self.cmd_backup_stats(channel, nickname)
|
||||
elif command == "backup_cleanup":
|
||||
await self.cmd_backup_cleanup(channel, nickname)
|
||||
|
||||
async def _is_admin(self, nickname):
|
||||
"""Check if user has admin privileges."""
|
||||
# This should be implemented based on your admin system
|
||||
# For now, using a simple check - replace with actual admin verification
|
||||
admin_users = ["admin", "megaproxy"] # Add your admin usernames
|
||||
return nickname.lower() in admin_users
|
||||
|
||||
async def cmd_backup(self, channel, nickname, args):
|
||||
"""Create a manual backup."""
|
||||
try:
|
||||
# Parse backup type from args
|
||||
backup_type = "manual"
|
||||
compress = True
|
||||
|
||||
if args:
|
||||
if "uncompressed" in args:
|
||||
compress = False
|
||||
if "daily" in args:
|
||||
backup_type = "daily"
|
||||
elif "weekly" in args:
|
||||
backup_type = "weekly"
|
||||
elif "monthly" in args:
|
||||
backup_type = "monthly"
|
||||
|
||||
self.send_message(channel, f"{nickname}: Creating {backup_type} backup...")
|
||||
|
||||
result = await self.backup_manager.create_backup(backup_type, compress)
|
||||
|
||||
if result["success"]:
|
||||
compression_text = "compressed" if compress else "uncompressed"
|
||||
self.send_message(channel,
|
||||
f"✅ {nickname}: Backup created successfully! "
|
||||
f"File: {result['backup_filename']} "
|
||||
f"({result['size_mb']:.1f}MB, {compression_text})"
|
||||
)
|
||||
else:
|
||||
self.send_message(channel, f"❌ {nickname}: Backup failed: {result['error']}")
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"❌ {nickname}: Error creating backup: {str(e)}")
|
||||
|
||||
async def cmd_restore(self, channel, nickname, args):
|
||||
"""Restore database from backup."""
|
||||
try:
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Usage: !restore <backup_filename>")
|
||||
return
|
||||
|
||||
backup_filename = args[0]
|
||||
|
||||
# Confirmation check
|
||||
self.send_message(channel,
|
||||
f"⚠️ {nickname}: This will restore the database from {backup_filename}. "
|
||||
f"Current database will be backed up first. Type '!restore {backup_filename} confirm' to proceed."
|
||||
)
|
||||
|
||||
if len(args) < 2 or args[1] != "confirm":
|
||||
return
|
||||
|
||||
self.send_message(channel, f"{nickname}: Restoring database from {backup_filename}...")
|
||||
|
||||
result = await self.backup_manager.restore_backup(backup_filename)
|
||||
|
||||
if result["success"]:
|
||||
self.send_message(channel,
|
||||
f"✅ {nickname}: Database restored successfully! "
|
||||
f"Current database backed up as: {result['current_backup']} "
|
||||
f"Verified {result['tables_verified']} tables."
|
||||
)
|
||||
|
||||
# Restart bot to reload data
|
||||
self.send_message(channel, f"{nickname}: ⚠️ Bot restart recommended to reload data.")
|
||||
|
||||
else:
|
||||
self.send_message(channel, f"❌ {nickname}: Restore failed: {result['error']}")
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"❌ {nickname}: Error restoring backup: {str(e)}")
|
||||
|
||||
async def cmd_list_backups(self, channel, nickname):
|
||||
"""List available backups."""
|
||||
try:
|
||||
backups = await self.backup_manager.list_backups()
|
||||
|
||||
if not backups:
|
||||
self.send_message(channel, f"{nickname}: No backups found.")
|
||||
return
|
||||
|
||||
self.send_message(channel, f"{nickname}: Available backups:")
|
||||
|
||||
# Show up to 10 most recent backups
|
||||
for backup in backups[:10]:
|
||||
age = datetime.now() - backup["created_at"]
|
||||
age_str = self._format_age(age)
|
||||
|
||||
compression = "📦" if backup["compressed"] else "📄"
|
||||
type_emoji = {"daily": "🌅", "weekly": "📅", "monthly": "🗓️", "manual": "🔧"}.get(backup["type"], "📋")
|
||||
|
||||
self.send_message(channel,
|
||||
f" {type_emoji}{compression} {backup['filename']} "
|
||||
f"({backup['size_mb']:.1f}MB, {age_str} ago)"
|
||||
)
|
||||
|
||||
if len(backups) > 10:
|
||||
self.send_message(channel, f" ... and {len(backups) - 10} more backups")
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"❌ {nickname}: Error listing backups: {str(e)}")
|
||||
|
||||
async def cmd_backup_stats(self, channel, nickname):
|
||||
"""Show backup statistics."""
|
||||
try:
|
||||
stats = await self.backup_manager.get_backup_stats()
|
||||
|
||||
if not stats["success"]:
|
||||
self.send_message(channel, f"❌ {nickname}: Error getting stats: {stats['error']}")
|
||||
return
|
||||
|
||||
if stats["total_backups"] == 0:
|
||||
self.send_message(channel, f"{nickname}: No backups found.")
|
||||
return
|
||||
|
||||
self.send_message(channel, f"{nickname}: Backup Statistics:")
|
||||
self.send_message(channel, f" 📊 Total backups: {stats['total_backups']}")
|
||||
self.send_message(channel, f" 💾 Total size: {stats['total_size_mb']:.1f}MB")
|
||||
|
||||
if stats["oldest_backup"]:
|
||||
oldest_age = datetime.now() - stats["oldest_backup"]
|
||||
newest_age = datetime.now() - stats["newest_backup"]
|
||||
self.send_message(channel, f" 📅 Oldest: {self._format_age(oldest_age)} ago")
|
||||
self.send_message(channel, f" 🆕 Newest: {self._format_age(newest_age)} ago")
|
||||
|
||||
# Show breakdown by type
|
||||
for backup_type, type_stats in stats["by_type"].items():
|
||||
type_emoji = {"daily": "🌅", "weekly": "📅", "monthly": "🗓️", "manual": "🔧"}.get(backup_type, "📋")
|
||||
self.send_message(channel,
|
||||
f" {type_emoji} {backup_type.title()}: {type_stats['count']} backups "
|
||||
f"({type_stats['size_mb']:.1f}MB)"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"❌ {nickname}: Error getting backup stats: {str(e)}")
|
||||
|
||||
async def cmd_backup_cleanup(self, channel, nickname):
|
||||
"""Clean up old backups based on retention policy."""
|
||||
try:
|
||||
self.send_message(channel, f"{nickname}: Cleaning up old backups...")
|
||||
|
||||
result = await self.backup_manager.cleanup_old_backups()
|
||||
|
||||
if result["success"]:
|
||||
if result["cleaned_count"] > 0:
|
||||
self.send_message(channel,
|
||||
f"✅ {nickname}: Cleaned up {result['cleaned_count']} old backups. "
|
||||
f"{result['remaining_backups']} backups remaining."
|
||||
)
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: No old backups to clean up.")
|
||||
else:
|
||||
self.send_message(channel, f"❌ {nickname}: Cleanup failed: {result['error']}")
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"❌ {nickname}: Error during cleanup: {str(e)}")
|
||||
|
||||
def _format_age(self, age):
|
||||
"""Format a timedelta as human-readable age."""
|
||||
if age.days > 0:
|
||||
return f"{age.days}d {age.seconds // 3600}h"
|
||||
elif age.seconds > 3600:
|
||||
return f"{age.seconds // 3600}h {(age.seconds % 3600) // 60}m"
|
||||
elif age.seconds > 60:
|
||||
return f"{age.seconds // 60}m"
|
||||
else:
|
||||
return f"{age.seconds}s"
|
||||
|
||||
async def get_backup_status(self):
|
||||
"""Get current backup system status for monitoring."""
|
||||
try:
|
||||
stats = await self.backup_manager.get_backup_stats()
|
||||
|
||||
return {
|
||||
"scheduler_running": self.scheduler.running,
|
||||
"total_backups": stats.get("total_backups", 0),
|
||||
"total_size_mb": stats.get("total_size_mb", 0),
|
||||
"last_backup": stats.get("newest_backup"),
|
||||
"backup_types": stats.get("by_type", {})
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
async def shutdown(self):
|
||||
"""Shutdown the backup system gracefully."""
|
||||
if self.scheduler:
|
||||
self.scheduler.stop_scheduler()
|
||||
|
||||
if self.scheduler_task and not self.scheduler_task.done():
|
||||
self.scheduler_task.cancel()
|
||||
try:
|
||||
await self.scheduler_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
self.logger.info("Backup system shutdown complete")
|
||||
|
|
@ -33,11 +33,19 @@ class BaseModule(ABC):
|
|||
|
||||
def send_message(self, target, message):
|
||||
"""Send message through the bot"""
|
||||
self.bot.send_message(target, message)
|
||||
# Use sync wrapper if available (new bot), otherwise fallback to old method
|
||||
if hasattr(self.bot, 'send_message_sync'):
|
||||
self.bot.send_message_sync(target, message)
|
||||
else:
|
||||
self.bot.send_message(target, message)
|
||||
|
||||
def send_pm(self, nickname, message):
|
||||
"""Send private message to user"""
|
||||
self.bot.send_message(nickname, message)
|
||||
# Use sync wrapper if available (new bot), otherwise fallback to old method
|
||||
if hasattr(self.bot, 'send_message_sync'):
|
||||
self.bot.send_message_sync(nickname, message)
|
||||
else:
|
||||
self.bot.send_message(nickname, message)
|
||||
|
||||
async def get_player(self, nickname):
|
||||
"""Get player from database"""
|
||||
|
|
|
|||
236
modules/connection_monitor.py
Normal file
236
modules/connection_monitor.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
from modules.base_module import BaseModule
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
|
||||
class ConnectionMonitor(BaseModule):
|
||||
"""Module for monitoring IRC connection status and providing connection commands."""
|
||||
|
||||
def __init__(self, bot, database, game_engine=None):
|
||||
super().__init__(bot, database)
|
||||
self.game_engine = game_engine
|
||||
self.start_time = datetime.now()
|
||||
|
||||
def get_commands(self):
|
||||
"""Return list of available connection monitoring commands."""
|
||||
return [
|
||||
"status", "uptime", "ping", "reconnect", "connection_stats"
|
||||
]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
"""Handle connection monitoring commands."""
|
||||
|
||||
if command == "status":
|
||||
await self.cmd_status(channel, nickname)
|
||||
elif command == "uptime":
|
||||
await self.cmd_uptime(channel, nickname)
|
||||
elif command == "ping":
|
||||
await self.cmd_ping(channel, nickname)
|
||||
elif command == "reconnect":
|
||||
await self.cmd_reconnect(channel, nickname)
|
||||
elif command == "connection_stats":
|
||||
await self.cmd_connection_stats(channel, nickname)
|
||||
|
||||
async def cmd_status(self, channel, nickname):
|
||||
"""Show bot connection status."""
|
||||
try:
|
||||
# Get connection manager if available
|
||||
connection_manager = getattr(self.bot, 'connection_manager', None)
|
||||
|
||||
if not connection_manager:
|
||||
self.send_message(channel, f"{nickname}: Connection manager not available")
|
||||
return
|
||||
|
||||
stats = connection_manager.get_connection_stats()
|
||||
state = stats.get("state", "unknown")
|
||||
connected = stats.get("connected", False)
|
||||
|
||||
# Status emoji
|
||||
status_emoji = "🟢" if connected else "🔴"
|
||||
|
||||
# Build status message
|
||||
status_msg = f"{status_emoji} {nickname}: Bot Status - {state.upper()}"
|
||||
|
||||
if connected:
|
||||
uptime = stats.get("uptime", "unknown")
|
||||
message_count = stats.get("message_count", 0)
|
||||
status_msg += f" | Uptime: {uptime} | Messages: {message_count}"
|
||||
else:
|
||||
reconnect_attempts = stats.get("reconnect_attempts", 0)
|
||||
status_msg += f" | Reconnect attempts: {reconnect_attempts}"
|
||||
|
||||
self.send_message(channel, status_msg)
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: Error getting status: {str(e)}")
|
||||
|
||||
async def cmd_uptime(self, channel, nickname):
|
||||
"""Show bot uptime."""
|
||||
try:
|
||||
uptime = datetime.now() - self.start_time
|
||||
|
||||
# Format uptime
|
||||
days = uptime.days
|
||||
hours, remainder = divmod(uptime.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
uptime_str = f"{days}d {hours}h {minutes}m {seconds}s"
|
||||
|
||||
# Get additional stats if available
|
||||
connection_manager = getattr(self.bot, 'connection_manager', None)
|
||||
if connection_manager:
|
||||
stats = connection_manager.get_connection_stats()
|
||||
reconnections = stats.get("total_reconnections", 0)
|
||||
failures = stats.get("connection_failures", 0)
|
||||
|
||||
uptime_str += f" | Reconnections: {reconnections} | Failures: {failures}"
|
||||
|
||||
self.send_message(channel, f"⏰ {nickname}: Bot uptime: {uptime_str}")
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: Error getting uptime: {str(e)}")
|
||||
|
||||
async def cmd_ping(self, channel, nickname):
|
||||
"""Test bot responsiveness."""
|
||||
try:
|
||||
start_time = datetime.now()
|
||||
|
||||
# Get connection status
|
||||
connection_manager = getattr(self.bot, 'connection_manager', None)
|
||||
connected = False
|
||||
|
||||
if connection_manager:
|
||||
connected = connection_manager.is_connected()
|
||||
|
||||
end_time = datetime.now()
|
||||
response_time = (end_time - start_time).total_seconds() * 1000
|
||||
|
||||
# Status indicators
|
||||
connection_status = "🟢 Connected" if connected else "🔴 Disconnected"
|
||||
ping_emoji = "🏓" if response_time < 100 else "🐌"
|
||||
|
||||
self.send_message(channel,
|
||||
f"{ping_emoji} {nickname}: Pong! Response time: {response_time:.1f}ms | {connection_status}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: Error during ping: {str(e)}")
|
||||
|
||||
async def cmd_reconnect(self, channel, nickname):
|
||||
"""Force reconnection (admin only)."""
|
||||
try:
|
||||
# Check if user is admin
|
||||
if not await self._is_admin(nickname):
|
||||
self.send_message(channel, f"{nickname}: This command requires admin privileges.")
|
||||
return
|
||||
|
||||
connection_manager = getattr(self.bot, 'connection_manager', None)
|
||||
if not connection_manager:
|
||||
self.send_message(channel, f"{nickname}: Connection manager not available.")
|
||||
return
|
||||
|
||||
self.send_message(channel, f"{nickname}: Initiating manual reconnection...")
|
||||
|
||||
# Force reconnection by stopping and starting
|
||||
await connection_manager.stop()
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Start connection manager in background
|
||||
asyncio.create_task(connection_manager.start())
|
||||
|
||||
self.send_message(channel, f"{nickname}: Reconnection initiated.")
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: Error during reconnection: {str(e)}")
|
||||
|
||||
async def cmd_connection_stats(self, channel, nickname):
|
||||
"""Show detailed connection statistics."""
|
||||
try:
|
||||
connection_manager = getattr(self.bot, 'connection_manager', None)
|
||||
|
||||
if not connection_manager:
|
||||
self.send_message(channel, f"{nickname}: Connection manager not available")
|
||||
return
|
||||
|
||||
stats = connection_manager.get_connection_stats()
|
||||
|
||||
# Build detailed stats message
|
||||
lines = [
|
||||
f"📊 {nickname}: Connection Statistics",
|
||||
f"State: {stats.get('state', 'unknown').upper()}",
|
||||
f"Connected: {'Yes' if stats.get('connected') else 'No'}",
|
||||
f"Uptime: {stats.get('uptime', 'unknown')}",
|
||||
f"Messages: {stats.get('message_count', 0)}",
|
||||
f"Reconnections: {stats.get('total_reconnections', 0)}",
|
||||
f"Failures: {stats.get('connection_failures', 0)}",
|
||||
f"Reconnect attempts: {stats.get('reconnect_attempts', 0)}"
|
||||
]
|
||||
|
||||
# Add timing information
|
||||
last_message = stats.get('last_message_time')
|
||||
if last_message:
|
||||
lines.append(f"Last message: {last_message}")
|
||||
|
||||
last_ping = stats.get('last_ping_time')
|
||||
if last_ping:
|
||||
lines.append(f"Last ping: {last_ping}")
|
||||
|
||||
# Send each line
|
||||
for line in lines:
|
||||
self.send_message(channel, line)
|
||||
await asyncio.sleep(0.3) # Small delay to prevent flooding
|
||||
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: Error getting connection stats: {str(e)}")
|
||||
|
||||
async def _is_admin(self, nickname):
|
||||
"""Check if user has admin privileges."""
|
||||
# This should match the admin system in other modules
|
||||
admin_users = ["admin", "megaproxy", "megasconed"]
|
||||
return nickname.lower() in admin_users
|
||||
|
||||
async def get_connection_health(self):
|
||||
"""Get connection health status for monitoring."""
|
||||
try:
|
||||
connection_manager = getattr(self.bot, 'connection_manager', None)
|
||||
|
||||
if not connection_manager:
|
||||
return {
|
||||
"healthy": False,
|
||||
"error": "Connection manager not available"
|
||||
}
|
||||
|
||||
stats = connection_manager.get_connection_stats()
|
||||
connected = stats.get("connected", False)
|
||||
|
||||
# Check if connection is healthy
|
||||
healthy = connected and stats.get("reconnect_attempts", 0) < 5
|
||||
|
||||
return {
|
||||
"healthy": healthy,
|
||||
"connected": connected,
|
||||
"state": stats.get("state", "unknown"),
|
||||
"uptime": stats.get("uptime"),
|
||||
"message_count": stats.get("message_count", 0),
|
||||
"reconnect_attempts": stats.get("reconnect_attempts", 0),
|
||||
"total_reconnections": stats.get("total_reconnections", 0),
|
||||
"connection_failures": stats.get("connection_failures", 0)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"healthy": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def get_module_stats(self):
|
||||
"""Get module-specific statistics."""
|
||||
uptime = datetime.now() - self.start_time
|
||||
|
||||
return {
|
||||
"module_name": "ConnectionMonitor",
|
||||
"module_uptime": str(uptime),
|
||||
"commands_available": len(self.get_commands()),
|
||||
"start_time": self.start_time
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue