294 lines
10 KiB
Python
294 lines
10 KiB
Python
"""
|
|
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)
|