Code organization and refactoring

This commit is contained in:
2026-01-26 16:13:29 -05:00
parent 9c3b4c8335
commit a64f9a3d74
23 changed files with 495 additions and 396 deletions

4
routers/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
"""Router modules for FastAPI endpoints."""
from . import pages, factions, assignments, bot, discord_mappings, config
__all__ = ["pages", "factions", "assignments", "bot", "discord_mappings", "config"]

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.

65
routers/assignments.py Normal file
View File

@@ -0,0 +1,65 @@
"""Group assignment management endpoints."""
from pathlib import Path
from fastapi import APIRouter, HTTPException
from models import AssignMemberRequest, RemoveAssignmentRequest
from services.server_state import STATE
from utils import sync_state_from_file
router = APIRouter(prefix="/api", tags=["assignments"])
@router.get("/assignments")
async def api_get_assignments():
"""
Return assignments snapshot:
{ "1": { "friendly": [...], "enemy": [...] }, "2": {...}, ... }
"""
snap = await STATE.get_assignments_snapshot()
return snap
@router.post("/assign_member")
async def api_assign_member(req: AssignMemberRequest):
group_id = req.group_id
kind = req.kind
member_id = req.member_id
# Validate group
if group_id not in STATE.groups:
raise HTTPException(status_code=400, detail="invalid group_id")
# Validate member exists in STATE (if not present, attempt to load from file)
if kind not in ("friendly", "enemy"):
raise HTTPException(status_code=400, detail="invalid kind")
coll = STATE.friendly if kind == "friendly" else STATE.enemy
if member_id not in coll:
# Try to load members from file into STATE and re-check
file_path = Path("data/friendly_members.json") if kind == "friendly" else Path("data/enemy_members.json")
await sync_state_from_file(file_path, kind)
if member_id not in coll:
raise HTTPException(status_code=404, detail="member not found")
await STATE.assign_member(member_id, kind, group_id)
return {"status": "ok", "group": group_id, "kind": kind, "member_id": member_id}
@router.post("/remove_member_assignment")
async def api_remove_member_assignment(req: RemoveAssignmentRequest):
member_id = req.member_id
await STATE.remove_member_assignment(member_id)
return {"status": "ok", "member_id": member_id}
@router.post("/clear_assignments")
async def api_clear_assignments():
await STATE.clear_all_assignments()
return {"status": "ok"}
@router.post("/reset_groups")
async def reset_groups():
# Clear all assignments in server state
await STATE.clear_all_assignments()
return {"success": True}

46
routers/bot.py Normal file
View File

@@ -0,0 +1,46 @@
"""Discord bot control endpoints."""
from fastapi import APIRouter, HTTPException
from typing import Optional
from models import BotControl
from services.server_state import STATE
from services.bot_assignment import BotAssignmentManager
router = APIRouter(prefix="/api", tags=["bot"])
# Global reference to assignment manager (set by main.py)
assignment_manager: Optional[BotAssignmentManager] = None
def set_assignment_manager(manager: BotAssignmentManager):
"""Set the global assignment manager reference."""
global assignment_manager
assignment_manager = manager
@router.get("/bot_status")
async def api_bot_status():
"""Get current bot status"""
active_count = len(assignment_manager.active_targets) if assignment_manager else 0
return {
"bot_running": STATE.bot_running,
"active_assignments": active_count,
"discord_mappings_count": len(assignment_manager.discord_mapping) if assignment_manager else 0
}
@router.post("/bot_control")
async def api_bot_control(req: BotControl):
if req.action not in ("start", "stop"):
raise HTTPException(status_code=400, detail="invalid action")
STATE.bot_running = (req.action == "start")
# Start or stop the assignment manager
if assignment_manager:
if req.action == "start":
await assignment_manager.start()
else:
await assignment_manager.stop()
return {"status": "ok", "bot_running": STATE.bot_running}

102
routers/config.py Normal file
View File

@@ -0,0 +1,102 @@
"""Application configuration management endpoints."""
import json
from pathlib import Path
from fastapi import APIRouter, HTTPException
from models import ConfigUpdateRequest
import config as config_module
router = APIRouter(prefix="/api", tags=["config"])
def reload_config_from_file():
"""Reload config values from JSON into module globals"""
path = Path("data/config.json")
if not path.exists():
return
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")
async def get_config():
"""Get all config values (with sensitive values masked)"""
path = Path("data/config.json")
if not path.exists():
# Return defaults from config.py (masked)
config_values = {
"TORN_API_KEY": config_module.TORN_API_KEY,
"FFSCOUTER_KEY": config_module.FFSCOUTER_KEY,
"DISCORD_TOKEN": config_module.DISCORD_TOKEN,
"ALLOWED_CHANNEL_ID": config_module.ALLOWED_CHANNEL_ID,
"POLL_INTERVAL": config_module.POLL_INTERVAL,
"HIT_CHECK_INTERVAL": config_module.HIT_CHECK_INTERVAL,
"REASSIGN_DELAY": config_module.REASSIGN_DELAY,
"ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT,
"ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER
}
else:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
config_values = data.get("config", {})
# Mask sensitive values
masked_config = config_values.copy()
sensitive = ["TORN_API_KEY", "FFSCOUTER_KEY", "DISCORD_TOKEN"]
for key in sensitive:
if key in masked_config and masked_config[key]:
val = str(masked_config[key])
masked_config[key] = "****" + val[-4:] if len(val) > 4 else "****"
return {"config": masked_config, "sensitive_fields": sensitive}
@router.post("/config")
async def update_config(req: ConfigUpdateRequest):
"""Update a single config value"""
path = Path("data/config.json")
# Load existing or create from current config
if path.exists():
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = {
"comment": "Application configuration settings",
"config": {
"TORN_API_KEY": config_module.TORN_API_KEY,
"FFSCOUTER_KEY": config_module.FFSCOUTER_KEY,
"DISCORD_TOKEN": config_module.DISCORD_TOKEN,
"ALLOWED_CHANNEL_ID": config_module.ALLOWED_CHANNEL_ID,
"POLL_INTERVAL": config_module.POLL_INTERVAL,
"HIT_CHECK_INTERVAL": config_module.HIT_CHECK_INTERVAL,
"REASSIGN_DELAY": config_module.REASSIGN_DELAY,
"ASSIGNMENT_TIMEOUT": config_module.ASSIGNMENT_TIMEOUT,
"ASSIGNMENT_REMINDER": config_module.ASSIGNMENT_REMINDER
}
}
# Validate key exists
if req.key not in data["config"]:
raise HTTPException(status_code=400, detail="Invalid config key")
# Update value
data["config"][req.key] = req.value
# Save to file
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
# Reload config in memory
reload_config_from_file()
return {"status": "ok", "key": req.key}

View File

@@ -0,0 +1,85 @@
"""Discord ID to Torn ID mapping management endpoints."""
import json
from pathlib import Path
from fastapi import APIRouter, HTTPException
from typing import Optional
from models import DiscordMappingRequest
from services.bot_assignment import BotAssignmentManager
router = APIRouter(prefix="/api", tags=["discord_mappings"])
# Global reference to assignment manager (set by main.py)
assignment_manager: Optional[BotAssignmentManager] = None
def set_assignment_manager(manager: BotAssignmentManager):
"""Set the global assignment manager reference."""
global assignment_manager
assignment_manager = manager
@router.get("/discord_mappings")
async def get_discord_mappings():
"""Get all Torn ID to Discord ID mappings"""
path = Path("data/discord_mapping.json")
if not path.exists():
return {"mappings": {}}
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return {"mappings": data.get("mappings", {})}
@router.post("/discord_mapping")
async def add_discord_mapping(req: DiscordMappingRequest):
"""Add or update a Torn ID to Discord ID mapping"""
path = Path("data/discord_mapping.json")
# Load existing mappings
if path.exists():
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
else:
data = {"comment": "Map Torn player IDs to Discord user IDs", "mappings": {}}
# Update mapping
data["mappings"][str(req.torn_id)] = str(req.discord_id)
# Save back
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
# Reload in assignment manager
if assignment_manager:
assignment_manager.load_discord_mapping()
return {"status": "ok", "torn_id": req.torn_id, "discord_id": req.discord_id}
@router.delete("/discord_mapping/{torn_id}")
async def remove_discord_mapping(torn_id: int):
"""Remove a Discord mapping"""
path = Path("data/discord_mapping.json")
if not path.exists():
raise HTTPException(status_code=404, detail="No mappings found")
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
# Remove mapping
if str(torn_id) in data.get("mappings", {}):
del data["mappings"][str(torn_id)]
# Save back
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
# Reload in assignment manager
if assignment_manager:
assignment_manager.load_discord_mapping()
return {"status": "ok", "torn_id": torn_id}
else:
raise HTTPException(status_code=404, detail="Mapping not found")

75
routers/factions.py Normal file
View File

@@ -0,0 +1,75 @@
"""Faction data population and status management endpoints."""
import json
from pathlib import Path
from fastapi import APIRouter
from models import FactionRequest
from services.server_state import STATE
from services.torn_api import populate_friendly, populate_enemy, start_friendly_status_loop, start_enemy_status_loop
from utils import load_json_list
router = APIRouter(prefix="/api", tags=["factions"])
@router.post("/populate_friendly")
async def api_populate_friendly(data: FactionRequest):
await populate_friendly(data.faction_id)
# Return members list for frontend (already in STATE from populate_friendly)
members = [m.model_dump() for m in STATE.friendly.values()]
return {"status": "friendly populated", "id": data.faction_id, "members": members}
@router.post("/populate_enemy")
async def api_populate_enemy(data: FactionRequest):
await populate_enemy(data.faction_id)
# Return members list for frontend (already in STATE from populate_enemy)
members = [m.model_dump() for m in STATE.enemy.values()]
return {"status": "enemy populated", "id": data.faction_id, "members": members}
@router.post("/start_friendly_status")
async def api_start_friendly_status(data: FactionRequest):
await start_friendly_status_loop(data.faction_id, data.interval)
return {"status": "friendly status loop started", "id": data.faction_id, "interval": data.interval}
@router.post("/start_enemy_status")
async def api_start_enemy_status(data: FactionRequest):
await start_enemy_status_loop(data.faction_id, data.interval)
return {"status": "enemy status loop started", "id": data.faction_id, "interval": data.interval}
@router.get("/friendly_members")
async def get_friendly_members():
# Return list, but prefer STATE if populated
if STATE.friendly:
return [m.model_dump() for m in STATE.friendly.values()]
# fallback to file
path = Path("data/friendly_members.json")
return load_json_list(path)
@router.get("/enemy_members")
async def get_enemy_members():
if STATE.enemy:
return [m.model_dump() for m in STATE.enemy.values()]
path = Path("data/enemy_members.json")
return load_json_list(path)
@router.get("/friendly_status")
async def api_friendly_status():
path = Path("data/friendly_status.json")
if not path.exists():
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
@router.get("/enemy_status")
async def api_enemy_status():
path = Path("data/enemy_status.json")
if not path.exists():
return {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)

18
routers/pages.py Normal file
View File

@@ -0,0 +1,18 @@
"""Web page routes for dashboard and config page."""
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
router = APIRouter()
templates = Jinja2Templates(directory="templates")
@router.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
print(">>> DASHBOARD ROUTE LOADED")
return templates.TemplateResponse("dashboard.html", {"request": request})
@router.get("/config", response_class=HTMLResponse)
async def config_page(request: Request):
return templates.TemplateResponse("config.html", {"request": request})