From 85fa8a9170a7e13ab26a945c28ab89882e6e4748 Mon Sep 17 00:00:00 2001 From: ComputerTech312 Date: Sun, 5 Oct 2025 19:18:46 +0100 Subject: [PATCH] Fix database corruption handling and auto-creation - Added datetime import to fix NameError - Simplified database handling to create new file if missing or corrupted - Removed backup functionality per user request - Fixed duplicate method definitions - Enhanced error handling throughout database operations - Auto-creates duckhunt.json with proper structure on startup --- ADMIN_SECURITY.md | 46 -- duckhunt.json | 669 +++++++++++++++--- logs/duckhunt.log | 238 +++++++ src/__pycache__/db.cpython-312.pyc | Bin 15234 -> 17183 bytes src/__pycache__/duckhuntbot.cpython-312.pyc | Bin 62537 -> 66997 bytes src/__pycache__/game.cpython-312.pyc | Bin 25585 -> 25700 bytes src/__pycache__/levels.cpython-312.pyc | Bin 10514 -> 10514 bytes src/__pycache__/logging_utils.cpython-312.pyc | Bin 14028 -> 13976 bytes src/__pycache__/sasl.cpython-312.pyc | Bin 11011 -> 11011 bytes src/__pycache__/shop.cpython-312.pyc | Bin 21003 -> 21003 bytes src/__pycache__/utils.cpython-312.pyc | Bin 10447 -> 10447 bytes src/db.py | 179 +++-- src/duckhuntbot.py | 286 ++++++-- src/error_handling.py | 262 +++++++ src/utils.py | 107 ++- 15 files changed, 1542 insertions(+), 245 deletions(-) delete mode 100644 ADMIN_SECURITY.md create mode 100644 src/error_handling.py diff --git a/ADMIN_SECURITY.md b/ADMIN_SECURITY.md deleted file mode 100644 index 5da2462..0000000 --- a/ADMIN_SECURITY.md +++ /dev/null @@ -1,46 +0,0 @@ -# Enhanced Admin Configuration - -For better security, update your `config.json` to use hostmask-based admin authentication: - -## Current (Less Secure) - Nick Only: -```json -{ - "admins": [ - "peorth", - "computertech", - "colby" - ] -} -``` - -## Recommended (More Secure) - Hostmask Based: -```json -{ - "admins": [ - { - "nick": "peorth", - "hostmask": "peorth!*@trusted.domain.com" - }, - { - "nick": "computertech", - "hostmask": "computertech!*@*.isp.net" - }, - { - "nick": "colby", - "hostmask": "colby!user@192.168.*.*" - } - ] -} -``` - -## Migration Notes: -- The bot supports both formats for backward compatibility -- Nick-only authentication generates security warnings in logs -- Hostmask patterns use shell-style wildcards (* and ?) -- Consider using registered nick services for additional security - -## Security Benefits: -- Prevents nick spoofing attacks -- Allows IP/hostname restrictions -- Provides audit logging of admin access -- Maintains backward compatibility during migration \ No newline at end of file diff --git a/duckhunt.json b/duckhunt.json index 5a09b06..6c9cfe6 100644 --- a/duckhunt.json +++ b/duckhunt.json @@ -13,8 +13,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "mysteria": { "nick": "Mysteria", @@ -27,21 +40,25 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 5, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 5, + "confiscated_ammo": 5, + "confiscated_magazines": 3, "inventory": { "1": 1 }, - "temporary_effects": [ - { - "type": "insurance", - "protection": "friendly_fire", - "expires_at": 1759416116.11089, - "name": "Hunter's Insurance" - } - ] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "wobotkoala": { "nick": "WobotKoala", @@ -54,14 +71,25 @@ "gun_confiscated": false, "current_ammo": 4, "magazines": 3, - "confiscated_ammo": 5, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 5, + "confiscated_magazines": 3, "inventory": { "7": 1 }, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "boliver": { "nick": "Boliver", @@ -74,14 +102,25 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 5, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 5, + "confiscated_magazines": 3, "inventory": { "7": 1 }, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "papafrog": { "nick": "PapaFrog", @@ -96,8 +135,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "leetcode": { "nick": "leetcode", @@ -110,12 +162,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 5, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 5, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "overfl0w": { "nick": "overfl0w", @@ -130,8 +193,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "magicalpig": { "nick": "MagicalPig", @@ -144,12 +220,23 @@ "gun_confiscated": false, "current_ammo": 3, "magazines": 3, - "confiscated_ammo": 3, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 3, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "milambar": { "nick": "Milambar", @@ -164,8 +251,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "loulan": { "nick": "loulan", @@ -178,12 +278,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 3, - "confiscated_magazines": 2, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 3, + "confiscated_magazines": 2, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "h4": { "nick": "h4", @@ -196,12 +307,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 5, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 5, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "basti": { "nick": "Basti", @@ -216,8 +338,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "number1stunna": { "nick": "Number1Stunna", @@ -232,8 +367,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "wez": { "nick": "wez", @@ -248,8 +396,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "sander": { "nick": "Sander", @@ -264,8 +425,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "xternal": { "nick": "xternal", @@ -278,12 +452,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 4, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 4, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "wh": { "nick": "wh", @@ -298,8 +483,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "updog": { "nick": "updog", @@ -314,8 +512,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "hks": { "nick": "Hks", @@ -328,12 +539,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 5, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 5, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "wildting2": { "nick": "wildting2", @@ -348,8 +570,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "girafe2": { "nick": "girafe2", @@ -364,8 +599,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "kaitphone": { "nick": "kaitphone", @@ -378,12 +626,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 4, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 4, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "parabirb": { "nick": "parabirb", @@ -396,12 +655,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 5, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 5, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "deimos": { "nick": "DEIMOS", @@ -416,8 +686,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "twentytwo": { "nick": "twentytwo", @@ -430,12 +713,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 5, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 5, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "dg": { "nick": "DG", @@ -450,8 +744,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "helderheid": { "nick": "HelderHeid", @@ -466,8 +773,21 @@ "magazines": 2, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "neo_nemesis": { "nick": "Neo_Nemesis", @@ -482,8 +802,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "jeff_stacey": { "nick": "jeff_stacey", @@ -498,8 +831,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "p1x4l": { "nick": "p1x4l", @@ -512,12 +858,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 3, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 3, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "deadly": { "nick": "deadly", @@ -532,8 +889,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "alterego_": { "nick": "AlterEgo_", @@ -548,8 +918,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "peorth": { "nick": "Peorth", @@ -564,8 +947,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "magic": { "nick": "MAGIC", @@ -580,8 +976,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "koaladinosaur": { "nick": "KoalaDinosaur", @@ -596,8 +1005,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "knownsyntax": { "nick": "KnownSyntax", @@ -612,8 +1034,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "fifel": { "nick": "fifel", @@ -628,8 +1063,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "madafaka": { "nick": "Madafaka", @@ -642,12 +1090,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 3, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 3, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "antitheus": { "nick": "antitheus", @@ -660,12 +1119,23 @@ "gun_confiscated": false, "current_ammo": 6, "magazines": 3, - "confiscated_ammo": 5, - "confiscated_magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 5, + "confiscated_magazines": 3, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "l0op": { "nick": "L0op", @@ -680,8 +1150,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "norex": { "nick": "norex", @@ -696,8 +1179,21 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 }, "brandon": { "nick": "brandon", @@ -712,9 +1208,22 @@ "magazines": 3, "bullets_per_magazine": 6, "jam_chance": 15, + "confiscated_ammo": 0, + "confiscated_magazines": 0, "inventory": {}, - "temporary_effects": [] + "temporary_effects": [], + "best_time": 0.0, + "worst_time": 0.0, + "total_time_hunting": 0.0, + "level": 1, + "xp_gained": 0, + "hp_remaining": 0, + "victim": "", + "xp_lost": 0, + "ammo": 0, + "max_ammo": 0, + "chargers": 0 } }, - "last_save": "1759345603.3433115" + "last_save": "1759518733.8347418" } \ No newline at end of file diff --git a/logs/duckhunt.log b/logs/duckhunt.log index 4f54ee2..277b3a1 100644 --- a/logs/duckhunt.log +++ b/logs/duckhunt.log @@ -585,3 +585,241 @@ 19:48:26.215 📘 INFO DuckHuntBot 💾 Database saved 19:48:26.422 📘 INFO DuckHuntBot 🔌 IRC connection closed 19:48:26.422 📘 INFO DuckHuntBot ✅ Bot shutdown complete +19:21:05.583 INFO INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +19:21:05.584 INFO INFO DuckHuntBot Debug mode: ON +19:21:05.585 INFO INFO DuckHuntBot Log everything: YES +19:21:05.585 INFO INFO DuckHuntBot Unified format: YES +19:21:05.586 INFO INFO DuckHuntBot Console level: INFO +19:21:05.587 INFO INFO DuckHuntBot File level: DEBUG +19:21:05.588 INFO INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +19:21:05.589 INFO INFO DuckHuntBot 🤖 Initializing DuckHunt Bot components... +19:21:05.590 INFO INFO DuckHuntBot.DB Loaded 2 players from duckhunt.json +19:21:05.594 INFO INFO SASL Unified logging system initialized: all logs in duckhunt.log +19:21:05.594 INFO INFO SASL Debug mode: ON +19:21:05.595 INFO INFO SASL Log everything: YES +19:21:05.595 INFO INFO SASL Unified format: YES +19:21:05.595 INFO INFO SASL Console level: INFO +19:21:05.600 INFO INFO SASL File level: DEBUG +19:21:05.603 INFO INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +19:21:05.605 INFO INFO DuckHuntBot Configured 3 admin(s): peorth, computertech, colby +19:21:05.608 INFO INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +19:21:05.610 INFO INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +19:21:05.610 INFO INFO DuckHuntBot Starting DuckHunt Bot... +19:21:05.758 INFO INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +19:21:06.064 INFO INFO DuckHuntBot Successfully connected to irc.rizon.net:6697 +19:21:06.065 INFO INFO DuckHuntBot 🔐 Sending server password +19:21:06.066 INFO INFO DuckHuntBot Bot is now running! Press Ctrl+C to stop. +19:21:08.087 INFO INFO DuckHuntBot Successfully registered with IRC server +19:21:10.364 INFO INFO DuckHuntBot 🛑 Received SIGTERM (Ctrl+C), shutting down immediately... +19:21:10.365 INFO INFO DuckHuntBot Cancelled 5 running tasks +19:21:10.386 INFO INFO DuckHuntBot.Game Duck spawning loop cancelled +19:21:10.386 INFO INFO DuckHuntBot.Game Duck timeout loop cancelled +19:21:10.386 INFO INFO DuckHuntBot Message loop cancelled +19:21:10.387 INFO INFO DuckHuntBot Message loop ended +19:21:10.387 INFO INFO DuckHuntBot 🛑 Main loop cancelled +19:21:10.387 INFO INFO DuckHuntBot.Game Game loops cancelled +19:21:10.410 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 2 players [save_database:142] +19:21:10.410 INFO INFO DuckHuntBot Database saved +19:21:10.617 INFO INFO DuckHuntBot 🔌 IRC connection closed +19:21:10.620 INFO INFO DuckHuntBot Bot shutdown complete +19:23:31.803 INFO INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +19:23:31.803 INFO INFO DuckHuntBot Debug mode: ON +19:23:31.803 INFO INFO DuckHuntBot Log everything: YES +19:23:31.804 INFO INFO DuckHuntBot Unified format: YES +19:23:31.804 INFO INFO DuckHuntBot Console level: INFO +19:23:31.804 INFO INFO DuckHuntBot File level: DEBUG +19:23:31.804 INFO INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +19:23:31.804 INFO INFO DuckHuntBot 🤖 Initializing DuckHunt Bot components... +19:23:31.805 INFO INFO DuckHuntBot.DB Loaded 2 players from duckhunt.json +19:23:31.806 INFO INFO SASL Unified logging system initialized: all logs in duckhunt.log +19:23:31.806 INFO INFO SASL Debug mode: ON +19:23:31.806 INFO INFO SASL Log everything: YES +19:23:31.806 INFO INFO SASL Unified format: YES +19:23:31.806 INFO INFO SASL Console level: INFO +19:23:31.807 INFO INFO SASL File level: DEBUG +19:23:31.807 INFO INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +19:23:31.807 INFO INFO DuckHuntBot Configured 3 admin(s): peorth, computertech, colby +19:23:31.807 INFO INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +19:23:31.808 INFO INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +19:23:31.808 INFO INFO DuckHuntBot Starting DuckHunt Bot... +19:23:31.894 INFO INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +19:23:32.048 INFO INFO DuckHuntBot Successfully connected to irc.rizon.net:6697 +19:23:32.048 INFO INFO DuckHuntBot 🔐 Sending server password +19:23:32.049 INFO INFO DuckHuntBot Bot is now running! Press Ctrl+C to stop. +19:23:34.088 INFO INFO DuckHuntBot Successfully registered with IRC server +19:23:38.982 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:23:41.709 INFO INFO DuckHuntBot.Game Duck dropped Single Bullet for player ComputerTech +19:23:41.729 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 2 players [save_database:142] +19:23:50.254 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:23:51.613 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 2 players [save_database:142] +19:23:53.985 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:23:55.921 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 2 players [save_database:142] +19:23:57.354 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:23:58.121 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 2 players [save_database:142] +19:36:09.044 INFO INFO DuckHuntBot 🛑 Received SIGINT (Ctrl+C), shutting down immediately... +19:36:09.068 INFO INFO DuckHuntBot Cancelled 5 running tasks +19:36:09.087 INFO INFO DuckHuntBot.Game Duck spawning loop cancelled +19:36:09.088 INFO INFO DuckHuntBot.Game Duck timeout loop cancelled +19:36:09.089 INFO INFO DuckHuntBot Message loop cancelled +19:36:09.089 INFO INFO DuckHuntBot Message loop ended +19:36:09.090 INFO INFO DuckHuntBot 🛑 Main loop cancelled +19:36:09.094 INFO INFO DuckHuntBot.Game Game loops cancelled +19:36:09.121 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 2 players [save_database:142] +19:36:09.135 INFO INFO DuckHuntBot Database saved +19:36:09.388 INFO INFO DuckHuntBot 🔌 IRC connection closed +19:36:09.391 INFO INFO DuckHuntBot Bot shutdown complete +20:08:28.271 INFO INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +20:08:28.272 INFO INFO DuckHuntBot Debug mode: ON +20:08:28.272 INFO INFO DuckHuntBot Log everything: YES +20:08:28.273 INFO INFO DuckHuntBot Unified format: YES +20:08:28.273 INFO INFO DuckHuntBot Console level: INFO +20:08:28.273 INFO INFO DuckHuntBot File level: DEBUG +20:08:28.273 INFO INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +20:08:28.273 INFO INFO DuckHuntBot 🤖 Initializing DuckHunt Bot components... +20:08:28.274 INFO INFO DuckHuntBot.DB Loaded 2 players from duckhunt.json +20:08:28.275 INFO INFO SASL Unified logging system initialized: all logs in duckhunt.log +20:08:28.275 INFO INFO SASL Debug mode: ON +20:08:28.275 INFO INFO SASL Log everything: YES +20:08:28.275 INFO INFO SASL Unified format: YES +20:08:28.276 INFO INFO SASL Console level: INFO +20:08:28.276 INFO INFO SASL File level: DEBUG +20:08:28.276 INFO INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +20:08:28.276 INFO INFO DuckHuntBot Configured 3 admin(s): peorth, computertech, colby +20:08:28.277 INFO INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +20:08:28.278 INFO INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +20:08:28.279 INFO INFO DuckHuntBot Starting DuckHunt Bot... +20:08:28.370 INFO INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +20:08:28.894 INFO INFO DuckHuntBot Successfully connected to irc.rizon.net:6697 +20:08:28.895 INFO INFO DuckHuntBot 🔐 Sending server password +20:08:28.895 INFO INFO DuckHuntBot Bot is now running! Press Ctrl+C to stop. +20:08:30.110 INFO INFO DuckHuntBot Successfully registered with IRC server +20:08:39.693 INFO INFO DuckHuntBot 🛑 Received SIGINT (Ctrl+C), shutting down immediately... +20:08:39.693 INFO INFO DuckHuntBot Cancelled 5 running tasks +20:08:39.746 INFO INFO DuckHuntBot.Game Duck timeout loop cancelled +20:08:39.747 INFO INFO DuckHuntBot Message loop cancelled +20:08:39.747 INFO INFO DuckHuntBot Message loop ended +20:08:39.748 INFO INFO DuckHuntBot 🛑 Main loop cancelled +20:08:39.749 INFO INFO DuckHuntBot.Game Duck spawning loop cancelled +20:08:39.749 INFO INFO DuckHuntBot.Game Game loops cancelled +20:08:39.768 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 2 players [save_database:142] +20:08:39.768 INFO INFO DuckHuntBot Database saved +20:08:39.971 INFO INFO DuckHuntBot 🔌 IRC connection closed +20:08:39.972 INFO INFO DuckHuntBot Bot shutdown complete +20:08:44.634 INFO INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +20:08:44.634 INFO INFO DuckHuntBot Debug mode: ON +20:08:44.634 INFO INFO DuckHuntBot Log everything: YES +20:08:44.634 INFO INFO DuckHuntBot Unified format: YES +20:08:44.635 INFO INFO DuckHuntBot Console level: INFO +20:08:44.635 INFO INFO DuckHuntBot File level: DEBUG +20:08:44.635 INFO INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +20:08:44.635 INFO INFO DuckHuntBot 🤖 Initializing DuckHunt Bot components... +20:08:44.636 INFO INFO DuckHuntBot.DB Loaded 2 players from duckhunt.json +20:08:44.637 INFO INFO SASL Unified logging system initialized: all logs in duckhunt.log +20:08:44.637 INFO INFO SASL Debug mode: ON +20:08:44.637 INFO INFO SASL Log everything: YES +20:08:44.637 INFO INFO SASL Unified format: YES +20:08:44.638 INFO INFO SASL Console level: INFO +20:08:44.638 INFO INFO SASL File level: DEBUG +20:08:44.638 INFO INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +20:08:44.638 INFO INFO DuckHuntBot Configured 3 admin(s): peorth, computertech, colby +20:08:44.639 INFO INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +20:08:44.639 INFO INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +20:08:44.639 INFO INFO DuckHuntBot Starting DuckHunt Bot... +20:08:44.726 INFO INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +20:08:45.038 INFO INFO DuckHuntBot Successfully connected to irc.rizon.net:6697 +20:08:45.038 INFO INFO DuckHuntBot 🔐 Sending server password +20:08:45.039 INFO INFO DuckHuntBot Bot is now running! Press Ctrl+C to stop. +20:08:47.194 INFO INFO DuckHuntBot Successfully registered with IRC server +20:08:58.549 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 2 players [save_database:142] +19:38:43.828 INFO INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +19:38:43.829 INFO INFO DuckHuntBot Debug mode: ON +19:38:43.829 INFO INFO DuckHuntBot Log everything: YES +19:38:43.829 INFO INFO DuckHuntBot Unified format: YES +19:38:43.829 INFO INFO DuckHuntBot Console level: INFO +19:38:43.829 INFO INFO DuckHuntBot File level: DEBUG +19:38:43.829 INFO INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +19:38:43.830 INFO INFO DuckHuntBot 🤖 Initializing DuckHunt Bot components... +19:38:43.831 INFO INFO DuckHuntBot.DB Loaded 42 players from duckhunt.json +19:38:43.832 INFO INFO SASL Unified logging system initialized: all logs in duckhunt.log +19:38:43.832 INFO INFO SASL Debug mode: ON +19:38:43.832 INFO INFO SASL Log everything: YES +19:38:43.832 INFO INFO SASL Unified format: YES +19:38:43.832 INFO INFO SASL Console level: INFO +19:38:43.832 INFO INFO SASL File level: DEBUG +19:38:43.833 INFO INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +19:38:43.833 INFO INFO DuckHuntBot Configured 3 admin(s): peorth, computertech, colby +19:38:43.833 INFO INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +19:38:43.834 INFO INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +19:38:43.835 INFO INFO DuckHuntBot Starting DuckHunt Bot... +19:38:43.905 INFO INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +19:38:44.350 INFO INFO DuckHuntBot Successfully connected to irc.rizon.net:6697 +19:38:44.353 INFO INFO DuckHuntBot 🔐 Sending server password +19:38:44.354 INFO INFO DuckHuntBot Bot is now running! Press Ctrl+C to stop. +19:38:46.114 INFO INFO DuckHuntBot Successfully registered with IRC server +19:38:46.355 DEBUG DEBUG DuckHuntBot.Game Cleaned expired effects for mysteria [_clean_expired_effects:579] +19:38:56.847 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:38:57.430 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:38:57.945 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:38:58.593 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:38:59.335 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:38:59.949 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:39:00.540 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:39:01.158 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:39:01.836 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:40:15.791 INFO INFO DuckHuntBot 🛑 Received SIGINT (Ctrl+C), shutting down immediately... +19:40:15.794 INFO INFO DuckHuntBot Cancelled 5 running tasks +19:40:15.846 INFO INFO DuckHuntBot.Game Duck spawning loop cancelled +19:40:15.848 INFO INFO DuckHuntBot 🛑 Main loop cancelled +19:40:15.849 INFO INFO DuckHuntBot.Game Duck timeout loop cancelled +19:40:15.851 INFO INFO DuckHuntBot Message loop cancelled +19:40:15.853 INFO INFO DuckHuntBot Message loop ended +19:40:15.857 INFO INFO DuckHuntBot.Game Game loops cancelled +19:40:15.880 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 42 players [save_database:181] +19:40:15.881 INFO INFO DuckHuntBot Database saved +19:40:16.083 INFO INFO DuckHuntBot 🔌 IRC connection closed +19:40:16.084 INFO INFO DuckHuntBot Bot shutdown complete +19:40:16.577 INFO INFO DuckHuntBot Unified logging system initialized: all logs in duckhunt.log +19:40:16.577 INFO INFO DuckHuntBot Debug mode: ON +19:40:16.577 INFO INFO DuckHuntBot Log everything: YES +19:40:16.577 INFO INFO DuckHuntBot Unified format: YES +19:40:16.578 INFO INFO DuckHuntBot Console level: INFO +19:40:16.578 INFO INFO DuckHuntBot File level: DEBUG +19:40:16.578 INFO INFO DuckHuntBot Main log: /home/colby/duckhunt/logs/duckhunt.log +19:40:16.578 INFO INFO DuckHuntBot 🤖 Initializing DuckHunt Bot components... +19:40:16.579 INFO INFO DuckHuntBot.DB Loaded 42 players from duckhunt.json +19:40:16.580 INFO INFO SASL Unified logging system initialized: all logs in duckhunt.log +19:40:16.580 INFO INFO SASL Debug mode: ON +19:40:16.581 INFO INFO SASL Log everything: YES +19:40:16.581 INFO INFO SASL Unified format: YES +19:40:16.581 INFO INFO SASL Console level: INFO +19:40:16.581 INFO INFO SASL File level: DEBUG +19:40:16.581 INFO INFO SASL Main log: /home/colby/duckhunt/logs/duckhunt.log +19:40:16.581 INFO INFO DuckHuntBot Configured 3 admin(s): peorth, computertech, colby +19:40:16.582 INFO INFO DuckHuntBot.Levels Loaded 8 levels from /home/colby/duckhunt/levels.json +19:40:16.582 INFO INFO DuckHuntBot.Shop Loaded 9 shop items from /home/colby/duckhunt/shop.json +19:40:16.583 INFO INFO DuckHuntBot Starting DuckHunt Bot... +19:40:16.677 INFO INFO DuckHuntBot Attempting to connect to irc.rizon.net:6697 (attempt 1/3) +19:40:16.846 INFO INFO DuckHuntBot Successfully connected to irc.rizon.net:6697 +19:40:16.847 INFO INFO DuckHuntBot 🔐 Sending server password +19:40:16.848 INFO INFO DuckHuntBot Bot is now running! Press Ctrl+C to stop. +19:40:18.103 INFO INFO DuckHuntBot Successfully registered with IRC server +19:40:29.675 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:40:30.120 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:40:30.553 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:40:30.971 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:40:31.429 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:40:31.923 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +19:40:32.519 WARNING WARNING DuckHuntBot Admin access granted via nick-only authentication: ComputerTech!ComputerTe@ComputerTech.Network +20:03:55.179 INFO INFO DuckHuntBot.Game Normal duck spawned in #ct +20:12:13.757 INFO INFO DuckHuntBot 🛑 Received SIGINT (Ctrl+C), shutting down immediately... +20:12:13.761 INFO INFO DuckHuntBot Cancelled 5 running tasks +20:12:13.829 INFO INFO DuckHuntBot.Game Duck timeout loop cancelled +20:12:13.830 INFO INFO DuckHuntBot Message loop cancelled +20:12:13.830 INFO INFO DuckHuntBot Message loop ended +20:12:13.831 INFO INFO DuckHuntBot.Game Duck spawning loop cancelled +20:12:13.831 INFO INFO DuckHuntBot 🛑 Main loop cancelled +20:12:13.834 INFO INFO DuckHuntBot.Game Game loops cancelled +20:12:13.866 DEBUG DEBUG DuckHuntBot.DB Database saved successfully with 42 players [save_database:181] +20:12:13.868 INFO INFO DuckHuntBot Database saved +20:12:14.093 INFO INFO DuckHuntBot 🔌 IRC connection closed +20:12:14.096 INFO INFO DuckHuntBot Bot shutdown complete diff --git a/src/__pycache__/db.cpython-312.pyc b/src/__pycache__/db.cpython-312.pyc index 3eaf3570fc9f1961ebdf7c37c37616048ed7b6d4..e980035c6c3558eace9c46987771122430df853a 100644 GIT binary patch delta 6979 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 U*CI7_+~o9&%p`G4gpiT{FE(Q^82|tP delta 9275 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 G`~L#zVw17} diff --git a/src/__pycache__/game.cpython-312.pyc b/src/__pycache__/game.cpython-312.pyc index 8784b87614d860db05978288da02d1c41737aa09..c27db30629839487486bb148cf1d6ecc07a10f8b 100644 GIT binary patch delta 626 zcmex(obkyCM!wU$yj%=GV6*3LMq18BK2)#$#HT`o~W(Il)^pvptk7b zC#qVT7iqY2F|OVG%E*yb`3oC^jQotk1!~K+7HX{s*|-CyxMlMKdkaR!rpY%Q zyxBZXC|wruovh_p!{&6r@v?;5<~5G0ER07sUvkxEW;+aYanX^@%${P5j2|~kc`ak+ zJ`WP&1`!t~pY~N{y98ohp8U;Mk@3o82|s5hCZ^4?ezHsw*FX~2fy6ED^3;;}S0>L4c3@Q6d@ER%nGI~(&B=emN*%y1zW`LB z$yvk)QfLDrTtEcG6Cf7Im|J{_K%2@^<5SbpfOZxa-2us5nY=07hS7NQ`|vV0#)Qdf zaYl@VM!wU$yj%=Gka+J}#)IsQe5!Ixv-l=YaJJl>C&$S(`LU|@=4Tr2T#VhD z^^6@^8E6Yl^dV5{|TLMX`X+@c-c`5NJrODZo_uB_EZrd#EV8O^(H`&M0 zo6Y`!&}9*q$#WfR*z684UY2m$EbpYs!gye_tD816+fkssMaMRmd5SSIzTMpGwTzkj zB1niEL|mF|=cmec1;o5MInz&(@!I5WKWC zNx_yV)?W+O=7tyqGVR)Ao)8B{xy`;IvdnB?({4{L2rqR2JNOb%g(hbaA4s7Mh;RWB z5VwO^AY*RvB?4_KON~!WO9R?jTyzg4b8WI}gbkzq=A?)+HpZCAH{*;L-%jR<4`Tc{ zIX=FU4PyG_d-2t5A3@4LO^#2HWcv!yS(jkP=(2fl!el14Lm-j!lf#me*g}|D`99@L z-j{62$UgZ;vaA#bBT!Bdq<|kt{Nk|5%}*)KNwq8DoUEUs#$U|IDDjB_M1mCo01>UM Aw*UYD diff --git a/src/__pycache__/levels.cpython-312.pyc b/src/__pycache__/levels.cpython-312.pyc index 3646ac2ec1e7812917c42905d1b5de7c965a6882..cbb20c414735ddb20e81c53af98a0afb7a418b70 100644 GIT binary patch delta 20 acmbOfG%1MtG%qg~0}y;Zd}|{&yCwiaNd`Xv delta 20 acmbOfG%1MtG%qg~0}!m;dTk>&yCwiX_XXzw diff --git a/src/__pycache__/logging_utils.cpython-312.pyc b/src/__pycache__/logging_utils.cpython-312.pyc index 83560532bc8c64ae822b6c601cf957aee7a0321a..2ad6c34c1315bff84647e6632dd325f762045805 100644 GIT binary patch delta 1502 zcma*nUuauZ90%~6BsU33Z%xu^y>*Rt$)h->}xcJ$kx%Gu5@5 zjqVjc#rJ_}P!ybXa24$sh<6we4dW@n%Z?x2<0&c7tDcqdL`8Fc^J(F`g2nvRuE0YCrJNu*=s7&FQEx529)YrrZ>W8Rf$yw3lN*h-+Yenbz%8E;OX|9m|73^O9^{ACgE2)mwB#d z14GQ{&F+Mk?-2cbn4~DTSHtOrXdgRD??rbp)oev4-7B{WD&K{pgTvqn@FX}09s!Sn zL*Oy62v~vo;&E1_vG~qTCN=Y?_@`_U`BB)E;=gG!Z>aa9cv(@?R;v{X z3gd?8HJD(+t{Po5xlX?(M*mav1EI@dr?mACU$&EA-K->sogOQySz1j;1OF;#x{%%y zMw~>!T?@itx}4tbDT?mnDSAH{qdooS7OEJ0Rtl zU39(E6vum~GjFETKAh>tSUa@5mIin*t%O2Rry3T_h?WktQlu_A3^R8=VBU=0 zH&9|4>4M-!qw%;PB&8+8ppF-FtQMq^yLvBq@MnCSiAX(iaQ@GXAt z=G=4c$N8UguUp?(`q#Sduan=sg&*^GKGTnS;;~pMpDLw!#z>b-cDcxnY*|>TN`(t! zs!+*GbFnP!h{3J$`GWkXN*UuESM%$hg;I{Ldfw?@b<34k=01LZeL&Y&HEH?f#wWio zez01P;p}_sb06Jy!rJHc4I8er&d&``x!k+H4?Iu9VNPe7mY;q_Mu-G(9N-f3bs|g0 z^d0O3<@G&!`%)mZ)Dm6_hL&18XjV5_mcG>II`+Xo0`vj>z_Y-9XJ*@|i+&6AWW`Y! zVn7^t88{}f#{6cmw(qO34Ffp6I02jlMu8Dv40sJV1&jk;;56_$O$57p&tNzKB!RO) z3OEO(fehUYh8U-X;E_j3m~5WEl9jUwQxiFO<$(fl9#FYKTEqnauA&{VHh_WMBXPBE z*&SQ))ipnsvOgNNZQ4xy&p@}Qvlfm39BAWnuC;_1Ewt_qi2-R72Z2MtAaIzLTZN|X zH$|gu+u0>5wY|u)&fT_{8)?2GqVO96b_06=6L?boMOn_8t^ZGfDXgdpM%Bu{f?Ljx3G;ao-7W{1Ut2LbyhmUQk|x7RH@?(hdW%8$hXPjrhiLu{!_h|uqCvr>d9of zn6m9OFBX$YlZngHYBPx=HT7(`+VrsV@TLOaNqf5*H6^s|M7s{ScA;Wbj^FkfCu3uA zW7;qqHkB)p(c6rq$Vz&2;j{FWv0q+m&G>e(R%f0qud-E%O=I6_`n$WmcAROav&ZqV zDd)Sr&5V6WzwXQXFB;NmWsYVIN<>B)RX1Hj+&JBdj5C}5ik#Bkg~6iJeaG2|bF0tC z*nQ`2|5&~Ig&&%tj_W`d+deN{%zANOg#x`mt>$THpqnkyq-55a8St<-RKdA6JQAj- z>bFuC_!v)@MMgDk)l;Z&%dUEDUXd@IsA}>F%oMrl755N_-mH2pZriCGuX^RfSmBo| zVuF4;IKUoI%b}cVOL?T{q-P>w25S8nL@-`}YM@xo<>b5AUt3t%;wI*90O~1u2BS}b mFrXS%>8|psZn}tx2f*Xqz2+OTeC5o=B7aGIL2QuR(C#1VW@Een diff --git a/src/__pycache__/sasl.cpython-312.pyc b/src/__pycache__/sasl.cpython-312.pyc index 177f689a258bb26f98c76ce980845495fc3e7722..62c30d67301111440a7489b440435dece7a869da 100644 GIT binary patch delta 20 acmZn;YYyW+&CAQh00f^8-`dFiLkj>xf(IJ_ delta 20 acmZn;YYyW+&CAQh00e8dUfan1Lkj>vFb2Z_ diff --git a/src/__pycache__/shop.cpython-312.pyc b/src/__pycache__/shop.cpython-312.pyc index 0622191e2b804fa773a3c0dcb0841aff228463dc..a82f02402bbfae362e9a89453c2132d84216285d 100644 GIT binary patch delta 21 bcmeBP!q~lpk^3|+FBbz4d_H_@A$Je}Nxuf^ delta 21 bcmeBP!q~lpk^3|+FBbz41Yf_lkUIzfMpFh^ diff --git a/src/__pycache__/utils.cpython-312.pyc b/src/__pycache__/utils.cpython-312.pyc index b59912664a46c264cb57c82fb4ecac455de61141..2dd808ec9fb05b4276e2eed887de5eae1d410a9d 100644 GIT binary patch delta 20 acmX>fcs`K(G%qg~0}y;Zd}|~35e)!LtOs)d delta 20 acmX>fcs`K(G%qg~0}#x-e{Cc85e)!J$_Ft3 diff --git a/src/db.py b/src/db.py index 48cc7e6..194b407 100644 --- a/src/db.py +++ b/src/db.py @@ -1,12 +1,14 @@ """ -Simplified Database management for DuckHunt Bot -Focus on fixing missing field errors +Enhanced Database management for DuckHunt Bot +Focus on fixing missing field errors with improved error handling """ import json import logging import time import os +from datetime import datetime +from .error_handling import with_retry, RetryConfig, ErrorRecovery, sanitize_user_input class DuckDB: @@ -17,45 +19,85 @@ class DuckDB: self.bot = bot self.players = {} self.logger = logging.getLogger('DuckHuntBot.DB') + + # Error recovery configuration + self.error_recovery = ErrorRecovery() + self.save_retry_config = RetryConfig(max_attempts=3, base_delay=0.5, max_delay=5.0) + self.load_database() - def load_database(self): - """Load player data from JSON file with comprehensive error handling""" + def load_database(self) -> dict: + """Load the database, creating it if it doesn't exist""" try: - if os.path.exists(self.db_file): - with open(self.db_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - # Validate loaded data structure - if not isinstance(data, dict): - raise ValueError("Database root is not a dictionary") - - players_data = data.get('players', {}) - if not isinstance(players_data, dict): - raise ValueError("Players data is not a dictionary") + if not os.path.exists(self.db_file): + self.logger.info(f"Database file {self.db_file} not found, creating new one") + return self._create_default_database() + + with open(self.db_file, 'r') as f: + content = f.read().strip() - # Validate each player entry and ensure required fields - valid_players = {} - for nick, player_data in players_data.items(): - if isinstance(player_data, dict) and isinstance(nick, str): - # Sanitize and validate player data - valid_players[nick] = self._sanitize_player_data(player_data) - else: - self.logger.warning(f"Skipping invalid player entry: {nick}") - - self.players = valid_players - self.logger.info(f"Loaded {len(self.players)} players from {self.db_file}") - - else: - self.players = {} - self.logger.info("No existing database found, starting fresh") - - except (json.JSONDecodeError, UnicodeDecodeError) as e: - self.logger.error(f"Database file corrupted: {e}") - self.players = {} + if not content: + self.logger.warning("Database file is empty, creating new database") + return self._create_default_database() + + data = json.loads(content) + + # Validate basic structure + if not isinstance(data, dict): + raise ValueError("Database root is not a dictionary") + + # Initialize metadata if missing + if 'metadata' not in data: + data['metadata'] = { + 'version': '1.0', + 'created': datetime.now().isoformat(), + 'last_modified': datetime.now().isoformat() + } + + # Initialize players section if missing + if 'players' not in data: + data['players'] = {} + + # Update last_modified + data['metadata']['last_modified'] = datetime.now().isoformat() + + self.logger.info(f"Successfully loaded database with {len(data.get('players', {}))} players") + return data + + except (json.JSONDecodeError, ValueError) as e: + self.logger.error(f"Database corruption detected: {e}. Creating new database.") + return self._create_default_database() except Exception as e: self.logger.error(f"Error loading database: {e}") - self.players = {} + return self._create_default_database() + + def _create_default_database(self) -> dict: + """Create a new default database file with proper structure""" + try: + default_data = { + "players": {}, + "last_save": str(time.time()), + "version": "1.0", + "created": time.strftime("%Y-%m-%d %H:%M:%S"), + "description": "DuckHunt Bot Player Database" + } + + with open(self.db_file, 'w', encoding='utf-8') as f: + json.dump(default_data, f, indent=2, ensure_ascii=False, sort_keys=True) + + self.logger.info(f"Created new database file: {self.db_file}") + return default_data + + except Exception as e: + self.logger.error(f"Failed to create default database: {e}") + # Return a minimal valid structure even if file creation fails + return { + "players": {}, + "last_save": str(time.time()), + "version": "1.0", + "created": time.strftime("%Y-%m-%d %H:%M:%S"), + "description": "DuckHunt Bot Player Database" + } def _sanitize_player_data(self, player_data): """Sanitize and validate player data, ensuring ALL required fields exist""" @@ -146,30 +188,53 @@ class DuckDB: self.logger.error(f"Error sanitizing player data: {e}") return self.create_player(player_data.get('nick', 'Unknown') if isinstance(player_data, dict) else 'Unknown') + @with_retry(RetryConfig(max_attempts=3, base_delay=0.5, max_delay=5.0), + exceptions=(OSError, PermissionError, IOError)) def save_database(self): - """Save all player data to JSON file with comprehensive error handling""" + """Save all player data to JSON file with retry logic and comprehensive error handling""" + return self._save_database_impl() + + def _save_database_impl(self): + """Internal implementation of database save""" temp_file = f"{self.db_file}.tmp" try: # Prepare data with validation data = { 'players': {}, - 'last_save': str(time.time()) + 'last_save': str(time.time()), + 'version': '1.0' } # Validate and clean player data before saving + valid_count = 0 for nick, player_data in self.players.items(): if isinstance(nick, str) and isinstance(player_data, dict): - data['players'][nick] = self._sanitize_player_data(player_data) + try: + sanitized_nick = sanitize_user_input(nick, max_length=50) + data['players'][sanitized_nick] = self._sanitize_player_data(player_data) + valid_count += 1 + except Exception as e: + self.logger.warning(f"Error processing player {nick} during save: {e}") else: self.logger.warning(f"Skipping invalid player data during save: {nick}") + if valid_count == 0: + raise ValueError("No valid player data to save") + # Write to temporary file first (atomic write) with open(temp_file, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2, ensure_ascii=False) + json.dump(data, f, indent=2, ensure_ascii=False, sort_keys=True) f.flush() os.fsync(f.fileno()) + # Verify temp file was written correctly + try: + with open(temp_file, 'r', encoding='utf-8') as f: + json.load(f) # Verify it's valid JSON + except json.JSONDecodeError: + raise IOError("Temporary file contains invalid JSON") + # Atomic replace if os.name == 'nt': # Windows if os.path.exists(self.db_file): @@ -178,10 +243,12 @@ class DuckDB: else: # Unix-like systems os.rename(temp_file, self.db_file) - self.logger.debug(f"Database saved successfully with {len(data['players'])} players") + self.logger.debug(f"Database saved successfully with {valid_count} players") + return True except Exception as e: - self.logger.error(f"Error saving database: {e}") + self.logger.error(f"Error in database save implementation: {e}") + raise # Re-raise for retry mechanism finally: # Clean up temp file if it still exists try: @@ -196,26 +263,42 @@ class DuckDB: # Validate and sanitize nick if not isinstance(nick, str) or not nick.strip(): self.logger.warning(f"Invalid nick provided: {nick}") - return None + return self.error_recovery.safe_execute( + lambda: self.create_player('Unknown'), + fallback={'nick': 'Unknown', 'xp': 0, 'ducks_shot': 0}, + logger=self.logger + ) - nick_lower = nick.lower().strip()[:50] + # Sanitize nick input + nick_clean = sanitize_user_input(nick, max_length=50, + allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-[]{}^`|\\') + nick_lower = nick_clean.lower().strip() + + if not nick_lower: + self.logger.warning(f"Empty nick after sanitization: {nick}") + return self.create_player('Unknown') if nick_lower not in self.players: - self.players[nick_lower] = self.create_player(nick) + self.players[nick_lower] = self.create_player(nick_clean) else: # Ensure existing players have all required fields player = self.players[nick_lower] if not isinstance(player, dict): self.logger.warning(f"Invalid player data for {nick_lower}, recreating") - self.players[nick_lower] = self.create_player(nick) + self.players[nick_lower] = self.create_player(nick_clean) else: - # Migrate and validate existing player data - self.players[nick_lower] = self._migrate_and_validate_player(player, nick) + # Migrate and validate existing player data with error recovery + validated = self.error_recovery.safe_execute( + lambda: self._migrate_and_validate_player(player, nick_clean), + fallback=self.create_player(nick_clean), + logger=self.logger + ) + self.players[nick_lower] = validated return self.players[nick_lower] except Exception as e: - self.logger.error(f"Error getting player {nick}: {e}") + self.logger.error(f"Critical error getting player {nick}: {e}") return self.create_player(nick if isinstance(nick, str) else 'Unknown') def _migrate_and_validate_player(self, player, nick): diff --git a/src/duckhuntbot.py b/src/duckhuntbot.py index 68159dd..07286eb 100644 --- a/src/duckhuntbot.py +++ b/src/duckhuntbot.py @@ -12,6 +12,7 @@ from .game import DuckGame from .sasl import SASLHandler from .shop import ShopManager from .levels import LevelManager +from .error_handling import ErrorRecovery, HealthChecker, sanitize_user_input, safe_format_message class DuckHuntBot: @@ -26,12 +27,19 @@ class DuckHuntBot: self.logger.info("🤖 Initializing DuckHunt Bot components...") + # Initialize error recovery systems + self.error_recovery = ErrorRecovery() + self.health_checker = HealthChecker(check_interval=60.0) + self.db = DuckDB(bot=self) self.game = DuckGame(self, self.db) self.messages = MessageManager() self.sasl_handler = SASLHandler(self, config) + # Set up health checks + self._setup_health_checks() + 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)}") @@ -41,6 +49,34 @@ class DuckHuntBot: shop_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shop.json') self.shop = ShopManager(shop_file, self.levels) + + def _setup_health_checks(self): + """Set up health monitoring checks""" + try: + # Database health check + self.health_checker.add_check( + 'database', + lambda: self.db is not None and len(self.db.players) >= 0, + critical=True + ) + + # IRC connection health check + self.health_checker.add_check( + 'irc_connection', + lambda: self.writer is not None and not self.writer.is_closing(), + critical=True + ) + + # Message system health check + self.health_checker.add_check( + 'messages', + lambda: self.messages is not None and len(self.messages.messages) > 0, + critical=False + ) + + self.logger.debug("Health checks configured") + except Exception as e: + self.logger.error(f"Error setting up health checks: {e}") def get_config(self, path, default=None): keys = path.split('.') @@ -233,15 +269,63 @@ class DuckHuntBot: return False def send_message(self, target, msg): - """Send message to target (channel or user) with error handling""" + """Send message to target (channel or user) with enhanced error handling""" if not isinstance(target, str) or not isinstance(msg, str): self.logger.warning(f"Invalid message parameters: target={type(target)}, msg={type(msg)}") return False - + + return self.error_recovery.safe_execute( + lambda: self._send_message_impl(target, msg), + fallback=False, + logger=self.logger + ) + + def _send_message_impl(self, target, msg): + """Internal implementation of send_message""" try: - sanitized_msg = msg.replace('\r', '').replace('\n', ' ').strip() - if not sanitized_msg: + # Sanitize target and message + safe_target = sanitize_user_input(target, max_length=100, + allowed_chars='#&+!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-[]{}^`|\\') + safe_msg = sanitize_user_input(msg, max_length=400) + + if not safe_target or not safe_msg: + self.logger.warning(f"Empty target or message after sanitization") return False + + # Split long messages to prevent IRC limits + max_msg_length = 400 # IRC message limit minus PRIVMSG overhead + + if len(safe_msg) <= max_msg_length: + messages = [safe_msg] + else: + # Split into chunks + messages = [] + words = safe_msg.split(' ') + current_msg = '' + + for word in words: + if len(current_msg + ' ' + word) <= max_msg_length: + current_msg += (' ' if current_msg else '') + word + else: + if current_msg: + messages.append(current_msg) + current_msg = word[:max_msg_length] # Truncate very long words + + if current_msg: + messages.append(current_msg) + + # Send all message parts + success_count = 0 + for i, message_part in enumerate(messages): + if i > 0: # Small delay between messages to avoid flooding + time.sleep(0.1) + + if self.send_raw(f"PRIVMSG {safe_target} :{message_part}"): + success_count += 1 + else: + self.logger.error(f"Failed to send message part {i+1}/{len(messages)}") + + return success_count == len(messages) return self.send_raw(f"PRIVMSG {target} :{sanitized_msg}") except Exception as e: @@ -322,8 +406,9 @@ class DuckHuntBot: self.logger.error(f"Critical error in handle_message: {e}") async def handle_command(self, user, channel, message): - """Handle bot commands with comprehensive error handling""" + """Handle bot commands with enhanced error handling and input validation""" try: + # Validate input parameters if not isinstance(message, str) or not message.startswith('!'): return @@ -331,8 +416,16 @@ class DuckHuntBot: self.logger.warning(f"Invalid user/channel types: {type(user)}, {type(channel)}") return + # Sanitize inputs + safe_message = sanitize_user_input(message, max_length=500) + safe_user = sanitize_user_input(user, max_length=200) + safe_channel = sanitize_user_input(channel, max_length=100) + + if not safe_message.startswith('!'): + return + try: - parts = message[1:].split() + parts = safe_message[1:].split() except Exception as e: self.logger.warning(f"Error parsing command '{message}': {e}") return @@ -343,27 +436,39 @@ class DuckHuntBot: cmd = parts[0].lower() args = parts[1:] if len(parts) > 1 else [] - try: - nick = user.split('!')[0] if '!' in user else user - if not nick: - self.logger.warning(f"Empty nick from user string: {user}") - return - except Exception as e: - self.logger.error(f"Error extracting nick from '{user}': {e}") + # Extract and validate nick with enhanced error handling + nick = self.error_recovery.safe_execute( + lambda: safe_user.split('!')[0] if '!' in safe_user else safe_user, + fallback='Unknown', + logger=self.logger + ) + + if not nick or nick == 'Unknown': + self.logger.warning(f"Could not extract valid nick from user string: {user}") return - try: - player = self.db.get_player(nick) - if player is None: - player = {} - except Exception as e: - self.logger.error(f"Error getting player data for {nick}: {e}") - player = {} + # Sanitize nick further + nick = sanitize_user_input(nick, max_length=50, + allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-[]{}^`|\\') - if channel.startswith('#'): - player['last_activity_channel'] = channel - player['last_activity_time'] = time.time() - self.db.players[nick.lower()] = player + # Get player data with error recovery + player = self.error_recovery.safe_execute( + lambda: self.db.get_player(nick), + fallback={'nick': nick, 'xp': 0, 'ducks_shot': 0, 'gun_confiscated': False}, + logger=self.logger + ) + + if player is None: + player = {'nick': nick, 'xp': 0, 'ducks_shot': 0, 'gun_confiscated': False} + + # Update activity tracking safely + if safe_channel.startswith('#'): + try: + player['last_activity_channel'] = safe_channel + player['last_activity_time'] = time.time() + self.db.players[nick.lower()] = player + except Exception as e: + self.logger.warning(f"Error updating player activity for {nick}: {e}") try: if player.get('ignored', False) and not self.is_admin(user): @@ -372,49 +477,142 @@ class DuckHuntBot: self.logger.error(f"Error checking admin/ignore status: {e}") return - await self._execute_command_safely(cmd, nick, channel, player, args, user) + await self._execute_command_safely(cmd, nick, safe_channel, player, args, safe_user) except Exception as e: self.logger.error(f"Critical error in handle_command: {e}") async def _execute_command_safely(self, cmd, nick, channel, player, args, user): - """Execute individual commands with error isolation""" + """Execute individual commands with enhanced error isolation and user feedback""" try: + # Sanitize command arguments + safe_args = [] + for arg in args: + safe_arg = sanitize_user_input(str(arg), max_length=100) + if safe_arg: + safe_args.append(safe_arg) + + # Execute command with error recovery + command_executed = False + if cmd == "bang": - await self.handle_bang(nick, channel, player) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_bang(nick, channel, player), + fallback=None, + logger=self.logger + ) elif cmd == "bef" or cmd == "befriend": - await self.handle_bef(nick, channel, player) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_bef(nick, channel, player), + fallback=None, + logger=self.logger + ) elif cmd == "reload": - await self.handle_reload(nick, channel, player) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_reload(nick, channel, player), + fallback=None, + logger=self.logger + ) elif cmd == "shop": - await self.handle_shop(nick, channel, player, args) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_shop(nick, channel, player, safe_args), + fallback=None, + logger=self.logger + ) elif cmd == "duckstats": - await self.handle_duckstats(nick, channel, player, args) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_duckstats(nick, channel, player, safe_args), + fallback=None, + logger=self.logger + ) elif cmd == "topduck": - await self.handle_topduck(nick, channel) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_topduck(nick, channel), + fallback=None, + logger=self.logger + ) elif cmd == "use": - await self.handle_use(nick, channel, player, args) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_use(nick, channel, player, safe_args), + fallback=None, + logger=self.logger + ) elif cmd == "give": - await self.handle_give(nick, channel, player, args) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_give(nick, channel, player, safe_args), + fallback=None, + logger=self.logger + ) elif cmd == "duckhelp": - await self.handle_duckhelp(nick, channel, player) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_duckhelp(nick, channel, player), + fallback=None, + logger=self.logger + ) elif cmd == "rearm" and self.is_admin(user): - await self.handle_rearm(nick, channel, args) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_rearm(nick, channel, safe_args), + fallback=None, + logger=self.logger + ) elif cmd == "disarm" and self.is_admin(user): - await self.handle_disarm(nick, channel, args) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_disarm(nick, channel, safe_args), + fallback=None, + logger=self.logger + ) elif cmd == "ignore" and self.is_admin(user): - await self.handle_ignore(nick, channel, args) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_ignore(nick, channel, safe_args), + fallback=None, + logger=self.logger + ) elif cmd == "unignore" and self.is_admin(user): - await self.handle_unignore(nick, channel, args) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_unignore(nick, channel, safe_args), + fallback=None, + logger=self.logger + ) elif cmd == "ducklaunch" and self.is_admin(user): - await self.handle_ducklaunch(nick, channel, args) + command_executed = True + await self.error_recovery.safe_execute_async( + lambda: self.handle_ducklaunch(nick, channel, safe_args), + fallback=None, + logger=self.logger + ) + + # If no command was executed, it might be an unknown command + if not command_executed: + self.logger.debug(f"Unknown command '{cmd}' from {nick}") + except Exception as e: - self.logger.error(f"Error executing command '{cmd}' for user {nick}: {e}") + self.logger.error(f"Critical error executing command '{cmd}' for user {nick}: {e}") + + # Provide user-friendly error message try: - error_msg = f"{nick} > An error occurred processing your command. Please try again." - self.send_message(channel, error_msg) + if channel.startswith('#'): + error_msg = safe_format_message( + "{nick} > ⚠️ Something went wrong processing your command. Please try again in a moment.", + nick=nick + ) + self.send_message(channel, error_msg) + else: + self.logger.debug("Skipping error message for private channel") except Exception as send_error: - self.logger.error(f"Error sending error message: {send_error}") + self.logger.error(f"Error sending user error message: {send_error}") def validate_target_player(self, target_nick, channel): """ diff --git a/src/error_handling.py b/src/error_handling.py new file mode 100644 index 0000000..acd9211 --- /dev/null +++ b/src/error_handling.py @@ -0,0 +1,262 @@ +""" +Enhanced error handling utilities for DuckHunt Bot +Includes retry mechanisms, circuit breakers, and graceful degradation +""" + +import asyncio +import logging +import time +from functools import wraps +from typing import Callable, Any, Optional, Union + + +class RetryConfig: + """Configuration for retry mechanisms""" + def __init__(self, max_attempts: int = 3, base_delay: float = 1.0, + max_delay: float = 60.0, exponential: bool = True): + self.max_attempts = max_attempts + self.base_delay = base_delay + self.max_delay = max_delay + self.exponential = exponential + + +class CircuitBreaker: + """Circuit breaker pattern for preventing cascading failures""" + + def __init__(self, failure_threshold: int = 5, timeout: float = 60.0): + self.failure_threshold = failure_threshold + self.timeout = timeout + self.failure_count = 0 + self.last_failure_time = None + self.state = 'closed' # closed, open, half-open + self.logger = logging.getLogger('DuckHuntBot.CircuitBreaker') + + def __call__(self, func: Callable) -> Callable: + @wraps(func) + async def wrapper(*args, **kwargs): + if self.state == 'open': + if self.last_failure_time is not None and time.time() - self.last_failure_time > self.timeout: + self.state = 'half-open' + self.logger.info("Circuit breaker moving to half-open state") + else: + raise Exception("Circuit breaker is open - operation blocked") + + try: + result = await func(*args, **kwargs) + if self.state == 'half-open': + self.state = 'closed' + self.failure_count = 0 + self.logger.info("Circuit breaker closed - service recovered") + return result + except Exception as e: + self.failure_count += 1 + self.last_failure_time = time.time() + + if self.failure_count >= self.failure_threshold: + self.state = 'open' + self.logger.warning(f"Circuit breaker opened after {self.failure_count} failures") + + raise e + + return wrapper + + +def with_retry(config: Optional[RetryConfig] = None, + exceptions: tuple = (Exception,)): + """Decorator for adding retry logic to functions""" + + if config is None: + config = RetryConfig() + + def decorator(func: Callable) -> Callable: + @wraps(func) + async def async_wrapper(*args, **kwargs): + logger = logging.getLogger(f'DuckHuntBot.Retry.{func.__name__}') + + for attempt in range(config.max_attempts): + try: + return await func(*args, **kwargs) + except exceptions as e: + if attempt == config.max_attempts - 1: + logger.error(f"Function {func.__name__} failed after {config.max_attempts} attempts: {e}") + raise + + delay = config.base_delay + if config.exponential: + delay *= (2 ** attempt) + delay = min(delay, config.max_delay) + + logger.warning(f"Attempt {attempt + 1}/{config.max_attempts} failed for {func.__name__}: {e}. Retrying in {delay}s") + await asyncio.sleep(delay) + + return None + + @wraps(func) + def sync_wrapper(*args, **kwargs): + logger = logging.getLogger(f'DuckHuntBot.Retry.{func.__name__}') + + for attempt in range(config.max_attempts): + try: + return func(*args, **kwargs) + except exceptions as e: + if attempt == config.max_attempts - 1: + logger.error(f"Function {func.__name__} failed after {config.max_attempts} attempts: {e}") + raise + + delay = config.base_delay + if config.exponential: + delay *= (2 ** attempt) + delay = min(delay, config.max_delay) + + logger.warning(f"Attempt {attempt + 1}/{config.max_attempts} failed for {func.__name__}: {e}. Retrying in {delay}s") + time.sleep(delay) + + return None + + # Return appropriate wrapper based on whether function is async + if asyncio.iscoroutinefunction(func): + return async_wrapper + else: + return sync_wrapper + + return decorator + + +class ErrorRecovery: + """Error recovery and graceful degradation utilities""" + + @staticmethod + def safe_execute(func: Callable, fallback: Any = None, + log_errors: bool = True, logger: Optional[logging.Logger] = None) -> Any: + """Safely execute a function with fallback value on error""" + if logger is None: + logger = logging.getLogger('DuckHuntBot.ErrorRecovery') + + try: + return func() + except Exception as e: + if log_errors: + logger.error(f"Error executing {func.__name__}: {e}") + return fallback + + @staticmethod + async def safe_execute_async(func: Callable, fallback: Any = None, + log_errors: bool = True, logger: Optional[logging.Logger] = None) -> Any: + """Safely execute an async function with fallback value on error""" + if logger is None: + logger = logging.getLogger('DuckHuntBot.ErrorRecovery') + + try: + return await func() + except Exception as e: + if log_errors: + logger.error(f"Error executing {func.__name__}: {e}") + return fallback + + @staticmethod + def validate_input(value: Any, validator: Callable, default: Any = None, + field_name: str = "input") -> Any: + """Validate input with fallback to default""" + try: + if validator(value): + return value + else: + raise ValueError(f"Validation failed for {field_name}") + except Exception: + return default + + +class HealthChecker: + """Health monitoring and alerting""" + + def __init__(self, check_interval: float = 30.0): + self.check_interval = check_interval + self.checks = {} + self.logger = logging.getLogger('DuckHuntBot.Health') + + def add_check(self, name: str, check_func: Callable, critical: bool = False): + """Add a health check function""" + self.checks[name] = { + 'func': check_func, + 'critical': critical, + 'last_success': None, + 'failure_count': 0 + } + + async def run_checks(self) -> dict: + """Run all health checks and return results""" + results = {} + + for name, check in self.checks.items(): + try: + result = await check['func']() if asyncio.iscoroutinefunction(check['func']) else check['func']() + check['last_success'] = time.time() + check['failure_count'] = 0 + results[name] = {'status': 'healthy', 'result': result} + except Exception as e: + check['failure_count'] += 1 + results[name] = { + 'status': 'unhealthy', + 'error': str(e), + 'failure_count': check['failure_count'] + } + + if check['critical'] and check['failure_count'] >= 3: + self.logger.error(f"Critical health check '{name}' failed {check['failure_count']} times: {e}") + + return results + + +def safe_format_message(template: str, **kwargs) -> str: + """Safely format message templates with error handling""" + try: + return template.format(**kwargs) + except KeyError as e: + logger = logging.getLogger('DuckHuntBot.MessageFormat') + logger.error(f"Missing template variable {e} in message: {template[:100]}...") + + # Try to provide safe fallback + safe_kwargs = {} + for key, value in kwargs.items(): + try: + safe_kwargs[key] = str(value) if value is not None else '' + except Exception: + safe_kwargs[key] = '' + + # Replace missing variables with placeholders + import re + def replace_missing(match): + var_name = match.group(1) + if var_name not in safe_kwargs: + return f"[{var_name}]" + return f"{{{var_name}}}" + + safe_template = re.sub(r'\{([^}]+)\}', replace_missing, template) + + try: + return safe_template.format(**safe_kwargs) + except Exception: + return f"[Message format error in template: {template[:50]}...]" + except Exception as e: + logger = logging.getLogger('DuckHuntBot.MessageFormat') + logger.error(f"Unexpected error formatting message: {e}") + return f"[Message error: {template[:50]}...]" + + +def sanitize_user_input(value: str, max_length: int = 100, + allowed_chars: Optional[str] = None) -> str: + """Sanitize user input to prevent injection and errors""" + if not isinstance(value, str): + value = str(value) + + # Limit length + value = value[:max_length] + + # Remove/replace dangerous characters + value = value.replace('\r', '').replace('\n', ' ') + + # Filter to allowed characters if specified + if allowed_chars: + value = ''.join(c for c in value if c in allowed_chars) + + return value.strip() \ No newline at end of file diff --git a/src/utils.py b/src/utils.py index de3ffd1..14f7e1e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -71,35 +71,88 @@ class MessageManager: } def get(self, key: str, **kwargs) -> str: - """Get a formatted message by key with color placeholder replacement""" - if key not in self.messages: - return f"[Missing message: {key}]" - - message = self.messages[key] - - # If message is an array, randomly select one - if isinstance(message, list): - if not message: - return f"[Empty message array: {key}]" - message = random.choice(message) - - # Ensure message is a string - if not isinstance(message, str): - return f"[Invalid message type: {key}]" - - # Replace color placeholders with IRC codes - if "colours" in self.messages and isinstance(self.messages["colours"], dict): - for color_name, color_code in self.messages["colours"].items(): - placeholder = "{" + color_name + "}" - message = message.replace(placeholder, color_code) - - # Format with provided variables + """Get a formatted message by key with enhanced error handling""" try: - return message.format(**kwargs) - except KeyError as e: - return f"[Message format error: {e}]" + if key not in self.messages: + return f"[Missing message: {key}]" + + message = self.messages[key] + + # If message is an array, randomly select one + if isinstance(message, list): + if not message: + return f"[Empty message array: {key}]" + message = random.choice(message) + + # Ensure message is a string + if not isinstance(message, str): + return f"[Invalid message type: {key}]" + + # Replace color placeholders with IRC codes + if "colours" in self.messages and isinstance(self.messages["colours"], dict): + for color_name, color_code in self.messages["colours"].items(): + placeholder = "{" + color_name + "}" + message = message.replace(placeholder, color_code) + + # Sanitize kwargs to prevent injection and ensure all values are safe + safe_kwargs = {} + for k, v in kwargs.items(): + try: + # Sanitize key and value + safe_key = str(k)[:50] if k is not None else 'unknown' + if isinstance(v, (int, float)): + safe_kwargs[safe_key] = v + elif v is None: + safe_kwargs[safe_key] = '' + else: + # Sanitize string values + safe_value = str(v)[:200] # Limit length + safe_value = safe_value.replace('\r', '').replace('\n', ' ') # Remove newlines + safe_kwargs[safe_key] = safe_value + except Exception: + safe_kwargs[str(k)] = '[error]' + + # Format with provided variables using safe formatting + try: + return message.format(**safe_kwargs) + except KeyError as e: + # Try to identify missing keys and provide defaults + missing_key = str(e).strip("'\"") + + # Common defaults for missing keys + defaults = { + 'nick': 'Player', + 'xp_gained': 0, + 'ducks_shot': 0, + 'ducks_befriended': 0, + 'hp_remaining': 0, + 'ammo': 0, + 'max_ammo': 0, + 'magazines': 0, + 'target': 'Unknown', + 'victim': 'someone', + 'xp_lost': 0, + 'xp': 0 + } + + # Add default for missing key + if missing_key in defaults: + safe_kwargs[missing_key] = defaults[missing_key] + else: + safe_kwargs[missing_key] = f'[{missing_key}]' + + try: + return message.format(**safe_kwargs) + except Exception: + return f"[Format error in {key}: missing {missing_key}]" + + except ValueError as e: + return f"[Format error in {key}: {e}]" + except Exception as e: + return f"[Message error in {key}: {e}]" + except Exception as e: - return f"[Message error: {e}]" + return f"[Critical message error: {e}]" def reload(self): """Reload messages from file"""