Petbot/modules/admin.py
megaproxy add7731d80 Implement comprehensive team builder configuration system
### Major Features Added:

**Team Configuration Management:**
- Add support for 3 different team configurations per player
- Implement save/load/rename functionality for team setups
- Add prominent team configuration UI with dropdown selector
- Create quick action buttons for instant configuration loading
- Add status tracking for current editing state

**Database Enhancements:**
- Add team_configurations table with player_id, slot_number, config_name
- Implement rename_team_configuration() method for configuration management
- Add proper indexing and foreign key constraints

**Web Interface Improvements:**
- Fix team builder template visibility issue (was using wrong template)
- Add comprehensive CSS styling for configuration elements
- Implement responsive design with proper hover effects and transitions
- Add visual feedback with status indicators and progress tracking

**API Endpoints:**
- Add /teambuilder/{nickname}/config/rename/{slot} for renaming configs
- Implement proper validation and error handling for all endpoints
- Add JSON response formatting with success/error states

**JavaScript Functionality:**
- Add switchActiveConfig() for configuration switching
- Implement quickSaveConfig() for instant team saving
- Add renameConfig() with user-friendly prompts
- Create loadConfigToEdit() for seamless configuration editing

**Admin & System Improvements:**
- Enhance weather command system with simplified single-word names
- Fix \!reload command to properly handle all 12 modules
- Improve IRC connection health monitoring with better PONG detection
- Add comprehensive TODO list tracking and progress management

**UI/UX Enhancements:**
- Position team configuration section prominently for maximum visibility
- Add clear instructions: "Save up to 3 different team setups for quick switching"
- Implement intuitive workflow: save → load → edit → rename → resave
- Add visual hierarchy with proper spacing and typography

### Technical Details:

**Problem Solved:**
- Team configuration functionality existed but was hidden in unused template
- Users reported "no indication i can save multiple team configs"
- Configuration management was not visible or accessible

**Solution:**
- Identified dual template system in webserver.py (line 4160 vs 5080)
- Added complete configuration section to actively used template
- Enhanced both CSS styling and JavaScript functionality
- Implemented full backend API support with database persistence

**Files Modified:**
- webserver.py: +600 lines (template fixes, API endpoints, CSS, JavaScript)
- src/database.py: +20 lines (rename_team_configuration method)
- modules/admin.py: +150 lines (weather improvements, enhanced commands)
- TODO.md: Updated progress tracking and completed items

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-15 22:40:23 +00:00

320 lines
No EOL
14 KiB
Python
Raw 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.

#!/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", "rate_stats", "rate_user", "rate_unban", "rate_reset", "weather", "setweather"]
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)
elif command == "weather":
await self.cmd_weather(channel, nickname, args)
elif command == "setweather":
await self.cmd_setweather(channel, nickname, args)
async def cmd_reload(self, channel, nickname):
"""Reload bot modules (admin only)"""
if not self.is_admin(nickname):
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
return
try:
# Trigger module reload in main bot
success = await self.bot.reload_modules()
if success:
self.send_message(channel, f"{nickname}: ✅ Modules reloaded successfully!")
else:
self.send_message(channel, f"{nickname}: ❌ Module reload failed!")
except Exception as 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)}")
async def cmd_weather(self, channel, nickname, args):
"""Check current weather in locations (admin only)"""
if not self.is_admin(nickname):
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
return
try:
if args and args[0].lower() != "all":
# Check weather for specific location
location_name = " ".join(args)
weather = await self.database.get_location_weather_by_name(location_name)
if weather:
self.send_message(channel,
f"🌤️ {nickname}: {location_name} - {weather['weather_type']} "
f"(modifier: {weather['spawn_modifier']}x, "
f"until: {weather['active_until'][:16]})")
else:
self.send_message(channel, f"{nickname}: Location '{location_name}' not found or no weather data.")
else:
# Show weather for all locations
all_weather = await self.database.get_all_location_weather()
if all_weather:
weather_info = []
for w in all_weather:
weather_info.append(f"{w['location_name']}: {w['weather_type']} ({w['spawn_modifier']}x)")
self.send_message(channel, f"🌤️ {nickname}: Current weather - " + " | ".join(weather_info))
else:
self.send_message(channel, f"{nickname}: No weather data available.")
except Exception as e:
self.send_message(channel, f"{nickname}: ❌ Error checking weather: {str(e)}")
async def cmd_setweather(self, channel, nickname, args):
"""Force change weather in a location or all locations (admin only)"""
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: !setweather <location|all> <weather_type> [duration_minutes]\n"
f"Weather types: sunny, rainy, storm, blizzard, earthquake, calm")
return
try:
import json
import random
import datetime
# Load weather patterns
with open("config/weather_patterns.json", "r") as f:
weather_data = json.load(f)
weather_types = list(weather_data["weather_types"].keys())
# Smart argument parsing - check if any arg is a weather type
location_arg = None
weather_type = None
duration = None
for i, arg in enumerate(args):
if arg.lower() in weather_types:
weather_type = arg.lower()
# Remove weather type from args for location parsing
remaining_args = args[:i] + args[i+1:]
break
if not weather_type:
self.send_message(channel, f"{nickname}: Please specify a valid weather type.")
return
# Parse location from remaining args
if remaining_args:
if remaining_args[0].lower() == "all":
location_arg = "all"
# Check if there's a duration after "all"
if len(remaining_args) > 1:
try:
duration = int(remaining_args[1])
except ValueError:
pass
else:
# Location name (might be multiple words)
location_words = []
for arg in remaining_args:
try:
# If it's a number, it's probably duration
duration = int(arg)
break
except ValueError:
# It's part of location name
location_words.append(arg)
location_arg = " ".join(location_words) if location_words else "all"
else:
location_arg = "all"
weather_config = weather_data["weather_types"][weather_type]
# Calculate duration
if not duration:
duration_range = weather_config.get("duration_minutes", [90, 180])
duration = random.randint(duration_range[0], duration_range[1])
end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration)
if location_arg.lower() == "all":
# Set weather for all locations
success = await self.database.set_weather_all_locations(
weather_type, end_time.isoformat(),
weather_config.get("spawn_modifier", 1.0),
",".join(weather_config.get("affected_types", []))
)
if success:
self.send_message(channel,
f"🌤️ {nickname}: Set {weather_type} weather for ALL locations! "
f"Duration: {duration} minutes, Modifier: {weather_config.get('spawn_modifier', 1.0)}x")
else:
self.send_message(channel, f"{nickname}: Failed to set weather for all locations.")
else:
# Set weather for specific location
location_name = location_arg if len(args) == 2 else " ".join(args[:-1])
success = await self.database.set_weather_for_location(
location_name, weather_type, end_time.isoformat(),
weather_config.get("spawn_modifier", 1.0),
",".join(weather_config.get("affected_types", []))
)
if success:
self.send_message(channel,
f"🌤️ {nickname}: Set {weather_type} weather for {location_name}! "
f"Duration: {duration} minutes, Modifier: {weather_config.get('spawn_modifier', 1.0)}x")
else:
self.send_message(channel, f"{nickname}: Failed to set weather for '{location_name}'. Location may not exist.")
except FileNotFoundError:
self.send_message(channel, f"{nickname}: ❌ Weather configuration file not found.")
except ValueError as e:
self.send_message(channel, f"{nickname}: ❌ Invalid duration: {str(e)}")
except Exception as e:
self.send_message(channel, f"{nickname}: ❌ Error setting weather: {str(e)}")