dynamic config reloading
This commit is contained in:
117
config.py
117
config.py
@@ -6,8 +6,16 @@ from dotenv import load_dotenv
|
|||||||
# Load environment variables from .env file (if it exists)
|
# Load environment variables from .env file (if it exists)
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
def load_from_json():
|
|
||||||
#Load config from JSON file if it exists
|
class DynamicConfig:
|
||||||
|
"""Dynamic configuration that reloads from JSON on each access"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._json_cache = None
|
||||||
|
self._cache_time = 0
|
||||||
|
|
||||||
|
def _load_from_json(self):
|
||||||
|
"""Load config from JSON file if it exists"""
|
||||||
path = Path("data/config.json")
|
path = Path("data/config.json")
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
return {}
|
return {}
|
||||||
@@ -20,43 +28,96 @@ def load_from_json():
|
|||||||
print(f"Error loading config from JSON: {e}")
|
print(f"Error loading config from JSON: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Load from JSON or use defaults
|
def _get_value(self, key: str, default, is_int: bool = False):
|
||||||
_config = load_from_json()
|
"""Get config value with priority: env vars > json > defaults"""
|
||||||
|
# 1. Try environment variable first (highest priority)
|
||||||
# Helper function to get config value from environment, then JSON, then default
|
|
||||||
def get_config(key: str, default, is_int: bool = False):
|
|
||||||
# 1. Try environment variable first
|
|
||||||
env_val = os.getenv(key)
|
env_val = os.getenv(key)
|
||||||
if env_val is not None:
|
if env_val is not None:
|
||||||
return int(env_val) if is_int else env_val
|
return int(env_val) if is_int else env_val
|
||||||
# 2. Try JSON config
|
|
||||||
json_val = _config.get(key)
|
# 2. Try JSON config (reload from file each time)
|
||||||
|
json_config = self._load_from_json()
|
||||||
|
json_val = json_config.get(key)
|
||||||
if json_val is not None:
|
if json_val is not None:
|
||||||
return json_val
|
return json_val
|
||||||
|
|
||||||
# 3. Use default
|
# 3. Use default
|
||||||
return default
|
return default
|
||||||
|
|
||||||
# Torn API
|
def reload(self):
|
||||||
TORN_API_KEY = get_config("TORN_API_KEY", "YOUR_TORN_API_KEY_HERE")
|
"""Force reload of JSON config (called after settings update)"""
|
||||||
|
# Just clear cache - next access will reload
|
||||||
|
self._json_cache = None
|
||||||
|
print("[CONFIG] Configuration reloaded from file")
|
||||||
|
|
||||||
# FFScouter API
|
# Torn API
|
||||||
FFSCOUTER_KEY = get_config("FFSCOUTER_KEY", "YOUR_FFSCOUTER_KEY_HERE")
|
@property
|
||||||
|
def TORN_API_KEY(self):
|
||||||
|
return self._get_value("TORN_API_KEY", "YOUR_TORN_API_KEY_HERE")
|
||||||
|
|
||||||
# Discord Bot
|
# FFScouter API
|
||||||
DISCORD_TOKEN = get_config("DISCORD_TOKEN", "YOUR_DISCORD_BOT_TOKEN_HERE")
|
@property
|
||||||
ALLOWED_CHANNEL_ID = get_config("ALLOWED_CHANNEL_ID", 0, is_int=True)
|
def FFSCOUTER_KEY(self):
|
||||||
|
return self._get_value("FFSCOUTER_KEY", "YOUR_FFSCOUTER_KEY_HERE")
|
||||||
|
|
||||||
# Intervals
|
# Discord Bot
|
||||||
HIT_CHECK_INTERVAL = get_config("HIT_CHECK_INTERVAL", 60, is_int=True)
|
@property
|
||||||
REASSIGN_DELAY = get_config("REASSIGN_DELAY", 120, is_int=True)
|
def DISCORD_TOKEN(self):
|
||||||
|
return self._get_value("DISCORD_TOKEN", "YOUR_DISCORD_BOT_TOKEN_HERE")
|
||||||
|
|
||||||
# Bot Assignment Settings
|
@property
|
||||||
ASSIGNMENT_TIMEOUT = get_config("ASSIGNMENT_TIMEOUT", 60, is_int=True) # Seconds before reassigning a target
|
def ALLOWED_CHANNEL_ID(self):
|
||||||
ASSIGNMENT_REMINDER = get_config("ASSIGNMENT_REMINDER", 45, is_int=True) # Seconds before sending reminder message
|
return self._get_value("ALLOWED_CHANNEL_ID", 0, is_int=True)
|
||||||
|
|
||||||
# Chain Timer Settings
|
# Intervals
|
||||||
CHAIN_TIMER_THRESHOLD = get_config("CHAIN_TIMER_THRESHOLD", 5, is_int=True) # Minutes - start assigning hits when chain timer is at or below this
|
@property
|
||||||
|
def HIT_CHECK_INTERVAL(self):
|
||||||
|
return self._get_value("HIT_CHECK_INTERVAL", 60, is_int=True)
|
||||||
|
|
||||||
# Authentication
|
@property
|
||||||
AUTH_PASSWORD = get_config("AUTH_PASSWORD", "YOUR_AUTH_PASSWORD_HERE") # Universal password for all users
|
def REASSIGN_DELAY(self):
|
||||||
JWT_SECRET = get_config("JWT_SECRET", "your-secret-key-change-this") # Secret key for JWT tokens
|
return self._get_value("REASSIGN_DELAY", 120, is_int=True)
|
||||||
|
|
||||||
|
# Bot Assignment Settings
|
||||||
|
@property
|
||||||
|
def ASSIGNMENT_TIMEOUT(self):
|
||||||
|
return self._get_value("ASSIGNMENT_TIMEOUT", 60, is_int=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ASSIGNMENT_REMINDER(self):
|
||||||
|
return self._get_value("ASSIGNMENT_REMINDER", 45, is_int=True)
|
||||||
|
|
||||||
|
# Chain Timer Settings
|
||||||
|
@property
|
||||||
|
def CHAIN_TIMER_THRESHOLD(self):
|
||||||
|
return self._get_value("CHAIN_TIMER_THRESHOLD", 5, is_int=True)
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
@property
|
||||||
|
def AUTH_PASSWORD(self):
|
||||||
|
return self._get_value("AUTH_PASSWORD", "YOUR_AUTH_PASSWORD_HERE")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def JWT_SECRET(self):
|
||||||
|
return self._get_value("JWT_SECRET", "your-secret-key-change-this")
|
||||||
|
|
||||||
|
|
||||||
|
# Create global config instance
|
||||||
|
config = DynamicConfig()
|
||||||
|
|
||||||
|
# Export reload function
|
||||||
|
def reload_config():
|
||||||
|
"""Reload configuration from file"""
|
||||||
|
config.reload()
|
||||||
|
|
||||||
|
# For backward compatibility, access config properties directly
|
||||||
|
# Code using "from config import TORN_API_KEY" needs to be changed to use config.TORN_API_KEY
|
||||||
|
# But we can't do that without breaking existing code, so we need a different approach
|
||||||
|
|
||||||
|
# Instead, we'll make these read from the config instance each time they're accessed
|
||||||
|
# by using __getattr__ at the module level
|
||||||
|
def __getattr__(name):
|
||||||
|
"""Dynamically fetch config values when accessed as module attributes"""
|
||||||
|
if hasattr(config, name):
|
||||||
|
return getattr(config, name)
|
||||||
|
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
||||||
|
|||||||
10
main.py
10
main.py
@@ -1,7 +1,7 @@
|
|||||||
# main.py
|
# main.py
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from config import ALLOWED_CHANNEL_ID, HIT_CHECK_INTERVAL, REASSIGN_DELAY, DISCORD_TOKEN
|
import config as app_config
|
||||||
from cogs.assignments import Assignments
|
from cogs.assignments import Assignments
|
||||||
from cogs.commands import HitCommands
|
from cogs.commands import HitCommands
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@ class HitDispatchBot(commands.Bot):
|
|||||||
enemy_queue=enemy_queue,
|
enemy_queue=enemy_queue,
|
||||||
active_assignments=active_assignments,
|
active_assignments=active_assignments,
|
||||||
enrolled_attackers=enrolled_attackers,
|
enrolled_attackers=enrolled_attackers,
|
||||||
hit_check=HIT_CHECK_INTERVAL,
|
hit_check=app_config.HIT_CHECK_INTERVAL,
|
||||||
reassign_delay=REASSIGN_DELAY,
|
reassign_delay=app_config.REASSIGN_DELAY,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class HitDispatchBot(commands.Bot):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def cog_check(self, ctx):
|
async def cog_check(self, ctx):
|
||||||
return ctx.channel.id == ALLOWED_CHANNEL_ID
|
return ctx.channel.id == app_config.ALLOWED_CHANNEL_ID
|
||||||
|
|
||||||
bot = HitDispatchBot(command_prefix="!", intents=intents)
|
bot = HitDispatchBot(command_prefix="!", intents=intents)
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ async def on_ready():
|
|||||||
async def start_bot():
|
async def start_bot():
|
||||||
try:
|
try:
|
||||||
print("Starting Discord bot...")
|
print("Starting Discord bot...")
|
||||||
await bot.start(DISCORD_TOKEN)
|
await bot.start(app_config.DISCORD_TOKEN)
|
||||||
except discord.LoginFailure:
|
except discord.LoginFailure:
|
||||||
print("ERROR: Invalid Discord token! Please set DISCORD_TOKEN in config.py")
|
print("ERROR: Invalid Discord token! Please set DISCORD_TOKEN in config.py")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -10,21 +10,11 @@ router = APIRouter(prefix="/api", tags=["config"])
|
|||||||
|
|
||||||
|
|
||||||
def reload_config_from_file():
|
def reload_config_from_file():
|
||||||
#Reload config values from JSON into module globals
|
"""Reload config values from JSON - triggers dynamic reload"""
|
||||||
path = Path("data/config.json")
|
# With the new dynamic config system, we just need to call reload
|
||||||
if not path.exists():
|
# which will cause all future property accesses to read from the updated file
|
||||||
return
|
config_module.reload_config()
|
||||||
|
print("[CONFIG] Configuration reloaded after settings update")
|
||||||
try:
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
# Update config module globals
|
|
||||||
for key, value in data.get("config", {}).items():
|
|
||||||
if hasattr(config_module, key):
|
|
||||||
setattr(config_module, key, value)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error reloading config from file: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config")
|
@router.get("/config")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import Dict, Optional
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from services.server_state import STATE
|
from services.server_state import STATE
|
||||||
from services.activity_log import activity_logger
|
from services.activity_log import activity_logger
|
||||||
from config import ASSIGNMENT_TIMEOUT, ASSIGNMENT_REMINDER, ALLOWED_CHANNEL_ID, CHAIN_TIMER_THRESHOLD, TORN_API_KEY
|
import config
|
||||||
|
|
||||||
class BotAssignmentManager:
|
class BotAssignmentManager:
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
@@ -54,7 +54,7 @@ class BotAssignmentManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = f"https://api.torn.com/v2/faction/{STATE.friendly_faction_id}/chain?key={TORN_API_KEY}"
|
url = f"https://api.torn.com/v2/faction/{STATE.friendly_faction_id}/chain?key={config.TORN_API_KEY}"
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as resp:
|
async with session.get(url) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
@@ -161,7 +161,7 @@ class BotAssignmentManager:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.chain_timeout = timeout
|
self.chain_timeout = timeout
|
||||||
threshold_seconds = CHAIN_TIMER_THRESHOLD * 60
|
threshold_seconds = config.CHAIN_TIMER_THRESHOLD * 60
|
||||||
|
|
||||||
# Check if we should enter chain mode
|
# Check if we should enter chain mode
|
||||||
if timeout > 0 and timeout <= threshold_seconds and not self.chain_active:
|
if timeout > 0 and timeout <= threshold_seconds and not self.chain_active:
|
||||||
@@ -203,7 +203,7 @@ class BotAssignmentManager:
|
|||||||
async def send_chain_expiration_warning(self):
|
async def send_chain_expiration_warning(self):
|
||||||
#Send @here alert that chain is about to expire
|
#Send @here alert that chain is about to expire
|
||||||
try:
|
try:
|
||||||
channel = self.bot.get_channel(ALLOWED_CHANNEL_ID)
|
channel = self.bot.get_channel(config.ALLOWED_CHANNEL_ID)
|
||||||
if channel and STATE.friendly_faction_id:
|
if channel and STATE.friendly_faction_id:
|
||||||
faction_link = f"https://www.torn.com/factions.php?step=your#/tab=chain"
|
faction_link = f"https://www.torn.com/factions.php?step=your#/tab=chain"
|
||||||
message = f"@here **CHAIN EXPIRING IN 30 SECONDS!** Attack a faction member to keep it alive!\n{faction_link}"
|
message = f"@here **CHAIN EXPIRING IN 30 SECONDS!** Attack a faction member to keep it alive!\n{faction_link}"
|
||||||
@@ -366,16 +366,16 @@ class BotAssignmentManager:
|
|||||||
|
|
||||||
# Send Discord message to channel
|
# Send Discord message to channel
|
||||||
attack_link = f"https://www.torn.com/loader.php?sid=attack&user2ID={enemy_id}"
|
attack_link = f"https://www.torn.com/loader.php?sid=attack&user2ID={enemy_id}"
|
||||||
message = f"**New target for {discord_user.mention}!**\n\n[**{enemy.name}** (Level {enemy.level})]({attack_link})\n\nYou have {ASSIGNMENT_TIMEOUT} seconds!"
|
message = f"**New target for {discord_user.mention}!**\n\n[**{enemy.name}** (Level {enemy.level})]({attack_link})\n\nYou have {config.ASSIGNMENT_TIMEOUT} seconds!"
|
||||||
|
|
||||||
channel = self.bot.get_channel(ALLOWED_CHANNEL_ID)
|
channel = self.bot.get_channel(config.ALLOWED_CHANNEL_ID)
|
||||||
if channel:
|
if channel:
|
||||||
await channel.send(message)
|
await channel.send(message)
|
||||||
print(f"Assigned {enemy.name} to {friendly.name} (Discord: {discord_user.name})")
|
print(f"Assigned {enemy.name} to {friendly.name} (Discord: {discord_user.name})")
|
||||||
# Log to activity
|
# Log to activity
|
||||||
await activity_logger.log_action("System", "Hit Assigned", f"{friendly.name} -> {enemy.name} (Level {enemy.level})")
|
await activity_logger.log_action("System", "Hit Assigned", f"{friendly.name} -> {enemy.name} (Level {enemy.level})")
|
||||||
else:
|
else:
|
||||||
print(f"Assignment channel {ALLOWED_CHANNEL_ID} not found")
|
print(f"Assignment channel {config.ALLOWED_CHANNEL_ID} not found")
|
||||||
self.active_targets[key]["failed"] = True
|
self.active_targets[key]["failed"] = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to send Discord message to channel: {e}")
|
print(f"Failed to send Discord message to channel: {e}")
|
||||||
@@ -434,12 +434,12 @@ class BotAssignmentManager:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Send reminder (only for successful assignments)
|
# Send reminder (only for successful assignments)
|
||||||
if elapsed >= ASSIGNMENT_REMINDER and not data["reminded"]:
|
if elapsed >= config.ASSIGNMENT_REMINDER and not data["reminded"]:
|
||||||
discord_id = data["discord_id"]
|
discord_id = data["discord_id"]
|
||||||
try:
|
try:
|
||||||
discord_user = await self.bot.fetch_user(discord_id)
|
discord_user = await self.bot.fetch_user(discord_id)
|
||||||
remaining = ASSIGNMENT_TIMEOUT - ASSIGNMENT_REMINDER
|
remaining = config.ASSIGNMENT_TIMEOUT - config.ASSIGNMENT_REMINDER
|
||||||
channel = self.bot.get_channel(ALLOWED_CHANNEL_ID)
|
channel = self.bot.get_channel(config.ALLOWED_CHANNEL_ID)
|
||||||
if channel:
|
if channel:
|
||||||
await channel.send(f"**Reminder:** {discord_user.mention} - Target {enemy.name} - {remaining} seconds left!")
|
await channel.send(f"**Reminder:** {discord_user.mention} - Target {enemy.name} - {remaining} seconds left!")
|
||||||
data["reminded"] = True
|
data["reminded"] = True
|
||||||
@@ -447,7 +447,7 @@ class BotAssignmentManager:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Reassign after timeout
|
# Reassign after timeout
|
||||||
if elapsed >= ASSIGNMENT_TIMEOUT:
|
if elapsed >= config.ASSIGNMENT_TIMEOUT:
|
||||||
to_reassign.append((data["group_id"], enemy_id))
|
to_reassign.append((data["group_id"], enemy_id))
|
||||||
del self.active_targets[key]
|
del self.active_targets[key]
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from config import FFSCOUTER_KEY
|
import config
|
||||||
|
|
||||||
async def fetch_batch_stats(ids: list[int]):
|
async def fetch_batch_stats(ids: list[int]):
|
||||||
#Fetches predicted stats for a list of Torn IDs in a single FFScouter request.
|
#Fetches predicted stats for a list of Torn IDs in a single FFScouter request.
|
||||||
@@ -9,7 +9,7 @@ async def fetch_batch_stats(ids: list[int]):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
ids_str = ",".join(map(str, ids))
|
ids_str = ",".join(map(str, ids))
|
||||||
url = f"https://ffscouter.com/api/v1/get-stats?key={FFSCOUTER_KEY}&targets={ids_str}"
|
url = f"https://ffscouter.com/api/v1/get-stats?key={config.FFSCOUTER_KEY}&targets={ids_str}"
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as resp:
|
async with session.get(url) as resp:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# services/torn_api.py
|
# services/torn_api.py
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import asyncio
|
import asyncio
|
||||||
from config import TORN_API_KEY
|
import config
|
||||||
from .ffscouter import fetch_batch_stats
|
from .ffscouter import fetch_batch_stats
|
||||||
from .server_state import STATE
|
from .server_state import STATE
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ async def populate_faction(faction_id: int, kind: str):
|
|||||||
#Fetch members + FFScouter estimates once and store in STATE.
|
#Fetch members + FFScouter estimates once and store in STATE.
|
||||||
#kind: "friendly" or "enemy"
|
#kind: "friendly" or "enemy"
|
||||||
|
|
||||||
url = f"https://api.torn.com/v2/faction/{faction_id}?selections=members&key={TORN_API_KEY}"
|
url = f"https://api.torn.com/v2/faction/{faction_id}?selections=members&key={config.TORN_API_KEY}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
@@ -86,7 +86,7 @@ async def refresh_status_loop(faction_id: int, kind: str, lock: asyncio.Lock, in
|
|||||||
#Periodically refresh member statuses in STATE.
|
#Periodically refresh member statuses in STATE.
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
url = f"https://api.torn.com/v2/faction/{faction_id}?selections=members&key={TORN_API_KEY}"
|
url = f"https://api.torn.com/v2/faction/{faction_id}?selections=members&key={config.TORN_API_KEY}"
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as resp:
|
async with session.get(url) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
|
|||||||
Reference in New Issue
Block a user