db = Database::getInstance(); } /** * GET /api/chat/rooms * Liste des conversations filtrées par rôle et entité */ public function getRooms(): void { Session::requireAuth(); try { $userId = Session::getUserId(); $entityId = Session::getEntityId(); // Récupérer le rôle de l'utilisateur $userRole = $this->getUserRole($userId); // Construction de la requête selon le rôle $sql = ' SELECT DISTINCT r.id, r.title, r.type, r.created_at, r.created_by, r.updated_at, -- Dernier message (SELECT m.content FROM chat_messages m WHERE m.room_id = r.id AND m.is_deleted = 0 ORDER BY m.sent_at DESC LIMIT 1) as last_message, (SELECT m.sent_at FROM chat_messages m WHERE m.room_id = r.id AND m.is_deleted = 0 ORDER BY m.sent_at DESC LIMIT 1) as last_message_at, -- Nombre de messages non lus (SELECT COUNT(*) FROM chat_messages m WHERE m.room_id = r.id AND m.sent_at > COALESCE(p.last_read_at, p.joined_at) AND m.sender_id != :user_id_count) as unread_count, -- Participants (SELECT COUNT(*) FROM chat_participants cp WHERE cp.room_id = r.id AND cp.left_at IS NULL) as participant_count FROM chat_rooms r INNER JOIN chat_participants p ON r.id = p.room_id WHERE r.is_active = 1 AND p.user_id = :user_id AND p.left_at IS NULL '; // Filtrage supplémentaire selon le rôle if ($userRole == 1) { // Utilisateur simple : seulement ses conversations privées et de groupe $sql .= ' AND r.type IN ("private", "group")'; } elseif ($userRole == 2) { // Admin d'entité : toutes les conversations de son entité $sql .= ' AND (p.entite_id = :entity_id OR r.type = "broadcast")'; } // Rôle > 2 : accès à toutes les conversations $sql .= ' ORDER BY COALESCE(last_message_at, r.created_at) DESC'; $stmt = $this->db->prepare($sql); $params = [ 'user_id' => $userId, 'user_id_count' => $userId ]; if ($userRole == 2) { $params['entity_id'] = $entityId; } $stmt->execute($params); $rooms = $stmt->fetchAll(PDO::FETCH_ASSOC); // Pour chaque room, récupérer les participants foreach ($rooms as &$room) { $room['participants'] = $this->getRoomParticipants($room['id']); } LogService::log('Récupération des conversations', [ 'level' => 'debug', 'user_id' => $userId, 'room_count' => count($rooms) ]); Response::json([ 'status' => 'success', 'rooms' => $rooms ]); } catch (PDOException $e) { LogService::log('Erreur lors de la récupération des conversations', [ 'level' => 'error', 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => 'Erreur serveur' ], 500); } } /** * POST /api/chat/rooms * Créer une nouvelle conversation */ public function createRoom(): void { Session::requireAuth(); try { $data = Request::getJson(); $userId = Session::getUserId(); $entityId = Session::getEntityId(); $userRole = $this->getUserRole($userId); // Validation des données if (!isset($data['type']) || !in_array($data['type'], ['private', 'group', 'broadcast'])) { Response::json([ 'status' => 'error', 'message' => 'Type de conversation invalide' ], 400); return; } // Vérification des permissions pour broadcast if ($data['type'] === 'broadcast' && $userRole < 2) { Response::json([ 'status' => 'error', 'message' => 'Permissions insuffisantes pour créer une diffusion' ], 403); return; } // Validation des participants if (!isset($data['participants']) || !is_array($data['participants']) || empty($data['participants'])) { Response::json([ 'status' => 'error', 'message' => 'Au moins un participant requis' ], 400); return; } // Pour une conversation privée, limiter à 2 participants (incluant le créateur) if ($data['type'] === 'private' && count($data['participants']) > 1) { Response::json([ 'status' => 'error', 'message' => 'Une conversation privée ne peut avoir que 2 participants' ], 400); return; } // Vérifier que tous les participants existent et sont accessibles $participantIds = array_map('intval', $data['participants']); if (!in_array($userId, $participantIds)) { $participantIds[] = $userId; // Ajouter le créateur } // Vérifier l'existence d'une conversation privée existante if ($data['type'] === 'private' && count($participantIds) === 2) { $existingRoom = $this->findExistingPrivateRoom($participantIds[0], $participantIds[1]); if ($existingRoom) { Response::json([ 'status' => 'success', 'room' => $existingRoom, 'existing' => true ]); return; } } // Générer un UUID pour la room $roomId = $this->generateUUID(); // Titre de la conversation $title = $data['title'] ?? null; if (!$title && $data['type'] === 'private') { // Pour une conversation privée, pas de titre par défaut $title = null; } $this->db->beginTransaction(); try { // Créer la room $stmt = $this->db->prepare(' INSERT INTO chat_rooms (id, title, type, created_by, created_at) VALUES (:id, :title, :type, :created_by, NOW()) '); $stmt->execute([ 'id' => $roomId, 'title' => $title, 'type' => $data['type'], 'created_by' => $userId ]); // Ajouter les participants $participantStmt = $this->db->prepare(' INSERT INTO chat_participants (room_id, user_id, role, entite_id, is_admin) VALUES (:room_id, :user_id, :role, :entite_id, :is_admin) '); foreach ($participantIds as $participantId) { $participantData = $this->getUserData($participantId); if (!$participantData) { throw new \Exception("Participant invalide: $participantId"); } $participantStmt->execute([ 'room_id' => $roomId, 'user_id' => $participantId, 'role' => $participantData['fk_role'], 'entite_id' => $participantData['fk_entite'], 'is_admin' => ($participantId === $userId) ? 1 : 0 ]); } // Si un message initial est fourni, l'envoyer if (isset($data['initial_message']) && !empty($data['initial_message'])) { $messageId = $this->generateUUID(); $msgStmt = $this->db->prepare(' INSERT INTO chat_messages (id, room_id, content, sender_id, sent_at) VALUES (:id, :room_id, :content, :sender_id, NOW()) '); $msgStmt->execute([ 'id' => $messageId, 'room_id' => $roomId, 'content' => $data['initial_message'], 'sender_id' => $userId ]); } $this->db->commit(); LogService::log('Conversation créée', [ 'level' => 'info', 'room_id' => $roomId, 'type' => $data['type'], 'created_by' => $userId, 'participant_count' => count($participantIds) ]); // Récupérer la room créée avec ses détails $room = $this->getRoomDetails($roomId); Response::json([ 'status' => 'success', 'room' => $room ], 201); } catch (\Exception $e) { $this->db->rollBack(); throw $e; } } catch (PDOException $e) { LogService::log('Erreur lors de la création de la conversation', [ 'level' => 'error', 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => 'Erreur serveur' ], 500); } catch (\Exception $e) { LogService::log('Erreur lors de la création de la conversation', [ 'level' => 'error', 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => $e->getMessage() ], 400); } } /** * GET /api/chat/rooms/{id}/messages * Récupérer les messages d'une conversation */ public function getRoomMessages(string $roomId): void { Session::requireAuth(); try { $userId = Session::getUserId(); // Vérifier que l'utilisateur est participant if (!$this->isUserInRoom($userId, $roomId)) { Response::json([ 'status' => 'error', 'message' => 'Accès non autorisé à cette conversation' ], 403); return; } // Paramètres de pagination $limit = isset($_GET['limit']) ? min(100, max(1, (int)$_GET['limit'])) : 50; $before = $_GET['before'] ?? null; // Message ID pour pagination $sql = ' SELECT m.id, m.content, m.sender_id, m.sent_at, m.edited_at, m.is_deleted, u.encrypted_name as sender_name, u.first_name as sender_first_name, -- Statut de lecture (SELECT COUNT(*) FROM chat_read_receipts r WHERE r.message_id = m.id) as read_count, (SELECT COUNT(*) FROM chat_read_receipts r WHERE r.message_id = m.id AND r.user_id = :user_id) as is_read FROM chat_messages m INNER JOIN users u ON m.sender_id = u.id WHERE m.room_id = :room_id '; $params = [ 'room_id' => $roomId, 'user_id' => $userId ]; if ($before) { $sql .= ' AND m.sent_at < (SELECT sent_at FROM chat_messages WHERE id = :before)'; $params['before'] = $before; } $sql .= ' ORDER BY m.sent_at DESC LIMIT :limit'; $stmt = $this->db->prepare($sql); foreach ($params as $key => $value) { if ($key === 'limit') { $stmt->bindValue($key, $value, PDO::PARAM_INT); } else { $stmt->bindValue($key, $value); } } $stmt->bindValue('limit', $limit, PDO::PARAM_INT); $stmt->execute(); $messages = $stmt->fetchAll(PDO::FETCH_ASSOC); // Déchiffrer les noms foreach ($messages as &$message) { $message['sender_name'] = ApiService::decryptData($message['sender_name']); $message['is_read'] = (bool)$message['is_read']; $message['is_mine'] = ($message['sender_id'] == $userId); } // Inverser pour avoir l'ordre chronologique $messages = array_reverse($messages); // Mettre à jour last_read_at pour ce participant $this->updateLastRead($roomId, $userId); Response::json([ 'status' => 'success', 'messages' => $messages, 'has_more' => count($messages) === $limit ]); } catch (PDOException $e) { LogService::log('Erreur lors de la récupération des messages', [ 'level' => 'error', 'room_id' => $roomId, 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => 'Erreur serveur' ], 500); } } /** * POST /api/chat/rooms/{id}/messages * Envoyer un message dans une conversation */ public function sendMessage(string $roomId): void { Session::requireAuth(); try { $data = Request::getJson(); $userId = Session::getUserId(); // Vérifier que l'utilisateur est participant if (!$this->isUserInRoom($userId, $roomId)) { Response::json([ 'status' => 'error', 'message' => 'Accès non autorisé à cette conversation' ], 403); return; } // Validation du contenu if (!isset($data['content']) || empty(trim($data['content']))) { Response::json([ 'status' => 'error', 'message' => 'Le message ne peut pas être vide' ], 400); return; } $content = trim($data['content']); // Limiter la longueur du message if (mb_strlen($content, 'UTF-8') > 5000) { Response::json([ 'status' => 'error', 'message' => 'Message trop long (max 5000 caractères)' ], 400); return; } $messageId = $this->generateUUID(); // Insérer le message $stmt = $this->db->prepare(' INSERT INTO chat_messages (id, room_id, content, sender_id, sent_at) VALUES (:id, :room_id, :content, :sender_id, NOW()) '); $stmt->execute([ 'id' => $messageId, 'room_id' => $roomId, 'content' => $content, 'sender_id' => $userId ]); // Mettre à jour la date de dernière modification de la room $updateStmt = $this->db->prepare(' UPDATE chat_rooms SET updated_at = NOW() WHERE id = :room_id '); $updateStmt->execute(['room_id' => $roomId]); // Récupérer le message créé avec les infos du sender $msgStmt = $this->db->prepare(' SELECT m.id, m.content, m.sender_id, m.sent_at, u.encrypted_name as sender_name, u.first_name as sender_first_name FROM chat_messages m INNER JOIN users u ON m.sender_id = u.id WHERE m.id = :id '); $msgStmt->execute(['id' => $messageId]); $message = $msgStmt->fetch(PDO::FETCH_ASSOC); $message['sender_name'] = ApiService::decryptData($message['sender_name']); $message['is_mine'] = true; $message['is_read'] = false; $message['read_count'] = 0; LogService::log('Message envoyé', [ 'level' => 'debug', 'room_id' => $roomId, 'message_id' => $messageId, 'sender_id' => $userId ]); Response::json([ 'status' => 'success', 'message' => $message ], 201); } catch (PDOException $e) { LogService::log('Erreur lors de l\'envoi du message', [ 'level' => 'error', 'room_id' => $roomId, 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => 'Erreur serveur' ], 500); } } /** * POST /api/chat/rooms/{id}/read * Marquer les messages comme lus */ public function markAsRead(string $roomId): void { Session::requireAuth(); try { $data = Request::getJson(); $userId = Session::getUserId(); // Vérifier que l'utilisateur est participant if (!$this->isUserInRoom($userId, $roomId)) { Response::json([ 'status' => 'error', 'message' => 'Accès non autorisé à cette conversation' ], 403); return; } // Si des message_ids spécifiques sont fournis if (isset($data['message_ids']) && is_array($data['message_ids'])) { $messageIds = $data['message_ids']; // Marquer ces messages spécifiques comme lus $stmt = $this->db->prepare(' INSERT IGNORE INTO chat_read_receipts (message_id, user_id, read_at) VALUES (:message_id, :user_id, NOW()) '); foreach ($messageIds as $messageId) { $stmt->execute([ 'message_id' => $messageId, 'user_id' => $userId ]); } } else { // Marquer tous les messages non lus de la room comme lus $stmt = $this->db->prepare(' INSERT IGNORE INTO chat_read_receipts (message_id, user_id, read_at) SELECT m.id, :user_id, NOW() FROM chat_messages m WHERE m.room_id = :room_id AND m.id NOT IN ( SELECT message_id FROM chat_read_receipts WHERE user_id = :user_id_check ) '); $stmt->execute([ 'user_id' => $userId, 'user_id_check' => $userId, 'room_id' => $roomId ]); } // Mettre à jour last_read_at $this->updateLastRead($roomId, $userId); // Compter les messages non lus restants $countStmt = $this->db->prepare(' SELECT COUNT(*) as unread_count FROM chat_messages m WHERE m.room_id = :room_id AND m.sender_id != :user_id AND m.id NOT IN ( SELECT message_id FROM chat_read_receipts WHERE user_id = :user_id_check ) '); $countStmt->execute([ 'room_id' => $roomId, 'user_id' => $userId, 'user_id_check' => $userId ]); $result = $countStmt->fetch(PDO::FETCH_ASSOC); Response::json([ 'status' => 'success', 'unread_count' => (int)$result['unread_count'] ]); } catch (PDOException $e) { LogService::log('Erreur lors du marquage comme lu', [ 'level' => 'error', 'room_id' => $roomId, 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => 'Erreur serveur' ], 500); } } /** * GET /api/chat/recipients * Liste des destinataires possibles selon le rôle */ public function getRecipients(): void { Session::requireAuth(); try { $userId = Session::getUserId(); $entityId = Session::getEntityId(); $userRole = $this->getUserRole($userId); $sql = ' SELECT u.id, u.encrypted_name as name, u.first_name, u.sect_name, u.fk_role as role, u.fk_entite as entite_id, e.encrypted_name as entite_name FROM users u LEFT JOIN entites e ON u.fk_entite = e.id WHERE u.chk_active = 1 AND u.id != :user_id '; $params = ['user_id' => $userId]; // Filtrage selon le rôle if ($userRole == 1) { // Utilisateur simple : seulement les utilisateurs de son entité $sql .= ' AND u.fk_entite = :entity_id'; $params['entity_id'] = $entityId; } elseif ($userRole == 2) { // Admin d'entité : // - Tous les membres actifs de son amicale (même entité) // - Les super-admins (fk_role=9) de l'entité 1 $sql .= ' AND ( u.fk_entite = :entity_id OR (u.fk_role = 9 AND u.fk_entite = 1) )'; $params['entity_id'] = $entityId; } elseif ($userRole == 9) { // Super-administrateur : // - Seulement les administrateurs actifs des amicales (fk_role=2) // - Et les autres super-admins (fk_role=9) $sql .= ' AND (u.fk_role = 2 OR u.fk_role = 9)'; } // Autres rôles (3-8) : pas de filtrage spécifique pour le moment $sql .= ' ORDER BY u.fk_entite, u.encrypted_name'; $stmt = $this->db->prepare($sql); $stmt->execute($params); $recipients = $stmt->fetchAll(PDO::FETCH_ASSOC); // Déchiffrer les données et organiser par entité $recipientsByEntity = []; $recipientsDecrypted = []; foreach ($recipients as &$recipient) { // Déchiffrer le nom $recipient['name'] = ApiService::decryptData($recipient['name']); // Déchiffrer le nom de l'entité $entiteName = $recipient['entite_name'] ? ApiService::decryptData($recipient['entite_name']) : 'Sans entité'; // Créer une copie pour recipients_by_entity $recipientCopy = $recipient; unset($recipientCopy['entite_name']); // Organiser par entité if (!isset($recipientsByEntity[$entiteName])) { $recipientsByEntity[$entiteName] = []; } $recipientsByEntity[$entiteName][] = $recipientCopy; // Remplacer entite_name chiffré par la version déchiffrée $recipient['entite_name'] = $entiteName; // Ajouter à la liste déchiffrée $recipientsDecrypted[] = $recipient; } Response::json([ 'status' => 'success', 'recipients' => $recipientsDecrypted, 'recipients_by_entity' => $recipientsByEntity ]); } catch (PDOException $e) { LogService::log('Erreur lors de la récupération des destinataires', [ 'level' => 'error', 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => 'Erreur serveur' ], 500); } } // ===== Méthodes utilitaires privées ===== /** * Récupérer le rôle d'un utilisateur */ private function getUserRole(int $userId): int { $stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?'); $stmt->execute([$userId]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result ? (int)$result['fk_role'] : 1; } /** * Récupérer les données d'un utilisateur */ private function getUserData(int $userId): ?array { $stmt = $this->db->prepare(' SELECT id, fk_role, fk_entite, encrypted_name, first_name FROM users WHERE id = ? AND chk_active = 1 '); $stmt->execute([$userId]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result ?: null; } /** * Vérifier si un utilisateur est dans une room */ private function isUserInRoom(int $userId, string $roomId): bool { $stmt = $this->db->prepare(' SELECT COUNT(*) as count FROM chat_participants WHERE room_id = ? AND user_id = ? AND left_at IS NULL '); $stmt->execute([$roomId, $userId]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result && $result['count'] > 0; } /** * Récupérer les participants d'une room */ private function getRoomParticipants(string $roomId): array { $stmt = $this->db->prepare(' SELECT p.user_id, p.is_admin, u.encrypted_name as name, u.first_name FROM chat_participants p INNER JOIN users u ON p.user_id = u.id WHERE p.room_id = ? AND p.left_at IS NULL '); $stmt->execute([$roomId]); $participants = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($participants as &$participant) { $participant['name'] = ApiService::decryptData($participant['name']); } return $participants; } /** * Récupérer les détails d'une room */ private function getRoomDetails(string $roomId): array { $stmt = $this->db->prepare(' SELECT r.id, r.title, r.type, r.created_at, r.created_by, r.updated_at FROM chat_rooms r WHERE r.id = ? '); $stmt->execute([$roomId]); $room = $stmt->fetch(PDO::FETCH_ASSOC); if ($room) { $room['participants'] = $this->getRoomParticipants($roomId); } return $room; } /** * Trouver une conversation privée existante entre deux utilisateurs */ private function findExistingPrivateRoom(int $user1, int $user2): ?array { $stmt = $this->db->prepare(' SELECT r.* FROM chat_rooms r WHERE r.type = "private" AND r.is_active = 1 AND EXISTS ( SELECT 1 FROM chat_participants p1 WHERE p1.room_id = r.id AND p1.user_id = ? AND p1.left_at IS NULL ) AND EXISTS ( SELECT 1 FROM chat_participants p2 WHERE p2.room_id = r.id AND p2.user_id = ? AND p2.left_at IS NULL ) AND ( SELECT COUNT(*) FROM chat_participants p WHERE p.room_id = r.id AND p.left_at IS NULL ) = 2 '); $stmt->execute([$user1, $user2]); $room = $stmt->fetch(PDO::FETCH_ASSOC); if ($room) { $room['participants'] = $this->getRoomParticipants($room['id']); return $room; } return null; } /** * Mettre à jour la date de dernière lecture */ private function updateLastRead(string $roomId, int $userId): void { $stmt = $this->db->prepare(' UPDATE chat_participants SET last_read_at = NOW() WHERE room_id = ? AND user_id = ? '); $stmt->execute([$roomId, $userId]); } /** * Générer un UUID v4 */ private function generateUUID(): string { return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); } }