# main.py import discord from discord.ext import commands from config import ALLOWED_CHANNEL_ID, HIT_CHECK_INTERVAL, REASSIGN_DELAY from cogs.assignments import Assignments from cogs.commands import HitCommands import asyncio import uvicorn from fastapi import FastAPI, Request, HTTPException from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel from services.server_state import STATE from services.torn_api import ( populate_friendly, populate_enemy, start_friendly_status_loop, start_enemy_status_loop, ) # ============================================================ # 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}) # ============================================================ # 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" # ============================================================ # Populate endpoints (memory-only) # ============================================================ @app.post("/api/populate_friendly") async def api_populate_friendly(data: FactionRequest): await populate_friendly(data.faction_id) # Return full member info including status 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) 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): 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): await start_enemy_status_loop(data.faction_id, data.interval) return {"status": "enemy status loop started", "id": data.faction_id, "interval": data.interval} # ============================================================ # Member endpoints # ============================================================ @app.get("/api/friendly_members") async def get_friendly_members(): """ Return a list of all friendly members with all their info, including current status. """ return [member.model_dump() for member in STATE.friendly.values()] @app.get("/api/enemy_members") async def get_enemy_members(): """ Return a list of all enemy members with all their info, including current status. """ return [member.model_dump() for member in STATE.enemy.values()] # ============================================================ # Status endpoints # ============================================================ @app.get("/api/friendly_status") async def api_friendly_status(): """ Return a dictionary of friendly member IDs to their current status. Example: { 12345: "Online", 67890: "Offline", ... } """ return {mid: member.status for mid, member in STATE.friendly.items()} @app.get("/api/enemy_status") async def api_enemy_status(): """ Return a dictionary of enemy member IDs to their current status. """ return {mid: member.status for mid, member in STATE.enemy.items()} # ============================================================ # Assignment endpoints # ============================================================ @app.get("/api/assignments") async def api_get_assignments(): 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 kind if kind not in ("friendly", "enemy"): raise HTTPException(status_code=400, detail="invalid kind") # Validate member exists in STATE coll = STATE.friendly if kind == "friendly" else STATE.enemy 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.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") return {"status": "ok", "bot_running": STATE.bot_running} # ============================================================ # Discord Bot Setup # ============================================================ intents = discord.Intents.default() intents.message_content = True enrolled_attackers = [] enemy_queue = [] active_assignments = {} round_robin_index = 0 class HitDispatchBot(commands.Bot): async def setup_hook(self): await self.add_cog( Assignments( self, enemy_queue=enemy_queue, active_assignments=active_assignments, enrolled_attackers=enrolled_attackers, hit_check=HIT_CHECK_INTERVAL, reassign_delay=REASSIGN_DELAY, ) ) await self.add_cog( HitCommands( self, enrolled_attackers=enrolled_attackers, enemy_queue=enemy_queue ) ) async def cog_check(self, ctx): return ctx.channel.id == ALLOWED_CHANNEL_ID bot = HitDispatchBot(command_prefix="!", intents=intents) @bot.event async def on_ready(): print(f"Logged in as {bot.user.name}") TOKEN = "YOUR_DISCORD_TOKEN" async def start_bot(): await bot.start(TOKEN) # ============================================================ # Main Entry Point # ============================================================ if __name__ == "__main__": loop = asyncio.get_event_loop() # Start Discord bot in background loop.create_task(start_bot()) # Run FastAPI app — keeps loop alive uvicorn.run(app, host="127.0.0.1", port=8000)