This commit is contained in:
2025-09-25 19:47:44 +01:00
parent eb041477dc
commit 5484548c30
6 changed files with 641 additions and 114 deletions

View File

@@ -2,30 +2,33 @@
"connection": { "connection": {
"server": "irc.rizon.net", "server": "irc.rizon.net",
"port": 6697, "port": 6697,
"nick": "DickHunt", "nick": "DuckHunt",
"channels": ["#ct"], "channels": [
"#ct"
],
"ssl": true, "ssl": true,
"password": "your_iline_password_here", "password": "duckyhunt789",
"max_retries": 3, "max_retries": 3,
"retry_delay": 5, "retry_delay": 5,
"timeout": 30 "timeout": 30
}, },
"sasl": { "sasl": {
"enabled": false, "enabled": true,
"username": "duckhunt", "username": "duckhunt",
"password": "duckhunt//789//" "password": "duckhunt//789//"
}, },
"admins": ["peorth", "computertech", "colby"], "admins": [
"peorth",
"computertech",
"colby"
],
"duck_spawning": { "duck_spawning": {
"spawn_min": 10, "spawn_min": 10,
"spawn_max": 30, "spawn_max": 30,
"timeout": 60, "timeout": 60,
"rearm_on_duck_shot": true "rearm_on_duck_shot": true
}, },
"duck_types": { "duck_types": {
"normal": { "normal": {
"xp": 10, "xp": 10,
@@ -44,7 +47,6 @@
"xp": 12 "xp": 12
} }
}, },
"player_defaults": { "player_defaults": {
"accuracy": 75, "accuracy": 75,
"magazines": 3, "magazines": 3,
@@ -52,7 +54,6 @@
"jam_chance": 15, "jam_chance": 15,
"xp": 0 "xp": 0
}, },
"gameplay": { "gameplay": {
"befriend_success_rate": 75, "befriend_success_rate": 75,
"befriend_xp": 5, "befriend_xp": 5,
@@ -63,15 +64,29 @@
"min_befriend_success_rate": 5, "min_befriend_success_rate": 5,
"max_befriend_success_rate": 95 "max_befriend_success_rate": 95
}, },
"features": { "features": {
"shop_enabled": true, "shop_enabled": true,
"inventory_enabled": true, "inventory_enabled": true,
"auto_rearm_enabled": true "auto_rearm_enabled": true
}, },
"limits": { "limits": {
"max_inventory_items": 20, "max_inventory_items": 20,
"max_temp_effects": 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
} }
} }

View File

@@ -2,17 +2,18 @@
"players": { "players": {
"computertech": { "computertech": {
"nick": "ComputerTech", "nick": "ComputerTech",
"xp": 45, "xp": 60,
"ducks_shot": 4, "ducks_shot": 5,
"accuracy": 61, "ducks_befriended": 2,
"accuracy": 62,
"gun_confiscated": false, "gun_confiscated": false,
"ducks_befriended": 1, "current_ammo": 5,
"inventory": {},
"temporary_effects": [],
"current_ammo": 6,
"magazines": 3, "magazines": 3,
"bullets_per_magazine": 6 "bullets_per_magazine": 6,
"jam_chance": 5,
"inventory": {},
"temporary_effects": []
} }
}, },
"last_save": "1758654759.6627305" "last_save": "1758825127.4272072"
} }

View File

@@ -6,7 +6,7 @@
], ],
"duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`", "duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`",
"fast_duck_flies_away": "The fast duck quickly 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": "{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": "{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}]", "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...", "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_success": "{nick} > *click* New magazine loaded! [Ammo: {ammo}/{max_ammo}] [Spare magazines: {chargers}]",
"reload_already_loaded": "{nick} > Your gun is already loaded!", "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.", "reload_not_armed": "{nick} > You are not armed.",
"shop_display": "DuckHunt Shop: {items} | You have {xp} XP", "shop_display": "DuckHunt Shop: {items} | You have {xp} XP",
"shop_item_format": "({id}) {name} - {price} XP", "shop_item_format": "({id}) {name} - {price} XP",
"help_header": "DuckHunt Commands:", "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_help_command": "!duckhelp - Show this help",
"help_admin_commands": "Admin: !rearm <player> | !disarm <player> | !ignore <player> | !unignore <player> | !ducklaunch", "help_admin_commands": "Admin: !rearm <player> | !disarm <player> | !ignore <player> | !unignore <player> | !ducklaunch [duck_type] (all support /msg)",
"admin_rearm_player": "[ADMIN] {target} has been rearmed by {admin}", "admin_rearm_player": "[ADMIN] {target} has been rearmed by {admin}",
"admin_rearm_all": "[ADMIN] All players have been rearmed by {admin}", "admin_rearm_all": "[ADMIN] All players have been rearmed by {admin}",
"admin_rearm_self": "[ADMIN] {admin} has rearmed themselves", "admin_rearm_self": "[ADMIN] {admin} has rearmed themselves",
@@ -58,6 +58,7 @@
"purple": "\u00036", "purple": "\u00036",
"orange": "\u00037", "orange": "\u00037",
"yellow": "\u00038", "yellow": "\u00038",
"gold": "\u00038",
"light_green": "\u00039", "light_green": "\u00039",
"cyan": "\u000310", "cyan": "\u000310",
"light_cyan": "\u000311", "light_cyan": "\u000311",

View File

@@ -262,3 +262,27 @@ class DuckDB:
'inventory': {}, 'inventory': {},
'temporary_effects': [] 'temporary_effects': []
} }
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 []

View File

@@ -26,6 +26,8 @@ class DuckHuntBot:
self.channels_joined = set() self.channels_joined = set()
self.shutdown_requested = False self.shutdown_requested = False
self.logger.info("🤖 Initializing DuckHunt Bot components...")
self.db = DuckDB(bot=self) self.db = DuckDB(bot=self)
self.game = DuckGame(self, self.db) self.game = DuckGame(self, self.db)
self.messages = MessageManager() self.messages = MessageManager()
@@ -34,6 +36,7 @@ class DuckHuntBot:
admins_list = self.get_config('admins', ['colby']) or ['colby'] admins_list = self.get_config('admins', ['colby']) or ['colby']
self.admins = [admin.lower() for admin in admins_list] 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 # Initialize shop manager
shop_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shop.json') 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}") self.logger.error(f"Error sanitizing/sending message: {e}")
return False return False
async def register_user(self): async def send_server_password(self):
"""Register user with IRC server""" """Send server password if configured (must be sent immediately after connection)"""
password = self.get_config('connection.password') password = self.get_config('connection.password')
if password and password != "your_iline_password_here": if password and password != "your_iline_password_here":
self.logger.info("🔐 Sending server password")
self.send_raw(f"PASS {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') nick = self.get_config('connection.nick', 'DuckHunt')
self.send_raw(f"NICK {nick}") self.send_raw(f"NICK {nick}")
self.send_raw(f"USER {nick} 0 * :{nick}") self.send_raw(f"USER {nick} 0 * :{nick}")
@@ -327,6 +335,8 @@ class DuckHuntBot:
await self.handle_shop(nick, channel, player, args) await self.handle_shop(nick, channel, player, args)
elif cmd == "duckstats": elif cmd == "duckstats":
await self.handle_duckstats(nick, channel, player) await self.handle_duckstats(nick, channel, player)
elif cmd == "topduck":
await self.handle_topduck(nick, channel)
elif cmd == "use": elif cmd == "use":
await self.handle_use(nick, channel, player, args) await self.handle_use(nick, channel, player, args)
elif cmd == "duckhelp": elif cmd == "duckhelp":
@@ -496,6 +506,45 @@ class DuckHuntBot:
for line in stats_lines: for line in stats_lines:
self.send_message(channel, line) 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): async def handle_duckhelp(self, nick, channel, _player):
"""Handle !duckhelp command""" """Handle !duckhelp command"""
help_lines = [ help_lines = [
@@ -565,7 +614,9 @@ class DuckHuntBot:
self.send_message(channel, message) self.send_message(channel, message)
async def handle_rearm(self, nick, channel, args): 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: if args:
target = args[0].lower() target = args[0].lower()
player = self.db.get_player(target) player = self.db.get_player(target)
@@ -577,10 +628,17 @@ class DuckHuntBot:
self.levels.update_player_magazines(player) self.levels.update_player_magazines(player)
player['current_ammo'] = player.get('bullets_per_magazine', 6) 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) self.send_message(channel, message)
else: else:
# Rearm the admin themselves if is_private_msg:
self.send_message(channel, f"{nick} > Usage: !rearm <player>")
return
# Rearm the admin themselves (only in channels)
player = self.db.get_player(nick) player = self.db.get_player(nick)
if player is None: if player is None:
player = {} player = {}
@@ -596,47 +654,162 @@ class DuckHuntBot:
self.db.save_database() self.db.save_database()
async def handle_disarm(self, nick, channel, args): async def handle_disarm(self, nick, channel, args):
"""Handle !disarm command (admin only)""" """Handle !disarm command (admin only) - supports private messages"""
def disarm_player(player): is_private_msg = not channel.startswith('#')
player['gun_confiscated'] = True
self._handle_single_target_admin_command( if not args:
args, 'usage_disarm', disarm_player, 'admin_disarm', nick, channel if is_private_msg:
) self.send_message(channel, f"{nick} > Usage: !disarm <player>")
else:
async def handle_ignore(self, nick, channel, args): message = self.messages.get('usage_disarm')
"""Handle !ignore command (admin only)""" self.send_message(channel, message)
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)
return return
# Force spawn a duck target = args[0].lower()
if channel not in self.game.ducks: player = self.db.get_player(target)
self.game.ducks[channel] = [] if player is None:
self.game.ducks[channel].append({"spawn_time": time.time()}) 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 <player>")
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 <player>")
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') duck_message = self.messages.get('duck_spawn')
# Only send the duck spawn message, no admin notification # Send duck spawn message to target channel
self.send_message(channel, duck_message) 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): async def message_loop(self):
@@ -720,6 +893,9 @@ class DuckHuntBot:
try: try:
await self.connect() await self.connect()
# Send server password immediately after connection (RFC requirement)
await self.send_server_password()
# Check if SASL should be used # Check if SASL should be used
if self.sasl_handler.should_authenticate(): if self.sasl_handler.should_authenticate():
await self.sasl_handler.start_negotiation() await self.sasl_handler.start_negotiation()

View File

@@ -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
import logging.handlers import logging.handlers
import os
import sys
from datetime import datetime
class DetailedColourFormatter(logging.Formatter): def load_config():
"""Console formatter with colour support""" """Load configuration from config.json"""
COLOURS = { try:
'DEBUG': '\033[94m', with open('config.json', 'r') as f:
'INFO': '\033[92m', config = json.load(f)
'WARNING': '\033[93m', return config
'ERROR': '\033[91m', except Exception as e:
'CRITICAL': '\033[95m', print(f"Warning: Could not load config.json: {e}")
'ENDC': '\033[0m' 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): def format(self, record):
colour = self.COLOURS.get(record.levelname, '') # Get colors
endc = self.COLOURS['ENDC'] level_color = self.COLORS.get(record.levelname, '')
msg = super().format(record) component_color = self.COMPONENT_COLORS.get(record.name, '\033[37m') # Default gray
return f"{colour}{msg}{endc}" 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): class EnhancedFileFormatter(logging.Formatter):
"""File formatter with extra context but no colours""" """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): def format(self, record):
return super().format(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
def setup_logger(name="DuckHuntBot"): class UnifiedFormatter(logging.Formatter):
"""Setup logger with console and file handlers""" """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 = logging.getLogger(name)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG if debug_enabled else logging.WARNING)
# Clear existing handlers to avoid duplicates
logger.handlers.clear() logger.handlers.clear()
console_handler = logging.StreamHandler() # === CONSOLE HANDLER ===
console_handler.setLevel(logging.INFO) console_handler = logging.StreamHandler(sys.stdout)
console_formatter = DetailedColourFormatter( console_handler.setLevel(console_level)
'%(asctime)s [%(levelname)s] %(name)s: %(message)s'
) # 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) console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler) logger.addHandler(console_handler)
try: # Create logs directory if it doesn't exist
file_handler = logging.handlers.RotatingFileHandler( logs_dir = "logs"
'duckhunt.log', if not os.path.exists(logs_dir):
maxBytes=10*1024*1024, os.makedirs(logs_dir)
backupCount=5
) try:
file_handler.setLevel(logging.DEBUG) # === MAIN LOG FILE (Rotating) ===
file_formatter = DetailedFileFormatter( main_log_handler = logging.handlers.RotatingFileHandler(
'%(asctime)s [%(levelname)-8s] %(name)s - %(funcName)s:%(lineno)d: %(message)s' os.path.join(logs_dir, 'duckhunt.log'),
) maxBytes=20*1024*1024, # 20MB
file_handler.setFormatter(file_formatter) backupCount=10,
logger.addHandler(file_handler) 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}")
logger.info("Enhanced logging system initialized with file rotation")
except Exception as e: except Exception as e:
logger.error(f"Failed to setup file logging: {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 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)