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:
megaproxy 2025-07-15 20:10:43 +00:00
parent f8ac661cd1
commit 915aa00bea
28 changed files with 5730 additions and 57 deletions

View file

@ -0,0 +1,229 @@
# PetBot Backup System Integration Guide
## Overview
The PetBot backup system provides automated database backups with rotation, compression, and restore capabilities. This system ensures data protection and disaster recovery for the PetBot project.
## Components
### 1. Core Backup Module (`src/backup_manager.py`)
**Features:**
- Automated database backups using SQLite backup API
- Gzip compression for space efficiency
- Retention policies (7 daily, 4 weekly, 12 monthly)
- Backup verification and integrity checks
- Restore functionality with automatic current database backup
- Database structure export for documentation
**Key Classes:**
- `BackupManager`: Main backup operations
- `BackupScheduler`: Automated scheduling system
### 2. IRC Command Module (`modules/backup_commands.py`)
**Commands:**
- `!backup [type] [uncompressed]` - Create manual backup
- `!restore <filename> [confirm]` - Restore from backup
- `!backups` - List available backups
- `!backup_stats` - Show backup statistics
- `!backup_cleanup` - Clean up old backups
**Security:**
- Admin-only commands
- Confirmation required for restore operations
- Automatic current database backup before restore
### 3. Configuration (`config/backup_config.json`)
**Settings:**
- Backup retention policies
- Compression settings
- Automated schedule configuration
- Monitoring and notification preferences
## Installation
### 1. Dependencies
Ensure the following Python packages are installed:
```bash
pip install aiosqlite
```
### 2. Directory Structure
Create the backup directory:
```bash
mkdir -p backups
```
### 3. Integration with Bot
Add the backup commands module to your bot's module loader in `src/bot.py`:
```python
from modules.backup_commands import BackupCommands
# In your bot initialization
self.backup_commands = BackupCommands(self, self.database)
```
### 4. Configuration
Update `modules/backup_commands.py` with your admin usernames:
```python
admin_users = ["admin", "your_username"] # Replace with actual admin usernames
```
## Usage
### Manual Backup Creation
```
!backup # Create manual compressed backup
!backup uncompressed # Create uncompressed backup
!backup daily # Create daily backup
```
### Listing Backups
```
!backups # List available backups
!backup_stats # Show detailed statistics
```
### Restore Operations
```
!restore backup_filename.db.gz # Show restore confirmation
!restore backup_filename.db.gz confirm # Actually perform restore
```
### Maintenance
```
!backup_cleanup # Remove old backups per retention policy
```
## Automated Scheduling
The backup system automatically creates:
- **Daily backups**: Every 24 hours
- **Weekly backups**: Every 7 days
- **Monthly backups**: Every 30 days
Automated cleanup removes old backups based on retention policy:
- Keep 7 most recent daily backups
- Keep 4 most recent weekly backups
- Keep 12 most recent monthly backups
## Monitoring
### Log Messages
The backup system logs important events:
- Backup creation success/failure
- Restore operations
- Cleanup operations
- Scheduler status
### Statistics
Use `!backup_stats` to monitor:
- Total backup count and size
- Backup age information
- Breakdown by backup type
## Security Considerations
1. **Access Control**: Only admin users can execute backup commands
2. **Confirmation**: Restore operations require explicit confirmation
3. **Safety Backup**: Current database is automatically backed up before restore
4. **Integrity Checks**: All backups are verified after creation
## Directory Structure
```
backups/
├── petbot_backup_daily_20240115_030000.db.gz
├── petbot_backup_weekly_20240114_020000.db.gz
├── petbot_backup_monthly_20240101_010000.db.gz
└── database_structure_20240115_120000.json
```
## Backup Filename Format
```
petbot_backup_{type}_{timestamp}.db[.gz]
```
- `{type}`: daily, weekly, monthly, manual
- `{timestamp}`: YYYYMMDD_HHMMSS format
- `.gz`: Added for compressed backups
## Troubleshooting
### Common Issues
1. **Permission Errors**: Ensure backup directory is writable
2. **Disk Space**: Monitor available space, backups are compressed by default
3. **Database Locked**: Backups use SQLite backup API to avoid locking issues
### Recovery Procedures
1. **Database Corruption**: Use most recent backup with `!restore`
2. **Backup Corruption**: Try previous backup or use database structure export
3. **Complete Loss**: Restore from most recent backup, may lose recent data
## Testing
Run the test suite to verify functionality:
```bash
python3 test_backup_simple.py
```
This tests:
- Basic backup creation
- Compression functionality
- Restore operations
- Directory structure
- Real database compatibility
## Performance
### Backup Sizes
- Uncompressed: ~2-5MB for typical database
- Compressed: ~200-500KB (60-90% reduction)
### Backup Speed
- Small databases (<10MB): 1-2 seconds
- Medium databases (10-100MB): 5-10 seconds
- Large databases (>100MB): 30+ seconds
## Future Enhancements
1. **Encryption**: Add backup encryption for sensitive data
2. **Remote Storage**: Support for cloud storage providers
3. **Differential Backups**: Only backup changed data
4. **Web Interface**: Backup management through web interface
5. **Email Notifications**: Alert on backup failures
## Support
For issues or questions about the backup system:
1. Check the logs for error messages
2. Run the test suite to verify functionality
3. Ensure all dependencies are installed
4. Verify directory permissions
## Version History
- **v1.0**: Initial implementation with basic backup/restore
- **v1.1**: Added compression and automated scheduling
- **v1.2**: Added IRC commands and admin controls
- **v1.3**: Added comprehensive testing and documentation

View file

@ -25,11 +25,23 @@ PetBot is a Discord/IRC Pokemon-style pet collecting bot with web interface. The
- Use Read tool for examining specific file content
- Use Glob tool for finding files by name patterns
### 3. Testing and Validation
- Always run `python3 -c "import webserver; print('✅ syntax check')"` after webserver changes
- Test database operations with simple validation scripts
- Check IRC bot functionality with `python3 run_bot_debug.py`
- Verify web interface functionality through browser testing
### 3. Bot Startup and Testing
- **Primary Startup**: `./start_petbot.sh` - One command that handles everything
- Automatically creates/activates virtual environment
- Installs/updates all dependencies
- Verifies core modules
- Creates required directories
- Launches bot with all features (IRC + web interface + rate limiting)
- **Rate Limiting Tests**: `source venv/bin/activate && python test_rate_limiting.py`
- **Web Interface**: Available at http://localhost:8080 after startup
- **Virtual Environment**: Required due to externally-managed-environment restriction
### 4. Important: Configuration Management
- **Admin User**: Edit `config.py` to change the single admin user (currently: megasconed)
- **Item Spawn Rates**: Edit `config/items.json` to adjust global spawn multiplier and individual rates
- **Startup Script**: Always update `start_petbot.sh` when adding dependencies
- **Central Config**: All major settings are in `config.py` for easy maintenance
- **Remember**: This is the user's primary interface - keep it working!
## Project Structure
@ -51,8 +63,14 @@ PetBot is a Discord/IRC Pokemon-style pet collecting bot with web interface. The
│ ├── admin.py # Administrative commands
│ └── team_builder.py # Team builder module (web-only)
├── webserver.py # Web server with unified templates
├── run_bot_debug.py # Bot startup and debug mode
├── help.html # Static help documentation
├── run_bot_with_reconnect.py # Main bot with IRC reconnection and rate limiting
├── start_petbot.sh # One-command startup script (handles venv and dependencies)
├── config.py # Central configuration (admin user, IRC, rate limiting)
├── config/items.json # Item configuration with global spawn multiplier
├── test_rate_limiting.py # Comprehensive rate limiting test suite
├── requirements.txt # Python package dependencies
├── rate_limiting_config.json # Rate limiting configuration reference
├── help.html # Static help documentation with rate limiting info
├── CHANGELOG.md # Version history and feature tracking
└── README.md # Project documentation
```

462
INSTALLATION.md Normal file
View file

@ -0,0 +1,462 @@
# PetBot Installation Guide
## Overview
PetBot is a Python-based IRC bot with web interface for Pokemon-style pet collecting. This guide covers complete installation and setup.
## Prerequisites
### System Requirements
- **Python**: 3.7 or higher
- **pip**: Python package installer
- **Operating System**: Linux, macOS, or Windows with Python support
- **Memory**: 512MB RAM minimum
- **Storage**: 1GB available space
- **Network**: Internet connection for IRC and web access
### Required Python Packages
- `irc>=20.3.0` - IRC client library
- `aiosqlite>=0.19.0` - Async SQLite database interface
- `python-dotenv>=1.0.0` - Environment variable loading
## Installation Methods
### Method 1: Automatic Installation (Recommended)
#### Step 1: Download and Run Installation Script
```bash
# Make the script executable
chmod +x install_prerequisites.sh
# Run the installation
./install_prerequisites.sh
```
#### Step 2: Alternative Python Script
If the shell script doesn't work:
```bash
python3 install_prerequisites.py
```
### Method 2: Manual Installation
#### Step 1: Install System Dependencies
**Ubuntu/Debian:**
```bash
sudo apt update
sudo apt install python3 python3-pip python3-venv
```
**CentOS/RHEL:**
```bash
sudo yum install python3 python3-pip
```
**macOS:**
```bash
# Install Homebrew if not installed
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install Python
brew install python3
```
**Windows:**
1. Download Python from https://python.org/downloads/
2. Run installer and check "Add Python to PATH"
3. Open Command Prompt as administrator
#### Step 2: Install Python Packages
```bash
# Install from requirements file
pip3 install -r requirements.txt
# Or install individually
pip3 install irc>=20.3.0 aiosqlite>=0.19.0 python-dotenv>=1.0.0
```
#### Step 3: Create Required Directories
```bash
mkdir -p data backups logs
```
#### Step 4: Make Scripts Executable (Linux/macOS)
```bash
chmod +x run_bot_debug.py
chmod +x run_bot_with_reconnect.py
chmod +x test_backup_simple.py
chmod +x test_reconnection.py
```
## Verification
### Test Installation
```bash
# Test backup system
python3 test_backup_simple.py
# Test IRC reconnection system
python3 test_reconnection.py
```
### Expected Output
Both tests should show:
```
🎉 All tests passed! ... is working correctly.
```
## Configuration
### IRC Settings
The bot connects to IRC with default settings:
```python
config = {
"server": "irc.libera.chat",
"port": 6667,
"nickname": "PetBot",
"channel": "#petz",
"command_prefix": "!"
}
```
To modify these settings, edit the configuration in:
- `run_bot_debug.py` (line 21-27)
- `run_bot_with_reconnect.py` (line 35-41)
### Database Configuration
The bot uses SQLite database stored in `data/petbot.db`. No additional configuration required.
### Web Server Configuration
The web server runs on port 8080 by default. To change:
- Edit `webserver.py` or bot runner files
- Update the port number in web server initialization
## Running the Bot
### Option 1: Debug Mode (Original)
```bash
python3 run_bot_debug.py
```
**Features:**
- Basic IRC connection
- Console debugging output
- Module loading and validation
- Web server on port 8080
### Option 2: Auto-Reconnect Mode (Recommended)
```bash
python3 run_bot_with_reconnect.py
```
**Features:**
- Automatic IRC reconnection
- Connection health monitoring
- Exponential backoff for reconnection
- Comprehensive logging
- Connection statistics
- Web server on port 8080
### Running as a Service (Linux)
Create a systemd service file:
```bash
sudo nano /etc/systemd/system/petbot.service
```
```ini
[Unit]
Description=PetBot IRC Bot
After=network.target
[Service]
Type=simple
User=petbot
WorkingDirectory=/path/to/petbot
ExecStart=/usr/bin/python3 run_bot_with_reconnect.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
Enable and start:
```bash
sudo systemctl enable petbot
sudo systemctl start petbot
sudo systemctl status petbot
```
## Web Interface
### Access
- **Local**: http://localhost:8080
- **Production**: Configure reverse proxy for HTTPS
### Available Pages
- `/` - Homepage and help
- `/players` - Player leaderboard
- `/player/<name>` - Player profile
- `/teambuilder/<name>` - Team builder with PIN verification
## Troubleshooting
### Common Issues
#### 1. ModuleNotFoundError
**Error:** `ModuleNotFoundError: No module named 'irc'`
**Solution:**
```bash
pip3 install irc>=20.3.0 aiosqlite>=0.19.0 python-dotenv>=1.0.0
```
#### 2. Permission Denied
**Error:** `Permission denied: './install_prerequisites.sh'`
**Solution:**
```bash
chmod +x install_prerequisites.sh
```
#### 3. Database Errors
**Error:** `sqlite3.OperationalError: unable to open database file`
**Solution:**
```bash
mkdir -p data
chmod 755 data
```
#### 4. IRC Connection Issues
**Error:** `Could not connect to irc.libera.chat:6667`
**Solution:**
- Check internet connection
- Verify IRC server is accessible
- Check firewall settings
- Try different IRC server/port
#### 5. Web Interface Not Accessible
**Error:** `Connection refused on port 8080`
**Solution:**
- Check if bot is running
- Verify port 8080 is not blocked
- Check firewall settings
- Try accessing via 127.0.0.1:8080
### Debugging
#### Enable Debug Logging
```bash
python3 run_bot_with_reconnect.py > logs/bot.log 2>&1
```
#### Check System Resources
```bash
# Check memory usage
free -h
# Check disk space
df -h
# Check network connectivity
ping irc.libera.chat
```
#### Test Individual Components
```bash
# Test database
python3 -c "from src.database import Database; db = Database(); print('Database OK')"
# Test IRC connection manager
python3 -c "from src.irc_connection_manager import IRCConnectionManager; print('IRC manager OK')"
# Test web server
python3 -c "import webserver; print('Web server OK')"
```
## Security Considerations
⚠️ **Important**: Review `issues.txt` for security vulnerabilities before production deployment.
### Immediate Actions Required
1. **HTTPS**: Run behind reverse proxy with SSL
2. **Authentication**: Implement web interface authentication
3. **Input Validation**: Fix XSS vulnerabilities
4. **Access Control**: Implement proper user authorization
### Production Checklist
- [ ] SSL/TLS certificate installed
- [ ] Reverse proxy configured (nginx/Apache)
- [ ] Firewall rules configured
- [ ] User authentication implemented
- [ ] Input validation added
- [ ] Security headers configured
- [ ] Database backups scheduled
- [ ] Log rotation configured
- [ ] Monitoring alerts set up
## Performance Optimization
### Database
- Enable WAL mode for better concurrent access
- Regular VACUUM operations
- Monitor database size and growth
### Web Server
- Use reverse proxy for static files
- Enable gzip compression
- Implement caching for static content
### IRC Connection
- Monitor connection stability
- Adjust reconnection parameters if needed
- Set up connection monitoring alerts
## Backup and Recovery
### Automated Backups
The bot includes automated backup system:
```bash
# Manual backup
python3 -c "
import asyncio
from src.backup_manager import BackupManager
bm = BackupManager()
result = asyncio.run(bm.create_backup('manual', True))
print(f'Backup created: {result}')
"
```
### Recovery
```bash
# List available backups
ls -la backups/
# Restore from backup (use admin command or direct restore)
python3 -c "
import asyncio
from src.backup_manager import BackupManager
bm = BackupManager()
result = asyncio.run(bm.restore_backup('backup_filename.db.gz'))
print(f'Restore result: {result}')
"
```
## Monitoring
### Connection Health
Use the connection monitoring commands:
```
!status # Check connection status
!uptime # Show uptime and stats
!ping # Test responsiveness
!connection_stats # Detailed statistics
```
### Log Monitoring
Monitor logs for:
- Connection failures
- Database errors
- Command execution errors
- Security events
### System Resources
Monitor:
- Memory usage
- CPU usage
- Disk space
- Network connectivity
## Development
### Adding New Features
1. Follow patterns in `CLAUDE.md`
2. Create new modules in `modules/`
3. Update documentation
4. Add tests
5. Test thoroughly
### Contributing
1. Review security issues in `issues.txt`
2. Follow coding conventions
3. Add comprehensive tests
4. Update documentation
5. Consider security implications
## Support
### Documentation
- `README.md` - Project overview
- `CLAUDE.md` - Development guidelines
- `TODO.md` - Current project status
- `issues.txt` - Security audit findings
- `BACKUP_SYSTEM_INTEGRATION.md` - Backup system guide
### Getting Help
1. Check error logs and console output
2. Review troubleshooting section
3. Test with provided test scripts
4. Check network connectivity
5. Verify all dependencies are installed
## Next Steps
1. ✅ Complete installation
2. ✅ Test basic functionality
3. ✅ Configure IRC settings
4. ✅ Start the bot
5. ✅ Access web interface
6. 📋 Review security issues
7. 🔧 Configure for production
8. 🚀 Deploy and monitor
Congratulations! Your PetBot should now be running successfully. 🐾

206
QUICKSTART.md Normal file
View file

@ -0,0 +1,206 @@
# PetBot Quick Start Guide
## Prerequisites Installation
### Method 1: Automatic Installation (Recommended)
Run the automatic installation script:
```bash
# Make the script executable
chmod +x install_prerequisites.sh
# Run the installation
./install_prerequisites.sh
```
Or use the Python version:
```bash
python3 install_prerequisites.py
```
### Method 2: Manual Installation
If you prefer manual installation:
```bash
# Install required packages
pip3 install -r requirements.txt
# Or install individually
pip3 install irc>=20.3.0 aiosqlite>=0.19.0 python-dotenv>=1.0.0
# Create required directories
mkdir -p data backups logs
```
## System Requirements
- **Python**: 3.7 or higher
- **pip**: Python package installer
- **Operating System**: Linux, macOS, or Windows with Python support
## Quick Start
### 1. Install Prerequisites
```bash
./install_prerequisites.sh
```
### 2. Test Basic Functionality
```bash
# Test backup system
python3 test_backup_simple.py
# Test IRC reconnection system
python3 test_reconnection.py
```
### 3. Run the Bot
#### Option A: Debug Mode (Original)
```bash
python3 run_bot_debug.py
```
#### Option B: With Auto-Reconnect (Recommended)
```bash
python3 run_bot_with_reconnect.py
```
### 4. Access Web Interface
Open your browser and go to:
- **Local**: http://localhost:8080
- **Production**: http://petz.rdx4.com (if configured)
## Configuration
### IRC Settings
Edit the configuration in the bot files:
```python
config = {
"server": "irc.libera.chat",
"port": 6667,
"nickname": "PetBot",
"channel": "#petz",
"command_prefix": "!"
}
```
### Database
The bot uses SQLite database stored in `data/petbot.db`. No additional setup required.
## Available Commands
### Basic Commands
- `!help` - Show available commands
- `!start` - Begin your pet journey
- `!stats` - View your stats
- `!pets` - View your pets
### Connection Monitoring
- `!status` - Show bot connection status
- `!uptime` - Show bot uptime
- `!ping` - Test bot responsiveness
### Admin Commands
- `!backup` - Create database backup
- `!restore <filename>` - Restore database
- `!reload` - Reload bot modules
- `!reconnect` - Force IRC reconnection
## Troubleshooting
### Common Issues
1. **ModuleNotFoundError**: Run the prerequisites installer
2. **Permission Denied**: Make sure scripts are executable (`chmod +x`)
3. **Database Errors**: Check that `data/` directory exists and is writable
4. **IRC Connection Issues**: Check network connectivity and IRC server status
### Getting Help
1. Check the error logs in console output
2. Review `CLAUDE.md` for development guidelines
3. Check `issues.txt` for known security issues
4. Review `TODO.md` for current project status
### Log Files
Logs are displayed in the console. For persistent logging, redirect output:
```bash
python3 run_bot_with_reconnect.py > logs/bot.log 2>&1
```
## Development
### Running Tests
```bash
# Test backup system
python3 test_backup_simple.py
# Test reconnection system
python3 test_reconnection.py
```
### Code Structure
```
PetBot/
├── src/ # Core system
│ ├── database.py # Database operations
│ ├── game_engine.py # Game logic
│ ├── bot.py # IRC bot core
│ ├── irc_connection_manager.py # Connection handling
│ └── backup_manager.py # Backup system
├── modules/ # Command modules
├── config/ # Configuration files
├── webserver.py # Web interface
└── run_bot_*.py # Bot runners
```
### Adding New Commands
1. Create a new method in appropriate module
2. Add command to `get_commands()` list
3. Test thoroughly
4. Update documentation
## Security
⚠️ **Important**: Review `issues.txt` for security vulnerabilities before production deployment.
Key security considerations:
- Run behind HTTPS reverse proxy
- Review XSS vulnerabilities in web interface
- Implement proper authentication
- Keep dependencies updated
## Next Steps
1. ✅ Run prerequisites installer
2. ✅ Test basic functionality
3. ✅ Start the bot
4. ✅ Access web interface
5. 📋 Review security issues
6. 🔧 Configure for production
7. 🚀 Deploy and monitor
## Support
For issues or questions:
1. Check this guide and documentation
2. Review error messages and logs
3. Test with the provided test scripts
4. Check network connectivity and dependencies
Happy bot hosting! 🐾

80
TODO.md
View file

@ -3,9 +3,9 @@
This file tracks completed work, pending bugs, enhancements, and feature ideas for the PetBot project.
## 📊 Summary
- **✅ Completed**: 14 items
- **🐛 Bugs**: 1 item
- **🔧 Enhancements**: 5 items
- **✅ Completed**: 17 items
- **🐛 Bugs**: 0 items
- **🔧 Enhancements**: 3 items
- **💡 Ideas**: 10 items
- **📋 Total**: 30 items tracked
@ -74,6 +74,24 @@ This file tracks completed work, pending bugs, enhancements, and feature ideas f
- Updated help system with web interface integration
- Enhanced project documentation for contributors
- [x] **Implement automated database backup system**
- Complete backup management system with BackupManager class
- Automated scheduling with daily, weekly, and monthly backups
- Backup compression using gzip for space efficiency
- Retention policies (7 daily, 4 weekly, 12 monthly backups)
- IRC admin commands for backup management (!backup, !restore, !backups, !backup_stats, !backup_cleanup)
- Comprehensive testing suite and integration documentation
- Database integrity verification and safe restore procedures
- [x] **IRC connection monitoring and auto-reconnect functionality**
- Advanced IRC connection manager with robust state tracking
- Health monitoring system with ping/pong heartbeat (60s intervals)
- Exponential backoff reconnection (1s to 5min with jitter)
- Connection statistics and monitoring commands (!status, !uptime, !ping, !reconnect, !connection_stats)
- Graceful error handling and recovery from network interruptions
- Comprehensive test suite covering 11 scenarios including edge cases
- Integration with existing bot architecture and module system
### Low Priority Completed ✅
- [x] **Create CLAUDE.md file documenting development patterns and conventions**
- Comprehensive development guide for AI-assisted development
@ -85,32 +103,54 @@ This file tracks completed work, pending bugs, enhancements, and feature ideas f
## 🐛 KNOWN BUGS
### Medium Priority Bugs 🔴
- [ ] **IRC connection monitoring and auto-reconnect functionality**
- Bot may lose connection without proper recovery
- Need robust reconnection logic with exponential backoff
- Monitor connection health and implement graceful reconnection
- [x] **IRC connection monitoring and auto-reconnect functionality**
- ✅ Bot may lose connection without proper recovery
- ✅ Need robust reconnection logic with exponential backoff
- ✅ Monitor connection health and implement graceful reconnection
- ✅ Implemented comprehensive IRC connection manager with state tracking
- ✅ Added health monitoring with ping/pong system
- ✅ Created exponential backoff with jitter for reconnection attempts
- ✅ Added connection statistics and monitoring commands
- ✅ Comprehensive test suite with 11 test scenarios
---
## 🔧 ENHANCEMENTS NEEDED
### High Priority Enhancements 🟠
- [ ] **Implement automated database backup system**
- Regular automated backups of SQLite database
- Backup rotation and retention policies
- Recovery procedures and testing
- [x] **Implement automated database backup system**
- ✅ Regular automated backups of SQLite database (daily, weekly, monthly)
- ✅ Backup rotation and retention policies (7 daily, 4 weekly, 12 monthly)
- ✅ Recovery procedures and testing (restore with confirmation)
- ✅ Compression support (gzip) for space efficiency
- ✅ IRC admin commands for backup management
- ✅ Automated scheduling with cleanup
- [ ] **Conduct security audit of web interface and IRC bot**
- Review all user input validation
- Audit authentication and authorization mechanisms
- Test for common web vulnerabilities (XSS, CSRF, injection attacks)
- Review IRC bot security practices
- [x] **Conduct security audit of web interface and IRC bot**
- ✅ Review all user input validation
- ✅ Audit authentication and authorization mechanisms
- ✅ Test for common web vulnerabilities (XSS, CSRF, injection attacks)
- ✅ Review IRC bot security practices
- ✅ Identified 23 security vulnerabilities (5 critical, 8 high, 7 medium, 3 low)
- ✅ Created comprehensive security report in issues.txt
- [ ] **Address security vulnerabilities from audit**
- Fix XSS vulnerabilities by implementing HTML escaping
- Add HTTP security headers (CSP, X-Frame-Options, etc.)
- Implement web interface authentication and authorization
- Fix path traversal vulnerabilities
- Add input validation and sanitization
- See issues.txt for complete list and remediation priorities
### Medium Priority Enhancements 🟡
- [ ] **Add rate limiting to prevent command spam and abuse**
- Implement per-user rate limiting on IRC commands
- Web interface request throttling
- Graceful handling of rate limit violations
- [x] **Add rate limiting to prevent command spam and abuse**
- ✅ Implemented comprehensive token bucket rate limiting system
- ✅ Per-user rate limiting on IRC commands with category-based limits
- ✅ Web interface request throttling with IP-based tracking
- ✅ Graceful handling of rate limit violations with user-friendly messages
- ✅ Admin commands for monitoring and management (!rate_stats, !rate_user, !rate_unban, !rate_reset)
- ✅ Automatic cleanup of old violations and expired bans
- ✅ Central configuration system with single admin user control
- [ ] **Implement comprehensive error logging and monitoring system**
- Structured logging with appropriate log levels

369
TROUBLESHOOTING.md Normal file
View file

@ -0,0 +1,369 @@
# PetBot Troubleshooting Guide
## Common Installation Issues
### Issue 1: externally-managed-environment Error
**Error Message:**
```
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.
```
**Cause:** Modern Python installations (3.11+) prevent direct pip installations to avoid conflicts with system packages.
**Solutions:**
#### Solution A: Use Fixed Installation Script (Recommended)
```bash
./install_prerequisites_simple.sh
```
This creates a virtual environment and installs packages there.
#### Solution B: Manual Virtual Environment Setup
```bash
# Create virtual environment
python3 -m venv venv
# Activate virtual environment
source venv/bin/activate
# Install packages
pip install -r requirements.txt
# Run bot (while venv is active)
python run_bot_with_reconnect.py
# Deactivate when done
deactivate
```
#### Solution C: System Package Installation (Ubuntu/Debian)
```bash
# Install system packages instead
sudo apt update
sudo apt install python3-pip python3-aiosqlite python3-dotenv
# For IRC library, you may still need pip in venv or --break-system-packages
pip install --break-system-packages irc>=20.3.0
```
#### Solution D: Force Installation (Not Recommended)
```bash
pip install --break-system-packages -r requirements.txt
```
⚠️ **Warning:** This can break system Python packages.
### Issue 2: venv Module Not Found
**Error Message:**
```
ModuleNotFoundError: No module named 'venv'
```
**Solution:**
```bash
# Ubuntu/Debian
sudo apt install python3-venv
# CentOS/RHEL
sudo yum install python3-venv
# Or try alternative
sudo apt install python3-virtualenv
```
### Issue 3: Permission Denied
**Error Message:**
```
Permission denied: './install_prerequisites_simple.sh'
```
**Solution:**
```bash
chmod +x install_prerequisites_simple.sh
./install_prerequisites_simple.sh
```
### Issue 4: Python Version Too Old
**Error Message:**
```
Python 3.6.x is not supported
```
**Solutions:**
#### Ubuntu/Debian:
```bash
# Add deadsnakes PPA for newer Python
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.11 python3.11-venv python3.11-pip
# Use specific version
python3.11 -m venv venv
```
#### CentOS/RHEL:
```bash
# Enable EPEL and install Python 3.9+
sudo yum install epel-release
sudo yum install python39 python39-pip
```
#### Manual Compilation:
```bash
# Download and compile Python (last resort)
wget https://www.python.org/ftp/python/3.11.0/Python-3.11.0.tgz
tar xzf Python-3.11.0.tgz
cd Python-3.11.0
./configure --enable-optimizations
make -j 8
sudo make altinstall
```
## Runtime Issues
### Issue 5: IRC Connection Failed
**Error Message:**
```
Could not connect to irc.libera.chat:6667
```
**Solutions:**
1. Check internet connection: `ping irc.libera.chat`
2. Try different IRC server/port
3. Check firewall settings
4. Use IRC over TLS (port 6697)
### Issue 6: Database Locked
**Error Message:**
```
sqlite3.OperationalError: database is locked
```
**Solutions:**
1. Stop all bot instances
2. Check for stale lock files: `rm -f data/petbot.db-wal data/petbot.db-shm`
3. Restart the bot
### Issue 7: Web Interface Not Accessible
**Error Message:**
```
Connection refused on port 8080
```
**Solutions:**
1. Check if bot is running
2. Verify port 8080 is not in use: `netstat -tlnp | grep 8080`
3. Try different port in webserver.py
4. Check firewall: `sudo ufw allow 8080`
### Issue 8: Module Import Errors
**Error Message:**
```
ModuleNotFoundError: No module named 'irc'
```
**Solutions:**
1. Make sure virtual environment is activated
2. Reinstall packages: `pip install -r requirements.txt`
3. Check Python path: `python -c "import sys; print(sys.path)"`
## Using Virtual Environment
### Daily Usage Pattern
```bash
# Activate virtual environment
source venv/bin/activate
# Run bot
python run_bot_with_reconnect.py
# Run tests
python test_backup_simple.py
# Deactivate when done
deactivate
```
### Or Use Wrapper Scripts
```bash
# These automatically handle venv activation
./run_petbot.sh # Start bot
./run_petbot_debug.sh # Debug mode
./test_petbot.sh # Run tests
./activate_petbot.sh # Manual activation
```
## Virtual Environment Management
### Check if Virtual Environment is Active
```bash
# Should show venv path if active
which python
# Or check environment variable
echo $VIRTUAL_ENV
```
### Recreate Virtual Environment
```bash
# Remove old venv
rm -rf venv
# Create new one
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### List Installed Packages
```bash
# Activate venv first
source venv/bin/activate
# List packages
pip list
# Show package info
pip show irc aiosqlite
```
## System-Specific Issues
### Ubuntu 22.04+ (externally-managed-environment)
- Use virtual environment (recommended)
- Or install system packages: `sudo apt install python3-aiosqlite`
- IRC library may still need pip installation
### CentOS/RHEL 8+
- Enable EPEL repository
- Install python39 or newer
- Use virtual environment
### macOS
- Install via Homebrew: `brew install python3`
- Virtual environment should work normally
### Windows
- Install Python from python.org
- Use Command Prompt or PowerShell
- Virtual environment commands:
```cmd
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
```
## Development Environment
### IDE/Editor Setup
#### VS Code
```json
{
"python.defaultInterpreterPath": "./venv/bin/python",
"python.terminal.activateEnvironment": true
}
```
#### PyCharm
- Set Project Interpreter to `./venv/bin/python`
- Enable "Add content roots to PYTHONPATH"
### Running Tests in IDE
Make sure to:
1. Set interpreter to venv Python
2. Set working directory to project root
3. Add project root to PYTHONPATH
## Quick Diagnosis Commands
### Check Python Setup
```bash
python3 --version
python3 -c "import sys; print(sys.executable)"
python3 -c "import sys; print(sys.path)"
```
### Check Package Installation
```bash
# In virtual environment
python -c "import irc, aiosqlite, dotenv; print('All packages OK')"
```
### Check File Permissions
```bash
ls -la *.sh *.py
ls -la data/ backups/
```
### Check Network Connectivity
```bash
ping irc.libera.chat
telnet irc.libera.chat 6667
curl -I http://localhost:8080
```
### Check System Resources
```bash
free -h # Memory
df -h # Disk space
ps aux | grep python # Running Python processes
```
## Getting Help
1. **Check Logs:** Look at console output for specific error messages
2. **Test Components:** Use individual test scripts to isolate issues
3. **Verify Environment:** Ensure virtual environment is activated
4. **Check Dependencies:** Verify all packages are installed correctly
5. **Review Documentation:** Check INSTALLATION.md for detailed setup
## Emergency Recovery
### Complete Reset
```bash
# Remove everything
rm -rf venv data/*.db backups/* logs/*
# Reinstall
./install_prerequisites_simple.sh
# Restart
./run_petbot.sh
```
### Backup Database Before Reset
```bash
# Create manual backup
cp data/petbot.db data/petbot_backup_$(date +%Y%m%d_%H%M%S).db
```
### Restore from Backup
```bash
# List available backups
ls -la backups/
# Restore (replace with actual filename)
cp backups/petbot_backup_daily_20240115_030000.db.gz /tmp/
gunzip /tmp/petbot_backup_daily_20240115_030000.db
cp /tmp/petbot_backup_daily_20240115_030000.db data/petbot.db
```
Remember: When in doubt, use the virtual environment! It solves most installation issues. 🐾

68
config.py Normal file
View file

@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""
PetBot Configuration
Central configuration file for admin users and other settings
"""
# =============================================================================
# ADMIN CONFIGURATION - Edit this to change the admin user
# =============================================================================
ADMIN_USER = "megasconed" # The single admin user who can run admin commands
# =============================================================================
# IRC Configuration
IRC_CONFIG = {
"server": "irc.libera.chat",
"port": 6667,
"nickname": "PetBot",
"channel": "#petz",
"command_prefix": "!"
}
# Web Server Configuration
WEB_CONFIG = {
"port": 8080,
"host": "localhost"
}
# Rate Limiting Configuration
RATE_LIMIT_CONFIG = {
"enabled": True,
"admin_users": [ADMIN_USER], # Uses the admin user from above
"categories": {
"basic": {
"requests_per_minute": 20,
"burst_capacity": 5,
"cooldown_seconds": 1
},
"gameplay": {
"requests_per_minute": 10,
"burst_capacity": 3,
"cooldown_seconds": 3
},
"management": {
"requests_per_minute": 5,
"burst_capacity": 2,
"cooldown_seconds": 5
},
"admin": {
"requests_per_minute": 100,
"burst_capacity": 10,
"cooldown_seconds": 0
},
"web": {
"requests_per_minute": 60,
"burst_capacity": 10,
"cooldown_seconds": 1
}
},
"global_limits": {
"max_requests_per_minute": 200,
"max_concurrent_users": 100
},
"violation_penalties": {
"warning_threshold": 3,
"temporary_ban_threshold": 10,
"temporary_ban_duration": 300 # 5 minutes
}
}

51
config/backup_config.json Normal file
View file

@ -0,0 +1,51 @@
{
"backup_settings": {
"database_path": "data/petbot.db",
"backup_directory": "backups",
"compression": {
"enabled": true,
"level": 6
},
"retention_policy": {
"daily_backups": 7,
"weekly_backups": 4,
"monthly_backups": 12,
"manual_backups": 20
},
"schedule": {
"daily": {
"enabled": true,
"hour": 3,
"minute": 0
},
"weekly": {
"enabled": true,
"day": "sunday",
"hour": 2,
"minute": 0
},
"monthly": {
"enabled": true,
"day": 1,
"hour": 1,
"minute": 0
}
},
"monitoring": {
"log_level": "INFO",
"alert_on_failure": true,
"max_backup_size_mb": 1000,
"min_free_space_mb": 500
}
},
"security": {
"admin_users": ["admin", "megaproxy"],
"backup_encryption": false,
"verify_integrity": true
},
"notifications": {
"success_notifications": false,
"failure_notifications": true,
"cleanup_notifications": true
}
}

View file

@ -1,4 +1,9 @@
{
"_config": {
"global_spawn_multiplier": 1.0,
"description": "Global multiplier for all item spawn rates. Set to 0.5 for half spawns, 2.0 for double spawns, etc.",
"admin_note": "Edit this value to globally adjust all item spawn rates. Individual item spawn_rate values can still be fine-tuned."
},
"healing_items": [
{
"id": 1,
@ -9,7 +14,7 @@
"effect": "heal",
"effect_value": 20,
"locations": ["all"],
"spawn_rate": 0.15
"spawn_rate": 0.0375
},
{
"id": 2,
@ -20,7 +25,7 @@
"effect": "heal",
"effect_value": 50,
"locations": ["all"],
"spawn_rate": 0.08
"spawn_rate": 0.02
},
{
"id": 3,
@ -31,7 +36,7 @@
"effect": "full_heal",
"effect_value": 100,
"locations": ["all"],
"spawn_rate": 0.03
"spawn_rate": 0.0075
},
{
"id": 4,
@ -42,7 +47,7 @@
"effect": "heal_status",
"effect_value": 15,
"locations": ["mystic_forest", "enchanted_grove"],
"spawn_rate": 0.12
"spawn_rate": 0.03
}
],
"battle_items": [
@ -55,7 +60,7 @@
"effect": "attack_boost",
"effect_value": 25,
"locations": ["all"],
"spawn_rate": 0.10
"spawn_rate": 0.025
},
{
"id": 6,
@ -66,7 +71,7 @@
"effect": "defense_boost",
"effect_value": 20,
"locations": ["crystal_caves", "frozen_peaks"],
"spawn_rate": 0.08
"spawn_rate": 0.02
},
{
"id": 7,
@ -77,7 +82,7 @@
"effect": "speed_boost",
"effect_value": 100,
"locations": ["all"],
"spawn_rate": 0.05
"spawn_rate": 0.0125
}
],
"rare_items": [
@ -90,7 +95,7 @@
"effect": "none",
"effect_value": 0,
"locations": ["volcanic_chamber"],
"spawn_rate": 0.02
"spawn_rate": 0.005
},
{
"id": 9,
@ -101,7 +106,7 @@
"effect": "none",
"effect_value": 0,
"locations": ["crystal_caves"],
"spawn_rate": 0.02
"spawn_rate": 0.005
},
{
"id": 10,
@ -112,7 +117,7 @@
"effect": "lucky_boost",
"effect_value": 50,
"locations": ["all"],
"spawn_rate": 0.01
"spawn_rate": 0.0025
},
{
"id": 11,
@ -123,7 +128,7 @@
"effect": "none",
"effect_value": 0,
"locations": ["forgotten_ruins"],
"spawn_rate": 0.01
"spawn_rate": 0.0025
}
],
"location_items": [
@ -136,7 +141,7 @@
"effect": "sell_value",
"effect_value": 100,
"locations": ["crystal_caves"],
"spawn_rate": 0.12
"spawn_rate": 0.03
},
{
"id": 13,
@ -147,7 +152,7 @@
"effect": "sell_value",
"effect_value": 200,
"locations": ["mystic_forest", "enchanted_grove"],
"spawn_rate": 0.06
"spawn_rate": 0.015
},
{
"id": 14,
@ -158,7 +163,7 @@
"effect": "sell_value",
"effect_value": 150,
"locations": ["volcanic_chamber"],
"spawn_rate": 0.10
"spawn_rate": 0.025
},
{
"id": 15,
@ -169,7 +174,7 @@
"effect": "sell_value",
"effect_value": 250,
"locations": ["frozen_peaks"],
"spawn_rate": 0.05
"spawn_rate": 0.0125
},
{
"id": 16,
@ -180,7 +185,7 @@
"effect": "sell_value",
"effect_value": 500,
"locations": ["forgotten_ruins"],
"spawn_rate": 0.03
"spawn_rate": 0.0075
}
],
"rarity_info": {

View file

@ -504,6 +504,75 @@
</div>
</div>
<div class="section">
<div class="section-header">⚡ Rate Limiting & Fair Play</div>
<div class="section-content">
<div class="info-box">
<h4>🛡️ Rate Limiting System</h4>
<p>PetBot uses a sophisticated rate limiting system to ensure fair play and prevent spam. Commands are organized into categories with different limits:</p>
<ul>
<li><strong>Basic Commands</strong> (!help, !ping, !status) - 20 per minute, 5 burst capacity</li>
<li><strong>Gameplay Commands</strong> (!explore, !battle, !catch) - 10 per minute, 3 burst capacity</li>
<li><strong>Management Commands</strong> (!pets, !activate, !stats) - 5 per minute, 2 burst capacity</li>
<li><strong>Web Interface</strong> - 60 requests per minute, 10 burst capacity</li>
</ul>
</div>
<div class="info-box">
<h4>📊 How It Works</h4>
<ul>
<li><strong>Token Bucket Algorithm</strong> - You have a "bucket" of tokens that refills over time</li>
<li><strong>Burst Capacity</strong> - You can use multiple commands quickly up to the burst limit</li>
<li><strong>Refill Rate</strong> - Tokens refill based on the requests per minute limit</li>
<li><strong>Cooldown Period</strong> - Brief cooldown after hitting limits before trying again</li>
</ul>
</div>
<div class="info-box">
<h4>⚠️ Violations & Penalties</h4>
<ul>
<li><strong>3 violations</strong> - Warning threshold reached (logged)</li>
<li><strong>10 violations</strong> - Temporary 5-minute ban from all commands</li>
<li><strong>Admin Override</strong> - Admins can unban users and reset violations</li>
<li><strong>Automatic Cleanup</strong> - Old violations and bans are automatically cleared</li>
</ul>
</div>
<div class="command-grid">
<div class="command">
<div class="command-name">!rate_stats</div>
<div class="command-desc">View global rate limiting statistics (Admin only).</div>
<div class="command-example">Example: !rate_stats</div>
</div>
<div class="command">
<div class="command-name">!rate_user &lt;username&gt;</div>
<div class="command-desc">Check rate limiting status for a specific user (Admin only).</div>
<div class="command-example">Example: !rate_user playername</div>
</div>
<div class="command">
<div class="command-name">!rate_unban &lt;username&gt;</div>
<div class="command-desc">Manually unban a user from rate limiting (Admin only).</div>
<div class="command-example">Example: !rate_unban playername</div>
</div>
<div class="command">
<div class="command-name">!rate_reset &lt;username&gt;</div>
<div class="command-desc">Reset violations for a user (Admin only).</div>
<div class="command-example">Example: !rate_reset playername</div>
</div>
</div>
<div class="info-box">
<h4>💡 Tips for Smooth Gameplay</h4>
<ul>
<li><strong>Play Naturally</strong> - Normal gameplay rarely hits rate limits</li>
<li><strong>Use the Web Interface</strong> - Higher limits for browsing and pet management</li>
<li><strong>Spread Out Commands</strong> - Avoid rapid-fire command spamming</li>
<li><strong>Check Your Status</strong> - If you get rate limited, wait a moment before trying again</li>
</ul>
</div>
</div>
</div>
<div class="footer">
<p><strong>🎮 PetBot v0.2.0</strong> - Pokemon-style pet collecting for IRC</p>
<p>Catch pets • Battle gyms • Collect items • Earn badges • Explore locations</p>

344
install_prerequisites.py Normal file
View file

@ -0,0 +1,344 @@
#!/usr/bin/env python3
"""
PetBot Prerequisites Installation Script
This script installs all required Python packages and dependencies for running PetBot.
"""
import subprocess
import sys
import os
import importlib.util
from pathlib import Path
def print_header():
"""Print installation header."""
print("=" * 60)
print("🐾 PetBot Prerequisites Installation Script")
print("=" * 60)
print()
def check_python_version():
"""Check if Python version is compatible."""
print("🔍 Checking Python version...")
version = sys.version_info
required_major = 3
required_minor = 7
if version.major < required_major or (version.major == required_major and version.minor < required_minor):
print(f"❌ Python {required_major}.{required_minor}+ required. Current version: {version.major}.{version.minor}.{version.micro}")
print("Please upgrade Python and try again.")
return False
print(f"✅ Python {version.major}.{version.minor}.{version.micro} is compatible")
return True
def check_pip_available():
"""Check if pip is available."""
print("\n🔍 Checking pip availability...")
try:
import pip
print("✅ pip is available")
return True
except ImportError:
pass
# Try alternative ways to check pip
try:
result = subprocess.run([sys.executable, "-m", "pip", "--version"],
capture_output=True, text=True, check=True)
print(f"✅ pip is available: {result.stdout.strip()}")
return True
except (subprocess.CalledProcessError, FileNotFoundError):
print("❌ pip is not available")
print("Please install pip first:")
print(" - On Ubuntu/Debian: sudo apt install python3-pip")
print(" - On CentOS/RHEL: sudo yum install python3-pip")
print(" - On macOS: brew install python3")
return False
def check_package_installed(package_name):
"""Check if a package is already installed."""
try:
importlib.import_module(package_name)
return True
except ImportError:
return False
def install_package(package_spec):
"""Install a package using pip."""
print(f"📦 Installing {package_spec}...")
try:
result = subprocess.run([
sys.executable, "-m", "pip", "install", package_spec
], capture_output=True, text=True, check=True)
print(f"✅ Successfully installed {package_spec}")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Failed to install {package_spec}")
print(f"Error output: {e.stderr}")
return False
def install_requirements():
"""Install packages from requirements.txt if it exists."""
print("\n📋 Installing packages from requirements.txt...")
requirements_file = Path("requirements.txt")
if not requirements_file.exists():
print("❌ requirements.txt not found")
return False
try:
result = subprocess.run([
sys.executable, "-m", "pip", "install", "-r", "requirements.txt"
], capture_output=True, text=True, check=True)
print("✅ Successfully installed packages from requirements.txt")
return True
except subprocess.CalledProcessError as e:
print("❌ Failed to install from requirements.txt")
print(f"Error output: {e.stderr}")
return False
def install_individual_packages():
"""Install individual packages if requirements.txt fails."""
print("\n📦 Installing individual packages...")
packages = [
("irc", "irc>=20.3.0", "IRC client library"),
("aiosqlite", "aiosqlite>=0.19.0", "Async SQLite database interface"),
("dotenv", "python-dotenv>=1.0.0", "Environment variable loading")
]
results = []
for import_name, package_spec, description in packages:
print(f"\n🔍 Checking {import_name} ({description})...")
if check_package_installed(import_name):
print(f"{import_name} is already installed")
results.append(True)
else:
print(f"{import_name} is not installed")
success = install_package(package_spec)
results.append(success)
return all(results)
def verify_installation():
"""Verify that all required packages are installed correctly."""
print("\n🔍 Verifying installation...")
test_imports = [
("irc", "IRC client library"),
("aiosqlite", "Async SQLite database"),
("dotenv", "Environment variable loading"),
("asyncio", "Async programming (built-in)"),
("sqlite3", "SQLite database (built-in)"),
("json", "JSON handling (built-in)"),
("socket", "Network communication (built-in)"),
("threading", "Threading (built-in)")
]
all_good = True
for module_name, description in test_imports:
try:
importlib.import_module(module_name)
print(f"{module_name}: {description}")
except ImportError as e:
print(f"{module_name}: {description} - {e}")
all_good = False
return all_good
def check_directory_structure():
"""Check if we're in the correct directory and required files exist."""
print("\n🔍 Checking directory structure...")
required_files = [
"requirements.txt",
"src/database.py",
"src/game_engine.py",
"src/bot.py",
"webserver.py",
"run_bot_debug.py"
]
missing_files = []
for file_path in required_files:
if not Path(file_path).exists():
missing_files.append(file_path)
if missing_files:
print("❌ Missing required files:")
for file_path in missing_files:
print(f" - {file_path}")
print("\nPlease make sure you're running this script from the PetBot project directory.")
return False
print("✅ All required files found")
return True
def create_data_directory():
"""Create data directory if it doesn't exist."""
print("\n🔍 Checking data directory...")
data_dir = Path("data")
if not data_dir.exists():
try:
data_dir.mkdir()
print("✅ Created data directory")
except Exception as e:
print(f"❌ Failed to create data directory: {e}")
return False
else:
print("✅ Data directory already exists")
return True
def create_backups_directory():
"""Create backups directory if it doesn't exist."""
print("\n🔍 Checking backups directory...")
backups_dir = Path("backups")
if not backups_dir.exists():
try:
backups_dir.mkdir()
print("✅ Created backups directory")
except Exception as e:
print(f"❌ Failed to create backups directory: {e}")
return False
else:
print("✅ Backups directory already exists")
return True
def test_basic_imports():
"""Test basic functionality by importing key modules."""
print("\n🧪 Testing basic imports...")
try:
# Test database import
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.database import Database
print("✅ Database module imports successfully")
# Test connection manager import
from src.irc_connection_manager import IRCConnectionManager
print("✅ IRC connection manager imports successfully")
# Test backup manager import
from src.backup_manager import BackupManager
print("✅ Backup manager imports successfully")
return True
except ImportError as e:
print(f"❌ Import test failed: {e}")
return False
def show_next_steps():
"""Show next steps after successful installation."""
print("\n🎉 Installation completed successfully!")
print("\n📋 Next steps:")
print("1. Review and update configuration files in the config/ directory")
print("2. Test the bot with: python3 run_bot_debug.py")
print("3. Or use the new reconnection system: python3 run_bot_with_reconnect.py")
print("4. Check the web interface at: http://localhost:8080")
print("5. Review CLAUDE.md for development guidelines")
print("\n🔧 Available test commands:")
print(" python3 test_backup_simple.py - Test backup system")
print(" python3 test_reconnection.py - Test IRC reconnection")
print("\n📚 Documentation:")
print(" README.md - Project overview")
print(" CLAUDE.md - Development guide")
print(" TODO.md - Current status")
print(" issues.txt - Security audit findings")
print(" BACKUP_SYSTEM_INTEGRATION.md - Backup system guide")
def main():
"""Main installation function."""
print_header()
# Check prerequisites
if not check_python_version():
return False
if not check_pip_available():
return False
if not check_directory_structure():
return False
# Create directories
if not create_data_directory():
return False
if not create_backups_directory():
return False
# Install packages
success = install_requirements()
if not success:
print("\n⚠️ requirements.txt installation failed, trying individual packages...")
success = install_individual_packages()
if not success:
print("\n❌ Package installation failed")
return False
# Verify installation
if not verify_installation():
print("\n❌ Installation verification failed")
return False
# Test imports
if not test_basic_imports():
print("\n❌ Basic import tests failed")
return False
# Show next steps
show_next_steps()
return True
if __name__ == "__main__":
try:
success = main()
if success:
print("\n✅ PetBot prerequisites installation completed successfully!")
sys.exit(0)
else:
print("\n❌ Installation failed. Please check the errors above.")
sys.exit(1)
except KeyboardInterrupt:
print("\n🛑 Installation interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n💥 Unexpected error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

257
install_prerequisites.sh Executable file
View file

@ -0,0 +1,257 @@
#!/bin/bash
# PetBot Prerequisites Installation Script (Shell Version)
# This script installs all required packages and sets up the environment
set -e # Exit on any error
echo "============================================================"
echo "🐾 PetBot Prerequisites Installation Script (Shell Version)"
echo "============================================================"
echo
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}${NC} $1"
}
print_error() {
echo -e "${RED}${NC} $1"
}
print_warning() {
echo -e "${YELLOW}⚠️${NC} $1"
}
print_info() {
echo -e "${BLUE}${NC} $1"
}
# Check if we're in the right directory
echo "🔍 Checking directory structure..."
if [ ! -f "requirements.txt" ] || [ ! -f "src/database.py" ] || [ ! -f "webserver.py" ]; then
print_error "Required files not found. Please run this script from the PetBot project directory."
exit 1
fi
print_status "Directory structure is correct"
# Check Python version
echo
echo "🔍 Checking Python version..."
python_version=$(python3 --version 2>&1)
if [ $? -ne 0 ]; then
print_error "Python 3 is not installed or not in PATH"
print_info "Please install Python 3.7+ and try again"
exit 1
fi
# Extract version numbers
version_string=$(echo "$python_version" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+')
major=$(echo "$version_string" | cut -d'.' -f1)
minor=$(echo "$version_string" | cut -d'.' -f2)
if [ "$major" -lt 3 ] || ([ "$major" -eq 3 ] && [ "$minor" -lt 7 ]); then
print_error "Python 3.7+ required. Current version: $python_version"
exit 1
fi
print_status "Python version is compatible: $python_version"
# Check pip availability
echo
echo "🔍 Checking pip availability..."
if ! command -v pip3 &> /dev/null; then
if ! python3 -m pip --version &> /dev/null; then
print_error "pip is not available"
print_info "Install pip with:"
print_info " Ubuntu/Debian: sudo apt install python3-pip"
print_info " CentOS/RHEL: sudo yum install python3-pip"
print_info " macOS: brew install python3"
exit 1
fi
PIP_CMD="python3 -m pip"
else
PIP_CMD="pip3"
fi
pip_version=$($PIP_CMD --version)
print_status "pip is available: $pip_version"
# Create required directories
echo
echo "🔍 Creating required directories..."
if [ ! -d "data" ]; then
mkdir -p data
print_status "Created data directory"
else
print_status "Data directory already exists"
fi
if [ ! -d "backups" ]; then
mkdir -p backups
print_status "Created backups directory"
else
print_status "Backups directory already exists"
fi
if [ ! -d "logs" ]; then
mkdir -p logs
print_status "Created logs directory"
else
print_status "Logs directory already exists"
fi
# Install packages from requirements.txt
echo
echo "📦 Installing packages from requirements.txt..."
if $PIP_CMD install -r requirements.txt; then
print_status "Successfully installed packages from requirements.txt"
else
print_error "Failed to install from requirements.txt"
echo
echo "📦 Trying individual package installation..."
# Try individual packages
packages=(
"irc>=20.3.0"
"aiosqlite>=0.19.0"
"python-dotenv>=1.0.0"
)
failed_packages=()
for package in "${packages[@]}"; do
echo "Installing $package..."
if $PIP_CMD install "$package"; then
print_status "Successfully installed $package"
else
print_error "Failed to install $package"
failed_packages+=("$package")
fi
done
if [ ${#failed_packages[@]} -gt 0 ]; then
print_error "Failed to install the following packages:"
for package in "${failed_packages[@]}"; do
echo " - $package"
done
exit 1
fi
fi
# Verify installation
echo
echo "🔍 Verifying installation..."
# List of modules to test
modules=(
"irc:IRC client library"
"aiosqlite:Async SQLite database"
"dotenv:Environment variable loading"
"asyncio:Async programming (built-in)"
"sqlite3:SQLite database (built-in)"
"json:JSON handling (built-in)"
"socket:Network communication (built-in)"
"threading:Threading (built-in)"
)
failed_imports=()
for module_info in "${modules[@]}"; do
module=$(echo "$module_info" | cut -d':' -f1)
description=$(echo "$module_info" | cut -d':' -f2)
if python3 -c "import $module" 2>/dev/null; then
print_status "$module: $description"
else
print_error "$module: $description - Import failed"
failed_imports+=("$module")
fi
done
if [ ${#failed_imports[@]} -gt 0 ]; then
print_error "Some modules failed to import:"
for module in "${failed_imports[@]}"; do
echo " - $module"
done
exit 1
fi
# Test basic project imports
echo
echo "🧪 Testing project imports..."
if python3 -c "
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath('.')))
from src.database import Database
from src.irc_connection_manager import IRCConnectionManager
from src.backup_manager import BackupManager
print('All project imports successful')
" 2>/dev/null; then
print_status "Project imports successful"
else
print_error "Project imports failed"
print_info "This might be due to missing dependencies or path issues"
exit 1
fi
# Check for optional system dependencies
echo
echo "🔍 Checking optional system dependencies..."
if command -v git &> /dev/null; then
print_status "Git is available"
else
print_warning "Git is not available (optional for development)"
fi
if command -v curl &> /dev/null; then
print_status "curl is available"
else
print_warning "curl is not available (optional for testing)"
fi
# Make scripts executable
echo
echo "🔧 Making scripts executable..."
chmod +x install_prerequisites.py
chmod +x run_bot_debug.py
chmod +x run_bot_with_reconnect.py
chmod +x test_backup_simple.py
chmod +x test_reconnection.py
print_status "Made scripts executable"
# Show summary
echo
echo "🎉 Installation completed successfully!"
echo
echo "📋 Next steps:"
echo "1. Review and update configuration files in the config/ directory"
echo "2. Test the bot with: python3 run_bot_debug.py"
echo "3. Or use the new reconnection system: python3 run_bot_with_reconnect.py"
echo "4. Check the web interface at: http://localhost:8080"
echo "5. Review CLAUDE.md for development guidelines"
echo
echo "🔧 Available test commands:"
echo " python3 test_backup_simple.py - Test backup system"
echo " python3 test_reconnection.py - Test IRC reconnection"
echo
echo "📚 Documentation:"
echo " README.md - Project overview"
echo " CLAUDE.md - Development guide"
echo " TODO.md - Current status"
echo " issues.txt - Security audit findings"
echo " BACKUP_SYSTEM_INTEGRATION.md - Backup system guide"
echo
echo "🚀 You're ready to run PetBot!"
echo "✅ All prerequisites have been installed successfully!"

376
install_prerequisites_fixed.py Executable file
View file

@ -0,0 +1,376 @@
#!/usr/bin/env python3
"""
PetBot Prerequisites Installation Script (Fixed for externally-managed-environment)
This script handles the externally-managed-environment error by using virtual environments.
"""
import subprocess
import sys
import os
import importlib.util
import venv
from pathlib import Path
def print_header():
"""Print installation header."""
print("=" * 60)
print("🐾 PetBot Prerequisites Installation Script (Fixed)")
print("=" * 60)
print()
def check_python_version():
"""Check if Python version is compatible."""
print("🔍 Checking Python version...")
version = sys.version_info
required_major = 3
required_minor = 7
if version.major < required_major or (version.major == required_major and version.minor < required_minor):
print(f"❌ Python {required_major}.{required_minor}+ required. Current version: {version.major}.{version.minor}.{version.micro}")
print("Please upgrade Python and try again.")
return False
print(f"✅ Python {version.major}.{version.minor}.{version.micro} is compatible")
return True
def check_venv_available():
"""Check if venv module is available."""
print("\n🔍 Checking venv availability...")
try:
import venv
print("✅ venv module is available")
return True
except ImportError:
print("❌ venv module is not available")
print("Please install python3-venv:")
print(" Ubuntu/Debian: sudo apt install python3-venv")
print(" CentOS/RHEL: sudo yum install python3-venv")
return False
def create_virtual_environment():
"""Create a virtual environment for the project."""
print("\n🔧 Creating virtual environment...")
venv_path = Path("venv")
if venv_path.exists():
print("✅ Virtual environment already exists")
return str(venv_path)
try:
venv.create(venv_path, with_pip=True)
print("✅ Virtual environment created successfully")
return str(venv_path)
except Exception as e:
print(f"❌ Failed to create virtual environment: {e}")
return None
def get_venv_python_path(venv_path):
"""Get the Python executable path in the virtual environment."""
if os.name == 'nt': # Windows
return os.path.join(venv_path, 'Scripts', 'python.exe')
else: # Linux/macOS
return os.path.join(venv_path, 'bin', 'python')
def get_venv_pip_path(venv_path):
"""Get the pip executable path in the virtual environment."""
if os.name == 'nt': # Windows
return os.path.join(venv_path, 'Scripts', 'pip.exe')
else: # Linux/macOS
return os.path.join(venv_path, 'bin', 'pip')
def install_packages_in_venv(venv_path):
"""Install packages in the virtual environment."""
print("\n📦 Installing packages in virtual environment...")
venv_pip = get_venv_pip_path(venv_path)
if not os.path.exists(venv_pip):
print("❌ pip not found in virtual environment")
return False
# Upgrade pip first
print("🔄 Upgrading pip...")
try:
subprocess.run([venv_pip, "install", "--upgrade", "pip"], check=True, capture_output=True)
print("✅ pip upgraded successfully")
except subprocess.CalledProcessError as e:
print(f"⚠️ pip upgrade failed: {e}")
# Install from requirements.txt
requirements_file = Path("requirements.txt")
if requirements_file.exists():
print("📋 Installing from requirements.txt...")
try:
result = subprocess.run([venv_pip, "install", "-r", "requirements.txt"],
check=True, capture_output=True, text=True)
print("✅ Successfully installed packages from requirements.txt")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Failed to install from requirements.txt: {e.stderr}")
# Install individual packages
print("📦 Installing individual packages...")
packages = [
"irc>=20.3.0",
"aiosqlite>=0.19.0",
"python-dotenv>=1.0.0"
]
success = True
for package in packages:
print(f"Installing {package}...")
try:
subprocess.run([venv_pip, "install", package], check=True, capture_output=True)
print(f"✅ Successfully installed {package}")
except subprocess.CalledProcessError as e:
print(f"❌ Failed to install {package}")
success = False
return success
def verify_installation_in_venv(venv_path):
"""Verify installation in virtual environment."""
print("\n🔍 Verifying installation in virtual environment...")
venv_python = get_venv_python_path(venv_path)
test_modules = [
"irc",
"aiosqlite",
"dotenv",
"asyncio",
"sqlite3",
"json",
"socket"
]
all_good = True
for module in test_modules:
try:
result = subprocess.run([venv_python, "-c", f"import {module}"],
check=True, capture_output=True)
print(f"{module}: Available")
except subprocess.CalledProcessError:
print(f"{module}: Not available")
all_good = False
return all_good
def create_activation_script():
"""Create a script to activate the virtual environment."""
print("\n🔧 Creating activation script...")
activation_script = """#!/bin/bash
# PetBot Virtual Environment Activation Script
echo "🐾 Activating PetBot virtual environment..."
if [ -d "venv" ]; then
source venv/bin/activate
echo "✅ Virtual environment activated"
echo "🚀 You can now run:"
echo " python run_bot_with_reconnect.py"
echo " python test_backup_simple.py"
echo " python test_reconnection.py"
echo ""
echo "💡 To deactivate: deactivate"
else
echo "❌ Virtual environment not found"
echo "Run: python3 install_prerequisites_fixed.py"
fi
"""
try:
with open("activate_petbot.sh", "w") as f:
f.write(activation_script)
os.chmod("activate_petbot.sh", 0o755)
print("✅ Created activate_petbot.sh")
return True
except Exception as e:
print(f"❌ Failed to create activation script: {e}")
return False
def create_run_scripts():
"""Create wrapper scripts that use the virtual environment."""
print("\n🔧 Creating wrapper scripts...")
scripts = {
"run_petbot.sh": """#!/bin/bash
# PetBot Runner Script (uses virtual environment)
if [ -d "venv" ]; then
source venv/bin/activate
echo "🚀 Starting PetBot with auto-reconnect..."
python run_bot_with_reconnect.py
else
echo "❌ Virtual environment not found"
echo "Run: python3 install_prerequisites_fixed.py"
fi
""",
"run_petbot_debug.sh": """#!/bin/bash
# PetBot Debug Runner Script (uses virtual environment)
if [ -d "venv" ]; then
source venv/bin/activate
echo "🚀 Starting PetBot in debug mode..."
python run_bot_debug.py
else
echo "❌ Virtual environment not found"
echo "Run: python3 install_prerequisites_fixed.py"
fi
""",
"test_petbot.sh": """#!/bin/bash
# PetBot Test Script (uses virtual environment)
if [ -d "venv" ]; then
source venv/bin/activate
echo "🧪 Running PetBot tests..."
echo "1. Testing backup system..."
python test_backup_simple.py
echo ""
echo "2. Testing reconnection system..."
python test_reconnection.py
else
echo "❌ Virtual environment not found"
echo "Run: python3 install_prerequisites_fixed.py"
fi
"""
}
success = True
for script_name, script_content in scripts.items():
try:
with open(script_name, "w") as f:
f.write(script_content)
os.chmod(script_name, 0o755)
print(f"✅ Created {script_name}")
except Exception as e:
print(f"❌ Failed to create {script_name}: {e}")
success = False
return success
def create_directories():
"""Create required directories."""
print("\n🔧 Creating required directories...")
directories = ["data", "backups", "logs"]
for directory in directories:
dir_path = Path(directory)
if not dir_path.exists():
try:
dir_path.mkdir()
print(f"✅ Created {directory}/ directory")
except Exception as e:
print(f"❌ Failed to create {directory}/ directory: {e}")
return False
else:
print(f"{directory}/ directory already exists")
return True
def show_usage_instructions():
"""Show usage instructions after installation."""
print("\n🎉 Installation completed successfully!")
print("\n📋 How to use PetBot:")
print("\n🔧 Method 1: Using activation script (recommended)")
print(" ./activate_petbot.sh")
print(" python run_bot_with_reconnect.py")
print("\n🔧 Method 2: Using wrapper scripts")
print(" ./run_petbot.sh # Start bot with auto-reconnect")
print(" ./run_petbot_debug.sh # Start bot in debug mode")
print(" ./test_petbot.sh # Run tests")
print("\n🔧 Method 3: Manual activation")
print(" source venv/bin/activate")
print(" python run_bot_with_reconnect.py")
print(" deactivate # when done")
print("\n📚 Documentation:")
print(" QUICKSTART.md # Quick start guide")
print(" INSTALLATION.md # Detailed installation guide")
print(" CLAUDE.md # Development guidelines")
print("\n🌐 Web Interface:")
print(" http://localhost:8080 # Access after starting bot")
print("\n⚠️ Important Notes:")
print(" - Always activate the virtual environment before running Python scripts")
print(" - Use the wrapper scripts for convenience")
print(" - The virtual environment is in the 'venv' directory")
def main():
"""Main installation function."""
print_header()
# Check prerequisites
if not check_python_version():
return False
if not check_venv_available():
return False
# Create virtual environment
venv_path = create_virtual_environment()
if not venv_path:
return False
# Install packages in virtual environment
if not install_packages_in_venv(venv_path):
print("\n❌ Package installation failed")
return False
# Verify installation
if not verify_installation_in_venv(venv_path):
print("\n❌ Installation verification failed")
return False
# Create directories
if not create_directories():
return False
# Create helper scripts
if not create_activation_script():
return False
if not create_run_scripts():
return False
# Show usage instructions
show_usage_instructions()
return True
if __name__ == "__main__":
try:
success = main()
if success:
print("\n✅ PetBot virtual environment setup completed successfully!")
print("🚀 Use './run_petbot.sh' to start the bot")
sys.exit(0)
else:
print("\n❌ Installation failed. Please check the errors above.")
sys.exit(1)
except KeyboardInterrupt:
print("\n🛑 Installation interrupted by user")
sys.exit(1)
except Exception as e:
print(f"\n💥 Unexpected error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

200
install_prerequisites_simple.sh Executable file
View file

@ -0,0 +1,200 @@
#!/bin/bash
# PetBot Prerequisites Installation Script (Simple - fixes externally-managed-environment)
set -e # Exit on any error
echo "============================================================"
echo "🐾 PetBot Prerequisites Installation (Simple Fix)"
echo "============================================================"
echo
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m'
print_success() { echo -e "${GREEN}${NC} $1"; }
print_error() { echo -e "${RED}${NC} $1"; }
print_warning() { echo -e "${YELLOW}⚠️${NC} $1"; }
echo "🔍 Checking Python and venv..."
# Check Python
if ! command -v python3 &> /dev/null; then
print_error "Python 3 is not installed"
exit 1
fi
python_version=$(python3 --version)
print_success "Python available: $python_version"
# Check venv
if ! python3 -c "import venv" 2>/dev/null; then
print_error "python3-venv is not available"
echo "Install with: sudo apt install python3-venv"
exit 1
fi
print_success "venv module is available"
echo
echo "🔧 Setting up virtual environment..."
# Create virtual environment if it doesn't exist
if [ ! -d "venv" ]; then
python3 -m venv venv
print_success "Virtual environment created"
else
print_success "Virtual environment already exists"
fi
# Activate virtual environment
source venv/bin/activate
print_success "Virtual environment activated"
echo
echo "📦 Installing packages in virtual environment..."
# Upgrade pip
pip install --upgrade pip
# Install packages
if [ -f "requirements.txt" ]; then
pip install -r requirements.txt
print_success "Installed from requirements.txt"
else
# Install individual packages
pip install "irc>=20.3.0" "aiosqlite>=0.19.0" "python-dotenv>=1.0.0"
print_success "Installed individual packages"
fi
echo
echo "🔍 Verifying installation..."
# Test imports
python -c "import irc, aiosqlite, dotenv, asyncio, sqlite3; print('All modules imported successfully')"
print_success "All required modules are available"
echo
echo "🔧 Creating directories..."
# Create required directories
mkdir -p data backups logs
print_success "Created required directories"
echo
echo "🔧 Creating helper scripts..."
# Create activation script
cat > activate_petbot.sh << 'EOF'
#!/bin/bash
echo "🐾 Activating PetBot virtual environment..."
if [ -d "venv" ]; then
source venv/bin/activate
echo "✅ Virtual environment activated"
echo "🚀 You can now run:"
echo " python run_bot_with_reconnect.py"
echo " python test_backup_simple.py"
echo " python test_reconnection.py"
echo ""
echo "💡 To deactivate: deactivate"
else
echo "❌ Virtual environment not found"
fi
EOF
# Create run script
cat > run_petbot.sh << 'EOF'
#!/bin/bash
if [ -d "venv" ]; then
source venv/bin/activate
echo "🚀 Starting PetBot with auto-reconnect..."
python run_bot_with_reconnect.py
else
echo "❌ Virtual environment not found"
echo "Run: ./install_prerequisites_simple.sh"
fi
EOF
# Create debug run script
cat > run_petbot_debug.sh << 'EOF'
#!/bin/bash
if [ -d "venv" ]; then
source venv/bin/activate
echo "🚀 Starting PetBot in debug mode..."
python run_bot_debug.py
else
echo "❌ Virtual environment not found"
echo "Run: ./install_prerequisites_simple.sh"
fi
EOF
# Create test script
cat > test_petbot.sh << 'EOF'
#!/bin/bash
if [ -d "venv" ]; then
source venv/bin/activate
echo "🧪 Running PetBot tests..."
echo "1. Testing backup system..."
python test_backup_simple.py
echo
echo "2. Testing reconnection system..."
python test_reconnection.py
else
echo "❌ Virtual environment not found"
echo "Run: ./install_prerequisites_simple.sh"
fi
EOF
# Make scripts executable
chmod +x activate_petbot.sh run_petbot.sh run_petbot_debug.sh test_petbot.sh
print_success "Created helper scripts"
echo
echo "🧪 Testing installation..."
# Test basic imports
python -c "
import sys, os
sys.path.append('.')
from src.database import Database
from src.irc_connection_manager import IRCConnectionManager
from src.backup_manager import BackupManager
print('✅ All project modules imported successfully')
"
print_success "Installation test completed"
# Deactivate for clean finish
deactivate
echo
echo "🎉 Installation completed successfully!"
echo
echo "📋 How to use PetBot:"
echo
echo "🔧 Method 1: Using wrapper scripts (recommended)"
echo " ./run_petbot.sh # Start bot with auto-reconnect"
echo " ./run_petbot_debug.sh # Start bot in debug mode"
echo " ./test_petbot.sh # Run tests"
echo
echo "🔧 Method 2: Manual activation"
echo " source venv/bin/activate"
echo " python run_bot_with_reconnect.py"
echo " deactivate # when done"
echo
echo "🔧 Method 3: Using activation helper"
echo " ./activate_petbot.sh"
echo " # Then run commands normally"
echo
echo "🌐 Web Interface:"
echo " http://localhost:8080 # Access after starting bot"
echo
echo "⚠️ Important:"
echo " - Always use the wrapper scripts OR activate venv first"
echo " - The virtual environment is in the 'venv' directory"
echo " - This fixes the externally-managed-environment issue"
echo
echo "✅ Ready to run PetBot!"

264
issues.txt Normal file
View file

@ -0,0 +1,264 @@
PETBOT SECURITY AUDIT - ISSUES REPORT
=====================================
Generated: 2025-01-15
Auditor: Claude Code Assistant
Scope: Complete security audit of PetBot IRC bot and web interface
EXECUTIVE SUMMARY
================
This security audit identified 23 distinct security vulnerabilities across the PetBot application, ranging from critical to low severity. The most concerning issues are Cross-Site Scripting (XSS) vulnerabilities in the web interface, missing security headers, and inadequate access controls.
CRITICAL VULNERABILITIES (5 issues)
===================================
1. CRITICAL: XSS - Direct nickname injection in HTML output
File: webserver.py (lines 1191-1193)
Impact: Arbitrary JavaScript execution
Description: Player nicknames are directly inserted into HTML without escaping
Example: nickname = '"><script>alert("XSS")</script>'
Recommendation: Implement HTML escaping for all user output
2. CRITICAL: XSS - Page title injection
File: webserver.py (lines 2758, 2842, 4608)
Impact: JavaScript execution in page titles
Description: Nicknames inserted directly into <title> tags
Recommendation: Escape all dynamic content in page titles
3. CRITICAL: Missing HTTP security headers
File: webserver.py (entire file)
Impact: XSS, clickjacking, MIME sniffing attacks
Description: No CSP, X-Frame-Options, X-Content-Type-Options headers
Recommendation: Add security headers to all responses
4. CRITICAL: No HTTPS configuration
File: webserver.py (line 4773)
Impact: Data transmitted in plaintext
Description: Server runs HTTP only, no SSL/TLS
Recommendation: Implement HTTPS with valid certificates
5. CRITICAL: No web interface authentication
File: webserver.py (lines 564-588)
Impact: Unauthorized access to all player data
Description: Any user can access any player's profile via URL manipulation
Recommendation: Implement proper authentication and authorization
HIGH SEVERITY VULNERABILITIES (8 issues)
========================================
6. HIGH: XSS - Pet data injection
File: webserver.py (lines 2139-2154)
Impact: JavaScript execution through pet names
Description: Pet nicknames and species names inserted without escaping
Recommendation: Escape all pet data before HTML output
7. HIGH: XSS - Achievement data injection
File: webserver.py (lines 2167-2175)
Impact: JavaScript execution through achievement data
Description: Achievement names and descriptions not escaped
Recommendation: Escape achievement data in HTML output
8. HIGH: XSS - Inventory item injection
File: webserver.py (lines 2207-2214)
Impact: JavaScript execution through item data
Description: Item names and descriptions inserted without escaping
Recommendation: Escape all item data before HTML output
9. HIGH: Path traversal vulnerability
File: webserver.py (lines 564-565, 573-574, 584-585, 587-588)
Impact: Access to unauthorized resources
Description: Direct path extraction without validation
Example: /player/../../../etc/passwd
Recommendation: Implement path validation and sanitization
10. HIGH: SQL injection in reset script
File: reset_players.py (lines 57, 63)
Impact: Arbitrary SQL execution
Description: F-string interpolation in SQL queries
Recommendation: Use parameterized queries or validate table names
11. HIGH: Input validation gaps
File: Multiple modules
Impact: Various injection attacks
Description: Inconsistent input validation across modules
Recommendation: Implement comprehensive input validation
12. HIGH: Admin authentication bypass
File: admin.py (line 18), backup_commands.py (line 58)
Impact: Unauthorized admin access
Description: Hard-coded admin checks vulnerable to IRC spoofing
Recommendation: Implement secure admin authentication
13. HIGH: Information disclosure in error messages
File: webserver.py (lines 1111, 1331, 1643, 1872)
Impact: System information leakage
Description: Detailed error messages expose internal structure
Recommendation: Implement generic error messages for users
MEDIUM SEVERITY VULNERABILITIES (7 issues)
==========================================
14. MEDIUM: XSS - Error message injection
File: webserver.py (lines 1258-1267, 2075-2084, 2030-2039)
Impact: JavaScript execution through error messages
Description: Error messages containing user data not escaped
Recommendation: Escape all error message content
15. MEDIUM: Missing rate limiting
File: webserver.py (entire file)
Impact: Brute force attacks, DoS
Description: No rate limiting on any endpoints
Recommendation: Implement rate limiting especially for PIN verification
16. MEDIUM: Insecure session management
File: webserver.py (entire file)
Impact: Session attacks, CSRF
Description: No session tokens, CSRF protection, or timeouts
Recommendation: Implement proper session management
17. MEDIUM: SQL injection in backup manager
File: backup_manager.py (lines 349, 353)
Impact: Potential SQL execution
Description: F-string usage with table names from sqlite_master
Recommendation: Use proper SQL escaping for dynamic table names
18. MEDIUM: PIN system vulnerabilities
File: team_builder.py, database.py
Impact: Unauthorized team changes
Description: PIN delivery via IRC without additional verification
Recommendation: Enhance PIN system with additional verification
19. MEDIUM: Missing access controls
File: webserver.py (lines 584-588)
Impact: Unauthorized profile access
Description: Team builder accessible by anyone
Recommendation: Implement access control for team builders
20. MEDIUM: Debug information exposure
File: webserver.py (line 2766)
Impact: Information disclosure
Description: Extensive console logging exposes internals
Recommendation: Implement proper logging levels
LOW SEVERITY VULNERABILITIES (3 issues)
=======================================
21. LOW: Server binds to all interfaces
File: webserver.py (line 4773)
Impact: Increased attack surface
Description: Server accessible from all network interfaces
Recommendation: Bind to specific interface if possible
22. LOW: No request size limits
File: webserver.py (entire file)
Impact: DoS attacks
Description: No limits on request size or JSON payload
Recommendation: Implement request size limits
23. LOW: Missing security monitoring
File: webserver.py (entire file)
Impact: Limited attack detection
Description: No access logging or security monitoring
Recommendation: Implement comprehensive security logging
REMEDIATION PRIORITIES
=====================
IMMEDIATE (Critical Issues):
1. Fix XSS vulnerabilities by implementing HTML escaping
2. Add HTTP security headers (CSP, X-Frame-Options, etc.)
3. Implement HTTPS with valid SSL certificates
4. Add basic authentication for web interface
5. Fix path traversal vulnerabilities
HIGH PRIORITY (Within 1 week):
1. Implement input validation and sanitization
2. Fix SQL injection vulnerabilities
3. Enhance admin authentication system
4. Add rate limiting for all endpoints
5. Improve error handling to prevent information disclosure
MEDIUM PRIORITY (Within 1 month):
1. Implement proper session management
2. Add CSRF protection
3. Enhance PIN verification system
4. Implement access controls for all resources
5. Add security logging and monitoring
LOW PRIORITY (Within 3 months):
1. Network security hardening
2. Request size limits
3. Advanced security monitoring
4. Security testing automation
5. Security documentation updates
SECURITY TESTING RECOMMENDATIONS
================================
1. Automated vulnerability scanning
2. Penetration testing by security professionals
3. Code review by security experts
4. Input fuzzing tests
5. Authentication bypass testing
6. Session management testing
7. SQL injection testing
8. XSS testing with various payloads
9. CSRF testing
10. Rate limiting testing
POSITIVE SECURITY PRACTICES FOUND
=================================
1. Consistent use of parameterized SQL queries (prevents SQL injection)
2. PIN verification system uses cryptographically secure random generation
3. Database queries properly use ? placeholders for user input
4. No dangerous functions like eval() or exec() found
5. No system command execution with user input
6. JSON parsing includes proper error handling
7. Input normalization implemented in base module
8. PIN expiration mechanism (10 minutes) implemented
9. Single-use PIN system prevents replay attacks
10. Proper database transaction handling in critical operations
TECHNICAL DEBT CONSIDERATIONS
============================
1. Implement proper templating engine with auto-escaping
2. Add web application firewall (WAF)
3. Implement Content Security Policy (CSP)
4. Add security headers middleware
5. Implement proper logging framework
6. Add security unit tests
7. Implement secure configuration management
8. Add API rate limiting
9. Implement proper error handling framework
10. Add security monitoring and alerting
COMPLIANCE CONSIDERATIONS
========================
1. Data protection: Player data is publicly accessible
2. Access control: No authorization mechanism
3. Encryption: No HTTPS implementation
4. Logging: No security audit logs
5. Authentication: No proper user authentication
CONCLUSION
==========
The PetBot application has significant security vulnerabilities that should be addressed before production deployment. The most critical issues are XSS vulnerabilities and missing authentication controls. However, the application demonstrates good security practices in database operations and PIN generation.
Priority should be given to:
1. Implementing proper input validation and output escaping
2. Adding authentication and authorization mechanisms
3. Securing the web interface with HTTPS and security headers
4. Implementing rate limiting and session management
The development team should establish security practices including:
- Security code reviews
- Automated vulnerability scanning
- Regular security testing
- Security training for developers
- Incident response procedures
This audit provides a comprehensive foundation for improving the security posture of the PetBot application.

View file

@ -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
View 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")

View file

@ -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"""

View 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
}

61
rate_limiting_config.json Normal file
View file

@ -0,0 +1,61 @@
{
"rate_limiting": {
"enabled": true,
"description": "Rate limiting configuration for PetBot IRC commands and web interface",
"categories": {
"basic": {
"description": "Basic commands like !help, !ping, !status",
"requests_per_minute": 20,
"burst_capacity": 5,
"cooldown_seconds": 1,
"commands": ["help", "ping", "status", "uptime", "connection_stats"]
},
"gameplay": {
"description": "Gameplay commands like !explore, !catch, !battle",
"requests_per_minute": 10,
"burst_capacity": 3,
"cooldown_seconds": 3,
"commands": ["start", "explore", "catch", "battle", "attack", "moves", "flee", "travel", "weather", "gym"]
},
"management": {
"description": "Pet and inventory management commands",
"requests_per_minute": 5,
"burst_capacity": 2,
"cooldown_seconds": 5,
"commands": ["pets", "activate", "deactivate", "stats", "inventory", "use", "nickname"]
},
"admin": {
"description": "Administrative commands",
"requests_per_minute": 100,
"burst_capacity": 10,
"cooldown_seconds": 0,
"commands": ["backup", "restore", "backups", "backup_stats", "backup_cleanup", "reload", "reconnect", "rate_stats", "rate_user", "rate_unban", "rate_reset"]
},
"web": {
"description": "Web interface requests",
"requests_per_minute": 60,
"burst_capacity": 10,
"cooldown_seconds": 1
}
},
"admin_users": ["admin", "megaproxy", "megasconed"],
"global_limits": {
"max_requests_per_minute": 200,
"max_concurrent_users": 100,
"description": "Global limits across all users and commands"
},
"violation_penalties": {
"warning_threshold": 3,
"temporary_ban_threshold": 10,
"temporary_ban_duration": 300,
"description": "Penalties for rate limit violations (ban duration in seconds)"
},
"monitoring": {
"log_violations": true,
"log_bans": true,
"stats_reporting_interval": 300,
"cleanup_interval": 60,
"description": "Monitoring and logging configuration"
}
}
}

View file

@ -1,4 +1,44 @@
# PetBot Requirements
# Core dependencies for IRC bot functionality
# IRC client library for connecting to IRC servers
irc>=20.3.0
# Async SQLite database interface for database operations
aiosqlite>=0.19.0
# Environment variable loading (for configuration)
python-dotenv>=1.0.0
asyncio
# HTTP client for web interface testing and rate limiting tests
aiohttp>=3.8.0
# Note: The following are part of Python's standard library and don't need installation:
# - asyncio (async programming)
# - sqlite3 (synchronous SQLite operations)
# - json (JSON data handling)
# - socket (network communication)
# - threading (thread management)
# - time (time operations)
# - os (operating system interface)
# - sys (system parameters)
# - logging (logging framework)
# - pathlib (path handling)
# - urllib.parse (URL parsing)
# - datetime (date/time handling)
# - typing (type annotations)
# - enum (enumerations)
# - abc (abstract base classes)
# - importlib (import utilities)
# - signal (signal handling)
# - shutil (file operations)
# - gzip (compression)
# - tempfile (temporary files)
# - http.server (HTTP server)
# - random (random numbers)
# Development and testing dependencies (optional)
# These are not required for basic bot operation but useful for development:
# pytest>=7.0.0
# black>=22.0.0
# flake8>=4.0.0

482
run_bot_with_reconnect.py Normal file
View file

@ -0,0 +1,482 @@
#!/usr/bin/env python3
"""
PetBot with Advanced IRC Connection Management
Includes automatic reconnection, health monitoring, and graceful error handling.
"""
import asyncio
import sys
import os
import importlib
import logging
import signal
from datetime import datetime
# Add the project directory to the path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.database import Database
from src.game_engine import GameEngine
from src.irc_connection_manager import IRCConnectionManager, ConnectionState
from src.rate_limiter import RateLimiter, get_command_category
from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory, GymBattles, TeamBuilder
from webserver import PetBotWebServer
from config import IRC_CONFIG, RATE_LIMIT_CONFIG
class PetBotWithReconnect:
"""
Enhanced PetBot with robust IRC connection management.
"""
def __init__(self):
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger(__name__)
self.logger.info("🤖 PetBot with Auto-Reconnect - Initializing...")
# Core components
self.database = Database()
self.game_engine = GameEngine(self.database)
self.config = IRC_CONFIG
# Connection and state management
self.connection_manager = None
self.running = False
self.shutdown_requested = False
# Module management
self.modules = {}
self.command_map = {}
self.active_encounters = {}
# Web server
self.web_server = None
# Rate limiting
self.rate_limiter = None
# Statistics
self.startup_time = datetime.now()
self.command_count = 0
self.connection_events = []
self.logger.info("✅ Basic initialization complete")
async def initialize(self):
"""Initialize all async components."""
try:
self.logger.info("🔄 Initializing async components...")
# Initialize database
self.logger.info("🔄 Initializing database...")
await self.database.init_database()
self.logger.info("✅ Database initialized")
# Load game data
self.logger.info("🔄 Loading game data...")
await self.game_engine.load_game_data()
self.logger.info("✅ Game data loaded")
# Load modules
self.logger.info("🔄 Loading command modules...")
await self.load_modules()
self.logger.info("✅ Modules loaded")
# Validate player data
self.logger.info("🔄 Validating player data...")
await self.validate_all_player_data()
self.logger.info("✅ Player data validation complete")
# Initialize rate limiter with config
self.logger.info("🔄 Initializing rate limiter...")
self.rate_limiter = RateLimiter(RATE_LIMIT_CONFIG)
self.logger.info("✅ Rate limiter initialized")
# Start web server
self.logger.info("🔄 Starting web server...")
self.web_server = PetBotWebServer(self.database, port=8080, bot=self)
self.web_server.start_in_thread()
self.logger.info("✅ Web server started on port 8080")
# Initialize connection manager
self.logger.info("🔄 Initializing IRC connection manager...")
self.connection_manager = IRCConnectionManager(self.config, self)
self.connection_manager.set_callbacks(
on_connect=self.on_irc_connect,
on_disconnect=self.on_irc_disconnect,
on_message=self.on_irc_message,
on_connection_lost=self.on_connection_lost
)
self.logger.info("✅ IRC connection manager initialized")
# Start background tasks
self.logger.info("🔄 Starting background tasks...")
asyncio.create_task(self.background_validation_task())
asyncio.create_task(self.connection_stats_task())
self.logger.info("✅ Background tasks started")
self.logger.info("🎉 All components initialized successfully!")
except Exception as e:
self.logger.error(f"❌ Initialization failed: {e}")
raise
async def load_modules(self):
"""Load all command modules."""
module_classes = [
CoreCommands,
Exploration,
BattleSystem,
PetManagement,
Achievements,
Admin,
Inventory,
GymBattles,
TeamBuilder
]
self.modules = {}
self.command_map = {}
for module_class in module_classes:
try:
module_name = module_class.__name__
self.logger.info(f" Loading {module_name}...")
module_instance = module_class(self, self.database, self.game_engine)
self.modules[module_name] = module_instance
# Map commands to modules
commands = module_instance.get_commands()
for command in commands:
self.command_map[command] = module_instance
self.logger.info(f"{module_name}: {len(commands)} commands")
except Exception as e:
self.logger.error(f" ❌ Failed to load {module_name}: {e}")
raise
self.logger.info(f"✅ Loaded {len(self.modules)} modules with {len(self.command_map)} commands")
async def reload_modules(self):
"""Reload all modules (for admin use)."""
try:
self.logger.info("🔄 Reloading modules...")
# Reload module files
import modules
importlib.reload(modules.core_commands)
importlib.reload(modules.exploration)
importlib.reload(modules.battle_system)
importlib.reload(modules.pet_management)
importlib.reload(modules.achievements)
importlib.reload(modules.admin)
importlib.reload(modules.inventory)
importlib.reload(modules.gym_battles)
importlib.reload(modules.team_builder)
importlib.reload(modules)
# Reinitialize modules
await self.load_modules()
self.logger.info("✅ Modules reloaded successfully")
return True
except Exception as e:
self.logger.error(f"❌ Module reload failed: {e}")
return False
async def validate_all_player_data(self):
"""Validate and refresh all player data."""
try:
import aiosqlite
async with aiosqlite.connect(self.database.db_path) as db:
cursor = await db.execute("SELECT id, nickname FROM players")
players = await cursor.fetchall()
self.logger.info(f"🔄 Validating {len(players)} players...")
for player_id, nickname in players:
try:
# Check and award missing achievements
new_achievements = await self.game_engine.check_all_achievements(player_id)
if new_achievements:
self.logger.info(f" 🏆 {nickname}: Restored {len(new_achievements)} achievements")
# Validate team composition
team_composition = await self.database.get_team_composition(player_id)
if team_composition["active_pets"] == 0 and team_composition["total_pets"] > 0:
pets = await self.database.get_player_pets(player_id)
if pets:
first_pet = pets[0]
await self.database.activate_pet(player_id, str(first_pet["id"]))
self.logger.info(f" 🔧 {nickname}: Auto-activated pet {first_pet['nickname'] or first_pet['species_name']}")
except Exception as e:
self.logger.error(f" ❌ Error validating {nickname}: {e}")
self.logger.info("✅ Player data validation complete")
except Exception as e:
self.logger.error(f"❌ Player data validation failed: {e}")
async def background_validation_task(self):
"""Background task for periodic validation."""
while self.running:
try:
await asyncio.sleep(1800) # Run every 30 minutes
if self.running:
self.logger.info("🔄 Running periodic validation...")
await self.validate_all_player_data()
except asyncio.CancelledError:
break
except Exception as e:
self.logger.error(f"❌ Background validation error: {e}")
async def connection_stats_task(self):
"""Background task for connection statistics."""
while self.running:
try:
await asyncio.sleep(300) # Log stats every 5 minutes
if self.running and self.connection_manager:
stats = self.connection_manager.get_connection_stats()
if stats["connected"]:
self.logger.info(f"📊 Connection stats: {stats['message_count']} messages, {stats['total_reconnections']} reconnections")
except asyncio.CancelledError:
break
except Exception as e:
self.logger.error(f"❌ Connection stats error: {e}")
async def on_irc_connect(self):
"""Called when IRC connection is established."""
self.logger.info("🎉 IRC connection established successfully!")
self.connection_events.append({"type": "connect", "time": datetime.now()})
# Send welcome message to channel
if self.connection_manager:
await self.connection_manager.send_message(
self.config["channel"],
"🤖 PetBot is online and ready for commands! Use !help to get started."
)
async def on_irc_disconnect(self, error):
"""Called when IRC connection is lost."""
self.logger.warning(f"💔 IRC connection lost: {error}")
self.connection_events.append({"type": "disconnect", "time": datetime.now(), "error": str(error)})
async def on_connection_lost(self, error):
"""Called when connection is lost and reconnection will be attempted."""
self.logger.warning(f"🔄 Connection lost, will attempt reconnection: {error}")
self.connection_events.append({"type": "reconnect_attempt", "time": datetime.now(), "error": str(error)})
async def on_irc_message(self, line):
"""Called when IRC message is received."""
# Handle private messages and channel messages
parts = line.split()
if len(parts) < 4:
return
if parts[1] == "PRIVMSG":
channel = parts[2]
message = " ".join(parts[3:])[1:] # Remove leading ':'
# Extract nickname from hostmask
hostmask = parts[0][1:] # Remove leading ':'
nickname = hostmask.split('!')[0]
# Handle commands
if message.startswith(self.config["command_prefix"]):
self.logger.info(f"🎮 Command from {nickname}: {message}")
await self.handle_command(channel, nickname, message)
async def handle_command(self, channel, nickname, message):
"""Handle IRC commands with rate limiting."""
from modules.base_module import BaseModule
command_parts = message[1:].split()
if not command_parts:
return
command = BaseModule.normalize_input(command_parts[0])
args = BaseModule.normalize_input(command_parts[1:])
try:
# Check rate limit first
if self.rate_limiter:
category = get_command_category(command)
allowed, rate_limit_message = await self.rate_limiter.check_rate_limit(
nickname, category, command
)
if not allowed:
await self.send_message(channel, f"{nickname}: {rate_limit_message}")
return
self.command_count += 1
if command in self.command_map:
module = self.command_map[command]
self.logger.info(f"🔧 Executing {command} via {module.__class__.__name__}")
await module.handle_command(channel, nickname, command, args)
else:
await self.send_message(channel, f"{nickname}: Unknown command. Use !help for available commands.")
except Exception as e:
self.logger.error(f"❌ Command error: {e}")
await self.send_message(channel, f"{nickname}: Error processing command: {str(e)}")
async def send_message(self, target, message):
"""Send message via connection manager."""
if self.connection_manager:
success = await self.connection_manager.send_message(target, message)
if not success:
self.logger.warning(f"Failed to send message to {target}: {message}")
else:
self.logger.warning(f"No connection manager available to send message to {target}")
def send_message_sync(self, target, message):
"""Synchronous wrapper for send_message (for compatibility with old modules)."""
if hasattr(self, 'loop') and self.loop and self.loop.is_running():
# Schedule the coroutine to run in the existing event loop
asyncio.create_task(self.send_message(target, message))
else:
# Fallback - try to get current loop
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(self.send_message(target, message))
else:
loop.run_until_complete(self.send_message(target, message))
except Exception as e:
self.logger.error(f"Failed to send message synchronously: {e}")
async def send_team_builder_pin(self, nickname, pin_code):
"""Send team builder PIN via private message."""
message = f"🔐 Team Builder PIN: {pin_code} (expires in 10 minutes)"
await self.send_message(nickname, message)
def get_bot_stats(self):
"""Get comprehensive bot statistics."""
uptime = datetime.now() - self.startup_time
connection_stats = {}
if self.connection_manager:
connection_stats = self.connection_manager.get_connection_stats()
return {
"uptime": str(uptime),
"startup_time": self.startup_time,
"command_count": self.command_count,
"loaded_modules": len(self.modules),
"available_commands": len(self.command_map),
"connection_events": len(self.connection_events),
"connection_stats": connection_stats,
"running": self.running
}
async def start(self):
"""Start the bot."""
self.running = True
try:
# Initialize all components
await self.initialize()
# Start connection manager
self.logger.info("🚀 Starting IRC connection manager...")
await self.connection_manager.start()
except Exception as e:
self.logger.error(f"❌ Bot startup failed: {e}")
raise
finally:
await self.shutdown()
async def shutdown(self):
"""Gracefully shutdown the bot."""
if self.shutdown_requested:
return
self.shutdown_requested = True
self.logger.info("🔄 Shutting down bot...")
# Stop main loop
self.running = False
# Stop connection manager
if self.connection_manager:
await self.connection_manager.stop()
# Shutdown rate limiter
if self.rate_limiter:
try:
await self.rate_limiter.shutdown()
except Exception as e:
self.logger.error(f"Error shutting down rate limiter: {e}")
# Shutdown game engine
if self.game_engine:
try:
await self.game_engine.shutdown()
except Exception as e:
self.logger.error(f"Error shutting down game engine: {e}")
# Stop web server
if self.web_server:
try:
# Web server doesn't have async shutdown, so we'll just log it
self.logger.info("🔄 Web server shutdown (handled by thread)")
except Exception as e:
self.logger.error(f"Error shutting down web server: {e}")
self.logger.info("✅ Bot shutdown complete")
async def main():
"""Main entry point."""
bot = PetBotWithReconnect()
# Make bot instance globally accessible for webserver
sys.modules[__name__].bot_instance = bot
# Setup signal handlers for graceful shutdown
def signal_handler(signum, frame):
bot.logger.info(f"Received signal {signum}, initiating shutdown...")
asyncio.create_task(bot.shutdown())
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
try:
await bot.start()
except KeyboardInterrupt:
bot.logger.info("🛑 Keyboard interrupt received")
except Exception as e:
bot.logger.error(f"❌ Bot crashed: {e}")
import traceback
traceback.print_exc()
finally:
await bot.shutdown()
if __name__ == "__main__":
print("🐾 Starting PetBot with Auto-Reconnect...")
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n🔄 Bot stopping...")
except Exception as e:
print(f"❌ Fatal error: {e}")
import traceback
traceback.print_exc()
finally:
print("✅ Bot stopped")

458
src/backup_manager.py Normal file
View file

@ -0,0 +1,458 @@
import os
import shutil
import sqlite3
import gzip
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import asyncio
import aiosqlite
from pathlib import Path
import logging
class BackupManager:
def __init__(self, db_path: str = "data/petbot.db", backup_dir: str = "backups"):
self.db_path = db_path
self.backup_dir = Path(backup_dir)
self.backup_dir.mkdir(exist_ok=True)
# Backup configuration
self.max_daily_backups = 7 # Keep 7 daily backups
self.max_weekly_backups = 4 # Keep 4 weekly backups
self.max_monthly_backups = 12 # Keep 12 monthly backups
# Setup logging
self.logger = logging.getLogger(__name__)
async def create_backup(self, backup_type: str = "manual", compress: bool = True) -> Dict:
"""Create a database backup with optional compression."""
try:
# Check if database exists
if not os.path.exists(self.db_path):
return {"success": False, "error": "Database file not found"}
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_filename = f"petbot_backup_{backup_type}_{timestamp}.db"
if compress:
backup_filename += ".gz"
backup_path = self.backup_dir / backup_filename
# Create the backup
if compress:
await self._create_compressed_backup(backup_path)
else:
await self._create_regular_backup(backup_path)
# Get backup info
backup_info = await self._get_backup_info(backup_path)
# Log the backup
self.logger.info(f"Backup created: {backup_filename} ({backup_info['size_mb']:.1f}MB)")
return {
"success": True,
"backup_path": str(backup_path),
"backup_filename": backup_filename,
"backup_type": backup_type,
"timestamp": timestamp,
"compressed": compress,
"size_mb": backup_info["size_mb"]
}
except Exception as e:
self.logger.error(f"Backup creation failed: {str(e)}")
return {"success": False, "error": str(e)}
async def _create_regular_backup(self, backup_path: Path):
"""Create a regular SQLite backup using the backup API."""
def backup_db():
# Use SQLite backup API for consistent backup
source_conn = sqlite3.connect(self.db_path)
backup_conn = sqlite3.connect(str(backup_path))
# Perform the backup
source_conn.backup(backup_conn)
# Close connections
source_conn.close()
backup_conn.close()
# Run the backup in a thread to avoid blocking
await asyncio.get_event_loop().run_in_executor(None, backup_db)
async def _create_compressed_backup(self, backup_path: Path):
"""Create a compressed backup."""
def backup_and_compress():
# First create temporary uncompressed backup
temp_backup = backup_path.with_suffix('.tmp')
# Use SQLite backup API
source_conn = sqlite3.connect(self.db_path)
backup_conn = sqlite3.connect(str(temp_backup))
source_conn.backup(backup_conn)
source_conn.close()
backup_conn.close()
# Compress the backup
with open(temp_backup, 'rb') as f_in:
with gzip.open(backup_path, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
# Remove temporary file
temp_backup.unlink()
await asyncio.get_event_loop().run_in_executor(None, backup_and_compress)
async def _get_backup_info(self, backup_path: Path) -> Dict:
"""Get information about a backup file."""
stat = backup_path.stat()
return {
"size_bytes": stat.st_size,
"size_mb": stat.st_size / (1024 * 1024),
"created_at": datetime.fromtimestamp(stat.st_mtime),
"compressed": backup_path.suffix == '.gz'
}
async def list_backups(self) -> List[Dict]:
"""List all available backups with metadata."""
backups = []
for backup_file in self.backup_dir.glob("petbot_backup_*.db*"):
try:
info = await self._get_backup_info(backup_file)
# Parse backup filename for metadata
filename = backup_file.name
parts = filename.replace('.gz', '').replace('.db', '').split('_')
if len(parts) >= 4:
backup_type = parts[2]
timestamp = parts[3]
backups.append({
"filename": filename,
"path": str(backup_file),
"type": backup_type,
"timestamp": timestamp,
"created_at": info["created_at"],
"size_mb": info["size_mb"],
"compressed": info["compressed"]
})
except Exception as e:
self.logger.warning(f"Error reading backup {backup_file}: {e}")
continue
# Sort by creation time (newest first)
backups.sort(key=lambda x: x["created_at"], reverse=True)
return backups
async def restore_backup(self, backup_filename: str, target_path: str = None) -> Dict:
"""Restore a database from backup."""
try:
backup_path = self.backup_dir / backup_filename
if not backup_path.exists():
return {"success": False, "error": "Backup file not found"}
target_path = target_path or self.db_path
# Create backup of current database before restore
current_backup = await self.create_backup("pre_restore", compress=True)
if not current_backup["success"]:
return {"success": False, "error": "Failed to backup current database"}
# Restore the backup
if backup_path.suffix == '.gz':
await self._restore_compressed_backup(backup_path, target_path)
else:
await self._restore_regular_backup(backup_path, target_path)
# Verify the restored database
verification = await self._verify_database(target_path)
if not verification["success"]:
return {"success": False, "error": f"Restored database verification failed: {verification['error']}"}
self.logger.info(f"Database restored from backup: {backup_filename}")
return {
"success": True,
"backup_filename": backup_filename,
"target_path": target_path,
"current_backup": current_backup["backup_filename"],
"tables_verified": verification["table_count"]
}
except Exception as e:
self.logger.error(f"Restore failed: {str(e)}")
return {"success": False, "error": str(e)}
async def _restore_regular_backup(self, backup_path: Path, target_path: str):
"""Restore from regular backup."""
def restore():
shutil.copy2(backup_path, target_path)
await asyncio.get_event_loop().run_in_executor(None, restore)
async def _restore_compressed_backup(self, backup_path: Path, target_path: str):
"""Restore from compressed backup."""
def restore():
with gzip.open(backup_path, 'rb') as f_in:
with open(target_path, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
await asyncio.get_event_loop().run_in_executor(None, restore)
async def _verify_database(self, db_path: str) -> Dict:
"""Verify database integrity and structure."""
try:
async with aiosqlite.connect(db_path) as db:
# Check database integrity
cursor = await db.execute("PRAGMA integrity_check")
integrity_result = await cursor.fetchone()
if integrity_result[0] != "ok":
return {"success": False, "error": f"Database integrity check failed: {integrity_result[0]}"}
# Count tables
cursor = await db.execute("SELECT COUNT(*) FROM sqlite_master WHERE type='table'")
table_count = (await cursor.fetchone())[0]
# Basic table existence check
required_tables = ["players", "pets", "pet_species", "moves", "items"]
for table in required_tables:
cursor = await db.execute(f"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", (table,))
exists = (await cursor.fetchone())[0]
if not exists:
return {"success": False, "error": f"Required table '{table}' not found"}
return {"success": True, "table_count": table_count}
except Exception as e:
return {"success": False, "error": str(e)}
async def cleanup_old_backups(self) -> Dict:
"""Remove old backups based on retention policy."""
try:
backups = await self.list_backups()
# Group backups by type
daily_backups = [b for b in backups if b["type"] in ["daily", "manual"]]
weekly_backups = [b for b in backups if b["type"] == "weekly"]
monthly_backups = [b for b in backups if b["type"] == "monthly"]
cleaned_count = 0
# Clean daily backups (keep most recent)
if len(daily_backups) > self.max_daily_backups:
old_daily = daily_backups[self.max_daily_backups:]
for backup in old_daily:
await self._remove_backup(backup["path"])
cleaned_count += 1
# Clean weekly backups
if len(weekly_backups) > self.max_weekly_backups:
old_weekly = weekly_backups[self.max_weekly_backups:]
for backup in old_weekly:
await self._remove_backup(backup["path"])
cleaned_count += 1
# Clean monthly backups
if len(monthly_backups) > self.max_monthly_backups:
old_monthly = monthly_backups[self.max_monthly_backups:]
for backup in old_monthly:
await self._remove_backup(backup["path"])
cleaned_count += 1
self.logger.info(f"Cleaned up {cleaned_count} old backups")
return {
"success": True,
"cleaned_count": cleaned_count,
"remaining_backups": len(backups) - cleaned_count
}
except Exception as e:
self.logger.error(f"Cleanup failed: {str(e)}")
return {"success": False, "error": str(e)}
async def _remove_backup(self, backup_path: str):
"""Remove a backup file."""
try:
Path(backup_path).unlink()
self.logger.debug(f"Removed backup: {backup_path}")
except Exception as e:
self.logger.warning(f"Failed to remove backup {backup_path}: {e}")
async def get_backup_stats(self) -> Dict:
"""Get statistics about backups."""
try:
backups = await self.list_backups()
if not backups:
return {
"success": True,
"total_backups": 0,
"total_size_mb": 0,
"oldest_backup": None,
"newest_backup": None,
"by_type": {}
}
total_size = sum(b["size_mb"] for b in backups)
oldest = min(backups, key=lambda x: x["created_at"])
newest = max(backups, key=lambda x: x["created_at"])
# Group by type
by_type = {}
for backup in backups:
backup_type = backup["type"]
if backup_type not in by_type:
by_type[backup_type] = {"count": 0, "size_mb": 0}
by_type[backup_type]["count"] += 1
by_type[backup_type]["size_mb"] += backup["size_mb"]
return {
"success": True,
"total_backups": len(backups),
"total_size_mb": round(total_size, 1),
"oldest_backup": oldest["created_at"],
"newest_backup": newest["created_at"],
"by_type": by_type
}
except Exception as e:
self.logger.error(f"Failed to get backup stats: {str(e)}")
return {"success": False, "error": str(e)}
async def export_database_structure(self) -> Dict:
"""Export database schema for documentation/analysis."""
try:
structure = {
"export_time": datetime.now().isoformat(),
"database_path": self.db_path,
"tables": {}
}
async with aiosqlite.connect(self.db_path) as db:
# Get all tables
cursor = await db.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = await cursor.fetchall()
for table_name in [t[0] for t in tables]:
# Get table info
cursor = await db.execute(f"PRAGMA table_info({table_name})")
columns = await cursor.fetchall()
# Get row count
cursor = await db.execute(f"SELECT COUNT(*) FROM {table_name}")
row_count = (await cursor.fetchone())[0]
structure["tables"][table_name] = {
"columns": [
{
"name": col[1],
"type": col[2],
"not_null": bool(col[3]),
"default": col[4],
"primary_key": bool(col[5])
}
for col in columns
],
"row_count": row_count
}
# Save structure to file
structure_path = self.backup_dir / f"database_structure_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(structure_path, 'w') as f:
json.dump(structure, f, indent=2, default=str)
return {
"success": True,
"structure_path": str(structure_path),
"table_count": len(structure["tables"]),
"total_rows": sum(table["row_count"] for table in structure["tables"].values())
}
except Exception as e:
self.logger.error(f"Failed to export database structure: {str(e)}")
return {"success": False, "error": str(e)}
# Scheduler for automated backups
class BackupScheduler:
def __init__(self, backup_manager: BackupManager):
self.backup_manager = backup_manager
self.logger = logging.getLogger(__name__)
self.running = False
async def start_scheduler(self):
"""Start the backup scheduler."""
self.running = True
self.logger.info("Backup scheduler started")
while self.running:
try:
await self._check_and_create_backups()
await asyncio.sleep(3600) # Check every hour
except Exception as e:
self.logger.error(f"Scheduler error: {str(e)}")
await asyncio.sleep(3600) # Wait before retrying
def stop_scheduler(self):
"""Stop the backup scheduler."""
self.running = False
self.logger.info("Backup scheduler stopped")
async def _check_and_create_backups(self):
"""Check if backups are needed and create them."""
now = datetime.now()
# Check daily backup (every 24 hours)
if await self._should_create_backup("daily", hours=24):
result = await self.backup_manager.create_backup("daily", compress=True)
if result["success"]:
self.logger.info(f"Daily backup created: {result['backup_filename']}")
# Check weekly backup (every 7 days)
if await self._should_create_backup("weekly", days=7):
result = await self.backup_manager.create_backup("weekly", compress=True)
if result["success"]:
self.logger.info(f"Weekly backup created: {result['backup_filename']}")
# Check monthly backup (every 30 days)
if await self._should_create_backup("monthly", days=30):
result = await self.backup_manager.create_backup("monthly", compress=True)
if result["success"]:
self.logger.info(f"Monthly backup created: {result['backup_filename']}")
# Cleanup old backups
await self.backup_manager.cleanup_old_backups()
async def _should_create_backup(self, backup_type: str, hours: int = 0, days: int = 0) -> bool:
"""Check if a backup of the specified type should be created."""
try:
backups = await self.backup_manager.list_backups()
# Find most recent backup of this type
type_backups = [b for b in backups if b["type"] == backup_type]
if not type_backups:
return True # No backups of this type exist
most_recent = max(type_backups, key=lambda x: x["created_at"])
time_since = datetime.now() - most_recent["created_at"]
required_delta = timedelta(hours=hours, days=days)
return time_since >= required_delta
except Exception as e:
self.logger.error(f"Error checking backup schedule: {str(e)}")
return False

View file

@ -365,6 +365,9 @@ class GameEngine:
except FileNotFoundError:
return None
# Get global spawn multiplier from config
global_multiplier = items_data.get("_config", {}).get("global_spawn_multiplier", 1.0)
# Get all possible items for this location
available_items = []
location_name = location["name"].lower().replace(" ", "_")
@ -375,22 +378,25 @@ class GameEngine:
if "locations" in item:
item_locations = item["locations"]
if "all" in item_locations or location_name in item_locations:
available_items.append(item)
# Apply global multiplier to spawn rate
item_copy = item.copy()
item_copy["effective_spawn_rate"] = item.get("spawn_rate", 0.1) * global_multiplier
available_items.append(item_copy)
if not available_items:
return None
# Calculate total spawn rates for this location
total_rate = sum(item.get("spawn_rate", 0.1) for item in available_items)
# Calculate total spawn rates for this location (using effective rates)
total_rate = sum(item.get("effective_spawn_rate", 0.1) for item in available_items)
# 30% base chance of finding an item
if random.random() > 0.3:
return None
# Choose item based on spawn rates
# Choose item based on effective spawn rates (with global multiplier applied)
chosen_item = random.choices(
available_items,
weights=[item.get("spawn_rate", 0.1) for item in available_items]
weights=[item.get("effective_spawn_rate", 0.1) for item in available_items]
)[0]
# Add item to player's inventory

View file

@ -0,0 +1,395 @@
import asyncio
import socket
import time
import logging
import random
from enum import Enum
from typing import Optional, Callable, Dict, Any
from datetime import datetime, timedelta
class ConnectionState(Enum):
DISCONNECTED = "disconnected"
CONNECTING = "connecting"
CONNECTED = "connected"
AUTHENTICATED = "authenticated"
JOINED = "joined"
RECONNECTING = "reconnecting"
FAILED = "failed"
class IRCConnectionManager:
"""
Robust IRC connection manager with automatic reconnection,
health monitoring, and exponential backoff.
"""
def __init__(self, config: Dict[str, Any], bot_instance=None):
self.config = config
self.bot = bot_instance
self.socket = None
self.state = ConnectionState.DISCONNECTED
self.running = False
# Connection monitoring
self.last_ping_time = 0
self.last_pong_time = 0
self.ping_interval = 60 # Send PING every 60 seconds
self.ping_timeout = 120 # Expect PONG within 2 minutes
# Reconnection settings
self.reconnect_attempts = 0
self.max_reconnect_attempts = 50
self.base_reconnect_delay = 1 # Start with 1 second
self.max_reconnect_delay = 300 # Cap at 5 minutes
self.reconnect_jitter = 0.1 # 10% jitter
# Connection tracking
self.connection_start_time = None
self.last_successful_connection = None
self.total_reconnections = 0
self.connection_failures = 0
# Event callbacks
self.on_connect_callback = None
self.on_disconnect_callback = None
self.on_message_callback = None
self.on_connection_lost_callback = None
# Health monitoring
self.health_check_interval = 30 # Check health every 30 seconds
self.health_check_task = None
self.message_count = 0
self.last_message_time = 0
# Setup logging
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.INFO)
# Create console handler if none exists
if not self.logger.handlers:
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
def set_callbacks(self, on_connect=None, on_disconnect=None, on_message=None, on_connection_lost=None):
"""Set callback functions for connection events."""
self.on_connect_callback = on_connect
self.on_disconnect_callback = on_disconnect
self.on_message_callback = on_message
self.on_connection_lost_callback = on_connection_lost
async def start(self):
"""Start the connection manager."""
if self.running:
self.logger.warning("Connection manager is already running")
return
self.running = True
self.logger.info("Starting IRC connection manager")
# Start health monitoring
self.health_check_task = asyncio.create_task(self._health_monitor())
# Start connection loop
await self._connection_loop()
async def stop(self):
"""Stop the connection manager and close connections."""
self.running = False
self.logger.info("Stopping IRC connection manager")
# Cancel health monitoring
if self.health_check_task:
self.health_check_task.cancel()
try:
await self.health_check_task
except asyncio.CancelledError:
pass
# Close socket
await self._disconnect()
async def _connection_loop(self):
"""Main connection loop with automatic reconnection."""
while self.running:
try:
if self.state == ConnectionState.DISCONNECTED:
await self._connect()
if self.state in [ConnectionState.CONNECTED, ConnectionState.AUTHENTICATED, ConnectionState.JOINED]:
await self._handle_messages()
await asyncio.sleep(0.1)
except Exception as e:
self.logger.error(f"Error in connection loop: {e}")
await self._handle_connection_error(e)
async def _connect(self):
"""Connect to IRC server with retry logic."""
if self.reconnect_attempts >= self.max_reconnect_attempts:
self.logger.error(f"Maximum reconnection attempts ({self.max_reconnect_attempts}) reached")
self.state = ConnectionState.FAILED
return
self.state = ConnectionState.CONNECTING
self.connection_start_time = datetime.now()
try:
# Calculate reconnection delay with exponential backoff
if self.reconnect_attempts > 0:
delay = min(
self.base_reconnect_delay * (2 ** self.reconnect_attempts),
self.max_reconnect_delay
)
# Add jitter to prevent thundering herd
jitter = delay * self.reconnect_jitter * random.random()
delay += jitter
self.logger.info(f"Reconnection attempt {self.reconnect_attempts + 1}/{self.max_reconnect_attempts} after {delay:.1f}s delay")
await asyncio.sleep(delay)
# Test basic connectivity first
await self._test_connectivity()
# Create socket connection
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(10) # 10 second timeout for connection
# Connect to server
await asyncio.get_event_loop().run_in_executor(
None, self.socket.connect, (self.config["server"], self.config["port"])
)
self.socket.settimeout(1) # Shorter timeout for message handling
self.state = ConnectionState.CONNECTED
# Send IRC handshake
await self._send_handshake()
# Reset reconnection counter on successful connection
self.reconnect_attempts = 0
self.last_successful_connection = datetime.now()
self.total_reconnections += 1
self.logger.info(f"Successfully connected to {self.config['server']}:{self.config['port']}")
except Exception as e:
self.logger.error(f"Connection failed: {e}")
self.connection_failures += 1
self.reconnect_attempts += 1
await self._disconnect()
if self.on_connection_lost_callback:
await self.on_connection_lost_callback(e)
async def _test_connectivity(self):
"""Test basic network connectivity to IRC server."""
try:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_sock.settimeout(5)
await asyncio.get_event_loop().run_in_executor(
None, test_sock.connect, (self.config["server"], self.config["port"])
)
test_sock.close()
except Exception as e:
raise ConnectionError(f"Network connectivity test failed: {e}")
async def _send_handshake(self):
"""Send IRC handshake messages."""
nickname = self.config["nickname"]
await self._send_raw(f"NICK {nickname}")
await self._send_raw(f"USER {nickname} 0 * :{nickname}")
# Initialize ping tracking
self.last_ping_time = time.time()
self.last_pong_time = time.time()
async def _handle_messages(self):
"""Handle incoming IRC messages."""
try:
data = await asyncio.get_event_loop().run_in_executor(
None, self.socket.recv, 4096
)
if not data:
raise ConnectionError("Connection closed by server")
# Update message tracking
self.message_count += 1
self.last_message_time = time.time()
# Decode and process messages
lines = data.decode('utf-8', errors='ignore').strip().split('\n')
for line in lines:
if line.strip():
await self._process_line(line.strip())
except socket.timeout:
# Timeout is expected, continue
pass
except Exception as e:
raise ConnectionError(f"Message handling error: {e}")
async def _process_line(self, line):
"""Process a single IRC line."""
# Handle PING/PONG
if line.startswith("PING"):
pong_response = line.replace("PING", "PONG")
await self._send_raw(pong_response)
return
if line.startswith("PONG"):
self.last_pong_time = time.time()
return
# Handle connection completion
if "376" in line or "422" in line: # End of MOTD
if self.state == ConnectionState.CONNECTED:
self.state = ConnectionState.AUTHENTICATED
await self._send_raw(f"JOIN {self.config['channel']}")
# Handle successful channel join
if " JOIN " in line and self.config["channel"] in line:
if self.state == ConnectionState.AUTHENTICATED:
self.state = ConnectionState.JOINED
self.logger.info(f"Successfully joined {self.config['channel']}")
if self.on_connect_callback:
await self.on_connect_callback()
# Handle nickname conflicts
if "433" in line: # Nickname in use
new_nickname = f"{self.config['nickname']}_"
self.logger.warning(f"Nickname conflict, trying {new_nickname}")
await self._send_raw(f"NICK {new_nickname}")
# Handle disconnection
if "ERROR :Closing Link" in line:
raise ConnectionError("Server closed connection")
# Forward message to callback
if self.on_message_callback:
await self.on_message_callback(line)
async def _send_raw(self, message):
"""Send raw IRC message."""
if not self.socket:
raise ConnectionError("Not connected to IRC server")
try:
full_message = f"{message}\r\n"
await asyncio.get_event_loop().run_in_executor(
None, self.socket.send, full_message.encode('utf-8')
)
except Exception as e:
raise ConnectionError(f"Failed to send message: {e}")
async def send_message(self, target, message):
"""Send a message to a channel or user."""
if self.state != ConnectionState.JOINED:
self.logger.warning(f"Cannot send message, not joined to channel (state: {self.state})")
return False
try:
await self._send_raw(f"PRIVMSG {target} :{message}")
return True
except Exception as e:
self.logger.error(f"Failed to send message to {target}: {e}")
return False
async def _health_monitor(self):
"""Monitor connection health and send periodic pings."""
while self.running:
try:
await asyncio.sleep(self.health_check_interval)
if self.state == ConnectionState.JOINED:
await self._check_connection_health()
except asyncio.CancelledError:
break
except Exception as e:
self.logger.error(f"Health monitor error: {e}")
async def _check_connection_health(self):
"""Check if connection is healthy and send pings as needed."""
current_time = time.time()
# Send ping if interval has passed
if current_time - self.last_ping_time > self.ping_interval:
try:
await self._send_raw(f"PING :health_check_{int(current_time)}")
self.last_ping_time = current_time
except Exception as e:
self.logger.error(f"Failed to send ping: {e}")
raise ConnectionError("Health check ping failed")
# Check if we've received a pong recently
if current_time - self.last_pong_time > self.ping_timeout:
self.logger.warning("No PONG received within timeout period")
raise ConnectionError("Ping timeout - connection appears dead")
async def _handle_connection_error(self, error):
"""Handle connection errors and initiate reconnection."""
self.logger.error(f"Connection error: {error}")
# Notify callback
if self.on_disconnect_callback:
await self.on_disconnect_callback(error)
# Disconnect and prepare for reconnection
await self._disconnect()
# Set state for reconnection
if self.running:
self.state = ConnectionState.DISCONNECTED
self.reconnect_attempts += 1
async def _disconnect(self):
"""Disconnect from IRC server."""
if self.socket:
try:
self.socket.close()
except:
pass
self.socket = None
old_state = self.state
self.state = ConnectionState.DISCONNECTED
if old_state != ConnectionState.DISCONNECTED:
self.logger.info("Disconnected from IRC server")
def get_connection_stats(self) -> Dict[str, Any]:
"""Get connection statistics."""
uptime = None
if self.connection_start_time:
uptime = datetime.now() - self.connection_start_time
return {
"state": self.state.value,
"connected": self.state == ConnectionState.JOINED,
"uptime": str(uptime) if uptime else None,
"reconnect_attempts": self.reconnect_attempts,
"total_reconnections": self.total_reconnections,
"connection_failures": self.connection_failures,
"last_successful_connection": self.last_successful_connection,
"message_count": self.message_count,
"last_message_time": datetime.fromtimestamp(self.last_message_time) if self.last_message_time else None,
"last_ping_time": datetime.fromtimestamp(self.last_ping_time) if self.last_ping_time else None,
"last_pong_time": datetime.fromtimestamp(self.last_pong_time) if self.last_pong_time else None
}
def is_connected(self) -> bool:
"""Check if bot is connected and ready."""
return self.state == ConnectionState.JOINED
def get_state(self) -> ConnectionState:
"""Get current connection state."""
return self.state

426
src/rate_limiter.py Normal file
View file

@ -0,0 +1,426 @@
import time
import asyncio
from typing import Dict, Optional, Tuple
from datetime import datetime, timedelta
from enum import Enum
import logging
class CommandCategory(Enum):
"""Categories of commands with different rate limits."""
BASIC = "basic" # !help, !ping, !status
GAMEPLAY = "gameplay" # !explore, !catch, !battle
MANAGEMENT = "management" # !pets, !activate, !deactivate
ADMIN = "admin" # !backup, !reload, !reconnect
WEB = "web" # Web interface requests
class RateLimiter:
"""
Token bucket rate limiter with per-user tracking and command categories.
Features:
- Token bucket algorithm for smooth rate limiting
- Per-user rate tracking
- Different limits for different command categories
- Burst capacity handling
- Admin exemption
- Detailed logging and monitoring
"""
def __init__(self, config: Optional[Dict] = None):
self.logger = logging.getLogger(__name__)
# Default rate limit configuration
self.config = {
"enabled": True,
"categories": {
CommandCategory.BASIC: {
"requests_per_minute": 20,
"burst_capacity": 5,
"cooldown_seconds": 1
},
CommandCategory.GAMEPLAY: {
"requests_per_minute": 10,
"burst_capacity": 3,
"cooldown_seconds": 3
},
CommandCategory.MANAGEMENT: {
"requests_per_minute": 5,
"burst_capacity": 2,
"cooldown_seconds": 5
},
CommandCategory.ADMIN: {
"requests_per_minute": 100,
"burst_capacity": 10,
"cooldown_seconds": 0
},
CommandCategory.WEB: {
"requests_per_minute": 60,
"burst_capacity": 10,
"cooldown_seconds": 1
}
},
"admin_users": ["megasconed"], # This will be overridden by bot initialization
"global_limits": {
"max_requests_per_minute": 200,
"max_concurrent_users": 100
},
"violation_penalties": {
"warning_threshold": 3,
"temporary_ban_threshold": 10,
"temporary_ban_duration": 300 # 5 minutes
}
}
# Override with provided config
if config:
self._update_config(config)
# Rate limiting state
self.user_buckets: Dict[str, Dict] = {}
self.global_stats = {
"requests_this_minute": 0,
"minute_start": time.time(),
"active_users": set(),
"total_requests": 0,
"blocked_requests": 0
}
# Violation tracking
self.violations: Dict[str, Dict] = {}
self.banned_users: Dict[str, float] = {} # user -> ban_end_time
# Background cleanup task
self.cleanup_task = None
self.start_cleanup_task()
def _update_config(self, config: Dict):
"""Update configuration with provided values."""
def deep_update(base_dict, update_dict):
for key, value in update_dict.items():
if key in base_dict and isinstance(base_dict[key], dict) and isinstance(value, dict):
deep_update(base_dict[key], value)
else:
base_dict[key] = value
deep_update(self.config, config)
def start_cleanup_task(self):
"""Start background cleanup task."""
if self.cleanup_task is None or self.cleanup_task.done():
self.cleanup_task = asyncio.create_task(self._cleanup_loop())
async def _cleanup_loop(self):
"""Background task to clean up old data."""
while True:
try:
await asyncio.sleep(60) # Cleanup every minute
await self._cleanup_old_data()
except asyncio.CancelledError:
break
except Exception as e:
self.logger.error(f"Error in rate limiter cleanup: {e}")
async def _cleanup_old_data(self):
"""Clean up old rate limiting data."""
current_time = time.time()
# Clean up old user buckets (inactive for 10 minutes)
inactive_threshold = current_time - 600
inactive_users = [
user for user, data in self.user_buckets.items()
if data.get("last_request", 0) < inactive_threshold
]
for user in inactive_users:
del self.user_buckets[user]
# Clean up old violations (older than 1 hour)
violation_threshold = current_time - 3600
old_violations = [
user for user, data in self.violations.items()
if data.get("last_violation", 0) < violation_threshold
]
for user in old_violations:
del self.violations[user]
# Clean up expired bans
expired_bans = [
user for user, ban_end in self.banned_users.items()
if current_time > ban_end
]
for user in expired_bans:
del self.banned_users[user]
self.logger.info(f"Temporary ban expired for user: {user}")
# Reset global stats every minute
if current_time - self.global_stats["minute_start"] >= 60:
self.global_stats["requests_this_minute"] = 0
self.global_stats["minute_start"] = current_time
self.global_stats["active_users"].clear()
async def check_rate_limit(self, user: str, category: CommandCategory,
command: str = None) -> Tuple[bool, Optional[str]]:
"""
Check if a request is allowed under rate limiting.
Returns:
(allowed: bool, message: Optional[str])
"""
if not self.config["enabled"]:
return True, None
current_time = time.time()
# Check if user is temporarily banned
if user in self.banned_users:
if current_time < self.banned_users[user]:
remaining = int(self.banned_users[user] - current_time)
return False, f"⛔ You are temporarily banned for {remaining} seconds due to rate limit violations."
else:
del self.banned_users[user]
# Admin exemption
if user.lower() in [admin.lower() for admin in self.config["admin_users"]]:
return True, None
# Check global limits
if not await self._check_global_limits():
return False, "🚫 Server is currently overloaded. Please try again later."
# Check per-user rate limit
allowed, message = await self._check_user_rate_limit(user, category, current_time)
if allowed:
# Update global stats
self.global_stats["requests_this_minute"] += 1
self.global_stats["total_requests"] += 1
self.global_stats["active_users"].add(user)
else:
# Track violation
await self._track_violation(user, category, command)
self.global_stats["blocked_requests"] += 1
return allowed, message
async def _check_global_limits(self) -> bool:
"""Check global rate limits."""
# Check requests per minute
if (self.global_stats["requests_this_minute"] >=
self.config["global_limits"]["max_requests_per_minute"]):
return False
# Check concurrent users
if (len(self.global_stats["active_users"]) >=
self.config["global_limits"]["max_concurrent_users"]):
return False
return True
async def _check_user_rate_limit(self, user: str, category: CommandCategory,
current_time: float) -> Tuple[bool, Optional[str]]:
"""Check per-user rate limit using token bucket algorithm."""
category_config = self.config["categories"][category]
# Get or create user bucket
if user not in self.user_buckets:
self.user_buckets[user] = {
"tokens": category_config["burst_capacity"],
"last_refill": current_time,
"last_request": current_time
}
bucket = self.user_buckets[user]
# Calculate tokens to add (refill rate)
time_passed = current_time - bucket["last_refill"]
tokens_per_second = category_config["requests_per_minute"] / 60.0
tokens_to_add = time_passed * tokens_per_second
# Refill bucket (up to burst capacity)
bucket["tokens"] = min(
category_config["burst_capacity"],
bucket["tokens"] + tokens_to_add
)
bucket["last_refill"] = current_time
# Check if request is allowed
if bucket["tokens"] >= 1:
bucket["tokens"] -= 1
bucket["last_request"] = current_time
return True, None
else:
# Calculate cooldown time
time_since_last = current_time - bucket["last_request"]
cooldown = category_config["cooldown_seconds"]
if time_since_last < cooldown:
remaining = int(cooldown - time_since_last)
return False, f"⏱️ Rate limit exceeded. Please wait {remaining} seconds before using {category.value} commands."
else:
# Allow if cooldown has passed
bucket["tokens"] = category_config["burst_capacity"] - 1
bucket["last_request"] = current_time
return True, None
async def _track_violation(self, user: str, category: CommandCategory, command: str):
"""Track rate limit violations for potential penalties."""
current_time = time.time()
if user not in self.violations:
self.violations[user] = {
"count": 0,
"last_violation": current_time,
"categories": {}
}
violation = self.violations[user]
violation["count"] += 1
violation["last_violation"] = current_time
# Track by category
if category.value not in violation["categories"]:
violation["categories"][category.value] = 0
violation["categories"][category.value] += 1
# Check for penalties
penalty_config = self.config["violation_penalties"]
if violation["count"] >= penalty_config["temporary_ban_threshold"]:
# Temporary ban
ban_duration = penalty_config["temporary_ban_duration"]
self.banned_users[user] = current_time + ban_duration
self.logger.warning(f"User {user} temporarily banned for {ban_duration}s due to rate limit violations")
elif violation["count"] >= penalty_config["warning_threshold"]:
# Warning threshold reached
self.logger.warning(f"User {user} reached rate limit warning threshold ({violation['count']} violations)")
def get_user_stats(self, user: str) -> Dict:
"""Get rate limiting stats for a specific user."""
stats = {
"user": user,
"is_banned": user in self.banned_users,
"ban_expires": None,
"violations": 0,
"buckets": {},
"admin_exemption": user.lower() in [admin.lower() for admin in self.config["admin_users"]]
}
# Ban info
if stats["is_banned"]:
stats["ban_expires"] = datetime.fromtimestamp(self.banned_users[user])
# Violation info
if user in self.violations:
stats["violations"] = self.violations[user]["count"]
# Bucket info
if user in self.user_buckets:
bucket = self.user_buckets[user]
stats["buckets"] = {
"tokens": round(bucket["tokens"], 2),
"last_request": datetime.fromtimestamp(bucket["last_request"])
}
return stats
def get_global_stats(self) -> Dict:
"""Get global rate limiting statistics."""
return {
"enabled": self.config["enabled"],
"requests_this_minute": self.global_stats["requests_this_minute"],
"active_users": len(self.global_stats["active_users"]),
"total_requests": self.global_stats["total_requests"],
"blocked_requests": self.global_stats["blocked_requests"],
"banned_users": len(self.banned_users),
"tracked_users": len(self.user_buckets),
"total_violations": sum(v["count"] for v in self.violations.values()),
"config": self.config
}
def is_user_banned(self, user: str) -> bool:
"""Check if a user is currently banned."""
if user not in self.banned_users:
return False
if time.time() > self.banned_users[user]:
del self.banned_users[user]
return False
return True
def unban_user(self, user: str) -> bool:
"""Manually unban a user (admin function)."""
if user in self.banned_users:
del self.banned_users[user]
self.logger.info(f"User {user} manually unbanned")
return True
return False
def reset_user_violations(self, user: str) -> bool:
"""Reset violations for a user (admin function)."""
if user in self.violations:
del self.violations[user]
self.logger.info(f"Violations reset for user {user}")
return True
return False
async def shutdown(self):
"""Shutdown the rate limiter and cleanup tasks."""
if self.cleanup_task:
self.cleanup_task.cancel()
try:
await self.cleanup_task
except asyncio.CancelledError:
pass
self.logger.info("Rate limiter shutdown complete")
# Command category mapping
COMMAND_CATEGORIES = {
# Basic commands
"help": CommandCategory.BASIC,
"ping": CommandCategory.BASIC,
"status": CommandCategory.BASIC,
"uptime": CommandCategory.BASIC,
"connection_stats": CommandCategory.BASIC,
# Gameplay commands
"start": CommandCategory.GAMEPLAY,
"explore": CommandCategory.GAMEPLAY,
"catch": CommandCategory.GAMEPLAY,
"battle": CommandCategory.GAMEPLAY,
"attack": CommandCategory.GAMEPLAY,
"moves": CommandCategory.GAMEPLAY,
"flee": CommandCategory.GAMEPLAY,
"travel": CommandCategory.GAMEPLAY,
"weather": CommandCategory.GAMEPLAY,
"gym": CommandCategory.GAMEPLAY,
# Management commands
"pets": CommandCategory.MANAGEMENT,
"activate": CommandCategory.MANAGEMENT,
"deactivate": CommandCategory.MANAGEMENT,
"stats": CommandCategory.MANAGEMENT,
"inventory": CommandCategory.MANAGEMENT,
"use": CommandCategory.MANAGEMENT,
"nickname": CommandCategory.MANAGEMENT,
# Admin commands
"backup": CommandCategory.ADMIN,
"restore": CommandCategory.ADMIN,
"backups": CommandCategory.ADMIN,
"backup_stats": CommandCategory.ADMIN,
"backup_cleanup": CommandCategory.ADMIN,
"reload": CommandCategory.ADMIN,
"reconnect": CommandCategory.ADMIN,
}
def get_command_category(command: str) -> CommandCategory:
"""Get the rate limiting category for a command."""
return COMMAND_CATEGORIES.get(command.lower(), CommandCategory.GAMEPLAY)

103
start_petbot.sh Executable file
View file

@ -0,0 +1,103 @@
#!/bin/bash
#
# PetBot Startup Script
# Complete one-command startup for PetBot with all dependencies
#
# Usage: ./start_petbot.sh
#
set -e # Exit on any error
echo "🐾 Starting PetBot..."
echo "===================="
# Get script directory (works even if called from elsewhere)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
echo "📁 Working directory: $SCRIPT_DIR"
# Check if virtual environment exists
if [ ! -d "venv" ]; then
echo "❌ Virtual environment not found!"
echo "🔧 Creating virtual environment..."
python3 -m venv venv
echo "✅ Virtual environment created"
fi
# Activate virtual environment
echo "🔄 Activating virtual environment..."
source venv/bin/activate
# Check if requirements are installed
echo "🔄 Checking dependencies..."
if ! python -c "import aiosqlite, irc, dotenv" 2>/dev/null; then
echo "📦 Installing/updating dependencies..."
# Create requirements.txt if it doesn't exist
if [ ! -f "requirements.txt" ]; then
echo "📝 Creating requirements.txt..."
cat > requirements.txt << EOF
aiosqlite>=0.17.0
irc>=20.3.0
python-dotenv>=0.19.0
aiohttp>=3.8.0
EOF
fi
pip install --upgrade pip
pip install -r requirements.txt
echo "✅ Dependencies installed"
else
echo "✅ Dependencies already satisfied"
fi
# Verify core modules can be imported
echo "🔄 Verifying core modules..."
python3 -c "
import sys
sys.path.append('.')
try:
from src.database import Database
from src.game_engine import GameEngine
from src.rate_limiter import RateLimiter
from src.irc_connection_manager import IRCConnectionManager
from config import ADMIN_USER, IRC_CONFIG, RATE_LIMIT_CONFIG
print('✅ Core modules verified')
print(f' Admin user: {ADMIN_USER}')
except ImportError as e:
print(f'❌ Module import error: {e}')
sys.exit(1)
"
# Create data directory if it doesn't exist
if [ ! -d "data" ]; then
echo "📁 Creating data directory..."
mkdir -p data
fi
# Create backups directory if it doesn't exist
if [ ! -d "backups" ]; then
echo "📁 Creating backups directory..."
mkdir -p backups
fi
# Check if database exists, if not mention first-time setup
if [ ! -f "data/petbot.db" ]; then
echo " First-time setup detected - database will be created automatically"
fi
# Display startup information
echo ""
echo "🚀 Launching PetBot with Auto-Reconnect..."
echo "🌐 Web interface will be available at: http://localhost:8080"
echo "💬 IRC: Connecting to irc.libera.chat #petz"
echo "📊 Features: Rate limiting, auto-reconnect, web interface, team builder"
echo ""
echo "Press Ctrl+C to stop the bot"
echo "===================="
echo ""
# Launch the bot
exec python run_bot_with_reconnect.py

View file

@ -16,6 +16,7 @@ import time
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.database import Database
from src.rate_limiter import RateLimiter, CommandCategory
class PetBotRequestHandler(BaseHTTPRequestHandler):
"""HTTP request handler for PetBot web server"""
@ -30,6 +31,96 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
"""Get bot instance from server"""
return getattr(self.server, 'bot', None)
@property
def rate_limiter(self):
"""Get rate limiter from bot instance"""
bot = self.bot
return getattr(bot, 'rate_limiter', None) if bot else None
def get_client_ip(self):
"""Get client IP address for rate limiting"""
# Check for X-Forwarded-For header (in case of proxy)
forwarded_for = self.headers.get('X-Forwarded-For')
if forwarded_for:
return forwarded_for.split(',')[0].strip()
# Check for X-Real-IP header
real_ip = self.headers.get('X-Real-IP')
if real_ip:
return real_ip.strip()
# Fallback to client address
return self.client_address[0]
def check_rate_limit(self):
"""Check rate limit for web requests"""
if not self.rate_limiter:
return True, None
client_ip = self.get_client_ip()
# Use IP address as user identifier for web requests
user_identifier = f"web:{client_ip}"
# Run async rate limit check
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
allowed, message = loop.run_until_complete(
self.rate_limiter.check_rate_limit(user_identifier, CommandCategory.WEB)
)
return allowed, message
finally:
loop.close()
def send_rate_limit_error(self, message):
"""Send rate limit error response"""
self.send_response(429)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.send_header('Retry-After', '60')
self.end_headers()
content = f"""
<!DOCTYPE html>
<html>
<head>
<title>Rate Limit Exceeded - PetBot</title>
<style>
body {{
font-family: Arial, sans-serif;
max-width: 600px;
margin: 100px auto;
text-align: center;
background: #0f0f23;
color: #cccccc;
}}
.error-container {{
background: #2a2a4a;
padding: 30px;
border-radius: 10px;
border: 1px solid #444466;
}}
h1 {{ color: #ff6b6b; }}
.message {{
margin: 20px 0;
font-size: 1.1em;
}}
.retry {{
color: #66ff66;
margin-top: 20px;
}}
</style>
</head>
<body>
<div class="error-container">
<h1> Rate Limit Exceeded</h1>
<div class="message">{message}</div>
<div class="retry">Please wait before making more requests.</div>
</div>
</body>
</html>
"""
self.wfile.write(content.encode())
def send_json_response(self, data, status_code=200):
"""Send a JSON response"""
import json
@ -549,7 +640,13 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
</html>"""
def do_GET(self):
"""Handle GET requests"""
"""Handle GET requests with rate limiting"""
# Check rate limit first
allowed, rate_limit_message = self.check_rate_limit()
if not allowed:
self.send_rate_limit_error(rate_limit_message)
return
parsed_path = urlparse(self.path)
path = parsed_path.path
@ -576,7 +673,13 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
self.send_error(404, "Page not found")
def do_POST(self):
"""Handle POST requests"""
"""Handle POST requests with rate limiting"""
# Check rate limit first (POST requests have stricter limits)
allowed, rate_limit_message = self.check_rate_limit()
if not allowed:
self.send_json_response({"success": False, "error": rate_limit_message}, 429)
return
parsed_path = urlparse(self.path)
path = parsed_path.path