Files
afterwork-server-impl-quarkus/src/main/java/com/lions/dev/websocket/ChatWebSocketNext.java
2026-02-07 17:04:49 +00:00

327 lines
13 KiB
Java

package com.lions.dev.websocket;
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 io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketConnection;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket endpoint pour le chat en temps réel (WebSockets Next).
*
* Architecture v2.0:
* Client → WebSocket → MessageService → Kafka → Bridge → WebSocket → Destinataire
*
* 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)
* - Les confirmations de délivrance
*
* URL: ws://localhost:8080/chat/{userId}
*/
@WebSocket(path = "/afterwork/chat/{userId}")
@ApplicationScoped
public class ChatWebSocketNext {
@Inject
MessageService messageService;
// Map pour stocker les sessions WebSocket des utilisateurs connectés
private static final Map<UUID, WebSocketConnection> sessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(WebSocketConnection connection) {
String userId = connection.pathParam("userId");
try {
UUID userUUID = UUID.fromString(userId);
sessions.put(userUUID, connection);
Log.info("[CHAT-WS-NEXT] WebSocket ouvert pour l'utilisateur ID : " + userId + " (sessions actives: " + sessions.size() + ")");
// Envoyer un message de confirmation
String confirmation = buildJsonMessage("connected",
Map.of("message", "Connecté au chat"));
connection.sendText(confirmation);
} catch (IllegalArgumentException e) {
Log.error("[CHAT-WS-NEXT] UUID invalide: " + userId, e);
connection.close();
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de la connexion", e);
connection.close();
}
}
@OnClose
public void onClose(WebSocketConnection connection) {
try {
String userId = connection.pathParam("userId");
UUID userUUID = UUID.fromString(userId);
sessions.remove(userUUID);
Log.info("[CHAT-WS-NEXT] WebSocket fermé pour l'utilisateur ID : " + userId);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de la fermeture", e);
}
}
@OnTextMessage
public void onMessage(String message, WebSocketConnection connection) {
try {
String userId = connection.pathParam("userId");
Log.debug("[CHAT-WS-NEXT] Message reçu de " + userId + ": " + message);
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> raw = mapper.readValue(message, Map.class);
String type = (String) raw.get("type");
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) raw.get("data");
switch (type) {
case "message":
if (data != null) handleChatMessage(data, userId);
else Log.warn("[CHAT-WS-NEXT] Message sans 'data'");
break;
case "typing":
if (data != null) handleTypingIndicator(data, userId);
break;
case "read":
if (data != null) handleReadReceipt(data, userId);
else Log.warn("[CHAT-WS-NEXT] Read receipt sans 'data'");
break;
case "ping":
// Heartbeat - ignorer
break;
default:
Log.warn("[CHAT-WS-NEXT] Type inconnu: " + type);
}
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors du traitement du message", e);
}
}
/**
* Gère l'envoi d'un message de chat via WebSocket.
* Note: L'envoi principal passe par REST (POST /messages). Cette méthode
* est pour compatibilité si le client envoie via WebSocket.
*/
private void handleChatMessage(Map<String, Object> data, String senderId) {
try {
UUID senderUUID = UUID.fromString(senderId);
UUID recipientUUID = UUID.fromString((String) data.get("recipientId"));
String content = (String) data.get("content");
String messageType = data.getOrDefault("messageType", "text").toString();
String mediaUrl = (String) data.get("mediaUrl");
// Enregistrer le message dans la base de données
// MessageService publiera automatiquement dans Kafka
Message message = messageService.sendMessage(
senderUUID,
recipientUUID,
content,
messageType,
mediaUrl
);
// Créer le DTO de réponse
MessageResponseDTO response = new MessageResponseDTO(message);
String responseJson = buildJsonMessage("message",
Map.of("message", response));
// Envoyer confirmation à l'expéditeur
sendToUser(senderUUID, responseJson);
Log.info("[CHAT-WS-NEXT] Message traité de " + senderId + " à " + recipientUUID);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi du message", e);
}
}
/**
* Gère les indicateurs de frappe.
* data doit contenir recipientId (ID du destinataire) et isTyping.
*/
private void handleTypingIndicator(Map<String, Object> data, String userId) {
try {
Object recipientIdObj = data.get("recipientId");
if (recipientIdObj == null) {
Log.warn("[CHAT-WS-NEXT] Typing sans recipientId - ignoré");
return;
}
UUID recipientUUID = UUID.fromString(recipientIdObj.toString());
Object isTypingObj = data.get("isTyping");
boolean isTyping = isTypingObj instanceof Boolean ? (Boolean) isTypingObj : Boolean.parseBoolean(String.valueOf(isTypingObj));
String response = buildJsonMessage("typing", Map.of(
"conversationId", data.getOrDefault("conversationId", ""),
"userId", userId,
"isTyping", isTyping
));
sendToUser(recipientUUID, response);
Log.debug("[CHAT-WS-NEXT] Indicateur de frappe envoyé de " + userId + " à " + recipientUUID);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi de l'indicateur de frappe", e);
}
}
/**
* Gère les confirmations de lecture.
* Envoie type "read" (format attendu par le client Flutter).
*/
private void handleReadReceipt(Map<String, Object> data, String userId) {
try {
UUID messageUUID = UUID.fromString((String) data.get("messageId"));
Message message = messageService.markMessageAsRead(messageUUID);
if (message != null) {
UUID senderUUID = message.getSender().getId();
long now = System.currentTimeMillis();
String timestampIso = java.time.Instant.ofEpochMilli(now).toString();
String response = buildJsonMessage("read", Map.of(
"messageId", messageUUID.toString(),
"userId", userId,
"timestamp", timestampIso
));
sendToUser(senderUUID, response);
Log.info("[CHAT-WS-NEXT] Confirmation de lecture envoyée pour message " + messageUUID);
}
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors du traitement de la confirmation de lecture", e);
}
}
/**
* Envoie un message chat à un utilisateur spécifique via WebSocket.
* Appelé par le bridge Kafka → WebSocket.
*
* @param userId ID de l'utilisateur destinataire
* @param message Message JSON à envoyer
*/
public static void sendMessageToUser(UUID userId, String message) {
WebSocketConnection connection = sessions.get(userId);
if (connection == null || !connection.isOpen()) {
if (connection != null) {
sessions.remove(userId);
Log.debug("[CHAT-WS-NEXT] Connexion périmée supprimée pour " + userId);
}
Log.info("[CHAT-WS-NEXT] Destinataire " + userId + " non connecté, message non délivré (sessions: " + sessions.size() + ")");
return;
}
try {
String preview = message.length() > 300 ? message.substring(0, 300) + "..." : message;
Log.info("[CHAT-WS-NEXT] Envoi vers " + userId + " (" + message.length() + " car): " + preview);
connection.sendText(message);
Log.info("[CHAT-WS-NEXT] Message délivré à l'utilisateur: " + userId);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi à " + userId + ", connexion supprimée", e);
sessions.remove(userId);
}
}
/**
* Envoie une confirmation de délivrance à l'expéditeur via WebSocket.
*/
public static void sendDeliveryConfirmation(UUID senderId, Map<String, Object> confirmationData) {
WebSocketConnection connection = sessions.get(senderId);
if (connection == null || !connection.isOpen()) {
if (connection != null) sessions.remove(senderId);
Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation");
return;
}
try {
String response = buildJsonMessage("delivery_confirmation", confirmationData);
connection.sendText(response);
Log.debug("[CHAT-WS-NEXT] Confirmation de délivrance envoyée à: " + senderId);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation à " + senderId + ", connexion supprimée", e);
sessions.remove(senderId);
}
}
/**
* Envoie une confirmation de lecture à l'expéditeur via WebSocket.
*/
public static void sendReadConfirmation(UUID senderId, Map<String, Object> readData) {
WebSocketConnection connection = sessions.get(senderId);
if (connection == null || !connection.isOpen()) {
if (connection != null) sessions.remove(senderId);
Log.debug("[CHAT-WS-NEXT] Expéditeur " + senderId + " non connecté pour confirmation de lecture");
return;
}
try {
String response = buildJsonMessage("read_confirmation", readData);
connection.sendText(response);
Log.debug("[CHAT-WS-NEXT] Confirmation de lecture envoyée à: " + senderId);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur envoi confirmation lecture à " + senderId + ", connexion supprimée", e);
sessions.remove(senderId);
}
}
/**
* Envoie un message à un utilisateur (méthode privée pour usage interne).
*/
private void sendToUser(UUID userId, String message) {
WebSocketConnection connection = sessions.get(userId);
if (connection != null && connection.isOpen()) {
try {
connection.sendText(message);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur lors de l'envoi à " + userId, e);
}
}
}
/**
* Construit un message JSON.
*/
private static String buildJsonMessage(String type, Map<String, Object> data) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
Map<String, Object> message = Map.of(
"type", type,
"data", data,
"timestamp", System.currentTimeMillis()
);
return mapper.writeValueAsString(message);
} catch (Exception e) {
Log.error("[CHAT-WS-NEXT] Erreur construction JSON", e);
return "{\"type\":\"error\",\"data\":{\"message\":\"Erreur de construction\"}}";
}
}
/**
* Récupère le nombre d'utilisateurs connectés au chat.
*/
public static int getConnectedUsersCount() {
return sessions.size();
}
}