""" Refactored main.py - Step 1 of Server Architecture Improvement This is a refactored version of the original main.py that demonstrates the new modular architecture with separated concerns: - SessionManager: Handles session lifecycle and persistence - LobbyManager: Handles lobby management and chat - AuthManager: Handles authentication and name protection - WebSocket message routing: Clean message handling - Separated API modules: Admin, session, and lobby endpoints This maintains backward compatibility while providing a foundation for further improvements. """ from __future__ import annotations import os from contextlib import asynccontextmanager from fastapi import FastAPI, WebSocket, Path, Request, Response from fastapi.staticfiles import StaticFiles import httpx import ssl import websockets # Import our new modular components try: from core.session_manager import SessionManager from core.lobby_manager import LobbyManager from core.auth_manager import AuthManager from websocket.connection import WebSocketConnectionManager from api.admin import AdminAPI from api.sessions import SessionAPI from api.lobbies import LobbyAPI except ImportError: # Handle relative imports when running as module import sys import os sys.path.append(os.path.dirname(os.path.abspath(__file__))) from core.session_manager import SessionManager from core.lobby_manager import LobbyManager from core.auth_manager import AuthManager from websocket.connection import WebSocketConnectionManager from api.admin import AdminAPI from api.sessions import SessionAPI from api.lobbies import LobbyAPI from logger import logger # Configuration ADMIN_TOKEN = os.getenv("ADMIN_TOKEN") public_url = os.getenv("PUBLIC_URL", "/") if not public_url.endswith("/"): public_url += "/" # Global managers - these replace the global variables from original main.py session_manager: SessionManager = None lobby_manager: LobbyManager = None auth_manager: AuthManager = None websocket_manager: WebSocketConnectionManager = None # API instances admin_api: AdminAPI = None session_api: SessionAPI = None lobby_api: LobbyAPI = None @asynccontextmanager async def lifespan(app: FastAPI): """Lifespan context manager for startup and shutdown""" global session_manager, lobby_manager, auth_manager, websocket_manager global admin_api, session_api, lobby_api logger.info("Starting AI Voice Bot server with modular architecture...") # Initialize core managers session_manager = SessionManager() lobby_manager = LobbyManager(session_manager=session_manager) auth_manager = AuthManager() # Set up cross-manager dependencies session_manager.set_lobby_manager(lobby_manager) lobby_manager.set_name_protection_checker(auth_manager.is_name_protected) # Initialize WebSocket manager websocket_manager = WebSocketConnectionManager( session_manager=session_manager, lobby_manager=lobby_manager ) # Initialize API routers admin_api = AdminAPI( session_manager=session_manager, lobby_manager=lobby_manager, auth_manager=auth_manager, admin_token=ADMIN_TOKEN, public_url=public_url ) session_api = SessionAPI( session_manager=session_manager, public_url=public_url ) lobby_api = LobbyAPI( session_manager=session_manager, lobby_manager=lobby_manager, public_url=public_url ) # Register API routes app.include_router(admin_api.router) app.include_router(session_api.router) app.include_router(lobby_api.router) # Start background tasks await session_manager.start_background_tasks() logger.info("AI Voice Bot server started successfully!") logger.info(f"Server URL: {public_url}") logger.info(f"Sessions loaded: {session_manager.get_session_count()}") logger.info(f"Lobbies available: {lobby_manager.get_lobby_count()}") logger.info(f"Protected names: {auth_manager.get_protection_count()}") if ADMIN_TOKEN: logger.info("Admin endpoints protected with token") else: logger.warning("Admin endpoints are unprotected") yield # Shutdown logger.info("Shutting down AI Voice Bot server...") if session_manager: await session_manager.stop_background_tasks() await session_manager.cleanup_all_sessions() logger.info("Server shutdown complete") # Create FastAPI app with the new architecture app = FastAPI( title="AI Voice Bot Server", description="Modular AI Voice Bot Server with WebRTC support", version="2.0.0", lifespan=lifespan ) logger.info(f"Starting server with public URL: {public_url}") @app.websocket(f"{public_url}" + "ws/lobby/{{lobby_id}}/{{session_id}}") async def lobby_websocket( websocket: WebSocket, lobby_id: str = Path(...), session_id: str = Path(...) ): """WebSocket endpoint for lobby connections - now uses WebSocketConnectionManager""" await websocket_manager.handle_connection(websocket, lobby_id, session_id) # WebSocket proxy for React dev server (development mode) PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true" if not PRODUCTION: @app.websocket("/ws") async def websocket_proxy(websocket: WebSocket): """Proxy WebSocket connections to React dev server""" logger.info("REACT: WebSocket proxy connection established.") target_url = "wss://client:3000/ws" await websocket.accept() try: # Accept self-signed certs in dev for WSS ssl_ctx = ssl.create_default_context() ssl_ctx.check_hostname = False ssl_ctx.verify_mode = ssl.CERT_NONE async with websockets.connect(target_url, ssl=ssl_ctx) as target_ws: async def client_to_server(): try: while True: data = await websocket.receive_text() await target_ws.send(data) except Exception as e: logger.debug(f"Client to server error: {e}") async def server_to_client(): try: while True: data = await target_ws.recv() await websocket.send_text(data) except Exception as e: logger.debug(f"Server to client error: {e}") # Run both directions concurrently import asyncio await asyncio.gather( client_to_server(), server_to_client(), return_exceptions=True ) except Exception as e: logger.warning(f"WebSocket proxy error: {e}") finally: try: await websocket.close() except: pass # Serve static files or proxy to frontend development server client_build_path = "/client/build" if PRODUCTION: # In production, serve static files from the client build directory if os.path.exists(client_build_path): logger.info(f"Serving static files from: {client_build_path} at {public_url}") app.mount( public_url, StaticFiles(directory=client_build_path, html=True), name="static" ) else: logger.warning(f"Client build directory not found: {client_build_path}") else: # In development, proxy to the React dev server logger.info(f"Proxying static files to http://client:3000 at {public_url}") @app.api_route( f"{public_url}{{path:path}}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"], ) async def proxy_static(request: Request, path: str): # Do not proxy API or websocket paths if path.startswith("api/") or path.startswith("ws/"): return Response(status_code=404) url = f"https://client:3000/{public_url.strip('/')}/{path}" if not path: url = f"https://client:3000/{public_url.strip('/')}" # Prepare headers but remove problematic ones for proxying headers = dict(request.headers) # Remove host header to avoid conflicts headers.pop("host", None) # Remove accept-encoding to prevent compression issues headers.pop("accept-encoding", None) try: # Use HTTP instead of HTTPS for internal container communication async with httpx.AsyncClient(verify=False) as client: proxy_req = client.build_request( request.method, url, headers=headers, content=await request.body() ) proxy_resp = await client.send(proxy_req, stream=False) # Get response headers but filter out problematic encoding headers response_headers = dict(proxy_resp.headers) # Remove content-encoding and transfer-encoding to prevent conflicts response_headers.pop("content-encoding", None) response_headers.pop("transfer-encoding", None) response_headers.pop("content-length", None) # Let FastAPI calculate this return Response( content=proxy_resp.content, status_code=proxy_resp.status_code, headers=response_headers, media_type=proxy_resp.headers.get("content-type") ) except Exception as e: logger.warning(f"Proxy error for {path}: {e}") return Response(status_code=404) # Health check for the new architecture @app.get(f"{public_url}api/system/health") def system_health(): return { "status": "ok", "architecture": "modular", "version": "2.0.0", "managers": { "session_manager": "active" if session_manager else "inactive", "lobby_manager": "active" if lobby_manager else "inactive", "auth_manager": "active" if auth_manager else "inactive", "websocket_manager": "active" if websocket_manager else "inactive", } } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)