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 3eaf357..e980035 100644 Binary files a/src/__pycache__/db.cpython-312.pyc and b/src/__pycache__/db.cpython-312.pyc differ diff --git a/src/__pycache__/duckhuntbot.cpython-312.pyc b/src/__pycache__/duckhuntbot.cpython-312.pyc index acee0d2..c8d9555 100644 Binary files a/src/__pycache__/duckhuntbot.cpython-312.pyc and b/src/__pycache__/duckhuntbot.cpython-312.pyc differ diff --git a/src/__pycache__/game.cpython-312.pyc b/src/__pycache__/game.cpython-312.pyc index 8784b87..c27db30 100644 Binary files a/src/__pycache__/game.cpython-312.pyc and b/src/__pycache__/game.cpython-312.pyc differ diff --git a/src/__pycache__/levels.cpython-312.pyc b/src/__pycache__/levels.cpython-312.pyc index 3646ac2..cbb20c4 100644 Binary files a/src/__pycache__/levels.cpython-312.pyc and b/src/__pycache__/levels.cpython-312.pyc differ diff --git a/src/__pycache__/logging_utils.cpython-312.pyc b/src/__pycache__/logging_utils.cpython-312.pyc index 8356053..2ad6c34 100644 Binary files a/src/__pycache__/logging_utils.cpython-312.pyc and b/src/__pycache__/logging_utils.cpython-312.pyc differ diff --git a/src/__pycache__/sasl.cpython-312.pyc b/src/__pycache__/sasl.cpython-312.pyc index 177f689..62c30d6 100644 Binary files a/src/__pycache__/sasl.cpython-312.pyc and b/src/__pycache__/sasl.cpython-312.pyc differ diff --git a/src/__pycache__/shop.cpython-312.pyc b/src/__pycache__/shop.cpython-312.pyc index 0622191..a82f024 100644 Binary files a/src/__pycache__/shop.cpython-312.pyc and b/src/__pycache__/shop.cpython-312.pyc differ diff --git a/src/__pycache__/utils.cpython-312.pyc b/src/__pycache__/utils.cpython-312.pyc index b599126..2dd808e 100644 Binary files a/src/__pycache__/utils.cpython-312.pyc and b/src/__pycache__/utils.cpython-312.pyc differ 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"""