from __future__ import annotations import asyncio import logging from contextlib import asynccontextmanager from typing import Any from fastapi import Depends, FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from backend import config, db, telegram from backend.middleware import verify_webhook_secret from backend.models import ( RegisterRequest, RegisterResponse, SignalRequest, SignalResponse, ) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) aggregator = telegram.SignalAggregator(interval=10) @asynccontextmanager async def lifespan(app: FastAPI): # Startup await db.init_db() logger.info("Database initialized") await telegram.set_webhook(url=config.WEBHOOK_URL, secret=config.WEBHOOK_SECRET) logger.info("Webhook registered") task = asyncio.create_task(aggregator.run()) logger.info("Aggregator started") yield # Shutdown aggregator.stop() await aggregator.flush() task.cancel() try: await task except asyncio.CancelledError: pass logger.info("Aggregator stopped, final flush done") app = FastAPI(lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=[config.FRONTEND_ORIGIN], allow_methods=["POST"], allow_headers=["Content-Type"], ) @app.post("/api/register", response_model=RegisterResponse) async def register(body: RegisterRequest) -> RegisterResponse: result = await db.register_user(uuid=body.uuid, name=body.name) return RegisterResponse(user_id=result["user_id"], uuid=result["uuid"]) @app.post("/api/signal", response_model=SignalResponse) async def signal(body: SignalRequest) -> SignalResponse: geo = body.geo lat = geo.lat if geo else None lon = geo.lon if geo else None accuracy = geo.accuracy if geo else None signal_id = await db.save_signal( user_uuid=body.user_id, timestamp=body.timestamp, lat=lat, lon=lon, accuracy=accuracy, ) user_name = await db.get_user_name(body.user_id) await aggregator.add_signal( user_uuid=body.user_id, user_name=user_name, timestamp=body.timestamp, geo={"lat": lat, "lon": lon, "accuracy": accuracy} if geo else None, signal_id=signal_id, ) return SignalResponse(status="ok", signal_id=signal_id) @app.post("/api/webhook/telegram") async def webhook_telegram( request: Request, _: None = Depends(verify_webhook_secret), ) -> dict[str, Any]: update = await request.json() message = update.get("message", {}) text = message.get("text", "") if text.startswith("/start"): tg_user = message.get("from", {}) tg_user_id = str(tg_user.get("id", "")) first_name = tg_user.get("first_name", "") last_name = tg_user.get("last_name", "") name = (first_name + " " + last_name).strip() or tg_user_id if tg_user_id: await db.register_user(uuid=tg_user_id, name=name) logger.info("Telegram /start: registered user %s", tg_user_id) return {"ok": True}