nakama/moderation/backend-py/websocket_server.py

141 lines
4.5 KiB
Python
Raw Normal View History

2025-12-14 23:45:41 +00:00
"""
WebSocket server for moderation chat
"""
import socketio
import logging
2025-12-15 00:37:34 +00:00
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
2025-12-14 23:45:41 +00:00
logger = logging.getLogger(__name__)
# Create Socket.IO server
sio = socketio.AsyncServer(
async_mode='asgi',
cors_allowed_origins='*',
logger=False,
engineio_logger=False
)
# Track connected moderators
2025-12-15 00:37:34 +00:00
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')
2025-12-14 23:45:41 +00:00
2025-12-15 00:42:02 +00:00
# Namespace handlers for /mod-chat
@sio.on('connect', namespace='/mod-chat')
2025-12-15 00:37:34 +00:00
async def on_connect(sid, environ):
"""Handle client connection to /mod-chat namespace"""
logger.info(f"[WebSocket] Client connected to /mod-chat: {sid}")
# Don't authorize immediately - wait for 'auth' event
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
2025-12-15 00:42:02 +00:00
@sio.on('disconnect', namespace='/mod-chat')
2025-12-15 00:37:34 +00:00
async def on_disconnect(sid):
2025-12-14 23:45:41 +00:00
"""Handle client disconnection"""
2025-12-15 00:37:34 +00:00
logger.info(f"[WebSocket] Client disconnected from /mod-chat: {sid}")
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
if sid in connected_moderators:
del connected_moderators[sid]
broadcast_online()
2025-12-14 23:45:41 +00:00
2025-12-15 00:42:02 +00:00
@sio.on('auth', namespace='/mod-chat')
2025-12-15 00:37:34 +00:00
async def on_auth(sid, data):
"""Handle authentication for moderation chat"""
2025-12-14 23:45:41 +00:00
try:
2025-12-15 00:37:34 +00:00
username = normalize_username(data.get('username')) if data.get('username') else None
telegram_id = data.get('telegramId')
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
if not username or not telegram_id:
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')
2025-12-14 23:45:41 +00:00
return
2025-12-15 00:37:34 +00:00
# Check if user is owner
owner_usernames = settings.OWNER_USERNAMES_LIST
is_owner = username.lower() in [u.lower() for u in owner_usernames]
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
# Check if user is moderation admin
admin = await moderation_admins_collection().find_one({
'telegramId': str(telegram_id)
})
is_admin = admin is not None
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
if not is_owner and not is_admin:
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
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
# Store connection data
connected_moderators[sid] = {
2025-12-14 23:45:41 +00:00
'username': username,
2025-12-15 00:37:34 +00:00
'telegramId': telegram_id,
'isOwner': is_owner
}
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
logger.info(f"[WebSocket] Auth success: {username} (owner: {is_owner}, admin: {is_admin})")
await sio.emit('ready', namespace='/mod-chat', room=sid)
broadcast_online()
2025-12-14 23:45:41 +00:00
except Exception as e:
2025-12-15 00:37:34 +00:00
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')
2025-12-14 23:45:41 +00:00
2025-12-15 00:42:02 +00:00
@sio.on('message', namespace='/mod-chat')
2025-12-15 00:37:34 +00:00
async def on_message(sid, data):
2025-12-14 23:45:41 +00:00
"""Handle moderation chat message"""
try:
2025-12-15 00:37:34 +00:00
if sid not in connected_moderators:
logger.warning(f"[WebSocket] Message from unauthorized client: {sid}")
return
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
user_data = connected_moderators[sid]
text = (data.get('text') or '').strip()
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
if not text:
return
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
message = {
'id': f"{int(datetime.utcnow().timestamp() * 1000)}-{sid[:8]}",
'username': user_data['username'],
'telegramId': user_data['telegramId'],
'text': text,
'createdAt': datetime.utcnow().isoformat()
}
2025-12-14 23:45:41 +00:00
2025-12-15 00:37:34 +00:00
# Broadcast to all in namespace
await sio.emit('message', message, namespace='/mod-chat')
logger.info(f"[WebSocket] Message from {user_data['username']}: {text[:50]}...")
2025-12-14 23:45:41 +00:00
except Exception as e:
2025-12-15 00:37:34 +00:00
logger.error(f"[WebSocket] Error handling message: {e}")
import traceback
traceback.print_exc()
2025-12-14 23:45:41 +00:00
def get_socketio_app():
"""Get Socket.IO ASGI app"""
return socketio.ASGIApp(sio)