package com.lions.dev.websocket; import com.lions.dev.dto.request.chat.SendMessageRequestDTO; import com.lions.dev.dto.response.chat.MessageResponseDTO; import com.lions.dev.entity.chat.Message; import com.lions.dev.service.MessageService; import io.quarkus.logging.Log; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.websocket.*; import jakarta.websocket.server.PathParam; import jakarta.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** * WebSocket endpoint pour le chat en temps réel. * * Ce endpoint gère: * - La connexion/déconnexion des utilisateurs * - L'envoi et la réception de messages en temps réel * - Les indicateurs de frappe (typing indicators) * - Les confirmations de lecture (read receipts) * * URL: ws://localhost:8080/chat/ws/{userId} */ @ServerEndpoint("/chat/ws/{userId}") @ApplicationScoped public class ChatWebSocket { @Inject MessageService messageService; // Map pour stocker les sessions WebSocket des utilisateurs connectés private static final Map sessions = new ConcurrentHashMap<>(); /** * Appelé lorsqu'un utilisateur se connecte. * * @param session La session WebSocket * @param userId L'ID de l'utilisateur */ @OnOpen public void onOpen(Session session, @PathParam("userId") String userId) { try { UUID userUUID = UUID.fromString(userId); sessions.put(userUUID, session); Log.info("[LOG] WebSocket ouvert pour l'utilisateur ID : " + userId); // Envoyer un message de confirmation sendToUser(userUUID, "{\"type\":\"connected\",\"message\":\"Connecté au chat\"}"); } catch (Exception e) { Log.error("[ERROR] Erreur lors de la connexion WebSocket : " + e.getMessage(), e); } } /** * Appelé lorsqu'un message est reçu. * * @param message Le message reçu (au format JSON) * @param userId L'ID de l'utilisateur qui envoie le message */ @OnMessage public void onMessage(String message, @PathParam("userId") String userId) { try { Log.info("[LOG] Message reçu de l'utilisateur " + userId + " : " + message); // Parser le message JSON com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); Map messageData = mapper.readValue(message, Map.class); String type = (String) messageData.get("type"); switch (type) { case "message": handleChatMessage(messageData, userId); break; case "typing": handleTypingIndicator(messageData, userId); break; case "read": handleReadReceipt(messageData, userId); break; default: Log.warn("[WARN] Type de message inconnu : " + type); } } catch (Exception e) { Log.error("[ERROR] Erreur lors du traitement du message : " + e.getMessage(), e); } } /** * Appelé lorsqu'une erreur se produit. * * @param session La session WebSocket * @param error L'erreur */ @OnError public void onError(Session session, Throwable error) { Log.error("[ERROR] Erreur WebSocket : " + error.getMessage(), error); } /** * Appelé lorsqu'un utilisateur se déconnecte. * * @param session La session WebSocket * @param userId L'ID de l'utilisateur */ @OnClose public void onClose(Session session, @PathParam("userId") String userId) { try { UUID userUUID = UUID.fromString(userId); sessions.remove(userUUID); Log.info("[LOG] WebSocket fermé pour l'utilisateur ID : " + userId); } catch (Exception e) { Log.error("[ERROR] Erreur lors de la fermeture WebSocket : " + e.getMessage(), e); } } /** * Gère l'envoi d'un message de chat. */ private void handleChatMessage(Map messageData, String senderId) { try { UUID senderUUID = UUID.fromString(senderId); UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId")); String content = (String) messageData.get("content"); String messageType = messageData.getOrDefault("messageType", "text").toString(); String mediaUrl = (String) messageData.get("mediaUrl"); // Enregistrer le message dans la base de données Message message = messageService.sendMessage( senderUUID, recipientUUID, content, messageType, mediaUrl ); // Créer le DTO de réponse MessageResponseDTO response = new MessageResponseDTO(message); com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); String responseJson = mapper.writeValueAsString(Map.of( "type", "message", "data", response )); // Envoyer le message au destinataire s'il est connecté sendToUser(recipientUUID, responseJson); // Envoyer une confirmation à l'expéditeur sendToUser(senderUUID, responseJson); Log.info("[LOG] Message envoyé de " + senderId + " à " + recipientUUID); } catch (Exception e) { Log.error("[ERROR] Erreur lors de l'envoi du message : " + e.getMessage(), e); } } /** * Gère les indicateurs de frappe. */ private void handleTypingIndicator(Map messageData, String userId) { try { UUID recipientUUID = UUID.fromString((String) messageData.get("recipientId")); boolean isTyping = (boolean) messageData.getOrDefault("isTyping", false); com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); String response = mapper.writeValueAsString(Map.of( "type", "typing", "userId", userId, "isTyping", isTyping )); sendToUser(recipientUUID, response); Log.info("[LOG] Indicateur de frappe envoyé de " + userId + " à " + recipientUUID); } catch (Exception e) { Log.error("[ERROR] Erreur lors de l'envoi de l'indicateur de frappe : " + e.getMessage(), e); } } /** * Gère les confirmations de lecture. */ private void handleReadReceipt(Map messageData, String userId) { try { UUID messageUUID = UUID.fromString((String) messageData.get("messageId")); // Marquer le message comme lu Message message = messageService.markMessageAsRead(messageUUID); // Notifier l'expéditeur que le message a été lu UUID senderUUID = message.getSender().getId(); com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); String response = mapper.writeValueAsString(Map.of( "type", "read", "messageId", messageUUID.toString(), "readBy", userId )); sendToUser(senderUUID, response); Log.info("[LOG] Confirmation de lecture envoyée pour le message " + messageUUID); } catch (Exception e) { Log.error("[ERROR] Erreur lors de l'envoi de la confirmation de lecture : " + e.getMessage(), e); } } /** * Envoie un message à un utilisateur spécifique. * * @param userId L'ID de l'utilisateur * @param message Le message à envoyer */ private void sendToUser(UUID userId, String message) { Session session = sessions.get(userId); if (session != null && session.isOpen()) { try { session.getAsyncRemote().sendText(message); } catch (Exception e) { Log.error("[ERROR] Erreur lors de l'envoi du message à l'utilisateur " + userId + " : " + e.getMessage(), e); } } else { Log.warn("[WARN] Utilisateur " + userId + " non connecté ou session fermée"); } } /** * Diffuse un message à tous les utilisateurs connectés. * * @param message Le message à diffuser */ public void broadcast(String message) { sessions.values().forEach(session -> { if (session.isOpen()) { try { session.getAsyncRemote().sendText(message); } catch (Exception e) { Log.error("[ERROR] Erreur lors de la diffusion : " + e.getMessage(), e); } } }); } /** * Envoie un message chat à un utilisateur spécifique via WebSocket. * * Cette méthode est statique pour permettre son appel depuis les services * (comme MessageService) sans nécessiter une instance de ChatWebSocket. * * @param userId L'ID de l'utilisateur destinataire * @param messageData Les données du message (id, conversationId, content, etc.) */ public static void sendMessageToUser(UUID userId, Map messageData) { Session session = sessions.get(userId); if (session == null || !session.isOpen()) { Log.warn("[CHAT-WS] Utilisateur " + userId + " non connecté, message non envoyé"); return; } try { // Construire le message JSON au format attendu par le frontend com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); Map envelope = Map.of( "type", "message", "data", messageData ); String json = mapper.writeValueAsString(envelope); session.getAsyncRemote().sendText(json); Log.info("[CHAT-WS] Message envoyé à l'utilisateur " + userId); } catch (Exception e) { Log.error("[CHAT-WS] Erreur lors de l'envoi du message à " + userId + " : " + e.getMessage(), e); } } /** * Envoie une confirmation de délivrance à l'expéditeur via WebSocket. * * Cette méthode est appelée lorsqu'un message est délivré au destinataire * pour notifier l'expéditeur que le message a bien été reçu. * * @param userId L'ID de l'utilisateur (expéditeur) à notifier * @param deliveryData Les données de confirmation (messageId, isDelivered, timestamp) */ public static void sendDeliveryConfirmation(UUID userId, Map deliveryData) { Session session = sessions.get(userId); if (session == null || !session.isOpen()) { Log.warn("[CHAT-WS] Utilisateur " + userId + " non connecté, confirmation de délivrance non envoyée"); return; } try { com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); Map envelope = Map.of( "type", "delivered", "data", deliveryData ); String json = mapper.writeValueAsString(envelope); session.getAsyncRemote().sendText(json); Log.info("[CHAT-WS] Confirmation de délivrance envoyée à l'utilisateur " + userId); } catch (Exception e) { Log.error("[CHAT-WS] Erreur lors de l'envoi de la confirmation de délivrance : " + e.getMessage(), e); } } /** * Envoie une confirmation de lecture à l'expéditeur via WebSocket. * * @param userId L'ID de l'utilisateur expéditeur * @param readData Les données de confirmation de lecture */ public static void sendReadConfirmation(UUID userId, Map readData) { Session session = sessions.get(userId); if (session == null || !session.isOpen()) { Log.warn("[CHAT-WS] Utilisateur " + userId + " non connecté, confirmation de lecture non envoyée"); return; } try { com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); Map envelope = Map.of( "type", "read", "data", readData ); String json = mapper.writeValueAsString(envelope); session.getAsyncRemote().sendText(json); Log.info("[CHAT-WS] Confirmation de lecture envoyée à l'utilisateur " + userId); } catch (Exception e) { Log.error("[CHAT-WS] Erreur lors de l'envoi de la confirmation de lecture : " + e.getMessage(), e); } } /** * Récupère le nombre d'utilisateurs connectés. * * @return Le nombre d'utilisateurs connectés */ public static int getConnectedUsersCount() { return sessions.size(); } }