- Backend: FastAPI + Telethon v2 WebSocket server - Frontend: React + TypeScript + Vite + Zustand - Features: Phone auth, 2FA, real-time bot chat - Fix: Use chats= instead of from_users= to sync messages from all devices - Config: BOT_USERNAME=AnvilForgeBot Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
131 lines
3.6 KiB
Python
131 lines
3.6 KiB
Python
import uuid
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.responses import FileResponse
|
|
|
|
from config import HOST, PORT
|
|
from websocket.handler import manager
|
|
|
|
# Frontend dist directory
|
|
FRONTEND_DIR = Path(__file__).parent.parent / "frontend" / "dist"
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
logger.info("Starting Telegram Web Client server...")
|
|
yield
|
|
logger.info("Shutting down server...")
|
|
# Cleanup all connections
|
|
for session_id in list(manager.active_connections.keys()):
|
|
await manager.disconnect(session_id)
|
|
|
|
|
|
app = FastAPI(
|
|
title="Telegram Web Client",
|
|
description="WebSocket-based Telegram bot chat client",
|
|
version="1.0.0",
|
|
lifespan=lifespan
|
|
)
|
|
|
|
# CORS configuration
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # Configure appropriately for production
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
@app.get("/api")
|
|
async def api_root():
|
|
return {"status": "ok", "message": "Telegram Web Client API"}
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "healthy"}
|
|
|
|
|
|
@app.websocket("/ws")
|
|
async def websocket_endpoint(websocket: WebSocket):
|
|
"""Main WebSocket endpoint for chat"""
|
|
session_id = str(uuid.uuid4())
|
|
|
|
try:
|
|
await manager.connect(websocket, session_id)
|
|
|
|
# Send session ID to client
|
|
await manager.send_message(session_id, {
|
|
"type": "connected",
|
|
"data": {"session_id": session_id}
|
|
})
|
|
|
|
while True:
|
|
data = await websocket.receive_json()
|
|
await manager.handle_message(session_id, data)
|
|
|
|
except WebSocketDisconnect:
|
|
logger.info(f"Client disconnected: {session_id}")
|
|
except Exception as e:
|
|
logger.error(f"WebSocket error: {e}")
|
|
finally:
|
|
await manager.disconnect(session_id)
|
|
|
|
|
|
@app.websocket("/ws/{session_id}")
|
|
async def websocket_with_session(websocket: WebSocket, session_id: str):
|
|
"""WebSocket endpoint with specific session ID (for reconnection)"""
|
|
try:
|
|
await manager.connect(websocket, session_id)
|
|
|
|
await manager.send_message(session_id, {
|
|
"type": "connected",
|
|
"data": {"session_id": session_id}
|
|
})
|
|
|
|
while True:
|
|
data = await websocket.receive_json()
|
|
await manager.handle_message(session_id, data)
|
|
|
|
except WebSocketDisconnect:
|
|
logger.info(f"Client disconnected: {session_id}")
|
|
except Exception as e:
|
|
logger.error(f"WebSocket error: {e}")
|
|
finally:
|
|
await manager.disconnect(session_id)
|
|
|
|
|
|
# Mount static files if frontend is built
|
|
if FRONTEND_DIR.exists():
|
|
app.mount("/assets", StaticFiles(directory=FRONTEND_DIR / "assets"), name="assets")
|
|
|
|
@app.get("/")
|
|
async def serve_index():
|
|
return FileResponse(FRONTEND_DIR / "index.html")
|
|
|
|
@app.get("/{full_path:path}")
|
|
async def serve_spa(full_path: str):
|
|
# Serve static file if exists, otherwise index.html for SPA
|
|
file_path = FRONTEND_DIR / full_path
|
|
if file_path.exists() and file_path.is_file():
|
|
return FileResponse(file_path)
|
|
return FileResponse(FRONTEND_DIR / "index.html")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host=HOST, port=PORT)
|