|
|
import os |
|
|
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, status, HTTPException |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse |
|
|
from fastapi.staticfiles import StaticFiles |
|
|
from fastapi.templating import Jinja2Templates |
|
|
import json |
|
|
import uvicorn |
|
|
from typing import Dict, Set, Optional, Any |
|
|
import logging |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
app = FastAPI(title="EFT Group Map API") |
|
|
|
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=["*"], |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static") |
|
|
templates = Jinja2Templates(directory="templates") |
|
|
|
|
|
|
|
|
maps_data = {} |
|
|
try: |
|
|
with open("data.json", "r", encoding="utf8") as file: |
|
|
maps_data = json.load(file) |
|
|
except FileNotFoundError: |
|
|
logger.error("data.json nicht gefunden") |
|
|
maps_data = {} |
|
|
|
|
|
|
|
|
@app.get("/", response_class=FileResponse) |
|
|
async def read_index(request: Request): |
|
|
return templates.TemplateResponse("map.html", {"request": request, "maps": maps_data}) |
|
|
|
|
|
|
|
|
@app.get("/map/{normalized_name}", response_class=FileResponse) |
|
|
async def get_map_by_normalized_name(request: Request, normalized_name: str): |
|
|
map_entry = maps_data.get(normalized_name) |
|
|
filtered_maps = [map_entry] if map_entry else [] |
|
|
return templates.TemplateResponse("map.html", {"request": request, "maps": filtered_maps}) |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/map/{map_name}") |
|
|
async def get_map_data(map_name: str): |
|
|
"""API-Endpunkt zum Abrufen von Kartendaten""" |
|
|
map_entry = maps_data.get(map_name) |
|
|
if not map_entry: |
|
|
raise HTTPException(status_code=404, detail="Map not found") |
|
|
return JSONResponse(content=map_entry) |
|
|
|
|
|
|
|
|
class ConnectionManager: |
|
|
def __init__(self): |
|
|
self.groups: Dict[str, Set[WebSocket]] = {} |
|
|
self.connections: Dict[WebSocket, str] = {} |
|
|
self.client_types: Dict[WebSocket, str] = {} |
|
|
self.message_stats = {"total_sent": 0, "total_received": 0} |
|
|
self.group_leaders: Dict[str, WebSocket] = {} |
|
|
|
|
|
async def connect(self, websocket: WebSocket): |
|
|
await websocket.accept() |
|
|
logger.info(f"Neue WebSocket-Verbindung: {websocket.client}") |
|
|
|
|
|
def disconnect(self, websocket: WebSocket): |
|
|
if websocket in self.connections: |
|
|
group_name = self.connections[websocket] |
|
|
self.remove_from_group(websocket, group_name) |
|
|
logger.info(f"Client disconnected from group '{group_name}'") |
|
|
|
|
|
async def join_group(self, websocket: WebSocket, group_name: str, client_type: str = "web"): |
|
|
""" |
|
|
Client einer Gruppe hinzufügen |
|
|
|
|
|
Args: |
|
|
websocket: WebSocket-Verbindung |
|
|
group_name: Name der Gruppe |
|
|
client_type: Typ des Clients ("monitor" oder "web") |
|
|
""" |
|
|
|
|
|
if websocket in self.connections: |
|
|
old_group = self.connections[websocket] |
|
|
self.remove_from_group(websocket, old_group) |
|
|
|
|
|
|
|
|
if group_name not in self.groups: |
|
|
self.groups[group_name] = set() |
|
|
self.group_leaders[group_name] = websocket |
|
|
logger.info(f"Neue Gruppe '{group_name}' erstellt von {client_type}-Client") |
|
|
|
|
|
self.groups[group_name].add(websocket) |
|
|
self.connections[websocket] = group_name |
|
|
self.client_types[websocket] = client_type |
|
|
|
|
|
|
|
|
is_first_client = self.group_leaders.get(group_name) == websocket |
|
|
client_info = { |
|
|
"type": "joined", |
|
|
"group": group_name, |
|
|
"client_type": client_type, |
|
|
"is_first": is_first_client, |
|
|
"message": f"Erfolgreich Gruppe '{group_name}' beigetreten als {client_type}-Client", |
|
|
} |
|
|
|
|
|
logger.info(f"Client ({client_type}) joined group '{group_name}' (First: {is_first_client})") |
|
|
return client_info |
|
|
|
|
|
def remove_from_group(self, websocket: WebSocket, group_name: str): |
|
|
if group_name in self.groups and websocket in self.groups[group_name]: |
|
|
self.groups[group_name].remove(websocket) |
|
|
|
|
|
if self.group_leaders.get(group_name) == websocket and self.groups[group_name]: |
|
|
|
|
|
self.group_leaders[group_name] = next(iter(self.groups[group_name])) |
|
|
logger.info(f"Neuer Gruppenleiter für '{group_name}' gewählt") |
|
|
elif not self.groups[group_name]: |
|
|
del self.groups[group_name] |
|
|
if group_name in self.group_leaders: |
|
|
del self.group_leaders[group_name] |
|
|
logger.info(f"Group '{group_name}' deleted (no members)") |
|
|
|
|
|
if websocket in self.connections: |
|
|
del self.connections[websocket] |
|
|
if websocket in self.client_types: |
|
|
del self.client_types[websocket] |
|
|
|
|
|
async def broadcast_to_group(self, message: str, group_name: str, exclude: Optional[WebSocket] = None): |
|
|
if group_name in self.groups: |
|
|
disconnected = set() |
|
|
for connection in self.groups[group_name].copy(): |
|
|
|
|
|
if connection == exclude: |
|
|
continue |
|
|
|
|
|
try: |
|
|
await connection.send_text(message) |
|
|
self.message_stats["total_sent"] += 1 |
|
|
except Exception as e: |
|
|
logger.error(f"Fehler beim Senden an Client: {e}") |
|
|
disconnected.add(connection) |
|
|
|
|
|
|
|
|
for conn in disconnected: |
|
|
self.disconnect(conn) |
|
|
|
|
|
def get_stats(self): |
|
|
return {"active_groups": len(self.groups), "total_connections": len(self.connections), **self.message_stats} |
|
|
|
|
|
def get_group_info(self, group_name: str) -> Dict[str, Any]: |
|
|
"""Gibt Informationen über eine Gruppe zurück""" |
|
|
if group_name not in self.groups: |
|
|
return {"group": group_name, "members": 0, "clients": []} |
|
|
|
|
|
members = [] |
|
|
for ws in self.groups[group_name]: |
|
|
client_type = self.client_types.get(ws, "unknown") |
|
|
is_leader = self.group_leaders.get(group_name) == ws |
|
|
members.append({"client_type": client_type, "is_leader": is_leader}) |
|
|
|
|
|
return {"group": group_name, "members": len(members), "clients": members} |
|
|
|
|
|
|
|
|
manager = ConnectionManager() |
|
|
|
|
|
|
|
|
@app.websocket("/ws") |
|
|
async def websocket_endpoint(websocket: WebSocket): |
|
|
await manager.connect(websocket) |
|
|
try: |
|
|
while True: |
|
|
data = await websocket.receive_text() |
|
|
manager.message_stats["total_received"] += 1 |
|
|
|
|
|
try: |
|
|
message = json.loads(data) |
|
|
|
|
|
|
|
|
if message.get("type") == "join": |
|
|
group_name = message.get("group") |
|
|
client_type = message.get("client_type", "web") |
|
|
if group_name: |
|
|
join_info = await manager.join_group(websocket, group_name, client_type) |
|
|
|
|
|
await websocket.send_text(json.dumps(join_info)) |
|
|
|
|
|
|
|
|
notification = { |
|
|
"type": "client_joined", |
|
|
"group": group_name, |
|
|
"client_type": client_type, |
|
|
"message": f"Neuer {client_type}-Client ist der Gruppe beigetreten", |
|
|
} |
|
|
await manager.broadcast_to_group(json.dumps(notification), group_name, exclude=websocket) |
|
|
else: |
|
|
error_msg = {"type": "error", "message": "Missing group name in join request"} |
|
|
await websocket.send_text(json.dumps(error_msg)) |
|
|
|
|
|
|
|
|
elif message.get("type") == "ping": |
|
|
await websocket.send_text(json.dumps({"type": "pong"})) |
|
|
|
|
|
|
|
|
else: |
|
|
if websocket in manager.connections: |
|
|
group_name = manager.connections[websocket] |
|
|
await manager.broadcast_to_group(data, group_name) |
|
|
else: |
|
|
error_msg = {"type": "error", "message": "Join a group before sending messages"} |
|
|
await websocket.send_text(json.dumps(error_msg)) |
|
|
|
|
|
except json.JSONDecodeError: |
|
|
logger.warning(f"Invalid JSON received: {data}") |
|
|
if websocket in manager.connections: |
|
|
group_name = manager.connections[websocket] |
|
|
await manager.broadcast_to_group(data, group_name) |
|
|
|
|
|
except WebSocketDisconnect: |
|
|
logger.info("WebSocket disconnected") |
|
|
manager.disconnect(websocket) |
|
|
except Exception as e: |
|
|
logger.error(f"Unerwarteter Fehler: {e}") |
|
|
manager.disconnect(websocket) |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/health") |
|
|
async def health_check(): |
|
|
return {"status": "healthy", "websocket_stats": manager.get_stats()} |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/group/{group_name}") |
|
|
async def get_group_info(group_name: str): |
|
|
"""API-Endpunkt zum Abrufen von Gruppeninformationen""" |
|
|
group_info = manager.get_group_info(group_name) |
|
|
if group_info["members"] == 0: |
|
|
raise HTTPException(status_code=404, detail="Group not found") |
|
|
return JSONResponse(content=group_info) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info") |
|
|
|