from contextlib import asynccontextmanager import structlog import uvicorn from fastapi import Depends, FastAPI from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html from fastapi.openapi.utils import get_openapi from fastapi.responses import JSONResponse from parser_bot.access import require_admin from parser_bot.api.routes import router from parser_bot.config import settings from parser_bot.scheduler.poller import build_scheduler from parser_bot.telegram.client import is_authorized, start_client, stop_client structlog.configure( processors=[ structlog.processors.TimeStamper(fmt="iso"), structlog.processors.add_log_level, structlog.processors.JSONRenderer(), ] ) log = structlog.get_logger() @asynccontextmanager async def lifespan(app: FastAPI): await start_client() scheduler = build_scheduler() scheduler.start() authorized = await is_authorized() log.info( "startup", poll_interval=settings.poll_interval_seconds, authorized=authorized ) if not authorized: log.warning("not_authorized", action="open monitoring-tg in portal") try: yield finally: scheduler.shutdown(wait=False) await stop_client() log.info("shutdown") def create_app() -> FastAPI: public_base = settings.public_base_path.rstrip("/") # Disable the default /docs, /redoc and /openapi.json — we serve our own # admin-gated versions below. app = FastAPI( title="parser-tg-bot", lifespan=lifespan, docs_url=None, redoc_url=None, openapi_url=None, ) app.include_router(router, prefix="/api/v1") @app.get("/healthz") async def healthz() -> dict[str, str]: return {"status": "ok"} @app.get("/", include_in_schema=False) async def index() -> dict[str, str]: return {"service": "monitoring-tg", "ui": "portal"} # Admin-only: OpenAPI surface. Custom routes so we can wrap them in # `require_admin`; the auto-generated ones from FastAPI bypass it. @app.get( "/openapi.json", include_in_schema=False, dependencies=[Depends(require_admin)], ) async def openapi_json() -> JSONResponse: return JSONResponse( get_openapi( title=app.title, version=app.version, openapi_version=app.openapi_version, description=app.description, routes=app.routes, ) ) @app.get( "/docs", include_in_schema=False, dependencies=[Depends(require_admin)], ) async def docs(): return get_swagger_ui_html( openapi_url=f"{public_base}/openapi.json" if public_base else "/openapi.json", title=app.title + " — docs", ) @app.get( "/redoc", include_in_schema=False, dependencies=[Depends(require_admin)], ) async def redoc(): return get_redoc_html( openapi_url=f"{public_base}/openapi.json" if public_base else "/openapi.json", title=app.title + " — redoc", ) return app app = create_app() def main() -> None: uvicorn.run( "parser_bot.main:app", host=settings.api_host, port=settings.api_port, log_config=None, ) if __name__ == "__main__": main()