from __future__ import annotations import logging from pathlib import Path from fastapi import FastAPI, status from fastapi.responses import JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from aiogram import Bot from aiogram import Dispatcher from app.config import settings from app.services.payment_service import PaymentService from app.webapi.app import create_web_api_app from app.webapi.docs import add_redoc_endpoint from app.cabinet.routes import router as cabinet_router from . import payments from . import telegram logger = logging.getLogger(__name__) def _attach_docs_alias(app: FastAPI, docs_url: str | None) -> None: if not docs_url: return alias_path = "/doc" if alias_path == docs_url: return for route in app.router.routes: if getattr(route, "path", None) == alias_path: return target_url = docs_url @app.get(alias_path, include_in_schema=False) async def redirect_doc() -> RedirectResponse: # pragma: no cover - simple redirect return RedirectResponse(url=target_url, status_code=status.HTTP_307_TEMPORARY_REDIRECT) def _create_base_app() -> FastAPI: docs_config = settings.get_web_api_docs_config() if settings.is_web_api_enabled(): app = create_web_api_app() else: app = FastAPI( title="Bedolaga Unified Server", version=settings.WEB_API_VERSION, docs_url=docs_config.get("docs_url"), redoc_url=None, openapi_url=docs_config.get("openapi_url"), ) add_redoc_endpoint( app, redoc_url=docs_config.get("redoc_url"), openapi_url=docs_config.get("openapi_url"), title="Bedolaga Unified Server", ) # Add cabinet routes even when web API is disabled if settings.is_cabinet_enabled(): from fastapi.middleware.cors import CORSMiddleware cabinet_origins = settings.get_cabinet_allowed_origins() app.add_middleware( CORSMiddleware, allow_origins=["*"] if "*" in cabinet_origins else cabinet_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(cabinet_router) _attach_docs_alias(app, app.docs_url) return app def _mount_miniapp_static(app: FastAPI) -> tuple[bool, Path]: static_path: Path = settings.get_miniapp_static_path() if not static_path.exists(): logger.debug("Miniapp static path %s does not exist, skipping mount", static_path) return False, static_path try: app.mount("/miniapp/static", StaticFiles(directory=static_path), name="miniapp-static") logger.info("πŸ“¦ Miniapp static files mounted at /miniapp/static from %s", static_path) except RuntimeError as error: # pragma: no cover - defensive guard logger.warning("НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΡΠΌΠΎΠ½Ρ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ статичСскиС Ρ„Π°ΠΉΠ»Ρ‹ ΠΌΠΈΠ½ΠΈΠ°ΠΏΠΏΠ°: %s", error) return False, static_path return True, static_path def create_unified_app( bot: Bot, dispatcher: Dispatcher, payment_service: PaymentService, *, enable_telegram_webhook: bool, ) -> FastAPI: app = _create_base_app() app.state.bot = bot app.state.dispatcher = dispatcher app.state.payment_service = payment_service payments_router = payments.create_payment_router(bot, payment_service) if payments_router: app.include_router(payments_router) payment_providers_state = { "tribute": settings.TRIBUTE_ENABLED, "mulenpay": settings.is_mulenpay_enabled(), "cryptobot": settings.is_cryptobot_enabled(), "yookassa": settings.is_yookassa_enabled(), "pal24": settings.is_pal24_enabled(), "wata": settings.is_wata_enabled(), "heleket": settings.is_heleket_enabled(), "freekassa": settings.is_freekassa_enabled(), } if enable_telegram_webhook: telegram_processor = telegram.TelegramWebhookProcessor( bot=bot, dispatcher=dispatcher, queue_maxsize=settings.get_webhook_queue_maxsize(), worker_count=settings.get_webhook_worker_count(), enqueue_timeout=settings.get_webhook_enqueue_timeout(), shutdown_timeout=settings.get_webhook_shutdown_timeout(), ) app.state.telegram_webhook_processor = telegram_processor @app.on_event("startup") async def start_telegram_webhook_processor() -> None: # pragma: no cover - event hook await telegram_processor.start() @app.on_event("shutdown") async def stop_telegram_webhook_processor() -> None: # pragma: no cover - event hook await telegram_processor.stop() app.include_router(telegram.create_telegram_router(bot, dispatcher, processor=telegram_processor)) else: telegram_processor = None miniapp_mounted, miniapp_path = _mount_miniapp_static(app) unified_health_path = "/health/unified" if settings.is_web_api_enabled() else "/health" @app.get(unified_health_path) async def unified_health() -> JSONResponse: webhook_path = settings.get_telegram_webhook_path() if enable_telegram_webhook else None telegram_state = { "enabled": enable_telegram_webhook, "running": bool(telegram_processor and telegram_processor.is_running), "url": settings.get_telegram_webhook_url(), "path": webhook_path, "secret_configured": bool(settings.WEBHOOK_SECRET_TOKEN), "queue_maxsize": settings.get_webhook_queue_maxsize(), "workers": settings.get_webhook_worker_count(), } payment_state = { "enabled": bool(payments_router), "providers": payment_providers_state, } miniapp_state = { "mounted": miniapp_mounted, "path": str(miniapp_path), } return JSONResponse( { "status": "ok", "bot_run_mode": settings.get_bot_run_mode(), "web_api_enabled": settings.is_web_api_enabled(), "payment_webhooks": payment_state, "telegram_webhook": telegram_state, "miniapp_static": miniapp_state, } ) return app