JCEF migration: add jcefmaven dep, make JavaFX optional, create LogBridge, wire JCEFLauncher entry point

This commit is contained in:
SashegDev
2026-07-02 11:49:09 +00:00
parent 0d61ad1107
commit 8eabe232ff
17 changed files with 3017 additions and 133 deletions
+154 -2
View File
@@ -11,7 +11,7 @@ import httpx
import json
import structlog
from cachetools import TTLCache
from fastapi import Depends, FastAPI, HTTPException, Request, Response
from fastapi import Depends, FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect, Query
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
@@ -28,8 +28,9 @@ from log_manager import init_logging
from auth import get_current_user, router as auth_router, init_db, verify_jwt
from roles import Permissions, has_permission
from admin_router import router as admin_router
from friends import router as friends_router, init_friends_db
from friends import router as friends_router, init_friends_db, get_friends_ids
from playtime import router as playtime_router, init_playtime_db
from ws_manager import manager
import asyncio
import hashlib
@@ -222,8 +223,14 @@ async def lifespan(app: FastAPI):
asyncio.create_task(periodic_sync())
# Start WebSocket admin clients broadcaster
broadcaster = asyncio.create_task(admin_clients_broadcaster())
yield
# Cancel WebSocket broadcaster
broadcaster.cancel()
# Cleanup proxy client
if proxy_client:
await proxy_client.aclose()
@@ -925,6 +932,151 @@ app.include_router(friends_router)
app.include_router(playtime_router)
# ====================== WEBSOCKET ======================
async def admin_clients_broadcaster():
"""Periodically push client list to connected admins via WebSocket"""
while True:
await asyncio.sleep(5)
try:
from auth import get_db
from roles import ROLE_MODERATOR, ROLE_ELDER, ROLE_CREATOR
admin_ids = [uid for uid in list(manager._connections.keys())]
if not admin_ids:
continue
# Check which are actually admins
with get_db() as conn:
admin_rows = conn.execute(
"SELECT id FROM users WHERE id IN ({}) AND role >= ?".format(
",".join("?" for _ in admin_ids)),
admin_ids + [ROLE_MODERATOR]
).fetchall()
real_admins = [r["id"] for r in admin_rows]
if not real_admins:
continue
stale_cutoff = time.time() - 300
with get_db() as conn:
conn.execute("UPDATE user_status SET is_online = 0 WHERE last_seen < ?", (stale_cutoff,))
conn.commit()
rows = conn.execute("""
SELECT u.id, u.username, us.is_online, us.current_pack, us.last_seen,
(SELECT s.ip_address FROM user_sessions s
WHERE s.user_id = u.id AND s.is_active = 1
ORDER BY s.created_at DESC LIMIT 1) as ip_address
FROM users u
JOIN user_status us ON u.id = us.user_id
WHERE us.is_online = 1
ORDER BY us.last_seen DESC
""").fetchall()
clients = []
for row in rows:
clients.append({
"id": row["id"], "username": row["username"],
"online": True, "current_pack": row["current_pack"] or "",
"last_seen": str(row["last_seen"]) if row["last_seen"] else None,
"ip": row["ip_address"] or ""
})
await manager.broadcast_admin_clients(clients, real_admins)
except Exception as exc:
logger.debug("admin_clients_broadcaster error", error=str(exc))
@app.websocket("/ws")
async def websocket_endpoint(
ws: WebSocket,
token: str = Query(...),
):
try:
payload = verify_jwt(token)
if payload is None:
await ws.close(code=4001, reason="Invalid token")
return
user_id = payload.get("user_id")
if user_id is None:
await ws.close(code=4001, reason="Invalid token payload")
return
except Exception:
await ws.close(code=4001, reason="Invalid token")
return
await manager.connect(user_id, ws)
# Send initial friends list
try:
with get_db() as conn:
cutoff = time.time() - 300
conn.execute("UPDATE user_status SET is_online = 0 WHERE last_seen < ?", (cutoff,))
conn.commit()
rows = conn.execute("""
SELECT u.id, u.username, u.role,
COALESCE(us.is_online, 0) as online,
COALESCE(us.current_pack, '') as current_pack,
us.last_seen
FROM friendships f
JOIN users u ON (CASE WHEN f.requester_id = ? THEN f.target_id ELSE f.requester_id END) = u.id
LEFT JOIN user_status us ON u.id = us.user_id
WHERE (f.requester_id = ? OR f.target_id = ?) AND f.status = 'accepted'
""", (user_id, user_id, user_id))
friends = []
for row in rows:
friends.append({
"id": row["id"], "username": row["username"],
"role": row["role"], "online": bool(row["online"]),
"current_pack": row["current_pack"],
"last_seen": str(row["last_seen"]) if row["last_seen"] else None
})
await manager.send_json(user_id, {"type": "friends_update", "friends": friends})
# Send initial friend requests
with get_db() as conn:
req_rows = conn.execute("""
SELECT u.id, u.username, u.role, f.created_at
FROM friendships f
JOIN users u ON f.requester_id = u.id
WHERE f.target_id = ? AND f.status = 'pending'
""", (user_id,))
requests = []
for row in req_rows:
requests.append({
"id": row["id"], "username": row["username"],
"role": row["role"], "created_at": str(row["created_at"]) if row["created_at"] else None
})
await manager.send_json(user_id, {"type": "requests_update", "requests": requests})
except Exception as exc:
logger.warning("WS initial data error", error=str(exc))
try:
while True:
data = await ws.receive_text()
try:
msg = json.loads(data)
msg_type = msg.get("type")
if msg_type == "ping":
await ws.send_text(json.dumps({"type": "pong"}))
elif msg_type == "status":
current_pack = msg.get("current_pack", "")
with get_db() as conn:
conn.execute("""
INSERT INTO user_status (user_id, is_online, current_pack, last_seen)
VALUES (?, 1, ?, CURRENT_TIMESTAMP)
ON CONFLICT(user_id) DO UPDATE SET
is_online = 1,
current_pack = COALESCE(?, user_status.current_pack),
last_seen = CURRENT_TIMESTAMP
""", (user_id, current_pack, current_pack))
# Notify friends
friend_ids = get_friends_ids(user_id)
await manager.broadcast_status_change(user_id, friend_ids)
except json.JSONDecodeError:
pass
except WebSocketDisconnect:
pass
except Exception as exc:
logger.warning("WS error", user_id=user_id, error=str(exc))
finally:
manager.disconnect(user_id, ws)
# Monkey patch to catch invalid HTTP requests
original_data_received = HttpToolsProtocol.data_received