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
This commit is contained in:
@@ -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
|
|
||||||
669
duckhunt.json
669
duckhunt.json
@@ -13,8 +13,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"mysteria": {
|
||||||
"nick": "Mysteria",
|
"nick": "Mysteria",
|
||||||
@@ -27,21 +40,25 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 5,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 5,
|
"jam_chance": 5,
|
||||||
|
"confiscated_ammo": 5,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"1": 1
|
"1": 1
|
||||||
},
|
},
|
||||||
"temporary_effects": [
|
"temporary_effects": [],
|
||||||
{
|
"best_time": 0.0,
|
||||||
"type": "insurance",
|
"worst_time": 0.0,
|
||||||
"protection": "friendly_fire",
|
"total_time_hunting": 0.0,
|
||||||
"expires_at": 1759416116.11089,
|
"level": 1,
|
||||||
"name": "Hunter's Insurance"
|
"xp_gained": 0,
|
||||||
}
|
"hp_remaining": 0,
|
||||||
]
|
"victim": "",
|
||||||
|
"xp_lost": 0,
|
||||||
|
"ammo": 0,
|
||||||
|
"max_ammo": 0,
|
||||||
|
"chargers": 0
|
||||||
},
|
},
|
||||||
"wobotkoala": {
|
"wobotkoala": {
|
||||||
"nick": "WobotKoala",
|
"nick": "WobotKoala",
|
||||||
@@ -54,14 +71,25 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 4,
|
"current_ammo": 4,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 5,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 5,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"7": 1
|
"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": {
|
"boliver": {
|
||||||
"nick": "Boliver",
|
"nick": "Boliver",
|
||||||
@@ -74,14 +102,25 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 5,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 5,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"7": 1
|
"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": {
|
"papafrog": {
|
||||||
"nick": "PapaFrog",
|
"nick": "PapaFrog",
|
||||||
@@ -96,8 +135,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"leetcode": {
|
||||||
"nick": "leetcode",
|
"nick": "leetcode",
|
||||||
@@ -110,12 +162,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 5,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 5,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"overfl0w": {
|
||||||
"nick": "overfl0w",
|
"nick": "overfl0w",
|
||||||
@@ -130,8 +193,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"magicalpig": {
|
||||||
"nick": "MagicalPig",
|
"nick": "MagicalPig",
|
||||||
@@ -144,12 +220,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 3,
|
"current_ammo": 3,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 3,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 3,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"milambar": {
|
||||||
"nick": "Milambar",
|
"nick": "Milambar",
|
||||||
@@ -164,8 +251,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"loulan": {
|
||||||
"nick": "loulan",
|
"nick": "loulan",
|
||||||
@@ -178,12 +278,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 3,
|
|
||||||
"confiscated_magazines": 2,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 3,
|
||||||
|
"confiscated_magazines": 2,
|
||||||
"inventory": {},
|
"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": {
|
"h4": {
|
||||||
"nick": "h4",
|
"nick": "h4",
|
||||||
@@ -196,12 +307,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 5,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 5,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"basti": {
|
||||||
"nick": "Basti",
|
"nick": "Basti",
|
||||||
@@ -216,8 +338,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"number1stunna": {
|
||||||
"nick": "Number1Stunna",
|
"nick": "Number1Stunna",
|
||||||
@@ -232,8 +367,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"wez": {
|
||||||
"nick": "wez",
|
"nick": "wez",
|
||||||
@@ -248,8 +396,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"sander": {
|
||||||
"nick": "Sander",
|
"nick": "Sander",
|
||||||
@@ -264,8 +425,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"xternal": {
|
||||||
"nick": "xternal",
|
"nick": "xternal",
|
||||||
@@ -278,12 +452,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 4,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 4,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"wh": {
|
||||||
"nick": "wh",
|
"nick": "wh",
|
||||||
@@ -298,8 +483,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"updog": {
|
||||||
"nick": "updog",
|
"nick": "updog",
|
||||||
@@ -314,8 +512,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"hks": {
|
||||||
"nick": "Hks",
|
"nick": "Hks",
|
||||||
@@ -328,12 +539,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 5,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 5,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"wildting2": {
|
||||||
"nick": "wildting2",
|
"nick": "wildting2",
|
||||||
@@ -348,8 +570,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"girafe2": {
|
||||||
"nick": "girafe2",
|
"nick": "girafe2",
|
||||||
@@ -364,8 +599,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"kaitphone": {
|
||||||
"nick": "kaitphone",
|
"nick": "kaitphone",
|
||||||
@@ -378,12 +626,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 4,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 4,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"parabirb": {
|
||||||
"nick": "parabirb",
|
"nick": "parabirb",
|
||||||
@@ -396,12 +655,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 5,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 5,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"deimos": {
|
||||||
"nick": "DEIMOS",
|
"nick": "DEIMOS",
|
||||||
@@ -416,8 +686,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"twentytwo": {
|
||||||
"nick": "twentytwo",
|
"nick": "twentytwo",
|
||||||
@@ -430,12 +713,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 5,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 5,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"dg": {
|
||||||
"nick": "DG",
|
"nick": "DG",
|
||||||
@@ -450,8 +744,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"helderheid": {
|
||||||
"nick": "HelderHeid",
|
"nick": "HelderHeid",
|
||||||
@@ -466,8 +773,21 @@
|
|||||||
"magazines": 2,
|
"magazines": 2,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"neo_nemesis": {
|
||||||
"nick": "Neo_Nemesis",
|
"nick": "Neo_Nemesis",
|
||||||
@@ -482,8 +802,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"jeff_stacey": {
|
||||||
"nick": "jeff_stacey",
|
"nick": "jeff_stacey",
|
||||||
@@ -498,8 +831,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"p1x4l": {
|
||||||
"nick": "p1x4l",
|
"nick": "p1x4l",
|
||||||
@@ -512,12 +858,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 3,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 3,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"deadly": {
|
||||||
"nick": "deadly",
|
"nick": "deadly",
|
||||||
@@ -532,8 +889,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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_": {
|
"alterego_": {
|
||||||
"nick": "AlterEgo_",
|
"nick": "AlterEgo_",
|
||||||
@@ -548,8 +918,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"peorth": {
|
||||||
"nick": "Peorth",
|
"nick": "Peorth",
|
||||||
@@ -564,8 +947,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"magic": {
|
||||||
"nick": "MAGIC",
|
"nick": "MAGIC",
|
||||||
@@ -580,8 +976,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"koaladinosaur": {
|
||||||
"nick": "KoalaDinosaur",
|
"nick": "KoalaDinosaur",
|
||||||
@@ -596,8 +1005,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"knownsyntax": {
|
||||||
"nick": "KnownSyntax",
|
"nick": "KnownSyntax",
|
||||||
@@ -612,8 +1034,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"fifel": {
|
||||||
"nick": "fifel",
|
"nick": "fifel",
|
||||||
@@ -628,8 +1063,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"madafaka": {
|
||||||
"nick": "Madafaka",
|
"nick": "Madafaka",
|
||||||
@@ -642,12 +1090,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 3,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 3,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"antitheus": {
|
||||||
"nick": "antitheus",
|
"nick": "antitheus",
|
||||||
@@ -660,12 +1119,23 @@
|
|||||||
"gun_confiscated": false,
|
"gun_confiscated": false,
|
||||||
"current_ammo": 6,
|
"current_ammo": 6,
|
||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"confiscated_ammo": 5,
|
|
||||||
"confiscated_magazines": 3,
|
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 5,
|
||||||
|
"confiscated_magazines": 3,
|
||||||
"inventory": {},
|
"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": {
|
"l0op": {
|
||||||
"nick": "L0op",
|
"nick": "L0op",
|
||||||
@@ -680,8 +1150,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"norex": {
|
||||||
"nick": "norex",
|
"nick": "norex",
|
||||||
@@ -696,8 +1179,21 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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": {
|
"brandon": {
|
||||||
"nick": "brandon",
|
"nick": "brandon",
|
||||||
@@ -712,9 +1208,22 @@
|
|||||||
"magazines": 3,
|
"magazines": 3,
|
||||||
"bullets_per_magazine": 6,
|
"bullets_per_magazine": 6,
|
||||||
"jam_chance": 15,
|
"jam_chance": 15,
|
||||||
|
"confiscated_ammo": 0,
|
||||||
|
"confiscated_magazines": 0,
|
||||||
"inventory": {},
|
"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"
|
||||||
}
|
}
|
||||||
@@ -585,3 +585,241 @@
|
|||||||
19:48:26.215 📘 INFO DuckHuntBot 💾 Database saved
|
19:48:26.215 📘 INFO DuckHuntBot 💾 Database saved
|
||||||
19:48:26.422 📘 INFO DuckHuntBot 🔌 IRC connection closed
|
19:48:26.422 📘 INFO DuckHuntBot 🔌 IRC connection closed
|
||||||
19:48:26.422 📘 INFO DuckHuntBot ✅ Bot shutdown complete
|
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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
169
src/db.py
169
src/db.py
@@ -1,12 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
Simplified Database management for DuckHunt Bot
|
Enhanced Database management for DuckHunt Bot
|
||||||
Focus on fixing missing field errors
|
Focus on fixing missing field errors with improved error handling
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from .error_handling import with_retry, RetryConfig, ErrorRecovery, sanitize_user_input
|
||||||
|
|
||||||
|
|
||||||
class DuckDB:
|
class DuckDB:
|
||||||
@@ -17,45 +19,85 @@ class DuckDB:
|
|||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.players = {}
|
self.players = {}
|
||||||
self.logger = logging.getLogger('DuckHuntBot.DB')
|
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()
|
self.load_database()
|
||||||
|
|
||||||
def load_database(self):
|
def load_database(self) -> dict:
|
||||||
"""Load player data from JSON file with comprehensive error handling"""
|
"""Load the database, creating it if it doesn't exist"""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(self.db_file):
|
if not os.path.exists(self.db_file):
|
||||||
with open(self.db_file, 'r', encoding='utf-8') as f:
|
self.logger.info(f"Database file {self.db_file} not found, creating new one")
|
||||||
data = json.load(f)
|
return self._create_default_database()
|
||||||
|
|
||||||
# Validate loaded data structure
|
with open(self.db_file, 'r') as f:
|
||||||
if not isinstance(data, dict):
|
content = f.read().strip()
|
||||||
raise ValueError("Database root is not a dictionary")
|
|
||||||
|
|
||||||
players_data = data.get('players', {})
|
if not content:
|
||||||
if not isinstance(players_data, dict):
|
self.logger.warning("Database file is empty, creating new database")
|
||||||
raise ValueError("Players data is not a dictionary")
|
return self._create_default_database()
|
||||||
|
|
||||||
# Validate each player entry and ensure required fields
|
data = json.loads(content)
|
||||||
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
|
# Validate basic structure
|
||||||
self.logger.info(f"Loaded {len(self.players)} players from {self.db_file}")
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("Database root is not a dictionary")
|
||||||
|
|
||||||
else:
|
# Initialize metadata if missing
|
||||||
self.players = {}
|
if 'metadata' not in data:
|
||||||
self.logger.info("No existing database found, starting fresh")
|
data['metadata'] = {
|
||||||
|
'version': '1.0',
|
||||||
|
'created': datetime.now().isoformat(),
|
||||||
|
'last_modified': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
# Initialize players section if missing
|
||||||
self.logger.error(f"Database file corrupted: {e}")
|
if 'players' not in data:
|
||||||
self.players = {}
|
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:
|
except Exception as e:
|
||||||
self.logger.error(f"Error loading database: {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):
|
def _sanitize_player_data(self, player_data):
|
||||||
"""Sanitize and validate player data, ensuring ALL required fields exist"""
|
"""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}")
|
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')
|
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):
|
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"
|
temp_file = f"{self.db_file}.tmp"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Prepare data with validation
|
# Prepare data with validation
|
||||||
data = {
|
data = {
|
||||||
'players': {},
|
'players': {},
|
||||||
'last_save': str(time.time())
|
'last_save': str(time.time()),
|
||||||
|
'version': '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Validate and clean player data before saving
|
# Validate and clean player data before saving
|
||||||
|
valid_count = 0
|
||||||
for nick, player_data in self.players.items():
|
for nick, player_data in self.players.items():
|
||||||
if isinstance(nick, str) and isinstance(player_data, dict):
|
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:
|
else:
|
||||||
self.logger.warning(f"Skipping invalid player data during save: {nick}")
|
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)
|
# Write to temporary file first (atomic write)
|
||||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
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()
|
f.flush()
|
||||||
os.fsync(f.fileno())
|
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
|
# Atomic replace
|
||||||
if os.name == 'nt': # Windows
|
if os.name == 'nt': # Windows
|
||||||
if os.path.exists(self.db_file):
|
if os.path.exists(self.db_file):
|
||||||
@@ -178,10 +243,12 @@ class DuckDB:
|
|||||||
else: # Unix-like systems
|
else: # Unix-like systems
|
||||||
os.rename(temp_file, self.db_file)
|
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:
|
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:
|
finally:
|
||||||
# Clean up temp file if it still exists
|
# Clean up temp file if it still exists
|
||||||
try:
|
try:
|
||||||
@@ -196,26 +263,42 @@ class DuckDB:
|
|||||||
# Validate and sanitize nick
|
# Validate and sanitize nick
|
||||||
if not isinstance(nick, str) or not nick.strip():
|
if not isinstance(nick, str) or not nick.strip():
|
||||||
self.logger.warning(f"Invalid nick provided: {nick}")
|
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:
|
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:
|
else:
|
||||||
# Ensure existing players have all required fields
|
# Ensure existing players have all required fields
|
||||||
player = self.players[nick_lower]
|
player = self.players[nick_lower]
|
||||||
if not isinstance(player, dict):
|
if not isinstance(player, dict):
|
||||||
self.logger.warning(f"Invalid player data for {nick_lower}, recreating")
|
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:
|
else:
|
||||||
# Migrate and validate existing player data
|
# Migrate and validate existing player data with error recovery
|
||||||
self.players[nick_lower] = self._migrate_and_validate_player(player, nick)
|
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]
|
return self.players[nick_lower]
|
||||||
|
|
||||||
except Exception as e:
|
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')
|
return self.create_player(nick if isinstance(nick, str) else 'Unknown')
|
||||||
|
|
||||||
def _migrate_and_validate_player(self, player, nick):
|
def _migrate_and_validate_player(self, player, nick):
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .game import DuckGame
|
|||||||
from .sasl import SASLHandler
|
from .sasl import SASLHandler
|
||||||
from .shop import ShopManager
|
from .shop import ShopManager
|
||||||
from .levels import LevelManager
|
from .levels import LevelManager
|
||||||
|
from .error_handling import ErrorRecovery, HealthChecker, sanitize_user_input, safe_format_message
|
||||||
|
|
||||||
|
|
||||||
class DuckHuntBot:
|
class DuckHuntBot:
|
||||||
@@ -26,12 +27,19 @@ class DuckHuntBot:
|
|||||||
|
|
||||||
self.logger.info("🤖 Initializing DuckHunt Bot components...")
|
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.db = DuckDB(bot=self)
|
||||||
self.game = DuckGame(self, self.db)
|
self.game = DuckGame(self, self.db)
|
||||||
self.messages = MessageManager()
|
self.messages = MessageManager()
|
||||||
|
|
||||||
self.sasl_handler = SASLHandler(self, config)
|
self.sasl_handler = SASLHandler(self, config)
|
||||||
|
|
||||||
|
# Set up health checks
|
||||||
|
self._setup_health_checks()
|
||||||
|
|
||||||
admins_list = self.get_config('admins', ['colby']) or ['colby']
|
admins_list = self.get_config('admins', ['colby']) or ['colby']
|
||||||
self.admins = [admin.lower() for admin in admins_list]
|
self.admins = [admin.lower() for admin in admins_list]
|
||||||
self.logger.info(f"Configured {len(self.admins)} admin(s): {', '.join(self.admins)}")
|
self.logger.info(f"Configured {len(self.admins)} admin(s): {', '.join(self.admins)}")
|
||||||
@@ -42,6 +50,34 @@ class DuckHuntBot:
|
|||||||
shop_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shop.json')
|
shop_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'shop.json')
|
||||||
self.shop = ShopManager(shop_file, self.levels)
|
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):
|
def get_config(self, path, default=None):
|
||||||
keys = path.split('.')
|
keys = path.split('.')
|
||||||
value = self.config
|
value = self.config
|
||||||
@@ -233,16 +269,64 @@ class DuckHuntBot:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def send_message(self, target, msg):
|
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):
|
if not isinstance(target, str) or not isinstance(msg, str):
|
||||||
self.logger.warning(f"Invalid message parameters: target={type(target)}, msg={type(msg)}")
|
self.logger.warning(f"Invalid message parameters: target={type(target)}, msg={type(msg)}")
|
||||||
return False
|
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:
|
try:
|
||||||
sanitized_msg = msg.replace('\r', '').replace('\n', ' ').strip()
|
# Sanitize target and message
|
||||||
if not sanitized_msg:
|
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
|
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}")
|
return self.send_raw(f"PRIVMSG {target} :{sanitized_msg}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error sanitizing/sending message: {e}")
|
self.logger.error(f"Error sanitizing/sending message: {e}")
|
||||||
@@ -322,8 +406,9 @@ class DuckHuntBot:
|
|||||||
self.logger.error(f"Critical error in handle_message: {e}")
|
self.logger.error(f"Critical error in handle_message: {e}")
|
||||||
|
|
||||||
async def handle_command(self, user, channel, message):
|
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:
|
try:
|
||||||
|
# Validate input parameters
|
||||||
if not isinstance(message, str) or not message.startswith('!'):
|
if not isinstance(message, str) or not message.startswith('!'):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -331,8 +416,16 @@ class DuckHuntBot:
|
|||||||
self.logger.warning(f"Invalid user/channel types: {type(user)}, {type(channel)}")
|
self.logger.warning(f"Invalid user/channel types: {type(user)}, {type(channel)}")
|
||||||
return
|
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:
|
try:
|
||||||
parts = message[1:].split()
|
parts = safe_message[1:].split()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Error parsing command '{message}': {e}")
|
self.logger.warning(f"Error parsing command '{message}': {e}")
|
||||||
return
|
return
|
||||||
@@ -343,27 +436,39 @@ class DuckHuntBot:
|
|||||||
cmd = parts[0].lower()
|
cmd = parts[0].lower()
|
||||||
args = parts[1:] if len(parts) > 1 else []
|
args = parts[1:] if len(parts) > 1 else []
|
||||||
|
|
||||||
try:
|
# Extract and validate nick with enhanced error handling
|
||||||
nick = user.split('!')[0] if '!' in user else user
|
nick = self.error_recovery.safe_execute(
|
||||||
if not nick:
|
lambda: safe_user.split('!')[0] if '!' in safe_user else safe_user,
|
||||||
self.logger.warning(f"Empty nick from user string: {user}")
|
fallback='Unknown',
|
||||||
return
|
logger=self.logger
|
||||||
except Exception as e:
|
)
|
||||||
self.logger.error(f"Error extracting nick from '{user}': {e}")
|
|
||||||
|
if not nick or nick == 'Unknown':
|
||||||
|
self.logger.warning(f"Could not extract valid nick from user string: {user}")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
# Sanitize nick further
|
||||||
player = self.db.get_player(nick)
|
nick = sanitize_user_input(nick, max_length=50,
|
||||||
if player is None:
|
allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-[]{}^`|\\')
|
||||||
player = {}
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Error getting player data for {nick}: {e}")
|
|
||||||
player = {}
|
|
||||||
|
|
||||||
if channel.startswith('#'):
|
# Get player data with error recovery
|
||||||
player['last_activity_channel'] = channel
|
player = self.error_recovery.safe_execute(
|
||||||
player['last_activity_time'] = time.time()
|
lambda: self.db.get_player(nick),
|
||||||
self.db.players[nick.lower()] = player
|
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:
|
try:
|
||||||
if player.get('ignored', False) and not self.is_admin(user):
|
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}")
|
self.logger.error(f"Error checking admin/ignore status: {e}")
|
||||||
return
|
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:
|
except Exception as e:
|
||||||
self.logger.error(f"Critical error in handle_command: {e}")
|
self.logger.error(f"Critical error in handle_command: {e}")
|
||||||
|
|
||||||
async def _execute_command_safely(self, cmd, nick, channel, player, args, user):
|
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:
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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):
|
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):
|
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):
|
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):
|
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):
|
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:
|
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:
|
try:
|
||||||
error_msg = f"{nick} > An error occurred processing your command. Please try again."
|
if channel.startswith('#'):
|
||||||
self.send_message(channel, error_msg)
|
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:
|
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):
|
def validate_target_player(self, target_nick, channel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
262
src/error_handling.py
Normal file
262
src/error_handling.py
Normal file
@@ -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()
|
||||||
107
src/utils.py
107
src/utils.py
@@ -71,35 +71,88 @@ class MessageManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get(self, key: str, **kwargs) -> str:
|
def get(self, key: str, **kwargs) -> str:
|
||||||
"""Get a formatted message by key with color placeholder replacement"""
|
"""Get a formatted message by key with enhanced error handling"""
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
return message.format(**kwargs)
|
if key not in self.messages:
|
||||||
except KeyError as e:
|
return f"[Missing message: {key}]"
|
||||||
return f"[Message format error: {e}]"
|
|
||||||
|
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:
|
except Exception as e:
|
||||||
return f"[Message error: {e}]"
|
return f"[Critical message error: {e}]"
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
"""Reload messages from file"""
|
"""Reload messages from file"""
|
||||||
|
|||||||
Reference in New Issue
Block a user