Starting phase 2
This commit is contained in:
parent
65c1954db5
commit
b5d2605d99
224
server/main.py
224
server/main.py
@ -58,23 +58,27 @@ if not public_url.endswith("/"):
|
|||||||
|
|
||||||
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", None)
|
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
|
# Global managers - these replace the global variables from original main.py
|
||||||
session_manager: SessionManager = None
|
session_manager: SessionManager = None
|
||||||
lobby_manager: LobbyManager = None
|
lobby_manager: LobbyManager = None
|
||||||
auth_manager: AuthManager = None
|
auth_manager: AuthManager = None
|
||||||
websocket_manager: WebSocketConnectionManager = None
|
websocket_manager: WebSocketConnectionManager = None
|
||||||
|
|
||||||
# API routers
|
|
||||||
admin_api: AdminAPI = None
|
|
||||||
session_api: SessionAPI = None
|
|
||||||
lobby_api: LobbyAPI = None
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""Lifespan context manager for startup and shutdown events"""
|
"""Lifespan context manager for startup and shutdown events"""
|
||||||
global session_manager, lobby_manager, auth_manager, websocket_manager
|
global session_manager, lobby_manager, auth_manager, websocket_manager
|
||||||
global admin_api, session_api, lobby_api
|
|
||||||
|
|
||||||
# Startup
|
# Startup
|
||||||
logger.info("Starting AI Voice Bot server with modular architecture...")
|
logger.info("Starting AI Voice Bot server with modular architecture...")
|
||||||
@ -88,14 +92,11 @@ async def lifespan(app: FastAPI):
|
|||||||
session_manager.load()
|
session_manager.load()
|
||||||
|
|
||||||
# Restore lobbies for existing sessions
|
# 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 session in session_manager.get_all_sessions():
|
||||||
for lobby_info in session.lobbies:
|
for lobby_info in session.lobbies:
|
||||||
# Create lobby if it doesn't exist
|
|
||||||
lobby = lobby_manager.create_or_get_lobby(
|
lobby = lobby_manager.create_or_get_lobby(
|
||||||
name=lobby_info.name, private=lobby_info.private
|
name=lobby_info.name, private=lobby_info.private
|
||||||
)
|
)
|
||||||
# Add session to lobby (but don't trigger events during startup)
|
|
||||||
with lobby.lock:
|
with lobby.lock:
|
||||||
lobby.sessions[session.id] = session
|
lobby.sessions[session.id] = session
|
||||||
|
|
||||||
@ -109,7 +110,7 @@ async def lifespan(app: FastAPI):
|
|||||||
auth_manager=auth_manager,
|
auth_manager=auth_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize API routers
|
# Create and register API routes
|
||||||
admin_api = AdminAPI(
|
admin_api = AdminAPI(
|
||||||
session_manager=session_manager,
|
session_manager=session_manager,
|
||||||
lobby_manager=lobby_manager,
|
lobby_manager=lobby_manager,
|
||||||
@ -126,11 +127,110 @@ async def lifespan(app: FastAPI):
|
|||||||
public_url=public_url,
|
public_url=public_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register API routes
|
# Register API routes during startup
|
||||||
app.include_router(admin_api.router)
|
app.include_router(admin_api.router)
|
||||||
app.include_router(session_api.router)
|
app.include_router(session_api.router)
|
||||||
app.include_router(lobby_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
|
# Start background tasks
|
||||||
await session_manager.start_background_tasks()
|
await session_manager.start_background_tasks()
|
||||||
|
|
||||||
@ -157,15 +257,8 @@ async def lifespan(app: FastAPI):
|
|||||||
logger.info("Server shutdown complete")
|
logger.info("Server shutdown complete")
|
||||||
|
|
||||||
|
|
||||||
# Create FastAPI app
|
# Set the lifespan
|
||||||
app = FastAPI(
|
app.router.lifespan_context = lifespan
|
||||||
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}")
|
|
||||||
|
|
||||||
|
|
||||||
@app.websocket(f"{public_url}" + "ws/lobby/{lobby_id}/{session_id}")
|
@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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user