128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
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")
|