nakama/moderation/backend-py/websocket_server.py

173 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
WebSocket server for moderation chat
"""
import socketio
import logging
from typing import Dict, Set, Optional
from datetime import datetime
from config import settings
from database import moderation_admins_collection
from utils.auth import normalize_username
logger = logging.getLogger(__name__)
# Create Socket.IO server
sio = socketio.AsyncServer(
async_mode='asgi',
cors_allowed_origins='*',
logger=True, # Включить логирование для отладки
engineio_logger=True, # Включить логирование Engine.IO
ping_timeout=60,
ping_interval=25
)
# Track connected moderators
connected_moderators: Dict[str, dict] = {} # {sid: {username, telegramId, isOwner}}
def broadcast_online():
"""Broadcast list of online moderators"""
unique = {}
for data in connected_moderators.values():
username = data.get('username')
if username:
unique[username] = data
online_list = list(unique.values())
sio.emit('online', online_list, namespace='/mod-chat')
# Обработчик для корневого namespace (для отладки)
@sio.on('connect', namespace='/')
async def on_connect_root(sid, environ):
"""Handle client connection to root namespace (should not happen)"""
print(f"[WebSocket] ⚠️ Client connected to ROOT namespace: {sid}")
logger.warning(f"[WebSocket] Client connected to ROOT namespace: {sid}")
# Отключаем, т.к. мы используем только /mod-chat
await sio.disconnect(sid, namespace='/')
# Namespace handlers for /mod-chat
@sio.on('connect', namespace='/mod-chat')
async def on_connect(sid, environ):
"""Handle client connection to /mod-chat namespace"""
print(f"[WebSocket] ✅ Client connected to /mod-chat: {sid}")
logger.info(f"[WebSocket] Client connected to /mod-chat: {sid}")
if environ:
print(f"[WebSocket] Environ keys: {list(environ.keys()) if isinstance(environ, dict) else 'N/A'}")
if isinstance(environ, dict):
print(f"[WebSocket] Query string: {environ.get('QUERY_STRING', 'N/A')}")
print(f"[WebSocket] Path: {environ.get('PATH_INFO', 'N/A')}")
# Don't authorize immediately - wait for 'auth' event
# Возвращаем True, чтобы разрешить подключение
return True
@sio.on('disconnect', namespace='/mod-chat')
async def on_disconnect(sid):
"""Handle client disconnection"""
logger.info(f"[WebSocket] Client disconnected from /mod-chat: {sid}")
if sid in connected_moderators:
del connected_moderators[sid]
broadcast_online()
@sio.on('auth', namespace='/mod-chat')
async def on_auth(sid, data):
"""Handle authentication for moderation chat"""
print(f"[WebSocket] 📥 Auth запрос от {sid}: {data}")
try:
username = normalize_username(data.get('username')) if data.get('username') else None
telegram_id = data.get('telegramId')
print(f"[WebSocket] Обработка auth: username={username}, telegramId={telegram_id}")
if not username or not telegram_id:
print(f"[WebSocket] ❌ Auth failed: missing username or telegramId")
logger.warning(f"[WebSocket] Auth failed: missing username or telegramId")
await sio.emit('unauthorized', namespace='/mod-chat', room=sid)
await sio.disconnect(sid, namespace='/mod-chat')
return
# Check if user is owner
owner_usernames = settings.OWNER_USERNAMES_LIST
print(f"[WebSocket] Owner usernames: {owner_usernames}")
is_owner = username.lower() in [u.lower() for u in owner_usernames]
print(f"[WebSocket] Is owner: {is_owner}")
# Check if user is moderation admin
admin = await moderation_admins_collection().find_one({
'telegramId': str(telegram_id)
})
is_admin = admin is not None
print(f"[WebSocket] Is admin: {is_admin}, admin data: {admin}")
if not is_owner and not is_admin:
print(f"[WebSocket] ❌ Access denied: {username} (telegramId: {telegram_id})")
logger.warning(f"[WebSocket] Access denied: {username} (telegramId: {telegram_id})")
await sio.emit('unauthorized', namespace='/mod-chat', room=sid)
await sio.disconnect(sid, namespace='/mod-chat')
return
# Store connection data
connected_moderators[sid] = {
'username': username,
'telegramId': telegram_id,
'isOwner': is_owner
}
print(f"[WebSocket] ✅ Auth success: {username} (owner: {is_owner}, admin: {is_admin})")
logger.info(f"[WebSocket] Auth success: {username} (owner: {is_owner}, admin: {is_admin})")
await sio.emit('ready', namespace='/mod-chat', room=sid)
broadcast_online()
except Exception as e:
print(f"[WebSocket] ❌ Error in auth: {type(e).__name__}: {e}")
logger.error(f"[WebSocket] Error in auth: {e}")
import traceback
traceback.print_exc()
await sio.emit('unauthorized', namespace='/mod-chat', room=sid)
await sio.disconnect(sid, namespace='/mod-chat')
@sio.on('message', namespace='/mod-chat')
async def on_message(sid, data):
"""Handle moderation chat message"""
try:
if sid not in connected_moderators:
logger.warning(f"[WebSocket] Message from unauthorized client: {sid}")
return
user_data = connected_moderators[sid]
text = (data.get('text') or '').strip()
if not text:
return
message = {
'id': f"{int(datetime.utcnow().timestamp() * 1000)}-{sid[:8]}",
'username': user_data['username'],
'telegramId': user_data['telegramId'],
'text': text,
'createdAt': datetime.utcnow().isoformat()
}
# Broadcast to all in namespace
await sio.emit('message', message, namespace='/mod-chat')
logger.info(f"[WebSocket] Message from {user_data['username']}: {text[:50]}...")
except Exception as e:
logger.error(f"[WebSocket] Error handling message: {e}")
import traceback
traceback.print_exc()
def get_socketio_app():
"""Get Socket.IO ASGI app"""
# Socket.IO ASGI app должен обернуть FastAPI app для правильной работы
# Но мы делаем это в main.py через SocketIOWrapper
# Здесь просто возвращаем ASGI app для Socket.IO
return socketio.ASGIApp(sio, socketio_path='/socket.io')