//! Gestion unifiée des salons, messages directs et modération //! //! Ce module fournit une interface unifiée pour: //! - Gestion des salons (création, suppression, permissions) //! - Messages directs entre utilisateurs //! - Système de modération avancé //! - Gestion des rôles et permissions //! - Intégration avec les métriques et logs use crate::authentication::{Role, UserSession}; use crate::error::{ChatError, Result}; use crate::prometheus_metrics::PrometheusMetrics; use crate::structured_logging::chat_logs; use chrono::{DateTime, Utc}; use std::time::Duration; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; /// Types de salons #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum RoomType { /// Salon public - visible par tous Public, /// Salon privé - invitation uniquement Private, /// Salon direct - conversation entre 2 utilisateurs Direct, /// Salon système - géré par le système System, } /// Permissions dans un salon #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum RoomPermission { /// Lire les messages Read, /// Envoyer des messages Write, /// Modifier les messages Edit, /// Supprimer les messages Delete, /// Inviter des utilisateurs Invite, /// Gérer le salon Manage, /// Modérer le salon Moderate, } /// Statut d'un salon #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum RoomStatus { /// Salon actif Active, /// Salon archivé Archived, /// Salon supprimé Deleted, /// Salon suspendu Suspended, } /// Configuration d'un salon #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RoomConfig { /// Nom du salon pub name: String, /// Description du salon pub description: Option, /// Type du salon pub room_type: RoomType, /// Permissions par défaut pub default_permissions: HashSet, /// Limite de membres (None = illimitée) pub max_members: Option, /// Activer l'historique des messages pub enable_history: bool, /// Activer les réactions pub enable_reactions: bool, /// Activer les mentions pub enable_mentions: bool, /// Activer les fils de discussion pub enable_threads: bool, /// Mots-clés interdits pub forbidden_words: HashSet, /// Activer la modération automatique pub auto_moderation: bool, } impl Default for RoomConfig { fn default() -> Self { Self { name: "Nouveau salon".to_string(), description: None, room_type: RoomType::Public, default_permissions: HashSet::from([RoomPermission::Read, RoomPermission::Write]), max_members: Some(1000), enable_history: true, enable_reactions: true, enable_mentions: true, enable_threads: true, forbidden_words: HashSet::new(), auto_moderation: false, } } } /// Salon de chat #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Room { /// ID unique du salon pub id: Uuid, /// Configuration du salon pub config: RoomConfig, /// Créateur du salon pub creator_id: i32, /// Date de création pub created_at: DateTime, /// Date de dernière activité pub last_activity: DateTime, /// Statut du salon pub status: RoomStatus, /// Membres du salon (user_id -> permissions) pub members: HashMap>, /// Modérateurs du salon pub moderators: HashSet, /// Administrateurs du salon pub administrators: HashSet, /// Messages épinglés pub pinned_messages: Vec, /// Tags du salon pub tags: HashSet, } impl Room { /// Crée un nouveau salon pub fn new(creator_id: i32, config: RoomConfig) -> Self { let now = Utc::now(); Self { id: Uuid::new_v4(), config, creator_id, created_at: now, last_activity: now, status: RoomStatus::Active, members: HashMap::new(), moderators: HashSet::new(), administrators: HashSet::new(), pinned_messages: Vec::new(), tags: HashSet::new(), } } /// Ajoute un membre au salon pub fn add_member(&mut self, user_id: i32, permissions: HashSet) { self.members.insert(user_id, permissions); self.last_activity = Utc::now(); } /// Retire un membre du salon pub fn remove_member(&mut self, user_id: i32) { self.members.remove(&user_id); self.moderators.remove(&user_id); self.administrators.remove(&user_id); self.last_activity = Utc::now(); } /// Vérifie si un utilisateur a une permission spécifique pub fn has_permission(&self, user_id: i32, permission: &RoomPermission) -> bool { // L'administrateur du salon a toutes les permissions if self.administrators.contains(&user_id) { return true; } // Vérifier les permissions du membre if let Some(member_permissions) = self.members.get(&user_id) { member_permissions.contains(permission) } else { false } } /// Vérifie si un utilisateur peut envoyer des messages pub fn can_send_messages(&self, user_id: i32) -> bool { self.has_permission(user_id, &RoomPermission::Write) } /// Vérifie si un utilisateur peut modérer pub fn can_moderate(&self, user_id: i32) -> bool { self.administrators.contains(&user_id) || self.moderators.contains(&user_id) || self.has_permission(user_id, &RoomPermission::Moderate) } /// Ajoute un modérateur pub fn add_moderator(&mut self, user_id: i32) -> Result<()> { if !self.members.contains_key(&user_id) { return Err(ChatError::validation_error( "Utilisateur n'est pas membre du salon", )); } self.moderators.insert(user_id); self.last_activity = Utc::now(); Ok(()) } /// Retire un modérateur pub fn remove_moderator(&mut self, user_id: i32) { self.moderators.remove(&user_id); self.last_activity = Utc::now(); } /// Épingle un message pub fn pin_message(&mut self, message_id: Uuid) -> Result<()> { if self.pinned_messages.len() >= 10 { return Err(ChatError::validation_error("Trop de messages épinglés")); } if !self.pinned_messages.contains(&message_id) { self.pinned_messages.push(message_id); self.last_activity = Utc::now(); } Ok(()) } /// Désépingle un message pub fn unpin_message(&mut self, message_id: Uuid) { self.pinned_messages.retain(|&id| id != message_id); self.last_activity = Utc::now(); } /// Met à jour la dernière activité pub fn update_activity(&mut self) { self.last_activity = Utc::now(); } /// Archive le salon pub fn archive(&mut self) { self.status = RoomStatus::Archived; self.last_activity = Utc::now(); } /// Supprime le salon pub fn delete(&mut self) { self.status = RoomStatus::Deleted; self.last_activity = Utc::now(); } } /// Message de chat #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChatMessage { /// ID unique du message pub id: Uuid, /// ID du salon ou de la conversation pub room_id: Uuid, /// ID de l'expéditeur pub sender_id: i32, /// Nom d'utilisateur de l'expéditeur pub sender_username: String, /// Contenu du message pub content: String, /// Type de message pub message_type: MessageType, /// Message parent (pour les fils de discussion) pub parent_message_id: Option, /// Date d'envoi pub sent_at: DateTime, /// Date de modification pub edited_at: Option>, /// Message supprimé pub deleted: bool, /// Réactions au message pub reactions: HashMap>, /// Mentions dans le message pub mentions: Vec, /// Fichiers joints pub attachments: Vec, /// Métadonnées du message pub metadata: HashMap, } /// Types de messages #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum MessageType { /// Message texte normal Text, /// Message système System, /// Message de bienvenue Welcome, /// Message de modération Moderation, /// Message de fichier File, /// Message d'image Image, /// Message de code Code, /// Message de commande Command, } /// Fichier joint #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Attachment { /// ID du fichier pub id: Uuid, /// Nom du fichier pub filename: String, /// Type MIME pub mime_type: String, /// Taille en bytes pub size_bytes: u64, /// URL de téléchargement pub download_url: String, /// Date d'upload pub uploaded_at: DateTime, } /// Action de modération #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModerationAction { /// ID de l'action pub id: Uuid, /// Type d'action pub action_type: ModerationActionType, /// ID du modérateur pub moderator_id: i32, /// ID de l'utilisateur ciblé pub target_user_id: Option, /// ID du message ciblé pub target_message_id: Option, /// ID du salon pub room_id: Uuid, /// Raison de l'action pub reason: String, /// Durée de la sanction (si applicable) pub duration: Option, /// Date de l'action pub created_at: DateTime, /// Action active pub active: bool, } /// Types d'actions de modération #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ModerationActionType { /// Avertissement Warning, /// Suppression de message DeleteMessage, /// Modification de message EditMessage, /// Bannissement temporaire TemporaryBan, /// Bannissement permanent PermanentBan, /// Mute temporaire TemporaryMute, /// Mute permanent PermanentMute, /// Kick du salon Kick, /// Suspension du salon SuspendRoom, /// Suppression du salon DeleteRoom, } /// Gestionnaire de chat unifié pub struct ChatManager { /// Salons actifs rooms: Arc>>, /// Messages par salon messages: Arc>>>, /// Actions de modération moderation_actions: Arc>>, /// Métriques Prometheus metrics: Option>, } impl ChatManager { /// Crée un nouveau gestionnaire de chat pub fn new(metrics: Option>) -> Self { Self { rooms: Arc::new(RwLock::new(HashMap::new())), messages: Arc::new(RwLock::new(HashMap::new())), moderation_actions: Arc::new(RwLock::new(Vec::new())), metrics, } } /// Crée un nouveau salon pub async fn create_room( &self, creator_id: i32, creator_username: &str, config: RoomConfig, ) -> Result { let room = Room::new(creator_id, config.clone()); let room_id = room.id; // Ajouter le créateur comme administrateur let mut room = room; room.administrators.insert(creator_id); room.add_member(creator_id, room.config.default_permissions.clone()); // Sauvegarder le salon { let mut rooms = self.rooms.write().await; rooms.insert(room_id, room); } // Enregistrer les métriques if let Some(metrics) = &self.metrics { metrics.record_room_created(); metrics.update_active_rooms(self.get_active_rooms_count().await); } chat_logs::room_created( &room_id.to_string(), &config.name, creator_id, creator_username, ); Ok(room_id) } /// Supprime un salon pub async fn delete_room( &self, room_id: Uuid, deleter_id: i32, deleter_username: &str, ) -> Result<()> { let room_name = { let rooms = self.rooms.read().await; let room = rooms .get(&room_id) .ok_or_else(|| ChatError::validation_error("Salon non trouvé"))?; // Vérifier les permissions if !room.administrators.contains(&deleter_id) { return Err(ChatError::unauthorized("Permissions insuffisantes")); } room.config.name.clone() }; // Supprimer le salon { let mut rooms = self.rooms.write().await; if let Some(room) = rooms.get_mut(&room_id) { room.delete(); } } // Supprimer les messages { let mut messages = self.messages.write().await; messages.remove(&room_id); } // Enregistrer les métriques if let Some(metrics) = &self.metrics { metrics.record_room_deleted(); metrics.update_active_rooms(self.get_active_rooms_count().await); } chat_logs::room_deleted( &room_id.to_string(), &room_name, deleter_id, deleter_username, ); Ok(()) } /// Rejoint un salon pub async fn join_room(&self, room_id: Uuid, user_id: i32, username: &str) -> Result<()> { let mut rooms = self.rooms.write().await; let room = rooms .get_mut(&room_id) .ok_or_else(|| ChatError::validation_error("Salon non trouvé"))?; // Vérifier si le salon est actif if room.status != RoomStatus::Active { return Err(ChatError::validation_error("Salon non actif")); } // Vérifier la limite de membres if let Some(max_members) = room.config.max_members { if room.members.len() >= max_members as usize { return Err(ChatError::validation_error("Salon plein")); } } // Ajouter le membre room.add_member(user_id, room.config.default_permissions.clone()); chat_logs::room_created(&room_id.to_string(), &room.config.name, user_id, username); Ok(()) } /// Quitte un salon pub async fn leave_room(&self, room_id: Uuid, user_id: i32, username: &str) -> Result<()> { let mut rooms = self.rooms.write().await; let room = rooms .get_mut(&room_id) .ok_or_else(|| ChatError::validation_error("Salon non trouvé"))?; room.remove_member(user_id); chat_logs::room_deleted(&room_id.to_string(), &room.config.name, user_id, username); Ok(()) } /// Envoie un message dans un salon pub async fn send_message( &self, room_id: Uuid, sender_id: i32, sender_username: &str, content: String, message_type: MessageType, ) -> Result { // Vérifier les permissions { let rooms = self.rooms.read().await; let room = rooms .get(&room_id) .ok_or_else(|| ChatError::validation_error("Salon non trouvé"))?; if !room.can_send_messages(sender_id) { return Err(ChatError::unauthorized("Permissions insuffisantes")); } } // Créer le message let message = ChatMessage { id: Uuid::new_v4(), room_id, sender_id, sender_username: sender_username.to_string(), content: content.clone(), message_type, parent_message_id: None, sent_at: Utc::now(), edited_at: None, deleted: false, reactions: HashMap::new(), mentions: Vec::new(), attachments: Vec::new(), metadata: HashMap::new(), }; let message_id = message.id; // Sauvegarder le message { let mut messages = self.messages.write().await; messages.entry(room_id).or_default().push(message); } // Mettre à jour l'activité du salon { let mut rooms = self.rooms.write().await; if let Some(room) = rooms.get_mut(&room_id) { room.update_activity(); } } // Enregistrer les métriques if let Some(metrics) = &self.metrics { metrics.record_message_sent("text", "room"); metrics.record_message_size(content.len() as u64, "text"); } chat_logs::message_sent( message_id.to_string(), sender_id, sender_username, &room_id.to_string(), "text", content.len(), ); Ok(message_id) } /// Récupère les messages d'un salon pub async fn get_room_messages( &self, room_id: Uuid, limit: Option, offset: Option, ) -> Result> { let messages = self.messages.read().await; let room_messages = messages .get(&room_id) .ok_or_else(|| ChatError::validation_error("Salon non trouvé"))?; let offset = offset.unwrap_or(0); let limit = limit.unwrap_or(50).min(100); let start = room_messages.len().saturating_sub(offset + limit); let end = room_messages.len().saturating_sub(offset); Ok(room_messages[start..end].to_vec()) } /// Crée une conversation directe pub async fn create_direct_conversation(&self, user1_id: i32, user2_id: i32) -> Result { let config = RoomConfig { name: format!("DM_{}_{}", user1_id, user2_id), room_type: RoomType::Direct, max_members: Some(2), ..Default::default() }; let room_id = self.create_room(user1_id, "system", config).await?; // Ajouter le deuxième utilisateur self.join_room(room_id, user2_id, "user").await?; Ok(room_id) } /// Applique une action de modération pub async fn apply_moderation_action(&self, action: ModerationAction) -> Result<()> { // Sauvegarder l'action { let mut actions = self.moderation_actions.write().await; actions.push(action.clone()); } // Appliquer l'action selon le type match action.action_type { ModerationActionType::DeleteMessage => { if let Some(message_id) = action.target_message_id { self.delete_message(message_id, action.moderator_id).await?; } } ModerationActionType::Kick => { if let Some(user_id) = action.target_user_id { self.leave_room(action.room_id, user_id, "moderated") .await?; } } ModerationActionType::SuspendRoom => { self.suspend_room(action.room_id, action.moderator_id) .await?; } _ => { // Autres actions de modération } } // Enregistrer les métriques if let Some(metrics) = &self.metrics { metrics.record_moderation_action(&format!("{:?}", action.action_type)); } Ok(()) } /// Supprime un message async fn delete_message(&self, message_id: Uuid, moderator_id: i32) -> Result<()> { let mut messages = self.messages.write().await; for room_messages in messages.values_mut() { if let Some(message) = room_messages.iter_mut().find(|m| m.id == message_id) { message.deleted = true; message .metadata .insert("deleted_by".to_string(), moderator_id.to_string()); message .metadata .insert("deleted_at".to_string(), Utc::now().to_rfc3339()); return Ok(()); } } Err(ChatError::validation_error("Message non trouvé")) } /// Suspend un salon async fn suspend_room(&self, room_id: Uuid, _moderator_id: i32) -> Result<()> { let mut rooms = self.rooms.write().await; if let Some(room) = rooms.get_mut(&room_id) { room.status = RoomStatus::Suspended; room.last_activity = Utc::now(); } Ok(()) } /// Récupère le nombre de salons actifs async fn get_active_rooms_count(&self) -> u64 { let rooms = self.rooms.read().await; rooms .values() .filter(|room| room.status == RoomStatus::Active) .count() as u64 } /// Récupère les statistiques du chat pub async fn get_chat_stats(&self) -> ChatStats { let rooms = self.rooms.read().await; let messages = self.messages.read().await; let total_rooms = rooms.len(); let active_rooms = rooms .values() .filter(|room| room.status == RoomStatus::Active) .count(); let total_messages = messages.values().map(|msgs| msgs.len()).sum::(); let total_members = rooms.values().map(|room| room.members.len()).sum::(); ChatStats { total_rooms, active_rooms, total_messages, total_members, } } } /// Statistiques du chat #[derive(Debug, Clone, Serialize)] pub struct ChatStats { pub total_rooms: usize, pub active_rooms: usize, pub total_messages: usize, pub total_members: usize, } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_room_creation() { let manager = ChatManager::new(None); let config = RoomConfig::default(); let room_id = manager.create_room(1, "testuser", config).await.unwrap(); assert!(!room_id.is_nil()); } #[tokio::test] async fn test_room_permissions() { let room = Room::new(1, RoomConfig::default()); // Le créateur doit avoir toutes les permissions assert!(room.can_send_messages(1)); assert!(room.can_moderate(1)); // Un utilisateur non membre ne doit pas avoir de permissions assert!(!room.can_send_messages(2)); } #[tokio::test] async fn test_message_sending() { let manager = ChatManager::new(None); let config = RoomConfig::default(); let room_id = manager.create_room(1, "testuser", config).await.unwrap(); let message_id = manager .send_message( room_id, 1, "testuser", "Hello world".to_string(), MessageType::Text, ) .await .unwrap(); assert!(!message_id.is_nil()); } }