diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fdef372 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual Environment +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +*.log +logs/ +duckhunt.log* + +# Database and Runtime Data +duckhunt.json +duckhunt.db +*.db +*.sqlite +*.sqlite3 + +# Configuration (sensitive) +config.json +config_local.json +config_backup.json + +# Backups +backup/ +*.backup +*.bak + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp +.cache/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e2c7ca --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# DuckHunt IRC Bot + +A competitive IRC game bot implementing the classic DuckHunt mechanics with modern features. Players compete to shoot ducks that spawn in channels, managing ammunition, accuracy, and collecting items in a persistent progression system. + +## Features + +### Core Gameplay +- **Duck Spawning**: Ducks appear randomly in configured channels with ASCII art +- **Shooting Mechanics**: Players use `!bang` to shoot ducks with limited ammunition +- **Accuracy System**: Hit chances based on player skill that improves with successful shots +- **Gun Jamming**: Weapons can jam and require reloading based on reliability stats +- **Wild Shots**: Shooting without targets results in gun confiscation + +### Progression System +- **Experience Points**: Earned from successful duck kills and befriending +- **Level System**: 40 levels with titles and increasing XP requirements +- **Statistics Tracking**: Comprehensive stats including accuracy, best times, and shot counts +- **Leaderboards**: Top player rankings and personal statistics + +### Item System +- **Shop**: 8 different items available for purchase with earned money +- **Inventory**: Persistent item storage with quantity tracking +- **Item Effects**: Consumable and permanent items affecting gameplay +- **Competitive Drops**: Items drop to the ground for any player to grab with `!snatch` + +### Gun Mechanics +- **Ammunition Management**: Limited shots per magazine with reloading required +- **Charger System**: Multiple magazines with reload mechanics +- **Gun Confiscation**: Administrative punishment system for wild shooting +- **Reliability**: Weapon condition affecting jam probability + +## Installation + +### Requirements +- Python 3.7 or higher +- asyncio support +- SSL/TLS support for secure IRC connections + +### Setup +1. Clone the repository +2. Install Python dependencies (none required beyond standard library) +3. Copy and configure `config.json` +4. Run the bot + +```bash +python3 duckhunt.py +``` + +## Configuration + +The bot uses `config.json` for all configuration. Key sections include: + +### IRC Connection +```json +{ + "server": "irc.example.net", + "port": 6697, + "nick": "DuckHunt", + "channels": ["#games"], + "ssl": true +} +``` + +### SASL Authentication +```json +{ + "sasl": { + "enabled": true, + "username": "bot_username", + "password": "bot_password" + } +} +``` + +### Game Settings +- Duck spawn intervals and timing +- Sleep hours when ducks don't spawn +- Duck type probabilities and rewards +- Shop item prices and effects + +## Commands + +### Player Commands +- `!bang` - Shoot at a duck +- `!reload` - Reload your weapon +- `!bef` / `!befriend` - Attempt to befriend a duck instead of shooting +- `!shop` - View available items for purchase +- `!duckstats` - View your personal statistics +- `!topduck` - View the leaderboard +- `!snatch` - Grab items dropped by other players +- `!use [target]` - Use an item from inventory +- `!sell ` - Sell an item for half price + +### Admin Commands +- `!rearm [player]` - Restore confiscated guns +- `!disarm ` - Confiscate a player's gun +- `!ducklaunch` - Force spawn a duck +- `!reset [confirm]` - Reset player statistics + +## Architecture + +### Modular Design +- `duckhuntbot.py` - Main bot class and IRC handling +- `game.py` - Duck spawning and game mechanics +- `db.py` - Player data persistence and management +- `utils.py` - Input validation and IRC message parsing +- `sasl.py` - SASL authentication implementation +- `logging_utils.py` - Enhanced logging with rotation + +### Database +Player data is stored in JSON format with automatic backups. The system handles: +- Player statistics and progression +- Inventory and item management +- Configuration and preferences +- Historical data and records + +### Concurrency +Built on Python's asyncio framework for handling: +- IRC message processing +- Duck spawning timers +- Background cleanup tasks +- Multiple simultaneous players + +## Duck Types + +- **Normal Duck**: Standard rewards and difficulty +- **Fast Duck**: Higher XP but harder to hit +- **Rare Duck**: Bonus rewards and special drops +- **Boss Duck**: Challenging encounters with significant rewards + +## Item Types + +1. **Extra Shots** - Temporary ammunition boost +2. **Faster Reload** - Reduced reload time +3. **Accuracy Charm** - Permanent accuracy improvement +4. **Lucky Charm** - Increased rare duck encounters +5. **Friendship Bracelet** - Better befriending success rates +6. **Duck Caller** - Faster duck spawning +7. **Camouflage** - Temporary stealth mode +8. **Energy Drink** - Energy restoration + +## Development + +The codebase follows clean architecture principles with: +- Separation of concerns between IRC, game logic, and data persistence +- Comprehensive error handling and logging +- Input validation and sanitization +- Graceful shutdown handling +- Signal-based process management + +### Adding Features +New features can be added by: +1. Extending the command processing in `duckhuntbot.py` +2. Adding game mechanics to `game.py` +3. Updating data structures in `db.py` +4. Configuring behavior in `config.json` \ No newline at end of file diff --git a/duckhunt.py b/duckhunt.py index aa8414e..5340e9b 100644 --- a/duckhunt.py +++ b/duckhunt.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """ DuckHunt IRC Bot - Main Entry Point """ @@ -8,7 +7,6 @@ import json import sys import os -# Add src directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) from src.duckhuntbot import DuckHuntBot @@ -17,15 +15,12 @@ from src.duckhuntbot import DuckHuntBot def main(): """Main entry point for DuckHunt Bot""" try: - # Load configuration with open('config.json') as f: config = json.load(f) - # Create and run bot bot = DuckHuntBot(config) bot.logger.info("πŸ¦† Starting DuckHunt Bot...") - # Run the bot asyncio.run(bot.run()) except KeyboardInterrupt: diff --git a/src/__pycache__/db.cpython-312.pyc b/src/__pycache__/db.cpython-312.pyc index 37252ad..68ae343 100644 Binary files a/src/__pycache__/db.cpython-312.pyc and b/src/__pycache__/db.cpython-312.pyc differ diff --git a/src/__pycache__/duckhuntbot.cpython-312.pyc b/src/__pycache__/duckhuntbot.cpython-312.pyc index 506a3a3..0ed963a 100644 Binary files a/src/__pycache__/duckhuntbot.cpython-312.pyc and b/src/__pycache__/duckhuntbot.cpython-312.pyc differ diff --git a/src/__pycache__/game.cpython-312.pyc b/src/__pycache__/game.cpython-312.pyc index 130d269..0ab22ef 100644 Binary files a/src/__pycache__/game.cpython-312.pyc and b/src/__pycache__/game.cpython-312.pyc differ diff --git a/src/__pycache__/logging_utils.cpython-312.pyc b/src/__pycache__/logging_utils.cpython-312.pyc index 3488a6c..1ea0c6c 100644 Binary files a/src/__pycache__/logging_utils.cpython-312.pyc and b/src/__pycache__/logging_utils.cpython-312.pyc differ diff --git a/src/__pycache__/sasl.cpython-312.pyc b/src/__pycache__/sasl.cpython-312.pyc index 18a03e7..9736ca2 100644 Binary files a/src/__pycache__/sasl.cpython-312.pyc and b/src/__pycache__/sasl.cpython-312.pyc differ diff --git a/src/__pycache__/utils.cpython-312.pyc b/src/__pycache__/utils.cpython-312.pyc index 6cb9614..372fe94 100644 Binary files a/src/__pycache__/utils.cpython-312.pyc and b/src/__pycache__/utils.cpython-312.pyc differ diff --git a/src/db.py b/src/db.py index 36fd3af..a8d6379 100644 --- a/src/db.py +++ b/src/db.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """ Database functionality for DuckHunt Bot """ @@ -21,7 +20,6 @@ class DuckDB: def get_config(self, path, default=None): """Helper method to get config values (needs to be set by bot)""" - # This will be set by the main bot class if hasattr(self, '_config_getter'): return self._config_getter(path, default) return default @@ -48,7 +46,6 @@ class DuckDB: def save_database(self): """Save all player data to JSON file with error handling""" try: - # Atomic write to prevent corruption temp_file = f"{self.db_file}.tmp" data = { 'players': self.players, @@ -57,7 +54,6 @@ class DuckDB: with open(temp_file, 'w') as f: json.dump(data, f, indent=2) - # Atomic rename to replace old file os.replace(temp_file, self.db_file) except IOError as e: @@ -92,7 +88,6 @@ class DuckDB: if nick in self.players: player = self.players[nick] - # Ensure backward compatibility by adding missing fields self._ensure_player_fields(player) return player else: @@ -118,7 +113,6 @@ class DuckDB: 'level': 1, 'inventory': {}, 'ignored_users': [], - # Gun mechanics (eggdrop style) 'jammed': False, 'jammed_count': 0, 'total_ammo_used': 0, @@ -137,24 +131,23 @@ class DuckDB: def _ensure_player_fields(self, player): """Ensure player has all required fields for backward compatibility""" required_fields = { - 'shots': player.get('ammo', 6), # Map old 'ammo' to 'shots' - 'max_shots': player.get('max_ammo', 6), # Map old 'max_ammo' to 'max_shots' - 'chargers': player.get('chargers', 2), # Map old 'chargers' (magazines) - 'max_chargers': player.get('max_chargers', 2), # Map old 'max_chargers' + 'shots': player.get('ammo', 6), + 'max_shots': player.get('max_ammo', 6), + 'chargers': player.get('chargers', 2), + 'max_chargers': player.get('max_chargers', 2), 'reload_time': 5.0, - 'ducks_shot': player.get('caught', 0), # Map old 'caught' to 'ducks_shot' - 'ducks_befriended': player.get('befriended', 0), # Use existing befriended count + 'ducks_shot': player.get('caught', 0), + 'ducks_befriended': player.get('befriended', 0), 'accuracy_bonus': 0, 'xp_bonus': 0, 'charm_bonus': 0, - 'exp': player.get('xp', 0), # Map old 'xp' to 'exp' - 'money': player.get('coins', 100), # Map old 'coins' to 'money' + 'exp': player.get('xp', 0), + 'money': player.get('coins', 100), 'last_hunt': 0, 'last_reload': 0, 'level': 1, 'inventory': {}, 'ignored_users': [], - # Gun mechanics (eggdrop style) 'jammed': False, 'jammed_count': player.get('jammed_count', 0), 'total_ammo_used': player.get('total_ammo_used', 0), @@ -174,12 +167,11 @@ class DuckDB: """Save player data - batch saves for performance""" if not self._save_pending: self._save_pending = True - # Schedule delayed save to batch multiple writes asyncio.create_task(self._delayed_save()) async def _delayed_save(self): """Batch save to reduce disk I/O""" - await asyncio.sleep(0.5) # Small delay to batch saves + await asyncio.sleep(0.5) try: self.save_database() self.logger.debug("Database batch save completed") diff --git a/src/duckhuntbot.py b/src/duckhuntbot.py index 18194a3..e2c139c 100644 --- a/src/duckhuntbot.py +++ b/src/duckhuntbot.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """ Main DuckHunt IRC Bot """ @@ -33,26 +32,20 @@ class DuckHuntBot: self.channels_joined = set() self.shutdown_requested = False - # Initialize subsystems self.db = DuckDB() self.db.set_config_getter(self.get_config) self.game = DuckGame(self, self.db) - # Initialize SASL handler self.sasl_handler = SASLHandler(self, config) - # Admin configuration self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])] self.ignored_nicks = set() - # Duck spawn timing - self.duck_spawn_times = {} # Per-channel last spawn times - self.channel_records = {} # Per-channel shooting records + self.duck_spawn_times = {} + self.channel_records = {} - # Dropped items tracking - competitive snatching system - self.dropped_items = {} # Per-channel dropped items: {channel: [{'item': item_name, 'timestamp': time, 'dropper': nick}]} + self.dropped_items = {} - # Colors for IRC messages self.colors = { 'red': '\x0304', 'green': '\x0303', @@ -80,12 +73,10 @@ class DuckHuntBot: async def connect(self): """Connect to IRC server""" try: - # Setup SSL context if needed ssl_context = None if self.config.get('ssl', False): ssl_context = ssl.create_default_context() - # Connect to server self.reader, self.writer = await asyncio.open_connection( self.config['server'], self.config['port'], @@ -94,11 +85,9 @@ class DuckHuntBot: self.logger.info(f"Connected to {self.config['server']}:{self.config['port']}") - # Send server password if provided if self.config.get('password'): self.send_raw(f"PASS {self.config['password']}") - # Register with server await self.register_user() except Exception as e: @@ -116,7 +105,6 @@ class DuckHuntBot: if self.writer and not self.writer.is_closing(): try: self.writer.write(f'{msg}\r\n'.encode()) - # No await for drain() - let TCP handle buffering for speed except Exception as e: self.logger.error(f"Error sending message: {e}") @@ -141,27 +129,23 @@ class DuckHuntBot: async def send_user_message(self, nick, channel, message, message_type='default'): """Send message to user respecting their output mode preferences""" - # Get player to check preferences player = self.db.get_player(f"{nick}!user@host") if not player: - # Default to public if no player data self.send_message(channel, f"{nick} > {message}") return - # Check message output configuration force_public_types = self.get_config('message_output.force_public', {}) or {} if force_public_types.get(message_type, False): self.send_message(channel, f"{nick} > {message}") return - # Check user preference output_mode = player.get('settings', {}).get('output_mode', 'PUBLIC') if output_mode == 'NOTICE': self.send_raw(f'NOTICE {nick} :{message}') elif output_mode == 'PRIVMSG': self.send_message(nick, message) - else: # PUBLIC or default + else: self.send_message(channel, f"{nick} > {message}") async def auto_rearm_confiscated_guns(self, channel, shooter_nick): @@ -169,7 +153,6 @@ class DuckHuntBot: if not self.get_config('weapons.auto_rearm_on_duck_shot', True): return - # Find players with confiscated guns rearmed_players = [] for nick, player in self.db.players.items(): if player.get('gun_confiscated', False): @@ -190,7 +173,6 @@ class DuckHuntBot: def signal_handler(signum, frame): self.logger.info(f"Received signal {signum}, shutting down...") self.shutdown_requested = True - # Cancel any pending tasks for task in asyncio.all_tasks(): if not task.done(): task.cancel() @@ -203,11 +185,10 @@ class DuckHuntBot: async def handle_message(self, prefix, command, params, trailing): """Handle incoming IRC messages""" try: - if command == '001': # Welcome message + if command == '001': self.registered = True self.logger.info("Successfully registered with IRC server") - # Join channels for channel in self.config['channels']: self.send_raw(f'JOIN {channel}') @@ -222,14 +203,12 @@ class DuckHuntBot: target = params[0] message = trailing - # Handle commands if message.startswith('!') or target == self.config['nick']: await self.handle_command(prefix, target, message) elif command == 'PING': self.send_raw(f'PONG :{trailing}') - # Handle SASL messages elif command == 'CAP': await self.sasl_handler.handle_cap_response(params, trailing) elif command == 'AUTHENTICATE': @@ -249,24 +228,19 @@ class DuckHuntBot: nick = user.split('!')[0] nick_lower = nick.lower() - # Input validation if not InputValidator.validate_nickname(nick): return - # Check if user is ignored if nick_lower in self.ignored_nicks: return - # Sanitize message message = InputValidator.sanitize_message(message) if not message: return - # Determine response target is_private = channel == self.config['nick'] response_target = nick if is_private else channel - # Remove ! prefix for public commands if message.startswith('!'): cmd_parts = message[1:].split() else: @@ -278,12 +252,10 @@ class DuckHuntBot: cmd = cmd_parts[0].lower() args = cmd_parts[1:] if len(cmd_parts) > 1 else [] - # Get player data player = self.db.get_player(user) if not player: return - # Handle commands await self.process_command(nick, response_target, cmd, args, player, user) except Exception as e: @@ -291,7 +263,6 @@ class DuckHuntBot: async def process_command(self, nick, target, cmd, args, player, user): """Process individual commands""" - # Game commands if cmd == 'bang': await self.handle_bang(nick, target, player) elif cmd == 'reload': @@ -329,7 +300,6 @@ class DuckHuntBot: await self.handle_topduck(nick, target) elif cmd == 'snatch': await self.handle_snatch(nick, target, player) - # Admin commands elif cmd == 'rearm' and self.is_admin(user): target_nick = args[0] if args else None await self.handle_rearm(nick, target, player, target_nick) @@ -346,36 +316,29 @@ class DuckHuntBot: else: await self.send_user_message(nick, target, "Usage: !reset [confirm]") else: - # Unknown command pass async def handle_bang(self, nick, channel, player): """Handle !bang command - shoot at duck (eggdrop style)""" - # Check if gun is confiscated if player.get('gun_confiscated', False): await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot shoot.") return - # Check if gun is jammed if player.get('jammed', False): message = f"{nick} > Gun jammed! Use !reload" await self.send_user_message(nick, channel, message) return - # Check if player has ammunition if player['shots'] <= 0: message = f"{nick} > *click* You're out of ammo! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" await self.send_user_message(nick, channel, message) return - # Check if channel has ducks if channel not in self.game.ducks or not self.game.ducks[channel]: - # Fire shot anyway (wild shot into air) player['shots'] -= 1 player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1 player['wild_shots'] = player.get('wild_shots', 0) + 1 - # Check for gun jam after shooting if self.game.gun_jams(player): player['jammed'] = True player['jammed_count'] = player.get('jammed_count', 0) + 1 @@ -383,7 +346,6 @@ class DuckHuntBot: else: message = f"{nick} > *BANG* You shot at nothing! What were you aiming at? | 0 xp | {self.colors['red']}GUN CONFISCATED{self.colors['reset']}" - # Confiscate gun for wild shooting player['gun_confiscated'] = True player['confiscated_count'] = player.get('confiscated_count', 0) + 1 @@ -391,43 +353,35 @@ class DuckHuntBot: self.db.save_database() return - # Get first duck in channel duck = self.game.ducks[channel][0] player['shots'] -= 1 player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1 player['shot_at'] = player.get('shot_at', 0) + 1 - # Check for gun jam first (before checking hit) if self.game.gun_jams(player): player['jammed'] = True player['jammed_count'] = player.get('jammed_count', 0) + 1 message = f"{nick} > *BANG* *click* Gun jammed while shooting! | Ammo: {player['shots']}/{player['max_shots']}" self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}") else: - # Check if duck was hit (based on accuracy) hit_chance = min(0.7 + (player.get('accuracy', 0) * 0.001), 0.95) if random.random() < hit_chance: await self.handle_duck_hit(nick, channel, player, duck) else: await self.handle_duck_miss(nick, channel, player) - # Save player data self.db.save_database() async def handle_duck_hit(self, nick, channel, player, duck): """Handle successful duck hit (eggdrop style)""" - # Remove duck from channel self.game.ducks[channel].remove(duck) - # Calculate reaction time if available shot_time = time.time() reaction_time = shot_time - duck.get('spawn_time', shot_time) - # Award points and XP based on duck type points_earned = duck['points'] xp_earned = duck['xp'] - # Bonus for quick shots if reaction_time < 2.0: quick_bonus = int(points_earned * 0.5) points_earned += quick_bonus @@ -435,55 +389,43 @@ class DuckHuntBot: else: quick_shot_msg = "" - # Apply XP bonus xp_earned = int(xp_earned * (1 + player.get('xp_bonus', 0) * 0.001)) - # Update player stats player['ducks_shot'] += 1 player['exp'] += xp_earned player['money'] += points_earned player['last_hunt'] = time.time() - # Update accuracy (reward hits) current_accuracy = player.get('accuracy', 65) player['accuracy'] = min(current_accuracy + 1, 95) - # Track best time if 'best_time' not in player or reaction_time < player['best_time']: player['best_time'] = reaction_time - # Store reflex data player['total_reflex_time'] = player.get('total_reflex_time', 0) + reaction_time player['reflex_shots'] = player.get('reflex_shots', 0) + 1 - # Level up check await self.check_level_up(nick, channel, player) - # Eggdrop style hit message - exact format match message = f"{nick} > *BANG* you shot down the duck in {reaction_time:.2f} seconds. \\_X< *KWAK* [+{xp_earned} xp] [TOTAL DUCKS: {player['ducks_shot']}]" self.send_message(channel, f"{self.colors['green']}{message}{self.colors['reset']}") - # Award items occasionally - drop to ground for snatching - if random.random() < 0.1: # 10% chance + if random.random() < 0.1: await self.drop_random_item(nick, channel) async def handle_duck_miss(self, nick, channel, player): """Handle duck miss (eggdrop style)""" - # Reduce accuracy (penalize misses) current_accuracy = player.get('accuracy', 65) player['accuracy'] = max(current_accuracy - 2, 10) - # Track misses player['missed'] = player.get('missed', 0) + 1 - # Eggdrop style miss message message = f"{nick} > *BANG* You missed the duck! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}") - # Chance to scare other ducks with the noise if channel in self.game.ducks and len(self.game.ducks[channel]) > 1: for other_duck in self.game.ducks[channel][:]: - if random.random() < 0.2: # 20% chance each duck gets scared + if random.random() < 0.2: self.game.ducks[channel].remove(other_duck) self.send_message(channel, f"-.,ΒΈΒΈ.-Β·Β°'`'°·-.,ΒΈΒΈ.-Β·Β°'`'°· \\_o> The other ducks fly away, scared by the noise!") @@ -491,14 +433,11 @@ class DuckHuntBot: """Handle reload command (eggdrop style) - reload ammo and clear jams""" current_time = time.time() - # Check if gun is confiscated if player.get('gun_confiscated', False): await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot reload.") return - # Check if gun is jammed if player.get('jammed', False): - # Clear the jam player['jammed'] = False player['last_reload'] = current_time @@ -507,29 +446,25 @@ class DuckHuntBot: self.db.save_database() return - # Check if already full if player['shots'] >= player['max_shots']: message = f"{nick} > Gun is already fully loaded! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" await self.send_user_message(nick, channel, message) return - # Check if have chargers to reload with if player.get('chargers', 0) <= 0: message = f"{nick} > No chargers left to reload with! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: 0/{player.get('max_chargers', 2)}" await self.send_user_message(nick, channel, message) return - # Check reload time cooldown if current_time - player.get('last_reload', 0) < player['reload_time']: remaining = int(player['reload_time'] - (current_time - player.get('last_reload', 0))) message = f"{nick} > Reload cooldown: {remaining} seconds remaining | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" await self.send_user_message(nick, channel, message) return - # Perform reload old_shots = player['shots'] player['shots'] = player['max_shots'] - player['chargers'] = max(0, player.get('chargers', 2) - 1) # Use one charger + player['chargers'] = max(0, player.get('chargers', 2) - 1) player['last_reload'] = current_time shots_added = player['shots'] - old_shots @@ -537,27 +472,21 @@ class DuckHuntBot: self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") - # Save player data self.db.save_database() async def handle_befriend(self, nick, channel, player): """Handle !bef command - befriend a duck""" - # Check if channel has ducks if channel not in self.game.ducks or not self.game.ducks[channel]: await self.send_user_message(nick, channel, "There are no ducks to befriend!") return - # Get first duck duck = self.game.ducks[channel][0] - # Befriend success rate (starts at 50%, can be improved with items) befriend_chance = 0.5 + (player.get('charm_bonus', 0) * 0.001) if random.random() < befriend_chance: - # Remove duck from channel self.game.ducks[channel].remove(duck) - # Award XP and friendship bonus xp_earned = duck['xp'] friendship_bonus = duck['points'] // 2 @@ -565,17 +494,15 @@ class DuckHuntBot: player['money'] += friendship_bonus player['ducks_befriended'] += 1 - # Level up check await self.check_level_up(nick, channel, player) - # Random positive effect effects = [ ("luck", 10, "You feel lucky!"), ("charm_bonus", 5, "The duck teaches you about friendship!"), ("accuracy_bonus", 3, "The duck gives you aiming tips!") ] - if random.random() < 0.3: # 30% chance for bonus effect + if random.random() < 0.3: effect, amount, message = random.choice(effects) player[effect] = player.get(effect, 0) + amount bonus_msg = f" {message}" @@ -586,11 +513,9 @@ class DuckHuntBot: f"+{friendship_bonus} coins, +{xp_earned} XP.{bonus_msg}") self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}") - # Award items occasionally - if random.random() < 0.15: # 15% chance (higher than shooting) + if random.random() < 0.15: await self.award_random_item(nick, channel, player) else: - # Befriend failed miss_messages = [ f"The {duck['type']} duck doesn't trust you yet!", f"The {duck['type']} duck flies away from you!", @@ -601,10 +526,8 @@ class DuckHuntBot: message = f"{nick} {random.choice(miss_messages)}" self.send_message(channel, f"{self.colors['yellow']}{message}{self.colors['reset']}") - # Small penalty to charm player['charm_bonus'] = max(player.get('charm_bonus', 0) - 1, -50) - # Save player data self.db.save_database() async def handle_shop(self, nick, channel, player): @@ -634,7 +557,6 @@ class DuckHuntBot: await self.send_user_message(nick, channel, "Invalid item ID!") return - # Check if player has the item if 'inventory' not in player: player['inventory'] = {} @@ -643,7 +565,6 @@ class DuckHuntBot: await self.send_user_message(nick, channel, "You don't have that item!") return - # Get item info from built-in shop items shop_items = { 1: {'name': 'Extra Shots', 'price': 50}, 2: {'name': 'Faster Reload', 'price': 100}, @@ -659,7 +580,6 @@ class DuckHuntBot: await self.send_user_message(nick, channel, "Invalid item!") return - # Remove item and add money (50% of original price) player['inventory'][item_key] -= 1 if player['inventory'][item_key] <= 0: del player['inventory'][item_key] @@ -670,7 +590,6 @@ class DuckHuntBot: message = f"Sold {item_info['name']} for ${sell_price}!" await self.send_user_message(nick, channel, message) - # Save player data self.db.save_database() async def handle_use(self, nick, channel, item_id, player, target_nick=None): @@ -681,11 +600,9 @@ class DuckHuntBot: await self.send_user_message(nick, channel, "Invalid item ID!") return - # Check if it's a shop purchase or inventory use if 'inventory' not in player: player['inventory'] = {} - # Get item info from built-in shop items shop_items = { 1: {'name': 'Extra Shots', 'price': 50, 'consumable': True}, 2: {'name': 'Faster Reload', 'price': 100, 'consumable': True}, @@ -704,22 +621,17 @@ class DuckHuntBot: await self.send_user_message(nick, channel, "Invalid item ID!") return - # Check if player owns the item if item_key in player['inventory'] and player['inventory'][item_key] > 0: - # Use owned item await self.use_item_effect(player, item_id, nick, channel, target_nick) player['inventory'][item_key] -= 1 if player['inventory'][item_key] <= 0: del player['inventory'][item_key] else: - # Try to buy item if player['money'] >= item_info['price']: if item_info.get('consumable', True): - # Buy and immediately use consumable item player['money'] -= item_info['price'] await self.use_item_effect(player, item_id, nick, channel, target_nick) else: - # Buy permanent item and add to inventory player['money'] -= item_info['price'] player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1 await self.send_user_message(nick, channel, f"Purchased {item_info['name']}!") @@ -728,12 +640,10 @@ class DuckHuntBot: f"Not enough money! Need ${item_info['price']}, you have ${player['money']}") return - # Save player data self.db.save_database() async def handle_topduck(self, nick, channel): """Handle topduck command - show leaderboard""" - # Sort players by ducks shot sorted_players = sorted( [(name, data) for name, data in self.db.players.items()], key=lambda x: x[1]['ducks_shot'], @@ -744,7 +654,6 @@ class DuckHuntBot: await self.send_user_message(nick, channel, "No players found!") return - # Show top 5 await self.send_user_message(nick, channel, "=== TOP DUCK HUNTERS ===") for i, (name, data) in enumerate(sorted_players[:5], 1): stats = f"{i}. {name}: {data['ducks_shot']} ducks (Level {data['level']})" @@ -754,59 +663,49 @@ class DuckHuntBot: """Handle snatch command - grab dropped items competitively""" import time - # Check if there are any dropped items in this channel if channel not in self.dropped_items or not self.dropped_items[channel]: await self.send_user_message(nick, channel, f"{nick} > There are no items to snatch!") return - # Get the oldest dropped item (first come, first served) item = self.dropped_items[channel].pop(0) - # Check if item has expired (60 seconds timeout) current_time = time.time() if current_time - item['timestamp'] > 60: await self.send_user_message(nick, channel, f"{nick} > The item has disappeared!") - # Clean up any other expired items while we're at it self.dropped_items[channel] = [ i for i in self.dropped_items[channel] if current_time - i['timestamp'] <= 60 ] return - # Initialize player inventory if needed if 'inventory' not in player: player['inventory'] = {} - # Add item to player's inventory item_key = item['item_id'] player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1 - # Success message - eggdrop style message = f"{nick} snatched a {item['item_name']}! ⚑" self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") async def handle_rearm(self, nick, channel, player, target_nick=None): """Handle rearm command - restore confiscated guns""" if target_nick: - # Rearm another player (admin only) target_player = self.db.get_player(target_nick.lower()) if target_player: target_player['gun_confiscated'] = False target_player['shots'] = target_player['max_shots'] target_player['chargers'] = target_player.get('max_chargers', 2) target_player['jammed'] = False - target_player['last_reload'] = 0 # Reset reload timer + target_player['last_reload'] = 0 message = f"{nick} returned {target_nick}'s confiscated gun! | Ammo: {target_player['shots']}/{target_player['max_shots']} | Chargers: {target_player['chargers']}/{target_player.get('max_chargers', 2)}" self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") else: await self.send_user_message(nick, channel, "Player not found!") else: - # Check if gun is confiscated if not player.get('gun_confiscated', False): await self.send_user_message(nick, channel, f"{nick} > Your gun is not confiscated!") return - # Rearm self (automatic gun return system or admin) if self.is_admin(nick): player['gun_confiscated'] = False player['shots'] = player['max_shots'] @@ -819,8 +718,6 @@ class DuckHuntBot: await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! Wait for an admin or automatic return.") self.db.save_database() - # Save database - self.db.save_database() async def handle_disarm(self, nick, channel, target_nick): """Handle disarm command (admin only)""" @@ -830,7 +727,6 @@ class DuckHuntBot: message = f"Admin {nick} disarmed {target_nick}!" self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}") - # Save database self.db.save_database() else: await self.send_user_message(nick, channel, "Player not found!") @@ -856,9 +752,7 @@ class DuckHuntBot: ) await self.send_user_message(nick, channel, stats_msg) - # Show inventory if player has items if 'inventory' in player and player['inventory']: - # Get item names shop_items = { 1: 'Extra Shots', 2: 'Faster Reload', 3: 'Accuracy Charm', 4: 'Lucky Charm', 5: 'Friendship Bracelet', 6: 'Duck Caller', 7: 'Camouflage', 8: 'Energy Drink', @@ -950,9 +844,8 @@ class DuckHuntBot: if new_level > current_level: player['level'] = new_level - # Award level-up bonuses - player['max_shots'] = min(player['max_shots'] + 1, 10) # Max 10 shots - player['reload_time'] = max(player['reload_time'] - 0.5, 2.0) # Min 2 seconds + player['max_shots'] = min(player['max_shots'] + 1, 10) + player['reload_time'] = max(player['reload_time'] - 0.5, 2.0) message = (f"πŸŽ‰ {nick} leveled up to level {new_level}! " f"Max shots: {player['max_shots']}, " @@ -961,7 +854,6 @@ class DuckHuntBot: def calculate_level(self, exp): """Calculate level from experience points""" - # Exponential level curve: level = floor(sqrt(exp / 100)) import math return int(math.sqrt(exp / 100)) + 1 @@ -973,7 +865,6 @@ class DuckHuntBot: """Drop a random item to the ground for competitive snatching""" import time - # Simple random item IDs item_ids = [1, 2, 3, 4, 5, 6, 7, 8] item_id = random.choice(item_ids) item_key = str(item_id) @@ -986,11 +877,9 @@ class DuckHuntBot: item_name = item_names.get(item_key, f'Item {item_id}') - # Initialize channel dropped items if needed if channel not in self.dropped_items: self.dropped_items[channel] = [] - # Add item to dropped items dropped_item = { 'item_id': item_key, 'item_name': item_name, @@ -999,7 +888,6 @@ class DuckHuntBot: } self.dropped_items[channel].append(dropped_item) - # Announce the drop - eggdrop style message = f"🎁 A {item_name} has been dropped! Type !snatch to grab it!" self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}") @@ -1008,7 +896,6 @@ class DuckHuntBot: if 'inventory' not in player: player['inventory'] = {} - # Simple random item IDs item_ids = [1, 2, 3, 4, 5, 6, 7, 8] item_id = random.choice(item_ids) item_key = str(item_id) @@ -1028,33 +915,31 @@ class DuckHuntBot: async def use_item_effect(self, player, item_id, nick, channel, target_nick=None): """Apply item effects""" effects = { - 1: "Extra Shots! +3 shots", # Extra shots - 2: "Faster Reload! -1s reload time", # Faster reload - 3: "Accuracy Charm! +5 accuracy", # Accuracy charm - 4: "Lucky Charm! +10 luck", # Lucky charm - 5: "Friendship Bracelet! +5 charm", # Friendship bracelet - 6: "Duck Caller! Next duck spawns faster", # Duck caller - 7: "Camouflage! Ducks can't see you for 60s", # Camouflage - 8: "Energy Drink! +50 energy" # Energy drink + 1: "Extra Shots! +3 shots", + 2: "Faster Reload! -1s reload time", + 3: "Accuracy Charm! +5 accuracy", + 4: "Lucky Charm! +10 luck", + 5: "Friendship Bracelet! +5 charm", + 6: "Duck Caller! Next duck spawns faster", + 7: "Camouflage! Ducks can't see you for 60s", + 8: "Energy Drink! +50 energy" } - # Apply item effects - if item_id == 1: # Extra shots + if item_id == 1: player['shots'] = min(player['shots'] + 3, player['max_shots']) - elif item_id == 2: # Faster reload + elif item_id == 2: player['reload_time'] = max(player['reload_time'] - 1, 1) - elif item_id == 3: # Accuracy charm + elif item_id == 3: player['accuracy_bonus'] = player.get('accuracy_bonus', 0) + 5 - elif item_id == 4: # Lucky charm + elif item_id == 4: player['luck'] = player.get('luck', 0) + 10 - elif item_id == 5: # Friendship bracelet + elif item_id == 5: player['charm_bonus'] = player.get('charm_bonus', 0) + 5 - elif item_id == 6: # Duck caller - # Could implement faster duck spawning here + elif item_id == 6: pass - elif item_id == 7: # Camouflage + elif item_id == 7: player['camouflaged_until'] = time.time() + 60 - elif item_id == 8: # Energy drink + elif item_id == 8: player['energy'] = player.get('energy', 100) + 50 effect_msg = effects.get(item_id, "Unknown effect") @@ -1068,43 +953,33 @@ class DuckHuntBot: try: current_time = time.time() - # Clean up expired items from all channels for channel in list(self.dropped_items.keys()): if channel in self.dropped_items: original_count = len(self.dropped_items[channel]) - # Remove items older than 60 seconds self.dropped_items[channel] = [ item for item in self.dropped_items[channel] if current_time - item['timestamp'] <= 60 ] - # Optional: log cleanup if items were removed removed_count = original_count - len(self.dropped_items[channel]) if removed_count > 0: self.logger.debug(f"Cleaned up {removed_count} expired items from {channel}") - # Sleep for 30 seconds before next cleanup await asyncio.sleep(30) except Exception as e: self.logger.error(f"Error in cleanup_expired_items: {e}") - await asyncio.sleep(30) # Continue trying after error + await asyncio.sleep(30) async def run(self): """Main bot run loop""" - tasks = [] # Initialize tasks list early + tasks = [] try: - # Setup signal handlers self.setup_signal_handlers() - - # Load database self.db.load_database() - - # Connect to IRC await self.connect() - # Start background tasks tasks = [ asyncio.create_task(self.message_loop()), asyncio.create_task(self.game.spawn_ducks()), @@ -1112,17 +987,15 @@ class DuckHuntBot: asyncio.create_task(self.cleanup_expired_items()), ] - # Wait for shutdown or task completion try: while not self.shutdown_requested: - # Check if any critical task has failed for task in tasks: if task.done() and task.exception(): self.logger.error(f"Task failed: {task.exception()}") self.shutdown_requested = True break - await asyncio.sleep(0.1) # Short sleep to allow signal handling + await asyncio.sleep(0.1) except asyncio.CancelledError: self.logger.info("Main loop cancelled") @@ -1134,10 +1007,8 @@ class DuckHuntBot: self.logger.error(f"Bot error: {e}") raise finally: - # Cleanup self.logger.info("Shutting down bot...") - # Cancel all tasks for task in tasks: if not task.done(): task.cancel() @@ -1148,14 +1019,12 @@ class DuckHuntBot: except Exception as e: self.logger.error(f"Error cancelling task: {e}") - # Save database try: self.db.save_database() self.logger.info("Database saved") except Exception as e: self.logger.error(f"Error saving database: {e}") - # Close connection if self.writer and not self.writer.is_closing(): try: self.send_raw("QUIT :Bot shutting down") @@ -1171,7 +1040,6 @@ class DuckHuntBot: """Handle incoming IRC messages""" while not self.shutdown_requested and self.reader: try: - # Use a timeout so we can check shutdown_requested regularly line = await asyncio.wait_for(self.reader.readline(), timeout=1.0) if not line: self.logger.warning("Empty line received, connection may be closed") @@ -1183,7 +1051,6 @@ class DuckHuntBot: await self.handle_message(prefix, command, params, trailing) except asyncio.TimeoutError: - # Timeout is expected - just continue to check shutdown flag continue except asyncio.CancelledError: self.logger.info("Message loop cancelled") diff --git a/src/game.py b/src/game.py index cc5f74c..6adea68 100644 --- a/src/game.py +++ b/src/game.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """ Game mechanics for DuckHunt Bot """ @@ -17,10 +16,9 @@ class DuckGame: def __init__(self, bot, db): self.bot = bot self.db = db - self.ducks = {} # Format: {channel: [{'alive': True, 'spawn_time': time, 'id': uuid}, ...]} + self.ducks = {} self.logger = logging.getLogger('DuckHuntBot.Game') - # Colors for IRC messages self.colors = { 'red': '\x0304', 'green': '\x0303', @@ -84,29 +82,25 @@ class DuckGame: if start_hour <= end_hour: return start_hour <= current_hour <= end_hour - else: # Crosses midnight + else: return current_hour >= start_hour or current_hour <= end_hour def calculate_gun_reliability(self, player): """Calculate gun reliability with modifiers""" base_reliability = player.get('reliability', 70) - # Add weapon modifiers, items, etc. return min(100, max(0, base_reliability)) def gun_jams(self, player): """Check if gun jams (eggdrop style)""" - # Base jamming probability is inverse of reliability reliability = player.get('reliability', 70) - jam_chance = max(1, 101 - reliability) # Higher reliability = lower jam chance + jam_chance = max(1, 101 - reliability) - # Additional factors that increase jam chance if player.get('total_ammo_used', 0) > 100: - jam_chance += 2 # Gun gets more prone to jamming with use + jam_chance += 2 if player.get('jammed_count', 0) > 5: - jam_chance += 1 # Previously jammed guns are more prone to jamming + jam_chance += 1 - # Roll for jam (1-100, jam if roll <= jam_chance) return random.randint(1, 100) <= jam_chance async def scare_other_ducks(self, channel, shot_duck_id): @@ -114,16 +108,15 @@ class DuckGame: if channel not in self.ducks: return - for duck in self.ducks[channel][:]: # Copy list to avoid modification during iteration + for duck in self.ducks[channel][:]: if duck['id'] != shot_duck_id and duck['alive']: - # 30% chance to scare away other ducks if random.random() < 0.3: duck['alive'] = False self.ducks[channel].remove(duck) async def scare_duck_on_miss(self, channel, target_duck): """Scare duck when someone misses""" - if target_duck and random.random() < 0.15: # 15% chance + if target_duck and random.random() < 0.15: target_duck['alive'] = False if channel in self.ducks and target_duck in self.ducks[channel]: self.ducks[channel].remove(target_duck) @@ -133,7 +126,7 @@ class DuckGame: if not self.get_config('items.enabled', True): return - if random.random() < 0.1: # 10% chance + if random.random() < 0.1: items = [ ("a mirror", "mirror", "You can now deflect shots!"), ("some sand", "sand", "Throw this to blind opponents!"), @@ -172,7 +165,6 @@ class DuckGame: self.logger.debug(f"Max ducks already in {channel}") return - # Determine duck type if force_golden: duck_type = "golden" else: @@ -188,13 +180,11 @@ class DuckGame: else: duck_type = "normal" - # Get duck configuration duck_config = self.get_config(f'duck_types.{duck_type}', {}) if not duck_config.get('enabled', True): duck_type = "normal" duck_config = self.get_config('duck_types.normal', {}) - # Create duck duck = { 'id': str(uuid.uuid4())[:8], 'type': duck_type, @@ -206,14 +196,12 @@ class DuckGame: self.ducks[channel].append(duck) - # Send spawn message messages = duck_config.get('messages', [self.get_duck_spawn_message()]) spawn_message = random.choice(messages) self.bot.send_message(channel, spawn_message) self.logger.info(f"Spawned {duck_type} duck in {channel}") - # Alert users who have alerts enabled await self.send_duck_alerts(channel, duck_type) return duck @@ -223,8 +211,6 @@ class DuckGame: if not self.get_config('social.duck_alerts_enabled', True): return - # Implementation would iterate through players with alerts enabled - # For now, just log self.logger.debug(f"Duck alerts for {duck_type} duck in {channel}") async def spawn_ducks(self): @@ -232,7 +218,7 @@ class DuckGame: while not self.bot.shutdown_requested: try: if self.is_sleep_time(): - await asyncio.sleep(300) # Check every 5 minutes during sleep + await asyncio.sleep(300) continue for channel in self.bot.channels_joined: @@ -242,7 +228,6 @@ class DuckGame: if channel not in self.ducks: self.ducks[channel] = [] - # Clean up dead ducks self.ducks[channel] = [d for d in self.ducks[channel] if d['alive']] max_ducks = self.get_config('max_ducks_per_channel', 3) @@ -252,10 +237,10 @@ class DuckGame: min_spawn_time = self.get_config('duck_spawn_min', 1800) max_spawn_time = self.get_config('duck_spawn_max', 5400) - if random.random() < 0.1: # 10% chance each check + if random.random() < 0.1: await self.spawn_duck_now(channel) - await asyncio.sleep(random.randint(60, 300)) # Check every 1-5 minutes + await asyncio.sleep(random.randint(60, 300)) except asyncio.CancelledError: self.logger.info("Duck spawning loop cancelled") @@ -277,7 +262,7 @@ class DuckGame: if channel not in self.ducks: continue - for duck in self.ducks[channel][:]: # Copy to avoid modification + for duck in self.ducks[channel][:]: if not duck['alive']: continue @@ -291,7 +276,6 @@ class DuckGame: duck['alive'] = False self.ducks[channel].remove(duck) - # Send timeout message (eggdrop style) timeout_messages = [ "-.,ΒΈΒΈ.-Β·Β°'`'°·-.,ΒΈΒΈ.-Β·Β°'`'°· \\_o> The duck flew away!", "-.,ΒΈΒΈ.-Β·Β°'`'°·-.,ΒΈΒΈ.-Β·Β°'`'°· \\_O> *FLAP FLAP FLAP*", @@ -301,7 +285,7 @@ class DuckGame: self.bot.send_message(channel, random.choice(timeout_messages)) self.logger.debug(f"Duck timed out in {channel}") - await asyncio.sleep(10) # Check every 10 seconds + await asyncio.sleep(10) except asyncio.CancelledError: self.logger.info("Duck timeout checker cancelled") diff --git a/src/logging_utils.py b/src/logging_utils.py index e27cf89..77f22fc 100644 --- a/src/logging_utils.py +++ b/src/logging_utils.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """ Logging utilities for DuckHunt Bot """ @@ -10,12 +9,12 @@ import logging.handlers class DetailedColorFormatter(logging.Formatter): """Console formatter with color support""" COLORS = { - 'DEBUG': '\033[94m', # Blue - 'INFO': '\033[92m', # Green - 'WARNING': '\033[93m', # Yellow - 'ERROR': '\033[91m', # Red - 'CRITICAL': '\033[95m', # Magenta - 'ENDC': '\033[0m' # End color + 'DEBUG': '\033[94m', + 'INFO': '\033[92m', + 'WARNING': '\033[93m', + 'ERROR': '\033[91m', + 'CRITICAL': '\033[95m', + 'ENDC': '\033[0m' } def format(self, record): @@ -36,10 +35,8 @@ def setup_logger(name="DuckHuntBot"): logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) - # Clear any existing handlers logger.handlers.clear() - # Console handler with colors console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_formatter = DetailedColorFormatter( @@ -48,12 +45,11 @@ def setup_logger(name="DuckHuntBot"): console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) - # File handler with rotation for detailed logs try: file_handler = logging.handlers.RotatingFileHandler( 'duckhunt.log', - maxBytes=10*1024*1024, # 10MB per file - backupCount=5 # Keep 5 backup files + maxBytes=10*1024*1024, + backupCount=5 ) file_handler.setLevel(logging.DEBUG) file_formatter = DetailedFileFormatter( diff --git a/src/sasl.py b/src/sasl.py index d3ebf71..19cf36c 100644 --- a/src/sasl.py +++ b/src/sasl.py @@ -55,7 +55,6 @@ class SASLHandler: subcommand = params[1] if subcommand == "LS": - # Server listing capabilities caps = trailing.split() if trailing else [] self.logger.info(f"Server capabilities: {caps}") if "sasl" in caps: @@ -69,7 +68,6 @@ class SASLHandler: return False elif subcommand == "ACK": - # Server acknowledged capability request caps = trailing.split() if trailing else [] self.logger.info("SASL capability acknowledged") if "sasl" in caps: @@ -82,7 +80,6 @@ class SASLHandler: return False elif subcommand == "NAK": - # Server rejected capability request self.logger.warning("SASL capability rejected") self.bot.send_raw("CAP END") await self.bot.register_user() @@ -96,7 +93,6 @@ class SASLHandler: """ self.logger.info("Sending AUTHENTICATE PLAIN") self.bot.send_raw('AUTHENTICATE PLAIN') - # Small delay to ensure proper sequencing await asyncio.sleep(0.1) async def handle_authenticate_response(self, params): @@ -106,7 +102,6 @@ class SASLHandler: if params and params[0] == '+': self.logger.info("Server ready for SASL authentication") if self.username and self.password: - # Create auth string: username\0username\0password authpass = f'{self.username}{NULL_BYTE}{self.username}{NULL_BYTE}{self.password}' self.logger.debug(f"Auth string length: {len(authpass)} chars") self.logger.debug(f"Auth components: user='{self.username}', pass='{self.password[:3]}...'") @@ -125,14 +120,12 @@ class SASLHandler: async def handle_sasl_result(self, command, params, trailing): """Handle SASL authentication result.""" if command == "903": - # SASL success self.logger.info("SASL authentication successful!") self.authenticated = True await self.handle_903() return True elif command == "904": - # SASL failed self.logger.error("SASL authentication failed! (904 - Invalid credentials or account not found)") self.logger.error(f"Attempted username: {self.username}") self.logger.error(f"Password length: {len(self.password)} chars") @@ -145,28 +138,24 @@ class SASLHandler: return False elif command == "905": - # SASL too long self.logger.error("SASL authentication string too long") self.bot.send_raw("CAP END") await self.bot.register_user() return False elif command == "906": - # SASL aborted self.logger.error("SASL authentication aborted") self.bot.send_raw("CAP END") await self.bot.register_user() return False elif command == "907": - # Already authenticated self.logger.info("Already authenticated via SASL") self.authenticated = True await self.handle_903() return True elif command == "908": - # SASL mechanisms mechanisms = trailing.split() if trailing else [] self.logger.info(f"Available SASL mechanisms: {mechanisms}") if "PLAIN" not in mechanisms: @@ -182,7 +171,6 @@ class SASLHandler: Handles the 903 command by sending a CAP END command and triggering registration. """ self.bot.send_raw('CAP END') - # Trigger user registration after successful SASL auth await self.bot.register_user() def is_authenticated(self): diff --git a/src/utils.py b/src/utils.py index 7564c0c..6d7b592 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """ Utility functions for DuckHunt Bot """ @@ -15,7 +14,6 @@ class InputValidator: """Validate IRC nickname format""" if not nick or len(nick) > 30: return False - # RFC 2812 nickname pattern pattern = r'^[a-zA-Z\[\]\\`_^{|}][a-zA-Z0-9\[\]\\`_^{|}\-]*$' return bool(re.match(pattern, nick)) @@ -44,9 +42,8 @@ class InputValidator: """Sanitize user input message""" if not message: return "" - # Remove control characters and limit length sanitized = ''.join(char for char in message if ord(char) >= 32 or char in '\t\n') - return sanitized[:500] # Limit message length + return sanitized[:500] def parse_message(line):