Compare commits

..

2 Commits

Author SHA1 Message Date
8779ad3d3b Back to json for server side storage 2025-12-01 20:52:00 -05:00
c9808759f0 Reverting back to json for server side storage 2025-12-01 20:49:53 -05:00
4 changed files with 114 additions and 54 deletions

Binary file not shown.

168
main.py
View File

@@ -1,4 +1,4 @@
# main.py # main.py (updated)
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 from config import ALLOWED_CHANNEL_ID, HIT_CHECK_INTERVAL, REASSIGN_DELAY
@@ -7,19 +7,20 @@ from cogs.commands import HitCommands
import asyncio import asyncio
import uvicorn import uvicorn
import json
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, Request, HTTPException from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pydantic import BaseModel from pydantic import BaseModel
from services.server_state import STATE # import server state
from services.torn_api import ( from services.server_state import STATE, Member
populate_friendly,
populate_enemy, from services.torn_api import populate_friendly, populate_enemy, start_friendly_status_loop, start_enemy_status_loop
start_friendly_status_loop,
start_enemy_status_loop,
)
# ============================================================ # ============================================================
# FastAPI Setup # FastAPI Setup
@@ -55,80 +56,108 @@ class RemoveAssignmentRequest(BaseModel):
class BotControl(BaseModel): class BotControl(BaseModel):
action: str # "start" or "stop" action: str # "start" or "stop"
# ============================================================ # ================================
# Populate endpoints (memory-only) # 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") @app.post("/api/populate_friendly")
async def api_populate_friendly(data: FactionRequest): async def api_populate_friendly(data: FactionRequest):
await populate_friendly(data.faction_id) await populate_friendly(data.faction_id)
# Return full member info including status # sync STATE from file
members = [m.model_dump() for m in STATE.friendly.values()] await sync_state_from_file(Path("data/friendly_members.json"), "friendly")
return {"status": "friendly populated", "id": data.faction_id, "members": members} return {"status": "friendly populated", "id": data.faction_id}
@app.post("/api/populate_enemy") @app.post("/api/populate_enemy")
async def api_populate_enemy(data: FactionRequest): async def api_populate_enemy(data: FactionRequest):
await populate_enemy(data.faction_id) await populate_enemy(data.faction_id)
members = [m.model_dump() for m in STATE.enemy.values()] await sync_state_from_file(Path("data/enemy_memberes.json"), "enemy")
return {"status": "enemy populated", "id": data.faction_id, "members": members} return {"status": "enemy populated", "id": data.faction_id}
# -----------------------------
# ============================================================
# Start status refresh loops # Start status refresh loops
# ============================================================ # -----------------------------
@app.post("/api/start_friendly_status") @app.post("/api/start_friendly_status")
async def api_start_friendly_status(data: FactionRequest): 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) await start_friendly_status_loop(data.faction_id, data.interval)
return {"status": "friendly status loop started", "id": data.faction_id, "interval": data.interval} return {"status": "friendly status loop started", "id": data.faction_id, "interval": data.interval}
@app.post("/api/start_enemy_status") @app.post("/api/start_enemy_status")
async def api_start_enemy_status(data: FactionRequest): 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) await start_enemy_status_loop(data.faction_id, data.interval)
return {"status": "enemy status loop started", "id": data.faction_id, "interval": data.interval} return {"status": "enemy status loop started", "id": data.faction_id, "interval": data.interval}
# ============================================================ # =============================
# Member endpoints # Member JSON endpoints (unchanged)
# ============================================================ # =============================
@app.get("/api/friendly_members") @app.get("/api/friendly_members")
async def get_friendly_members(): async def get_friendly_members():
""" # Return list, but prefer STATE if populated
Return a list of all friendly members with all their info, if STATE.friendly:
including current status. return [m.model_dump() for m in STATE.friendly.values()]
""" # fallback to file
return [member.model_dump() for member in STATE.friendly.values()] path = Path("data/friendly_members.json")
return _load_json_list(path)
@app.get("/api/enemy_members") @app.get("/api/enemy_members")
async def get_enemy_members(): async def get_enemy_members():
""" if STATE.enemy:
Return a list of all enemy members with all their info, return [m.model_dump() for m in STATE.enemy.values()]
including current status. path = Path("data/enemy_members.json")
""" return _load_json_list(path)
return [member.model_dump() for member in STATE.enemy.values()]
# ============================================================ # =============================
# Status endpoints # Status JSON endpoints (unchanged)
# ============================================================ # =============================
@app.get("/api/friendly_status") @app.get("/api/friendly_status")
async def api_friendly_status(): async def api_friendly_status():
""" path = Path("data/friendly_status.json")
Return a dictionary of friendly member IDs to their current status. if not path.exists():
Example: { 12345: "Online", 67890: "Offline", ... } return {}
""" with open(path, "r", encoding="utf-8") as f:
return {mid: member.status for mid, member in STATE.friendly.items()} return json.load(f)
@app.get("/api/enemy_status") @app.get("/api/enemy_status")
async def api_enemy_status(): async def api_enemy_status():
""" path = Path("data/enemy_status.json")
Return a dictionary of enemy member IDs to their current status. if not path.exists():
""" return {}
return {mid: member.status for mid, member in STATE.enemy.items()} with open(path, "r", encoding="utf-8") as f:
return json.load(f)
# ============================================================ # =============================
# Assignment endpoints # Assignment endpoints (server-side)
# ============================================================ # =============================
@app.get("/api/assignments") @app.get("/api/assignments")
async def api_get_assignments(): async def api_get_assignments():
"""
Return assignments snapshot:
{ "1": { "friendly": [...], "enemy": [...] }, "2": {...}, ... }
"""
snap = await STATE.get_assignments_snapshot() snap = await STATE.get_assignments_snapshot()
return snap return snap
@@ -142,14 +171,17 @@ async def api_assign_member(req: AssignMemberRequest):
if group_id not in STATE.groups: if group_id not in STATE.groups:
raise HTTPException(status_code=400, detail="invalid group_id") raise HTTPException(status_code=400, detail="invalid group_id")
# Validate kind # Validate member exists in STATE (if not present, attempt to load from file)
if kind not in ("friendly", "enemy"): if kind not in ("friendly", "enemy"):
raise HTTPException(status_code=400, detail="invalid kind") raise HTTPException(status_code=400, detail="invalid kind")
# Validate member exists in STATE
coll = STATE.friendly if kind == "friendly" else STATE.enemy coll = STATE.friendly if kind == "friendly" else STATE.enemy
if member_id not in coll: if member_id not in coll:
raise HTTPException(status_code=404, detail="member not found") # 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) await STATE.assign_member(member_id, kind, group_id)
return {"status": "ok", "group": group_id, "kind": kind, "member_id": member_id} return {"status": "ok", "group": group_id, "kind": kind, "member_id": member_id}
@@ -165,9 +197,9 @@ async def api_clear_assignments():
await STATE.clear_all_assignments() await STATE.clear_all_assignments()
return {"status": "ok"} return {"status": "ok"}
# ============================================================ # =============================
# Bot control endpoint # Bot control endpoint
# ============================================================ # =============================
@app.post("/api/bot_control") @app.post("/api/bot_control")
async def api_bot_control(req: BotControl): async def api_bot_control(req: BotControl):
if req.action not in ("start", "stop"): if req.action not in ("start", "stop"):
@@ -175,6 +207,34 @@ async def api_bot_control(req: BotControl):
STATE.bot_running = (req.action == "start") STATE.bot_running = (req.action == "start")
return {"status": "ok", "bot_running": STATE.bot_running} return {"status": "ok", "bot_running": STATE.bot_running}
# ============================================================
# Reset Groups Endpoint
# ============================================================
@app.post("/api/reset_groups")
async def reset_groups():
# Load existing data
data = load_json("assigned_groups.json")
# Clear group assignments
for g in data["groups"].values():
g.clear()
# Reset friendly assigned_group
for f in data["friendly_members"]:
f["assigned_group"] = None
# Reset enemy assigned_group
for e in data["enemy_members"]:
e["assigned_group"] = None
# Save back to file
save_json("assigned_groups.json", data)
return { "success": True }
# ============================================================ # ============================================================
# Discord Bot Setup # Discord Bot Setup
# ============================================================ # ============================================================