import json import time import threading import asyncio import websockets from oedb.utils.logging import logger class WebSocketManager: """ Gestionnaire de WebSockets pour les fonctionnalités sociales d'OEDB. Gère les connexions WebSocket des utilisateurs et distribue les messages. Peut être utilisé soit de façon autonome, soit intégré avec uWSGI. """ def __init__(self): self.clients = {} self.positions = {} self.lock = threading.Lock() self.server = None async def handle_connection(self, websocket, path): """ Gère une connexion WebSocket entrante. Args: websocket: La connexion WebSocket. path: Le chemin de la demande. """ client_id = id(websocket) logger.debug(f"Tentative de connexion WebSocket reçue: {client_id} - {path}") try: logger.info(f"Nouvelle connexion WebSocket: {client_id} - {path}") # Ajouter le client à la liste with self.lock: self.clients[client_id] = { 'websocket': websocket, 'username': None, 'position': None, 'last_seen': time.time(), 'show_only_to_friends': False, } # Envoyer la liste des utilisateurs connectés await self.send_users_list(websocket) async for message in websocket: await self.handle_message(client_id, message) except websockets.exceptions.ConnectionClosed: logger.info(f"Connexion WebSocket fermée: {client_id}") except Exception as e: logger.error(f"Erreur WebSocket: {e}") finally: # Supprimer le client de la liste with self.lock: if client_id in self.clients: username = self.clients[client_id].get('username') if username and username in self.positions: del self.positions[username] del self.clients[client_id] # Informer les autres clients de la déconnexion await self.broadcast_users_list() async def handle_message(self, client_id, message): """ Traite un message WebSocket reçu. Args: client_id: L'ID du client qui a envoyé le message. message: Le message JSON reçu. """ try: data = json.loads(message) message_type = data.get('type') if message_type == 'position': await self.handle_position_update(client_id, data) elif message_type == 'pouet': await self.handle_pouet(data) elif message_type == 'friendRequest': await self.handle_friend_request(data) except json.JSONDecodeError: logger.error(f"Message JSON invalide: {message}") except Exception as e: logger.error(f"Erreur de traitement du message: {e}") async def handle_position_update(self, client_id, data): """ Traite une mise à jour de position d'un utilisateur. Args: client_id: L'ID du client qui a envoyé la mise à jour. data: Les données de position. """ username = data.get('username') position = data.get('position') show_only_to_friends = data.get('showOnlyToFriends', False) if not username or not position: return # Mettre à jour les informations du client with self.lock: if client_id in self.clients: self.clients[client_id]['username'] = username self.clients[client_id]['position'] = position self.clients[client_id]['last_seen'] = time.time() self.clients[client_id]['show_only_to_friends'] = show_only_to_friends # Mettre à jour la position dans le dictionnaire des positions self.positions[username] = { 'position': position, 'timestamp': data.get('timestamp'), 'show_only_to_friends': show_only_to_friends } # Diffuser la position à tous les autres clients await self.broadcast_position(username, position, data.get('timestamp'), show_only_to_friends) # Envoyer la liste mise à jour des utilisateurs await self.broadcast_users_list() async def handle_pouet(self, data): """ Traite un 'pouet pouet' envoyé d'un utilisateur à un autre. Args: data: Les données du pouet pouet. """ from_user = data.get('from') to_user = data.get('to') if not from_user or not to_user: return # Trouver le client destinataire recipient_client_id = None with self.lock: for client_id, client_info in self.clients.items(): if client_info.get('username') == to_user: recipient_client_id = client_id break if recipient_client_id and recipient_client_id in self.clients: # Envoyer le pouet au destinataire try: await self.clients[recipient_client_id]['websocket'].send(json.dumps({ 'type': 'pouet', 'from': from_user, 'timestamp': data.get('timestamp') })) logger.info(f"Pouet pouet envoyé de {from_user} à {to_user}") except Exception as e: logger.error(f"Erreur d'envoi de pouet pouet: {e}") async def handle_friend_request(self, data): """ Traite une demande d'ami d'un utilisateur à un autre. Args: data: Les données de la demande d'ami. """ from_user = data.get('from') to_user = data.get('to') if not from_user or not to_user: return # Trouver le client destinataire recipient_client_id = None with self.lock: for client_id, client_info in self.clients.items(): if client_info.get('username') == to_user: recipient_client_id = client_id break if recipient_client_id and recipient_client_id in self.clients: # Envoyer la demande d'ami au destinataire try: await self.clients[recipient_client_id]['websocket'].send(json.dumps({ 'type': 'friendRequest', 'from': from_user, 'timestamp': data.get('timestamp') })) logger.info(f"Demande d'ami envoyée de {from_user} à {to_user}") except Exception as e: logger.error(f"Erreur d'envoi de demande d'ami: {e}") async def broadcast_position(self, username, position, timestamp, show_only_to_friends): """ Diffuse la position d'un utilisateur à tous les autres utilisateurs. Args: username: Le nom d'utilisateur. position: La position de l'utilisateur. timestamp: L'horodatage de la mise à jour. show_only_to_friends: Indique si la position est visible uniquement par les amis. """ message = json.dumps({ 'type': 'position', 'username': username, 'position': position, 'timestamp': timestamp, 'showOnlyToFriends': show_only_to_friends }) with self.lock: for client_id, client_info in self.clients.items(): # Ne pas envoyer à l'utilisateur lui-même if client_info.get('username') == username: continue try: await client_info['websocket'].send(message) except Exception as e: logger.error(f"Erreur d'envoi de broadcast de position: {e}") async def send_users_list(self, websocket): """ Envoie la liste des utilisateurs connectés à un client spécifique. Args: websocket: La connexion WebSocket du client. """ users = [] with self.lock: for client_info in self.clients.values(): if client_info.get('username'): users.append({ 'username': client_info['username'], 'timestamp': time.time() }) try: await websocket.send(json.dumps({ 'type': 'users', 'users': users })) except Exception as e: logger.error(f"Erreur d'envoi de liste d'utilisateurs: {e}") async def broadcast_users_list(self): """ Diffuse la liste des utilisateurs connectés à tous les clients. """ users = [] with self.lock: for client_info in self.clients.values(): if client_info.get('username'): users.append({ 'username': client_info['username'], 'timestamp': time.time() }) message = json.dumps({ 'type': 'users', 'users': users }) with self.lock: for client_info in self.clients.values(): try: await client_info['websocket'].send(message) except Exception as e: logger.error(f"Erreur de broadcast de liste d'utilisateurs: {e}") async def cleanup_inactive_clients(self): """ Nettoie les clients inactifs (pas de mise à jour depuis plus de 5 minutes). """ inactive_clients = [] with self.lock: current_time = time.time() for client_id, client_info in self.clients.items(): if current_time - client_info['last_seen'] > 300: # 5 minutes inactive_clients.append(client_id) for client_id in inactive_clients: username = self.clients[client_id].get('username') if username and username in self.positions: del self.positions[username] del self.clients[client_id] if inactive_clients: logger.info(f"Nettoyage de {len(inactive_clients)} clients inactifs") await self.broadcast_users_list() async def cleanup_task(self): """ Tâche périodique pour nettoyer les clients inactifs. """ while True: await asyncio.sleep(60) # Exécuter toutes les minutes await self.cleanup_inactive_clients() async def start_server(self, host='0.0.0.0', port=8765): """ Démarre le serveur WebSocket. Args: host: L'hôte à écouter. port: Le port à écouter. """ self.server = await websockets.serve(self.handle_connection, host, port) logger.info(f"Serveur WebSocket démarré sur {host}:{port}") # Démarrer la tâche de nettoyage asyncio.create_task(self.cleanup_task()) # Garder le serveur en cours d'exécution await asyncio.Future() def start(self, host='0.0.0.0', port=8765): """ Démarre le serveur WebSocket dans un thread séparé. Args: host: L'hôte à écouter. port: Le port à écouter. """ def run_server(): asyncio.run(self.start_server(host, port)) server_thread = threading.Thread(target=run_server, daemon=True) server_thread.start() logger.info(f"Serveur WebSocket démarré dans un thread séparé sur {host}:{port}") # Créer une instance du gestionnaire WebSocket ws_manager = WebSocketManager() # Démarrer automatiquement le serveur WebSocket ws_manager.start(host='127.0.0.1', port=8765)