import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI, Depends from fastapi.middleware.cors import CORSMiddleware from app.internal.logger import logger from app.internal.mqtt_handler import mqtt_handler from app.internal.node_sweeper import sweeper_loop from app.internal.summarizer import summarizer_loop from app.config import settings from app.internal.auth import require_firebase_token, require_service_or_firebase_token from app.routers import nodes, systems, calls, upload, tokens, incidents, alerts from app.internal import firestore as fstore async def _release_orphaned_tokens(): """Release all in-use tokens on startup — voice connections don't survive server restarts.""" def _find(): from app.internal.firestore import db return [d for d in db.collection("bot_tokens").where("in_use", "==", True).stream()] results = await asyncio.to_thread(_find) for doc in results: await fstore.doc_update("bot_tokens", doc.id, { "in_use": False, "assigned_node_id": None, "assigned_at": None, }) if results: logger.info(f"Released {len(results)} orphaned token(s) on startup.") @asynccontextmanager async def lifespan(app: FastAPI): logger.info("DRB C2 Core starting.") await _release_orphaned_tokens() await mqtt_handler.connect() sweeper_task = asyncio.create_task(sweeper_loop()) summarizer_task = asyncio.create_task(summarizer_loop()) yield # --- app running --- logger.info("DRB C2 Core shutting down.") sweeper_task.cancel() summarizer_task.cancel() await mqtt_handler.disconnect() app = FastAPI(title="DRB C2 Core", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_methods=["*"], allow_headers=["*"], allow_credentials=True, ) app.include_router(nodes.router, dependencies=[Depends(require_service_or_firebase_token)]) app.include_router(systems.router, dependencies=[Depends(require_service_or_firebase_token)]) app.include_router(calls.router, dependencies=[Depends(require_service_or_firebase_token)]) app.include_router(tokens.router, dependencies=[Depends(require_service_or_firebase_token)]) app.include_router(incidents.router, dependencies=[Depends(require_service_or_firebase_token)]) app.include_router(alerts.router, dependencies=[Depends(require_service_or_firebase_token)]) app.include_router(upload.router) # auth is per-node, handled inline @app.get("/health") async def health(): return {"ok": True, "mqtt_connected": mqtt_handler.is_connected}