yeah
This commit is contained in:
39
config.json
39
config.json
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
24
src/db.py
24
src/db.py
@@ -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 []
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user