From 5484548c3019dfbbdc00d666583f1593d240ed49 Mon Sep 17 00:00:00 2001 From: ComputerTech312 Date: Thu, 25 Sep 2025 19:47:44 +0100 Subject: [PATCH] yeah --- config.json | 39 +++-- duckhunt.json | 19 +- messages.json | 9 +- src/db.py | 26 ++- src/duckhuntbot.py | 260 +++++++++++++++++++++++----- src/logging_utils.py | 402 ++++++++++++++++++++++++++++++++++++++----- 6 files changed, 641 insertions(+), 114 deletions(-) diff --git a/config.json b/config.json index 7b9ca3e..98c07e4 100644 --- a/config.json +++ b/config.json @@ -2,30 +2,33 @@ "connection": { "server": "irc.rizon.net", "port": 6697, - "nick": "DickHunt", - "channels": ["#ct"], + "nick": "DuckHunt", + "channels": [ + "#ct" + ], "ssl": true, - "password": "your_iline_password_here", + "password": "duckyhunt789", "max_retries": 3, "retry_delay": 5, "timeout": 30 }, - "sasl": { - "enabled": false, + "enabled": true, "username": "duckhunt", "password": "duckhunt//789//" }, - "admins": ["peorth", "computertech", "colby"], - + "admins": [ + "peorth", + "computertech", + "colby" + ], "duck_spawning": { "spawn_min": 10, "spawn_max": 30, "timeout": 60, "rearm_on_duck_shot": true }, - "duck_types": { "normal": { "xp": 10, @@ -44,7 +47,6 @@ "xp": 12 } }, - "player_defaults": { "accuracy": 75, "magazines": 3, @@ -52,7 +54,6 @@ "jam_chance": 15, "xp": 0 }, - "gameplay": { "befriend_success_rate": 75, "befriend_xp": 5, @@ -63,15 +64,29 @@ "min_befriend_success_rate": 5, "max_befriend_success_rate": 95 }, - "features": { "shop_enabled": true, "inventory_enabled": true, "auto_rearm_enabled": true }, - "limits": { "max_inventory_items": 20, "max_temp_effects": 20 + }, + "debug": { + "_comment_enabled": "Whether debug logging is enabled at all (true=debug mode, false=minimal logging)", + "enabled": true, + "_comment_log_level": "Overall logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)", + "log_level": "DEBUG", + "_comment_console_level": "Console output level - what shows in terminal (DEBUG, INFO, WARNING, ERROR)", + "console_log_level": "INFO", + "_comment_file_level": "File logging level - what gets written to log files (DEBUG, INFO, WARNING, ERROR)", + "file_log_level": "DEBUG", + "_comment_log_everything": "If true, logs ALL events. If false, logs only important events", + "log_everything": true, + "_comment_log_performance": "Whether to enable performance/metrics logging to performance.log", + "log_performance": true, + "_comment_unified_format": "If true, console and file logs use same format. If false, console has colors, file is plain", + "unified_format": true } } \ No newline at end of file diff --git a/duckhunt.json b/duckhunt.json index a21b702..e6f9d5b 100644 --- a/duckhunt.json +++ b/duckhunt.json @@ -2,17 +2,18 @@ "players": { "computertech": { "nick": "ComputerTech", - "xp": 45, - "ducks_shot": 4, - "accuracy": 61, + "xp": 60, + "ducks_shot": 5, + "ducks_befriended": 2, + "accuracy": 62, "gun_confiscated": false, - "ducks_befriended": 1, - "inventory": {}, - "temporary_effects": [], - "current_ammo": 6, + "current_ammo": 5, "magazines": 3, - "bullets_per_magazine": 6 + "bullets_per_magazine": 6, + "jam_chance": 5, + "inventory": {}, + "temporary_effects": [] } }, - "last_save": "1758654759.6627305" + "last_save": "1758825127.4272072" } \ No newline at end of file diff --git a/messages.json b/messages.json index a3d65cf..6a4055a 100644 --- a/messages.json +++ b/messages.json @@ -6,7 +6,7 @@ ], "duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`", "fast_duck_flies_away": "The fast duck quickly flies away! ·°'`'°-.,¸¸.·°'`", - "golden_duck_flies_away": "The golden duck flies away majestically. ·°'`'°-.,¸¸.·°'`", + "golden_duck_flies_away": "The {gold}golden duck{reset} flies away majestically. ·°'`'°-.,¸¸.·°'`", "bang_hit": "{nick} > *BANG* You shot the duck! [+{xp_gained} xp] [Total ducks: {ducks_shot}]", "bang_hit_golden": "{nick} > *BANG* You shot a GOLDEN DUCK! [{hp_remaining} HP remaining] [+{xp_gained} xp]", "bang_hit_golden_killed": "{nick} > *BANG* You killed the GOLDEN DUCK! [+{xp_gained} xp] [Total ducks: {ducks_shot}]", @@ -22,14 +22,14 @@ "bef_duck_shot": "{nick} > *gentle approach* The duck is already dead! You can't befriend it now...", "reload_success": "{nick} > *click* New magazine loaded! [Ammo: {ammo}/{max_ammo}] [Spare magazines: {chargers}]", "reload_already_loaded": "{nick} > Your gun is already loaded!", - "reload_no_chargers": "{nick} > You're out of spare magazines!", + "reload_no_chargers": "{nick} > You're out of ammo!", "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 | !bef - Befriend ducks | !reload - Reload your gun | !shop - View/buy from shop | !duckstats - View your stats and items | !use - Use inventory items", + "help_user_commands": "!bang - Shoot at ducks | !bef - Befriend ducks | !reload - Reload your gun | !shop - View/buy from shop | !duckstats - View your stats and items | !topduck - View leaderboards | !use - Use inventory items", "help_help_command": "!duckhelp - Show this help", - "help_admin_commands": "Admin: !rearm | !disarm | !ignore | !unignore | !ducklaunch", + "help_admin_commands": "Admin: !rearm | !disarm | !ignore | !unignore | !ducklaunch [duck_type] (all support /msg)", "admin_rearm_player": "[ADMIN] {target} has been rearmed by {admin}", "admin_rearm_all": "[ADMIN] All players have been rearmed by {admin}", "admin_rearm_self": "[ADMIN] {admin} has rearmed themselves", @@ -58,6 +58,7 @@ "purple": "\u00036", "orange": "\u00037", "yellow": "\u00038", + "gold": "\u00038", "light_green": "\u00039", "cyan": "\u000310", "light_cyan": "\u000311", diff --git a/src/db.py b/src/db.py index 47daebd..2fa3cf3 100644 --- a/src/db.py +++ b/src/db.py @@ -261,4 +261,28 @@ class DuckDB: 'gun_confiscated': False, 'inventory': {}, 'temporary_effects': [] - } \ No newline at end of file + } + + def get_leaderboard(self, category='xp', limit=3): + """Get top players by specified category""" + try: + # Create list of (nick, value) tuples + leaderboard = [] + + for nick, player_data in self.players.items(): + if category == 'xp': + value = player_data.get('xp', 0) + elif category == 'ducks_shot': + value = player_data.get('ducks_shot', 0) + else: + continue + + leaderboard.append((nick, value)) + + # Sort by value (descending) and take top N + leaderboard.sort(key=lambda x: x[1], reverse=True) + return leaderboard[:limit] + + except Exception as e: + self.logger.error(f"Error getting leaderboard for {category}: {e}") + return [] \ No newline at end of file diff --git a/src/duckhuntbot.py b/src/duckhuntbot.py index 659877d..77192c8 100644 --- a/src/duckhuntbot.py +++ b/src/duckhuntbot.py @@ -26,6 +26,8 @@ class DuckHuntBot: self.channels_joined = set() self.shutdown_requested = False + self.logger.info("🤖 Initializing DuckHunt Bot components...") + self.db = DuckDB(bot=self) self.game = DuckGame(self, self.db) self.messages = MessageManager() @@ -34,6 +36,7 @@ class DuckHuntBot: admins_list = self.get_config('admins', ['colby']) or ['colby'] self.admins = [admin.lower() for admin in admins_list] + self.logger.info(f"👑 Configured {len(self.admins)} admin(s): {', '.join(self.admins)}") # Initialize shop manager shop_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shop.json') @@ -184,12 +187,17 @@ class DuckHuntBot: self.logger.error(f"Error sanitizing/sending message: {e}") return False - async def register_user(self): - """Register user with IRC server""" + async def send_server_password(self): + """Send server password if configured (must be sent immediately after connection)""" password = self.get_config('connection.password') if password and password != "your_iline_password_here": + self.logger.info("🔐 Sending server password") self.send_raw(f"PASS {password}") - + return True + return False + + async def register_user(self): + """Register user with IRC server (NICK/USER commands)""" nick = self.get_config('connection.nick', 'DuckHunt') self.send_raw(f"NICK {nick}") self.send_raw(f"USER {nick} 0 * :{nick}") @@ -327,6 +335,8 @@ class DuckHuntBot: await self.handle_shop(nick, channel, player, args) elif cmd == "duckstats": await self.handle_duckstats(nick, channel, player) + elif cmd == "topduck": + await self.handle_topduck(nick, channel) elif cmd == "use": await self.handle_use(nick, channel, player, args) elif cmd == "duckhelp": @@ -496,6 +506,45 @@ class DuckHuntBot: for line in stats_lines: self.send_message(channel, line) + async def handle_topduck(self, nick, channel): + """Handle !topduck command - show leaderboards""" + try: + # Apply color formatting + bold = self.messages.messages.get('colours', {}).get('bold', '') + reset = self.messages.messages.get('colours', {}).get('reset', '') + + # Get top 3 by XP + top_xp = self.db.get_leaderboard('xp', 3) + + # Get top 3 by ducks shot + top_ducks = self.db.get_leaderboard('ducks_shot', 3) + + # Format XP leaderboard as single line + if top_xp: + xp_rankings = [] + for i, (player_nick, xp) in enumerate(top_xp, 1): + medal = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" + xp_rankings.append(f"{medal}{player_nick}:{xp}XP") + xp_line = f"🏆 {bold}Top XP{reset} " + " | ".join(xp_rankings) + self.send_message(channel, xp_line) + else: + self.send_message(channel, "🏆 No XP data available yet!") + + # Format ducks shot leaderboard as single line + if top_ducks: + duck_rankings = [] + for i, (player_nick, ducks) in enumerate(top_ducks, 1): + medal = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" + duck_rankings.append(f"{medal}{player_nick}:{ducks}") + duck_line = f"🦆 {bold}Top Hunters{reset} " + " | ".join(duck_rankings) + self.send_message(channel, duck_line) + else: + self.send_message(channel, "🦆 No duck hunting data available yet!") + + except Exception as e: + self.logger.error(f"Error in handle_topduck: {e}") + self.send_message(channel, f"{nick} > Error retrieving leaderboard data.") + async def handle_duckhelp(self, nick, channel, _player): """Handle !duckhelp command""" help_lines = [ @@ -565,7 +614,9 @@ class DuckHuntBot: self.send_message(channel, message) async def handle_rearm(self, nick, channel, args): - """Handle !rearm command (admin only)""" + """Handle !rearm command (admin only) - supports private messages""" + is_private_msg = not channel.startswith('#') + if args: target = args[0].lower() player = self.db.get_player(target) @@ -577,10 +628,17 @@ class DuckHuntBot: self.levels.update_player_magazines(player) player['current_ammo'] = player.get('bullets_per_magazine', 6) - message = self.messages.get('admin_rearm_player', target=target, admin=nick) + if is_private_msg: + message = f"{nick} > Rearmed {target}" + else: + message = self.messages.get('admin_rearm_player', target=target, admin=nick) self.send_message(channel, message) else: - # Rearm the admin themselves + if is_private_msg: + self.send_message(channel, f"{nick} > Usage: !rearm ") + return + + # Rearm the admin themselves (only in channels) player = self.db.get_player(nick) if player is None: player = {} @@ -596,47 +654,162 @@ class DuckHuntBot: self.db.save_database() async def handle_disarm(self, nick, channel, args): - """Handle !disarm command (admin only)""" - def disarm_player(player): - player['gun_confiscated'] = True + """Handle !disarm command (admin only) - supports private messages""" + is_private_msg = not channel.startswith('#') - self._handle_single_target_admin_command( - args, 'usage_disarm', disarm_player, 'admin_disarm', nick, channel - ) - - async def handle_ignore(self, nick, channel, args): - """Handle !ignore command (admin only)""" - def ignore_player(player): - player['ignored'] = True - - self._handle_single_target_admin_command( - args, 'usage_ignore', ignore_player, 'admin_ignore', nick, channel - ) - - async def handle_unignore(self, nick, channel, args): - """Handle !unignore command (admin only)""" - def unignore_player(player): - player['ignored'] = False - - self._handle_single_target_admin_command( - args, 'usage_unignore', unignore_player, 'admin_unignore', nick, channel - ) - - async def handle_ducklaunch(self, _nick, channel, _args): - """Handle !ducklaunch command (admin only)""" - if channel not in self.channels_joined: - message = self.messages.get('admin_ducklaunch_not_enabled') - self.send_message(channel, message) + if not args: + if is_private_msg: + self.send_message(channel, f"{nick} > Usage: !disarm ") + else: + message = self.messages.get('usage_disarm') + self.send_message(channel, message) return - # Force spawn a duck - if channel not in self.game.ducks: - self.game.ducks[channel] = [] - self.game.ducks[channel].append({"spawn_time": time.time()}) + target = args[0].lower() + player = self.db.get_player(target) + if player is None: + player = {} + player['gun_confiscated'] = True + + if is_private_msg: + message = f"{nick} > Disarmed {target}" + else: + message = self.messages.get('admin_disarm', target=target, admin=nick) + + self.send_message(channel, message) + self.db.save_database() + + async def handle_ignore(self, nick, channel, args): + """Handle !ignore command (admin only) - supports private messages""" + is_private_msg = not channel.startswith('#') + + if not args: + if is_private_msg: + self.send_message(channel, f"{nick} > Usage: !ignore ") + else: + message = self.messages.get('usage_ignore') + self.send_message(channel, message) + return + + target = args[0].lower() + player = self.db.get_player(target) + if player is None: + player = {} + player['ignored'] = True + + if is_private_msg: + message = f"{nick} > Ignored {target}" + else: + message = self.messages.get('admin_ignore', target=target, admin=nick) + + self.send_message(channel, message) + self.db.save_database() + + async def handle_unignore(self, nick, channel, args): + """Handle !unignore command (admin only) - supports private messages""" + is_private_msg = not channel.startswith('#') + + if not args: + if is_private_msg: + self.send_message(channel, f"{nick} > Usage: !unignore ") + else: + message = self.messages.get('usage_unignore') + self.send_message(channel, message) + return + + target = args[0].lower() + player = self.db.get_player(target) + if player is None: + player = {} + player['ignored'] = False + + if is_private_msg: + message = f"{nick} > Unignored {target}" + else: + message = self.messages.get('admin_unignore', target=target, admin=nick) + + self.send_message(channel, message) + self.db.save_database() + + async def handle_ducklaunch(self, nick, channel, args): + """Handle !ducklaunch command (admin only) - supports duck type specification""" + # For private messages, need to specify a target channel + target_channel = channel + is_private_msg = not channel.startswith('#') + + if is_private_msg: + if not args: + self.send_message(channel, f"{nick} > Usage: !ducklaunch [channel] [duck_type] - duck_type can be: normal, golden, fast") + return + target_channel = args[0] + duck_type_arg = args[1] if len(args) > 1 else "normal" + else: + duck_type_arg = args[0] if args else "normal" + + # Validate target channel + if target_channel not in self.channels_joined: + if is_private_msg: + self.send_message(channel, f"{nick} > Channel {target_channel} is not available for duckhunt") + else: + message = self.messages.get('admin_ducklaunch_not_enabled') + self.send_message(channel, message) + return + + # Validate duck type + duck_type_arg = duck_type_arg.lower() + valid_types = ["normal", "golden", "fast"] + if duck_type_arg not in valid_types: + self.send_message(channel, f"{nick} > Invalid duck type '{duck_type_arg}'. Valid types: {', '.join(valid_types)}") + return + + # Force spawn the specified duck type + import time + import random + + if target_channel not in self.game.ducks: + self.game.ducks[target_channel] = [] + + # Create duck based on specified type + current_time = time.time() + duck_id = f"{duck_type_arg}_duck_{int(current_time)}_{random.randint(1000, 9999)}" + + if duck_type_arg == "golden": + min_hp_val = self.get_config('duck_types.golden.min_hp', 3) + max_hp_val = self.get_config('duck_types.golden.max_hp', 5) + min_hp = int(min_hp_val) if min_hp_val is not None else 3 + max_hp = int(max_hp_val) if max_hp_val is not None else 5 + hp = random.randint(min_hp, max_hp) + duck = { + 'id': duck_id, + 'spawn_time': current_time, + 'channel': target_channel, + 'duck_type': 'golden', + 'max_hp': hp, + 'current_hp': hp + } + else: + # Both normal and fast ducks have 1 HP + duck = { + 'id': duck_id, + 'spawn_time': current_time, + 'channel': target_channel, + 'duck_type': duck_type_arg, + 'max_hp': 1, + 'current_hp': 1 + } + + self.game.ducks[target_channel].append(duck) duck_message = self.messages.get('duck_spawn') - # Only send the duck spawn message, no admin notification - self.send_message(channel, duck_message) + # Send duck spawn message to target channel + self.send_message(target_channel, duck_message) + + # Send confirmation to admin (either in channel or private message) + if is_private_msg: + self.send_message(channel, f"{nick} > Launched {duck_type_arg} duck in {target_channel}") + else: + # In channel, only send the duck message (no admin notification to avoid spam) + pass async def message_loop(self): @@ -720,6 +893,9 @@ class DuckHuntBot: try: await self.connect() + # Send server password immediately after connection (RFC requirement) + await self.send_server_password() + # Check if SASL should be used if self.sasl_handler.should_authenticate(): await self.sasl_handler.start_negotiation() diff --git a/src/logging_utils.py b/src/logging_utils.py index 9d2962b..2afd148 100644 --- a/src/logging_utils.py +++ b/src/logging_utils.py @@ -1,65 +1,375 @@ """ -Logging utilities for DuckHunt Bot +Enhanced logging utilities for DuckHunt Bot +Features: Colors, emojis, file rotation, structured formatting, configurable debug levels """ +import json import logging import logging.handlers +import os +import sys +from datetime import datetime -class DetailedColourFormatter(logging.Formatter): - """Console formatter with colour support""" - COLOURS = { - 'DEBUG': '\033[94m', - 'INFO': '\033[92m', - 'WARNING': '\033[93m', - 'ERROR': '\033[91m', - 'CRITICAL': '\033[95m', - 'ENDC': '\033[0m' +def load_config(): + """Load configuration from config.json""" + try: + with open('config.json', 'r') as f: + config = json.load(f) + return config + except Exception as e: + print(f"Warning: Could not load config.json: {e}") + return { + "debug": { + "enabled": True, + "log_level": "DEBUG", + "console_log_level": "INFO", + "file_log_level": "DEBUG", + "log_everything": True, + "log_performance": True, + "unified_format": True + } + } + + +class EnhancedColourFormatter(logging.Formatter): + """Enhanced console formatter with colors, emojis, and better formatting""" + + # ANSI color codes with styles + COLORS = { + 'DEBUG': '\033[36m', # Cyan + 'INFO': '\033[32m', # Green + 'WARNING': '\033[33m', # Yellow + 'ERROR': '\033[31m', # Red + 'CRITICAL': '\033[35m', # Magenta + 'RESET': '\033[0m', # Reset + 'BOLD': '\033[1m', # Bold + 'DIM': '\033[2m', # Dim + 'UNDERLINE': '\033[4m', # Underline + } + + # Emojis for different log levels + EMOJIS = { + 'DEBUG': '🔍', + 'INFO': '📘', + 'WARNING': '⚠️', + 'ERROR': '❌', + 'CRITICAL': '💥', + } + + # Component colors + COMPONENT_COLORS = { + 'DuckHuntBot': '\033[94m', # Light blue + 'DuckHuntBot.IRC': '\033[96m', # Light cyan + 'DuckHuntBot.Game': '\033[92m', # Light green + 'DuckHuntBot.Shop': '\033[93m', # Light yellow + 'DuckHuntBot.DB': '\033[95m', # Light magenta + 'SASL': '\033[97m', # White } def format(self, record): - colour = self.COLOURS.get(record.levelname, '') - endc = self.COLOURS['ENDC'] - msg = super().format(record) - return f"{colour}{msg}{endc}" + # Get colors + level_color = self.COLORS.get(record.levelname, '') + component_color = self.COMPONENT_COLORS.get(record.name, '\033[37m') # Default gray + reset = self.COLORS['RESET'] + bold = self.COLORS['BOLD'] + dim = self.COLORS['DIM'] + + # Get emoji + emoji = self.EMOJIS.get(record.levelname, '📝') + + # Format timestamp + timestamp = datetime.fromtimestamp(record.created).strftime('%H:%M:%S.%f')[:-3] + + # Format level with padding + level = f"{record.levelname:<8}" + + # Format component name with truncation + component = record.name + if len(component) > 20: + component = component[:17] + "..." + + # Build the formatted message + formatted_msg = ( + f"{dim}{timestamp}{reset} " + f"{emoji} " + f"{level_color}{bold}{level}{reset} " + f"{component_color}{component:<20}{reset} " + f"{record.getMessage()}" + ) + + # Add function/line info for DEBUG level + if record.levelno == logging.DEBUG: + func_info = f"{dim}[{record.funcName}:{record.lineno}]{reset}" + formatted_msg += f" {func_info}" + + return formatted_msg -class DetailedFileFormatter(logging.Formatter): - """File formatter with extra context but no colours""" - def format(self, record): - return super().format(record) - - -def setup_logger(name="DuckHuntBot"): - """Setup logger with console and file handlers""" - logger = logging.getLogger(name) - logger.setLevel(logging.DEBUG) +class EnhancedFileFormatter(logging.Formatter): + """Enhanced file formatter matching console format (no colors)""" + # Emojis for different log levels (same as console) + EMOJIS = { + 'DEBUG': '🔍', + 'INFO': '📘', + 'WARNING': '⚠️', + 'ERROR': '❌', + 'CRITICAL': '💥', + } + + def format(self, record): + # Get emoji (same as console) + emoji = self.EMOJIS.get(record.levelname, '📝') + + # Format timestamp (same as console - just time, not date) + timestamp = datetime.fromtimestamp(record.created).strftime('%H:%M:%S.%f')[:-3] + + # Format level with padding (same as console) + level = f"{record.levelname:<8}" + + # Format component name with truncation (same as console) + component = record.name + if len(component) > 20: + component = component[:17] + "..." + + # Build the formatted message (same style as console) + formatted_msg = ( + f"{timestamp} " + f"{emoji} " + f"{level} " + f"{component:<20} " + f"{record.getMessage()}" + ) + + # Add function/line info for DEBUG level (same as console) + if record.levelno == logging.DEBUG: + func_info = f"[{record.funcName}:{record.lineno}]" + formatted_msg += f" {func_info}" + + # Add exception info if present + if record.exc_info: + formatted_msg += f"\n{self.formatException(record.exc_info)}" + + return formatted_msg + + +class UnifiedFormatter(logging.Formatter): + """Unified formatter that works for both console and file output""" + + # ANSI color codes (only used when use_colors=True) + COLORS = { + 'DEBUG': '\033[36m', # Cyan + 'INFO': '\033[32m', # Green + 'WARNING': '\033[33m', # Yellow + 'ERROR': '\033[31m', # Red + 'CRITICAL': '\033[35m', # Magenta + 'RESET': '\033[0m', # Reset + 'BOLD': '\033[1m', # Bold + 'DIM': '\033[2m', # Dim + } + + # Emojis for different log levels + EMOJIS = { + 'DEBUG': '🔍', + 'INFO': '📘', + 'WARNING': '⚠️', + 'ERROR': '❌', + 'CRITICAL': '💥', + } + + # Component colors + COMPONENT_COLORS = { + 'DuckHuntBot': '\033[94m', # Light blue + 'DuckHuntBot.IRC': '\033[96m', # Light cyan + 'DuckHuntBot.Game': '\033[92m', # Light green + 'DuckHuntBot.Shop': '\033[93m', # Light yellow + 'DuckHuntBot.DB': '\033[95m', # Light magenta + 'SASL': '\033[97m', # White + } + + def __init__(self, use_colors=False): + super().__init__() + self.use_colors = use_colors + + def format(self, record): + # Get emoji + emoji = self.EMOJIS.get(record.levelname, '📝') + + # Format timestamp (same for both) + timestamp = datetime.fromtimestamp(record.created).strftime('%H:%M:%S.%f')[:-3] + + # Format level with padding + level = f"{record.levelname:<8}" + + # Format component name with truncation + component = record.name + if len(component) > 20: + component = component[:17] + "..." + + if self.use_colors: + # Console version with colors + level_color = self.COLORS.get(record.levelname, '') + component_color = self.COMPONENT_COLORS.get(record.name, '\033[37m') + reset = self.COLORS['RESET'] + bold = self.COLORS['BOLD'] + dim = self.COLORS['DIM'] + + formatted_msg = ( + f"{dim}{timestamp}{reset} " + f"{emoji} " + f"{level_color}{bold}{level}{reset} " + f"{component_color}{component:<20}{reset} " + f"{record.getMessage()}" + ) + + # Add function/line info for DEBUG level + if record.levelno == logging.DEBUG: + func_info = f"{dim}[{record.funcName}:{record.lineno}]{reset}" + formatted_msg += f" {func_info}" + else: + # File version without colors + formatted_msg = ( + f"{timestamp} " + f"{emoji} " + f"{level} " + f"{component:<20} " + f"{record.getMessage()}" + ) + + # Add function/line info for DEBUG level + if record.levelno == logging.DEBUG: + func_info = f"[{record.funcName}:{record.lineno}]" + formatted_msg += f" {func_info}" + + # Add exception info if present + if record.exc_info: + formatted_msg += f"\n{self.formatException(record.exc_info)}" + + return formatted_msg + + +class PerformanceFileFormatter(logging.Formatter): + """Separate formatter for performance/metrics logging""" + + def format(self, record): + timestamp = datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S') + + # Extract performance metrics if available + metrics = [] + for attr in ['duration', 'memory_usage', 'cpu_usage', 'users_count', 'channels_count']: + if hasattr(record, attr): + metrics.append(f"{attr}={getattr(record, attr)}") + + metrics_str = f" METRICS[{', '.join(metrics)}]" if metrics else "" + + return f"{timestamp} PERF | {record.getMessage()}{metrics_str}" + + +def setup_logger(name="DuckHuntBot", console_level=None, file_level=None): + """Setup enhanced logger with multiple handlers and beautiful formatting""" + # Load configuration + config = load_config() + debug_config = config.get("debug", {}) + + # Determine if debug is enabled + debug_enabled = debug_config.get("enabled", True) + log_everything = debug_config.get("log_everything", True) if debug_enabled else False + unified_format = debug_config.get("unified_format", True) + + # Set logging levels based on config + if console_level is None: + if debug_enabled and log_everything: + console_level = getattr(logging, debug_config.get("console_log_level", "DEBUG"), logging.DEBUG) + else: + console_level = logging.WARNING # Minimal logging + + if file_level is None: + if debug_enabled and log_everything: + file_level = getattr(logging, debug_config.get("file_log_level", "DEBUG"), logging.DEBUG) + else: + file_level = logging.ERROR # Only errors + + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG if debug_enabled else logging.WARNING) + + # Clear existing handlers to avoid duplicates logger.handlers.clear() - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_formatter = DetailedColourFormatter( - '%(asctime)s [%(levelname)s] %(name)s: %(message)s' - ) + # === CONSOLE HANDLER === + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(console_level) + + # Use unified format if configured, otherwise use colorful console format + if unified_format: + console_formatter = UnifiedFormatter(use_colors=True) + else: + console_formatter = EnhancedColourFormatter() + console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) - try: - file_handler = logging.handlers.RotatingFileHandler( - 'duckhunt.log', - maxBytes=10*1024*1024, - backupCount=5 - ) - file_handler.setLevel(logging.DEBUG) - file_formatter = DetailedFileFormatter( - '%(asctime)s [%(levelname)-8s] %(name)s - %(funcName)s:%(lineno)d: %(message)s' - ) - file_handler.setFormatter(file_formatter) - logger.addHandler(file_handler) - - logger.info("Enhanced logging system initialized with file rotation") - except Exception as e: - logger.error(f"Failed to setup file logging: {e}") + # Create logs directory if it doesn't exist + logs_dir = "logs" + if not os.path.exists(logs_dir): + os.makedirs(logs_dir) - return logger \ No newline at end of file + try: + # === MAIN LOG FILE (Rotating) === + main_log_handler = logging.handlers.RotatingFileHandler( + os.path.join(logs_dir, 'duckhunt.log'), + maxBytes=20*1024*1024, # 20MB + backupCount=10, + encoding='utf-8' + ) + main_log_handler.setLevel(file_level) + if unified_format: + main_log_formatter = UnifiedFormatter(use_colors=False) + else: + main_log_formatter = EnhancedFileFormatter() + main_log_handler.setFormatter(main_log_formatter) + logger.addHandler(main_log_handler) + + # Log initialization success with config info + logger.info("Unified logging system initialized: all logs in duckhunt.log") + logger.info(f"Debug mode: {'ON' if debug_enabled else 'OFF'}") + logger.info(f"Log everything: {'YES' if log_everything else 'NO'}") + logger.info(f"Unified format: {'YES' if unified_format else 'NO'}") + logger.info(f"Console level: {logging.getLevelName(console_level)}") + logger.info(f"File level: {logging.getLevelName(file_level)}") + logger.info(f"Main log: {main_log_handler.baseFilename}") + + except Exception as e: + # Fallback to simple file logging + try: + simple_handler = logging.FileHandler('duckhunt_fallback.log', encoding='utf-8') + simple_handler.setLevel(logging.DEBUG) + simple_formatter = logging.Formatter( + '%(asctime)s [%(levelname)-8s] %(name)s: %(message)s' + ) + simple_handler.setFormatter(simple_formatter) + logger.addHandler(simple_handler) + logger.error(f"❌ Failed to setup enhanced file logging: {e}") + logger.info("📝 Using fallback file logging") + except Exception as fallback_error: + logger.error(f"💥 Complete logging setup failure: {fallback_error}") + + return logger + + +def get_performance_logger(): + """Get a specialized logger for performance metrics""" + return setup_logger("DuckHuntBot.Performance", console_level=logging.WARNING) + + +def log_with_context(logger, level, message, **context): + """Log a message with additional context information""" + record = logger.makeRecord( + logger.name, level, '', 0, message, (), None + ) + + # Add context attributes to the record + for key, value in context.items(): + setattr(record, key, value) + + logger.handle(record) \ No newline at end of file