From a64f9a3d745a021d75ae4183a3db608eb6c3de6b Mon Sep 17 00:00:00 2001 From: jerick Date: Mon, 26 Jan 2026 16:13:29 -0500 Subject: [PATCH] Code organization and refactoring --- main.py | 414 +----------------- models/__init__.py | 18 + models/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 534 bytes models/__pycache__/api_models.cpython-311.pyc | Bin 0 -> 2120 bytes models/api_models.py | 32 ++ routers/__init__.py | 4 + routers/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 448 bytes .../__pycache__/assignments.cpython-311.pyc | Bin 0 -> 3621 bytes routers/__pycache__/bot.cpython-311.pyc | Bin 0 -> 2424 bytes routers/__pycache__/config.cpython-311.pyc | Bin 0 -> 5279 bytes .../discord_mappings.cpython-311.pyc | Bin 0 -> 4902 bytes routers/__pycache__/factions.cpython-311.pyc | Bin 0 -> 6094 bytes routers/__pycache__/pages.cpython-311.pyc | Bin 0 -> 1415 bytes routers/assignments.py | 65 +++ routers/bot.py | 46 ++ routers/config.py | 102 +++++ routers/discord_mappings.py | 85 ++++ routers/factions.py | 75 ++++ routers/pages.py | 18 + utils/__init__.py | 4 + utils/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 374 bytes .../__pycache__/file_helpers.cpython-311.pyc | Bin 0 -> 2033 bytes utils/file_helpers.py | 28 ++ 23 files changed, 495 insertions(+), 396 deletions(-) create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-311.pyc create mode 100644 models/__pycache__/api_models.cpython-311.pyc create mode 100644 models/api_models.py create mode 100644 routers/__init__.py create mode 100644 routers/__pycache__/__init__.cpython-311.pyc create mode 100644 routers/__pycache__/assignments.cpython-311.pyc create mode 100644 routers/__pycache__/bot.cpython-311.pyc create mode 100644 routers/__pycache__/config.cpython-311.pyc create mode 100644 routers/__pycache__/discord_mappings.cpython-311.pyc create mode 100644 routers/__pycache__/factions.cpython-311.pyc create mode 100644 routers/__pycache__/pages.cpython-311.pyc create mode 100644 routers/assignments.py create mode 100644 routers/bot.py create mode 100644 routers/config.py create mode 100644 routers/discord_mappings.py create mode 100644 routers/factions.py create mode 100644 routers/pages.py create mode 100644 utils/__init__.py create mode 100644 utils/__pycache__/__init__.cpython-311.pyc create mode 100644 utils/__pycache__/file_helpers.cpython-311.pyc create mode 100644 utils/file_helpers.py diff --git a/main.py b/main.py index 9b52ee7..659cad6 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -# main.py (updated) +# main.py import discord from discord.ext import commands from config import ALLOWED_CHANNEL_ID, HIT_CHECK_INTERVAL, REASSIGN_DELAY, DISCORD_TOKEN @@ -7,413 +7,30 @@ from cogs.commands import HitCommands import asyncio import uvicorn -import json -from pathlib import Path -from typing import Optional -from fastapi import FastAPI, Request, HTTPException -from fastapi.responses import HTMLResponse +from fastapi import FastAPI from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from pydantic import BaseModel -# import server state -from services.server_state import STATE, Member - -from services.torn_api import populate_friendly, populate_enemy, start_friendly_status_loop, start_enemy_status_loop from services.bot_assignment import BotAssignmentManager +# Import routers +from routers import pages, factions, assignments, discord_mappings, config +from routers import bot as bot_router + # ============================================================ # FastAPI Setup # ============================================================ app = FastAPI() -templates = Jinja2Templates(directory="templates") app.mount("/static", StaticFiles(directory="static"), name="static") -# ============================================================ -# Dashboard Webpage -# ============================================================ -@app.get("/", response_class=HTMLResponse) -async def dashboard(request: Request): - print(">>> DASHBOARD ROUTE LOADED") - return templates.TemplateResponse("dashboard.html", {"request": request}) - -@app.get("/config", response_class=HTMLResponse) -async def config_page(request: Request): - return templates.TemplateResponse("config.html", {"request": request}) - -# ============================================================ -# Pydantic models for JSON POST input -# ============================================================ -class FactionRequest(BaseModel): - faction_id: int - interval: int = 30 - -class AssignMemberRequest(BaseModel): - group_id: str # "1", "2", ... "5" - kind: str # "friendly" or "enemy" - member_id: int - -class RemoveAssignmentRequest(BaseModel): - member_id: int - -class BotControl(BaseModel): - action: str # "start" or "stop" - -class DiscordMappingRequest(BaseModel): - torn_id: int - discord_id: int - -class ConfigUpdateRequest(BaseModel): - key: str - value: Optional[str | int] - -# ================================ -# Helper: load JSON file into STATE -# ================================ -def _load_json_list(path: Path): - if not path.exists(): - return [] - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - -async def sync_state_from_file(path: Path, kind: str): - """ - Read JSON file (list of members dicts) and upsert into STATE. - Expected member dict keys: id, name, level, estimate, optionally status/hits. - """ - arr = _load_json_list(path) - received_ids = [] - for m in arr: - try: - await STATE.upsert_member(m, kind) - received_ids.append(int(m["id"])) - except Exception as e: - print(f"Skipping bad member entry while syncing: {m} -> {e}") - await STATE.remove_missing_members(received_ids, kind) - -# ----------------------------- -# Populate endpoints (populate JSON once) -# ----------------------------- -@app.post("/api/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} - -@app.post("/api/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} - -# ----------------------------- -# Start status refresh loops -# ----------------------------- -@app.post("/api/start_friendly_status") -async def api_start_friendly_status(data: FactionRequest): - from services.torn_api import start_friendly_status_loop - await start_friendly_status_loop(data.faction_id, data.interval) - return {"status": "friendly status loop started", "id": data.faction_id, "interval": data.interval} - -@app.post("/api/start_enemy_status") -async def api_start_enemy_status(data: FactionRequest): - from services.torn_api import start_enemy_status_loop - await start_enemy_status_loop(data.faction_id, data.interval) - return {"status": "enemy status loop started", "id": data.faction_id, "interval": data.interval} - -# ============================= -# Member JSON endpoints (unchanged) -# ============================= -@app.get("/api/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) - -@app.get("/api/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) - -# ============================= -# Status JSON endpoints (unchanged) -# ============================= -@app.get("/api/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) - -@app.get("/api/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) - -# ============================= -# Assignment endpoints (server-side) -# ============================= -@app.get("/api/assignments") -async def api_get_assignments(): - """ - Return assignments snapshot: - { "1": { "friendly": [...], "enemy": [...] }, "2": {...}, ... } - """ - snap = await STATE.get_assignments_snapshot() - return snap - -@app.post("/api/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} - -@app.post("/api/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} - -@app.post("/api/clear_assignments") -async def api_clear_assignments(): - await STATE.clear_all_assignments() - return {"status": "ok"} - -# ============================= -# Bot control endpoint -# ============================= -@app.get("/api/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 - } - -@app.post("/api/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} - -# ============================= -# Discord Mapping endpoints -# ============================= -@app.get("/api/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", {})} - -@app.post("/api/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} - -@app.delete("/api/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") - -# ============================= -# Config endpoints -# ============================= -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 - import config - for key, value in data.get("config", {}).items(): - if hasattr(config, key): - setattr(config, key, value) - except Exception as e: - print(f"Error reloading config from file: {e}") - -@app.get("/api/config") -async def get_config(): - """Get all config values (with sensitive values masked)""" - import config - - path = Path("data/config.json") - if not path.exists(): - # Return defaults from config.py (masked) - config_values = { - "TORN_API_KEY": config.TORN_API_KEY, - "FFSCOUTER_KEY": config.FFSCOUTER_KEY, - "DISCORD_TOKEN": config.DISCORD_TOKEN, - "ALLOWED_CHANNEL_ID": config.ALLOWED_CHANNEL_ID, - "POLL_INTERVAL": config.POLL_INTERVAL, - "HIT_CHECK_INTERVAL": config.HIT_CHECK_INTERVAL, - "REASSIGN_DELAY": config.REASSIGN_DELAY, - "ASSIGNMENT_TIMEOUT": config.ASSIGNMENT_TIMEOUT, - "ASSIGNMENT_REMINDER": config.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} - -@app.post("/api/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: - import config - data = { - "comment": "Application configuration settings", - "config": { - "TORN_API_KEY": config.TORN_API_KEY, - "FFSCOUTER_KEY": config.FFSCOUTER_KEY, - "DISCORD_TOKEN": config.DISCORD_TOKEN, - "ALLOWED_CHANNEL_ID": config.ALLOWED_CHANNEL_ID, - "POLL_INTERVAL": config.POLL_INTERVAL, - "HIT_CHECK_INTERVAL": config.HIT_CHECK_INTERVAL, - "REASSIGN_DELAY": config.REASSIGN_DELAY, - "ASSIGNMENT_TIMEOUT": config.ASSIGNMENT_TIMEOUT, - "ASSIGNMENT_REMINDER": config.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} - - -# ============================================================ -# Reset Groups Endpoint -# ============================================================ - -@app.post("/api/reset_groups") -async def reset_groups(): - # Clear all assignments in server state - await STATE.clear_all_assignments() - return {"success": True} - +# Include all routers +app.include_router(pages.router) +app.include_router(factions.router) +app.include_router(assignments.router) +app.include_router(bot_router.router) +app.include_router(discord_mappings.router) +app.include_router(config.router) # ============================================================ # Discord Bot Setup @@ -463,6 +80,11 @@ async def on_ready(): # Initialize assignment manager assignment_manager = BotAssignmentManager(bot) + + # Set assignment manager in routers + bot_router.set_assignment_manager(assignment_manager) + discord_mappings.set_assignment_manager(assignment_manager) + print("Bot assignment manager initialized") async def start_bot(): diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..20b3797 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,18 @@ +"""API models package.""" +from .api_models import ( + FactionRequest, + AssignMemberRequest, + RemoveAssignmentRequest, + BotControl, + DiscordMappingRequest, + ConfigUpdateRequest, +) + +__all__ = [ + "FactionRequest", + "AssignMemberRequest", + "RemoveAssignmentRequest", + "BotControl", + "DiscordMappingRequest", + "ConfigUpdateRequest", +] diff --git a/models/__pycache__/__init__.cpython-311.pyc b/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ce54bb5edce6f2455ba71b1d35ef545e45f15cc GIT binary patch literal 534 zcmb`DKT88K7{-&kYp+(SU=iuowMz#_5usWHhl*CZ90wu2q-X5q5|gx57yTCQF5P%(NLIpB^o2da%K=}%g1Hq t*fu8_JyE+>!Tphz7IPm$_zfDLpfUGC=*_|ZVK3Zv_pR{~x}|DX^bfIgm@5DP literal 0 HcmV?d00001 diff --git a/models/__pycache__/api_models.cpython-311.pyc b/models/__pycache__/api_models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15bee5e47995eb2bbe180cf884b5536d6f53167c GIT binary patch literal 2120 zcmbVMzi-<{6h8itL{X9@SB{Oeg(EZ_Dg?u>JL3Q@_=gzB#d%`p9~M#%5b z;-{UFTo$-oipxsQ04|^6a*{KFE2KC>az)@uDK0O$CE&^_&ZLzmq`v&}+efF=b_1td z>wA>-MD5VywFi$HHO`(7mD@`H z@^6~>^*7KxCycZe5KcWHOk+A#?!zcm+Zox@WKV~XGSSjcb{E%v97hnDhjHI>=!bQf z6d8^iFn(h9TJ>yXSe9$|nPo+$WkC*u9@<6AdOomw6GnpxQQ5L=*YyG$*A|w=^RU)C zat%m6KH2)Ab08QOoo9?Y-Q&)-*B$hk8;H&}J7GP~$4+M&$qSoX)DgZNbbqy;dO;@^ zuVecT#sE5Q_@_JzD@#B_+ye+d`Hd^pa6jB1?vGbi!=2aNk-iG=dx>E1%5?T(Q8$2I zEUFY#ML`+VApKO@>H{I1Be%)=PZ^);YaDTJ;Dfr6e(bn3%J(G=1E4R*L%|COMSw^X z0q0BDEg_Tv%R*a#cL#<=`h*a}-iXkjZ}2bWl!@#nxS z&X}rDI9jy!nDxCA7UzijHOY{#!pzpYOd^*!1DQ)AjoaYQ5ilu%#@xoGgGQJC8MOJ( z3${Er;9f7v#)aOhr$SwW55%%3Lj4j6gj>+kp%%;GZn!&y7VgISqVM8RzXoQpzO#y= z8Yrztx3(S8^*C+XzVEn4(@M$(9(S>jOgJ3KV-0cKw`8H*!7&6>PJ9J0r*Nqf?uC29 zy?@ULo|HI#oU}Op?}7P``0mwcC_Y%Q~ zlg1@l^+J@hEb4XPe#-`@C_Kk6M}|MS!?_OK_}Y;t5yh3oA*GgqBsSusaX-E@f8=#= zR-lPZz)MY0lrh;Hk2* zCc()?gl?ToYW)G;Tu$zR%em(~x7#gX>+5aGU+w?l$X|JzVYzRI8K3|G3aM8@;UQ1> z$QJ<$M2JEWp-99ihM)k{dx9vT{s?q}hrOYkG)$#J(n-m5I+iLuCAvAj7^JMCwd7T! zd!NwicOnzkq+r@4W0E&qR@!V6t$9%iX19!gB^$F#xz44cSdhBrRiRCk%WBMv^Ujuu zF{U!d*Z}2QMHM&*9YhXd2Z;r@NXio97qGMxtcE8C*`;PmXE#jo{5I>$e6qfp_1PUO zW$j3|@r9>EVe6wNO@587t4ys^>I`FE@do2weZNW$e?uRs9ecth)MpmHeF)(KbmySE U0E78|`n@%pulq4}PqnSw51v(sKmY&$ literal 0 HcmV?d00001 diff --git a/routers/__pycache__/assignments.cpython-311.pyc b/routers/__pycache__/assignments.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60a685e67c310352de962484d83cac6ff83a3515 GIT binary patch literal 3621 zcmcH*OKcm*b!K<@U6PV0%CfiuAIEW892tBw8oI?)nJ;&ZU47LTafB^wTf%eA2=q0DVnI%^wB^BtQ z!;o9`~06)LgERY? zR=ll|W$j)?vuyX|vQ{)VHSdTqZyf;7!22sVR^En5%6d>LD3+bKHD!&MMP*Glw5l|j zFYB&+prHjaf@Pwu>Gxg9&aYe5*ks3Pp~I07IS4(5@%k+QiwHwd+I~nNwBgUY9s3wU z^`ZCy`b_+dw1sL2OeIt|q8Vt*nq48K4ue@4tCTNWcT9VRBRtHEUml;yz~36t!GgvE zSbaD#G4ZQQnQ^V875#N)3wEwBTsVa}z|0osv6K}Ewy7CrnJre=@&)E3<)b`NRNX4)?ZO@9wrQ_I z5P&bO)ecb;jYNF*WrN|@16^VAQ;5z~^0x-DKzJ^`TX*IOLXnEVhu=4S@1X%g?U`woFJJ`kq z2e&H{+QekQmJd!?UHVe&t|pIgO;DvHFat+o2JdpYB-SO-mtK;)_CsyiTT)G|F)2R> zX>A^THZyChsiE%tEhV~6!`-J_0ei4biBK(26S{8&!Hn9$j8RK=w=E)S^zaRBUt_Q% zulY<0&p*|T74hOOQ!?$$npr8Su3+BHN^a0o#tm=XB1y#tcLv0Nl8jSS@X`LBc#nPBY-Ma;1W47;c1BJq1$pj>STW)tpR*uy@KG zVCmS!iJXP~6ELiCxSE%Y`+dnLwe5Era^lG}BwqjEv-tMA%^(W*e_DA~qoK3VUnNHO zM0)inG_g=mEI5e;8eU*1F*s={ozB95bA#}(a;$5{h8UXFMc!g9-Vpb(|J1lW<7PyNnN9JW3^V;HZO z0UT9_Y=+Tl2Q>(?1AKAt<tb)1$!!Q+Po zpv7uXJ3z)*fE-6f&H?848ObbwR;+l@@rv2=!rpK_I^{&CsB}aJTkZ{0d5U#u%Clo~ zvS4WNlytlr1z_zr|MmSbZ2eEKZ%4smg<8j+xwxH}&Aw>8k3%g545L$tFA&$yOPV2O zlS6uv?*fv`c6H1-ie;4(hFa7_ocXvZp zrBKi;YlU0@jmbp@STuXLjCCqPCSd=EFs!QpT2FKk$^B0Bzh7_YsZHfEDvfc@OW7ee zq$sLcP!u=BDw3h!=0(j7t>vLwmUY73S6=qqU=fPAVG&le2zxr*A^0HK)IsM7HqnUp zY3EhUK*iPpnHR^P_x0h5@TI}OE?lW>S~jn3JZQPI_@#QT{cZRykXkXcSuzb0A?=X$ z5MWcp7&p)*iY_(KDcaq=eK_jr`{+mXsNO*5X?Jg+bF{nfN0K|k&(H0}cYjSI*Xxn% zPUQM_5Gtw={_V)4k)5SF9(C|&QwZYzCIav;5eZ}dPLkn`J{qO*bRC~|@ad*-0^h>) z%+)4hlTOGe-|$hISA5b7b)0o@wke4CD!nw@4aWm#qovGt-2ah)e0gp8i i0ET$h--)&mca%C#J2=g_ry18Y!vj@2cK9}roc{n1-EXb{ literal 0 HcmV?d00001 diff --git a/routers/__pycache__/bot.cpython-311.pyc b/routers/__pycache__/bot.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd9df5581457063468b8c15628ec4fbad867ea5b GIT binary patch literal 2424 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 literal 0 HcmV?d00001 diff --git a/routers/__pycache__/discord_mappings.cpython-311.pyc b/routers/__pycache__/discord_mappings.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c45200864333eb4c5590f9860aae6b62ee7d4355 GIT binary patch literal 4902 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 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} diff --git a/routers/discord_mappings.py b/routers/discord_mappings.py new file mode 100644 index 0000000..1b1fc58 --- /dev/null +++ b/routers/discord_mappings.py @@ -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") diff --git a/routers/factions.py b/routers/factions.py new file mode 100644 index 0000000..1a1c04f --- /dev/null +++ b/routers/factions.py @@ -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) diff --git a/routers/pages.py b/routers/pages.py new file mode 100644 index 0000000..c815286 --- /dev/null +++ b/routers/pages.py @@ -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}) diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..5ed71a2 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,4 @@ +"""Utility functions package.""" +from .file_helpers import load_json_list, sync_state_from_file + +__all__ = ["load_json_list", "sync_state_from_file"] diff --git a/utils/__pycache__/__init__.cpython-311.pyc b/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9636fa5d33b0b14005d0caa65bdda829d85e559 GIT binary patch literal 374 zcmZusJxc>Y5Z%2?j3&m)N)ZmbG%3_dB#o&>kTl1_u_@UAH9FClV1sbg7vWvmWZPmam)jS(;yDg zFb*-|A#aR>xWSt@sJC^0Fmh5!w}_G=V@~SKMmc3OIu`v8?1%J%YENm(VQO^-N?KR# z+eOB}I_d-@MyHTSB~0kars3hL*Q{CqTr+@bx30lNs9YHP$EgWaDS#KW_AO|0eR)ns z)^o^I7|CX2s9BMU%vmxNcS7mBPDnz(Z-hBDz@^QpV-vX5juie2O8{gh9Y8-{Y_=~_ s%?l+4rsEgXuhNwov$xJ+d30K~&mLN}njPQv1pxPO6#xJL literal 0 HcmV?d00001 diff --git a/utils/__pycache__/file_helpers.cpython-311.pyc b/utils/__pycache__/file_helpers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8bf3f8c62fe7151c7ea4739182a66a1df2e95ce GIT binary patch literal 2033 zcmaJ?-D_M$6rZ^to9t${Y0^Yf>Bm5;bxV@sgCMCbq)l5}OR6Rc4J((MJG0GA?(SV? zX0u7W(LhB~4X95^DZ&bEY^g{eeKh_B8(DO@5D-Mrw@4I1pFDFn-E?j1+?g|H&V2o5 z&hO0qCY4GcNMHV3azE1%`b{++C1Xd~IuFV+qG$|JOf?UWVW@Ra%jsh}RyM;ka^{!` zy202gh22Vxlos0{U!X%$TO8)l(hLnBD4c{?eH z4)m?Q3by+V`>SXkOY_kfgeDt30bT_Z_ZvtYjT-7()xZj+o-pik1igZUQPO7gtEhxA zx{5G-)PO56=hBybn-ZH`8o4q|vg$hQJSCjTBF__75?es0=L*>$Vm@aUWw!4%xQ$kp zcYNyRuMMY-FvezJfCvr0!1AFnDSVI~tee)j!XDzRP>2Y5IC^#<^R8fAWF{GRovF-_ z?-X+^FGXgEO*79gs75AhJJR*@R>|fTbp_nHGhvPUGNT+MG76_vcbVuf%!Yf^W|rF5 zQc!t2Y}NurtbkZRzqYl1e)7|kH%={`+C4$Q*NF*4F|%QRw&n@Y+VLtzX`5eMzY~{1cYlQ!aPuGUJtY(^gf?A;!Px*Y^)m? zKp<%CQsp`j@B{rlw5)$(U=--(2GwBiMw0P{iUNab`y0(>6wNJ+mM$DYGK%l|grkTb zo;PKxF&$AoFm3@Y1M?R8%J}d6tAK9a{LoyQs@El4LUlqv9z>JM>ro2y|34qa5&dr_ z2&8uy#K3L@ysSt>{4B|_+&FNRP}h+n9Ysd50PK~-%}bv|FG7E`!nv6Ob0niXQ=%DU zip`1v;?iD{w{xtQc#7e@gbC^9YzYRxpjd8u-YgN)mPK)5!j+=F;&7Rh6Ef9aew}Adhptk)hUF(TM)x@ENv$cKQ3zuuD zy_?81FX6ge#G9HCzl1kBpRablShnwWt#|fUJNqlir{r3)=a=N+pOS~~=;gsOy?x>9 zVpThwS$Q>F>~!$_Ze*dh)vMb@vz0RsP9-7 zZ-a4Ri~7}g?l@P07 {e}") + await STATE.remove_missing_members(received_ids, kind)