""" Utility functions for DuckHunt Bot """ import re import json import os import random from typing import Optional, Tuple, List, Dict, Any class MessageManager: """Manages customizable IRC messages with color support""" def __init__(self, messages_file: str = "messages.json"): self.messages_file = messages_file self.messages = {} self.load_messages() def load_messages(self): """Load messages from JSON file""" try: if os.path.exists(self.messages_file): with open(self.messages_file, 'r', encoding='utf-8') as f: self.messages = json.load(f) else: # Fallback messages if file doesn't exist self.messages = self._get_default_messages() except Exception as e: print(f"Error loading messages: {e}, using defaults") self.messages = self._get_default_messages() def _get_default_messages(self) -> Dict[str, Any]: """Default fallback messages without colors""" return { "duck_spawn": [ "・゜゜・。。・゜゜\\_o< QUACK! A duck has appeared! Type !bang to shoot it!", "・゜゜・。。・゜゜\\_o< *flap flap* A wild duck landed! Use !bang to hunt it!", "A duck swoops into view! Quick, type !bang before it escapes!", "・゜゜・。。・゜゜\\_o< Quack quack! Fresh duck spotted! !bang to bag it!", "*rustling* A duck waddles out from the bushes! Fire with !bang!", "・゜゜・。。・゜゜\\_o< Splash! A duck surfaces! Shoot it with !bang!" ], "duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`", "bang_hit": "{nick} > *BANG* You shot the duck! [+{xp_gained} xp] [Total ducks: {ducks_shot}]", "bang_miss": "{nick} > *BANG* You missed the duck!", "bang_no_duck": "{nick} > *BANG* What did you shoot at? There is no duck in the area... [GUN CONFISCATED]", "bang_no_ammo": "{nick} > *click* You're out of ammo! Use !reload", "bang_not_armed": "{nick} > You are not armed.", "reload_success": "{nick} > *click* Reloaded! [Ammo: {ammo}/{max_ammo}] [Chargers: {chargers}]", "reload_already_loaded": "{nick} > Your gun is already loaded!", "reload_no_chargers": "{nick} > You're out of chargers!", "reload_not_armed": "{nick} > You are not armed.", "shop_display": "DuckHunt Shop: {items} | You have {xp} XP", "shop_item_format": "({id}) {name} - {price} XP", "help_header": "DuckHunt Commands:", "help_user_commands": "!bang - Shoot at ducks | !reload - Reload your gun | !shop - View the shop", "help_help_command": "!duckhelp - Show this help", "help_admin_commands": "Admin: !rearm | !disarm | !ignore | !unignore | !ducklaunch", "admin_rearm_player": "[ADMIN] {target} has been rearmed by {admin}", "admin_rearm_all": "[ADMIN] All players have been rearmed by {admin}", "admin_disarm": "[ADMIN] {target} has been disarmed by {admin}", "admin_ignore": "[ADMIN] {target} is now ignored by {admin}", "admin_unignore": "[ADMIN] {target} is no longer ignored by {admin}", "admin_ducklaunch": "[ADMIN] A duck has been launched by {admin}", "admin_ducklaunch_not_enabled": "[ADMIN] This channel is not enabled for duckhunt", "usage_rearm": "Usage: !rearm ", "usage_disarm": "Usage: !disarm ", "usage_ignore": "Usage: !ignore ", "usage_unignore": "Usage: !unignore " } def get(self, key: str, **kwargs) -> str: """Get a formatted message by key with enhanced error handling""" try: if key not in self.messages: return f"[Missing message: {key}]" message = self.messages[key] # If message is an array, randomly select one if isinstance(message, list): if not message: return f"[Empty message array: {key}]" message = random.choice(message) # Ensure message is a string if not isinstance(message, str): return f"[Invalid message type: {key}]" # Replace color placeholders with IRC codes if "colours" in self.messages and isinstance(self.messages["colours"], dict): for color_name, color_code in self.messages["colours"].items(): placeholder = "{" + color_name + "}" message = message.replace(placeholder, color_code) # Sanitize kwargs to prevent injection and ensure all values are safe safe_kwargs = {} for k, v in kwargs.items(): try: # Sanitize key and value safe_key = str(k)[:50] if k is not None else 'unknown' if isinstance(v, (int, float)): safe_kwargs[safe_key] = v elif v is None: safe_kwargs[safe_key] = '' else: # Sanitize string values safe_value = str(v)[:200] # Limit length safe_value = safe_value.replace('\r', '').replace('\n', ' ') # Remove newlines safe_kwargs[safe_key] = safe_value except Exception: safe_kwargs[str(k)] = '[error]' # Format with provided variables using safe formatting try: return message.format(**safe_kwargs) except KeyError as e: # Try to identify missing keys and provide defaults missing_key = str(e).strip("'\"") # Common defaults for missing keys defaults = { 'nick': 'Player', 'xp_gained': 0, 'ducks_shot': 0, 'ducks_befriended': 0, 'hp_remaining': 0, 'ammo': 0, 'max_ammo': 0, 'magazines': 0, 'target': 'Unknown', 'victim': 'someone', 'xp_lost': 0, 'xp': 0 } # Add default for missing key if missing_key in defaults: safe_kwargs[missing_key] = defaults[missing_key] else: safe_kwargs[missing_key] = f'[{missing_key}]' try: return message.format(**safe_kwargs) except Exception: return f"[Format error in {key}: missing {missing_key}]" except ValueError as e: return f"[Format error in {key}: {e}]" except Exception as e: return f"[Message error in {key}: {e}]" except Exception as e: return f"[Critical message error: {e}]" def reload(self): """Reload messages from file""" self.load_messages() class InputValidator: """Input validation utilities""" @staticmethod def validate_nickname(nick: str) -> bool: """Validate IRC nickname format""" if not nick or len(nick) > 30: return False pattern = r'^[a-zA-Z\[\]\\`_^{|}][a-zA-Z0-9\[\]\\`_^{|}\-]*$' return bool(re.match(pattern, nick)) @staticmethod def validate_channel(channel: str) -> bool: """Validate IRC channel format""" if not channel or len(channel) > 50: return False return channel.startswith('#') and ' ' not in channel @staticmethod def validate_numeric_input(value: str, min_val: Optional[int] = None, max_val: Optional[int] = None) -> Optional[int]: """Safely parse and validate numeric input""" try: num = int(value) if min_val is not None and num < min_val: return None if max_val is not None and num > max_val: return None return num except (ValueError, TypeError): return None @staticmethod def sanitize_message(message: str) -> str: """Sanitize user input message""" if not message: return "" sanitized = ''.join(char for char in message if ord(char) >= 32 or char in '\t\n') return sanitized[:500] def parse_irc_message(line: str) -> Tuple[str, str, List[str], str]: """Parse IRC message format with comprehensive error handling""" try: # Validate input if not isinstance(line, str): raise ValueError(f"Expected string, got {type(line)}") # Handle empty or whitespace-only lines if not line or not line.strip(): return '', '', [], '' line = line.strip() # Initialize return values prefix = '' trailing = '' command = '' params = [] # Handle prefix (starts with :) if line.startswith(':'): try: if ' ' in line[1:]: prefix, line = line[1:].split(' ', 1) else: # Handle malformed IRC line with no space after prefix prefix = line[1:] line = '' except ValueError: # If split fails, treat entire line as prefix prefix = line[1:] line = '' # Handle trailing parameter (starts with ' :') if line and ' :' in line: try: line, trailing = line.split(' :', 1) except ValueError: # If split fails, keep line as is pass # Parse command and parameters if line: try: parts = line.split() command = parts[0] if parts else '' params = parts[1:] if len(parts) > 1 else [] except Exception: # If parsing fails, try to extract at least the command command = line.split()[0] if line.split() else '' params = [] # Validate that we have at least a command if not command and not prefix: raise ValueError(f"No valid command or prefix found in line: {line[:50]}...") return prefix, command, params, trailing except Exception as e: # Log the error but return safe defaults to prevent crashes import logging logger = logging.getLogger(__name__) logger.warning(f"Error parsing IRC message '{line[:50]}...': {e}") return '', 'UNKNOWN', [], ''