yeah
This commit is contained in:
39
config.json
39
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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 <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_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",
|
||||
|
||||
24
src/db.py
24
src/db.py
@@ -262,3 +262,27 @@ class DuckDB:
|
||||
'inventory': {},
|
||||
'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.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 <player>")
|
||||
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 <player>")
|
||||
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 <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')
|
||||
|
||||
# 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()
|
||||
|
||||
@@ -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"""
|
||||
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):
|
||||
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"):
|
||||
"""Setup logger with console and file handlers"""
|
||||
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)
|
||||
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)
|
||||
# Create logs directory if it doesn't exist
|
||||
logs_dir = "logs"
|
||||
if not os.path.exists(logs_dir):
|
||||
os.makedirs(logs_dir)
|
||||
|
||||
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}")
|
||||
|
||||
logger.info("Enhanced logging system initialized with file rotation")
|
||||
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
|
||||
|
||||
|
||||
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