diff --git a/server/main.py b/server/main.py index abe250d..1ee3532 100644 --- a/server/main.py +++ b/server/main.py @@ -58,23 +58,27 @@ if not public_url.endswith("/"): ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", None) +# Create FastAPI app first +app = FastAPI( + title="AI Voice Bot Server (Refactored)", + description="WebRTC voice chat server with modular architecture", + version="2.0.0", +) + +logger.info(f"Starting server with public URL: {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 routers -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 events""" global session_manager, lobby_manager, auth_manager, websocket_manager - global admin_api, session_api, lobby_api # Startup logger.info("Starting AI Voice Bot server with modular architecture...") @@ -88,14 +92,11 @@ async def lifespan(app: FastAPI): session_manager.load() # Restore lobbies for existing sessions - # Note: This is a simplified version - full lobby restoration would be more complex for session in session_manager.get_all_sessions(): for lobby_info in session.lobbies: - # Create lobby if it doesn't exist lobby = lobby_manager.create_or_get_lobby( name=lobby_info.name, private=lobby_info.private ) - # Add session to lobby (but don't trigger events during startup) with lobby.lock: lobby.sessions[session.id] = session @@ -109,7 +110,7 @@ async def lifespan(app: FastAPI): auth_manager=auth_manager, ) - # Initialize API routers + # Create and register API routes admin_api = AdminAPI( session_manager=session_manager, lobby_manager=lobby_manager, @@ -126,11 +127,110 @@ async def lifespan(app: FastAPI): public_url=public_url, ) - # Register API routes + # Register API routes during startup app.include_router(admin_api.router) app.include_router(session_api.router) app.include_router(lobby_api.router) + # Register static file serving AFTER API routes to avoid conflicts + PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true" + client_build_path = os.path.join(os.path.dirname(__file__), "/client/build") + + if PRODUCTION: + 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.info(f"Proxying static files to http://client:3000 at {public_url}") + + import ssl + import httpx + + @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"{request.url.scheme}://client:3000/{public_url.strip('/')}/{path}" + if not path: + url = f"{request.url.scheme}://client:3000/{public_url.strip('/')}" + headers = dict(request.headers) + try: + # Accept self-signed certs in dev + 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=True) + content = await proxy_resp.aread() + + # Remove problematic headers for browser decoding + filtered_headers = { + k: v + for k, v in proxy_resp.headers.items() + if k.lower() + not in [ + "content-encoding", + "transfer-encoding", + "content-length", + ] + } + return Response( + content=content, + status_code=proxy_resp.status_code, + headers=filtered_headers, + ) + except Exception as e: + logger.error(f"Proxy error for {url}: {e}") + return Response("Proxy error", status_code=502) + + # WebSocket proxy for /ws (for React DevTools, etc.) + import websockets + + @app.websocket("/ws") + async def websocket_proxy(websocket: WebSocket): + logger.info("REACT: WebSocket proxy connection established.") + # Get scheme from websocket.url (should be 'ws' or 'wss') + scheme = websocket.url.scheme if hasattr(websocket, "url") else "ws" + target_url = f"{scheme}://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(): + while True: + msg = await websocket.receive_text() + await target_ws.send(msg) + + async def server_to_client(): + while True: + msg = await target_ws.recv() + if isinstance(msg, str): + await websocket.send_text(msg) + else: + await websocket.send_bytes(msg) + + try: + await asyncio.gather(client_to_server(), server_to_client()) + except (WebSocketDisconnect, websockets.ConnectionClosed): + logger.info("REACT: WebSocket proxy connection closed.") + except Exception as e: + logger.error(f"REACT: WebSocket proxy error: {e}") + await websocket.close() + # Start background tasks await session_manager.start_background_tasks() @@ -157,15 +257,8 @@ async def lifespan(app: FastAPI): logger.info("Server shutdown complete") -# Create FastAPI app -app = FastAPI( - title="AI Voice Bot Server (Refactored)", - description="WebRTC voice chat server with modular architecture", - version="2.0.0", - lifespan=lifespan, -) - -logger.info(f"Starting server with public URL: {public_url}") +# Set the lifespan +app.router.lifespan_context = lifespan @app.websocket(f"{public_url}" + "ws/lobby/{lobby_id}/{session_id}") @@ -202,97 +295,6 @@ def system_health(): } -# Serve static files or proxy to frontend development server -PRODUCTION = os.getenv("PRODUCTION", "false").lower() == "true" -client_build_path = os.path.join(os.path.dirname(__file__), "/client/build") - -if PRODUCTION: - 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.info(f"Proxying static files to http://client:3000 at {public_url}") - - import ssl - import httpx - - @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"{request.url.scheme}://client:3000/{public_url.strip('/')}/{path}" - if not path: - url = f"{request.url.scheme}://client:3000/{public_url.strip('/')}" - headers = dict(request.headers) - try: - # Accept self-signed certs in dev - 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=True) - content = await proxy_resp.aread() - - # Remove problematic headers for browser decoding - filtered_headers = { - k: v - for k, v in proxy_resp.headers.items() - if k.lower() - not in ["content-encoding", "transfer-encoding", "content-length"] - } - return Response( - content=content, - status_code=proxy_resp.status_code, - headers=filtered_headers, - ) - except Exception as e: - logger.error(f"Proxy error for {url}: {e}") - return Response("Proxy error", status_code=502) - - # WebSocket proxy for /ws (for React DevTools, etc.) - import websockets - - @app.websocket("/ws") - async def websocket_proxy(websocket: WebSocket): - logger.info("REACT: WebSocket proxy connection established.") - # Get scheme from websocket.url (should be 'ws' or 'wss') - scheme = websocket.url.scheme if hasattr(websocket, "url") else "ws" - target_url = f"{scheme}://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(): - while True: - msg = await websocket.receive_text() - await target_ws.send(msg) - - async def server_to_client(): - while True: - msg = await target_ws.recv() - if isinstance(msg, str): - await websocket.send_text(msg) - else: - await websocket.send_bytes(msg) - - try: - await asyncio.gather(client_to_server(), server_to_client()) - except (WebSocketDisconnect, websockets.ConnectionClosed): - logger.info("REACT: WebSocket proxy connection closed.") - except Exception as e: - logger.error(f"REACT: WebSocket proxy error: {e}") - await websocket.close() - - if __name__ == "__main__": import uvicorn