from __future__ import annotations import logging import mimetypes from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException, Query from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from app.bot import TelegramBotService from app.config import load_settings from app.db import Database logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) logger = logging.getLogger(__name__) settings = load_settings() database = Database(settings.database_path) bot_service: TelegramBotService | None = None @asynccontextmanager async def lifespan(_: FastAPI): global bot_service settings.data_dir.mkdir(parents=True, exist_ok=True) settings.avatars_dir.mkdir(parents=True, exist_ok=True) database.initialize() if settings.telegram_token: try: bot_service = TelegramBotService(settings, database) bot_service.start() except Exception: logger.exception("Telegram bot could not start; HTTP application remains available") else: logger.warning("TELEGRAM_TOKEN is empty; running HTTP application without the bot") yield if bot_service: bot_service.stop() app = FastAPI( title="Пожитки API", version="0.1.0", docs_url="/api/docs", openapi_url="/api/openapi.json", lifespan=lifespan, ) @app.get("/api/healthz") def health() -> dict[str, str]: return {"status": "ok"} @app.get("/api/config") def public_config() -> dict[str, str | None]: return {"bot_username": database.get_meta("bot_username")} @app.get("/api/users/{user_id}/summary") def user_summary(user_id: int) -> dict: result = database.summary(user_id) if result is None: raise HTTPException(status_code=404, detail="Пользователь не найден") return result @app.get("/api/users/{user_id}/friends") def user_friends(user_id: int) -> dict[str, list[int]]: return {"user_ids": database.friend_ids(user_id)} def parse_user_ids(raw: str | None) -> list[int] | None: if not raw: return None values: list[int] = [] for item in raw.split(","): item = item.strip() if not item: continue try: value = int(item) except ValueError as exc: raise HTTPException(status_code=422, detail="user_ids должен быть списком чисел") from exc if value <= 0: raise HTTPException(status_code=422, detail="user_ids должен содержать положительные ID") values.append(value) if len(values) > 100: raise HTTPException(status_code=422, detail="Можно запросить не более 100 пользователей") return values or None @app.get("/api/leaderboard") def leaderboard( user_ids: str | None = Query( default=None, description="Telegram user ID через запятую" ), limit: int = Query(default=100, ge=1, le=100), ) -> dict[str, list[dict]]: return {"items": database.leaderboard(user_ids=parse_user_ids(user_ids), limit=limit)} @app.get("/api/avatars/{user_id}", response_class=FileResponse) def avatar(user_id: int) -> FileResponse: summary = database.summary(user_id) if not summary or not summary["avatar_url"]: raise HTTPException(status_code=404, detail="Аватар не найден") with database.connect() as connection: row = connection.execute( "SELECT avatar_path FROM users WHERE user_id = ?", (user_id,) ).fetchone() if not row: raise HTTPException(status_code=404, detail="Аватар не найден") filename = Path(row["avatar_path"]).name path = settings.avatars_dir / filename if not path.is_file(): raise HTTPException(status_code=404, detail="Аватар не найден") media_type = mimetypes.guess_type(path)[0] or "image/jpeg" return FileResponse(path, media_type=media_type) static_dir = Path(__file__).resolve().parent.parent / "static" app.mount("/", StaticFiles(directory=static_dir, html=True), name="webapp")