veza/veza-chat-server/src/chat_management.rs
2025-12-03 20:33:26 +01:00

794 lines
23 KiB
Rust

//! 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<String>,
/// Type du salon
pub room_type: RoomType,
/// Permissions par défaut
pub default_permissions: HashSet<RoomPermission>,
/// Limite de membres (None = illimitée)
pub max_members: Option<u32>,
/// 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<String>,
/// 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<Utc>,
/// Date de dernière activité
pub last_activity: DateTime<Utc>,
/// Statut du salon
pub status: RoomStatus,
/// Membres du salon (user_id -> permissions)
pub members: HashMap<i32, HashSet<RoomPermission>>,
/// Modérateurs du salon
pub moderators: HashSet<i32>,
/// Administrateurs du salon
pub administrators: HashSet<i32>,
/// Messages épinglés
pub pinned_messages: Vec<Uuid>,
/// Tags du salon
pub tags: HashSet<String>,
}
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<RoomPermission>) {
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<Uuid>,
/// Date d'envoi
pub sent_at: DateTime<Utc>,
/// Date de modification
pub edited_at: Option<DateTime<Utc>>,
/// Message supprimé
pub deleted: bool,
/// Réactions au message
pub reactions: HashMap<String, Vec<i32>>,
/// Mentions dans le message
pub mentions: Vec<i32>,
/// Fichiers joints
pub attachments: Vec<Attachment>,
/// Métadonnées du message
pub metadata: HashMap<String, String>,
}
/// 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<Utc>,
}
/// 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<i32>,
/// ID du message ciblé
pub target_message_id: Option<Uuid>,
/// ID du salon
pub room_id: Uuid,
/// Raison de l'action
pub reason: String,
/// Durée de la sanction (si applicable)
pub duration: Option<Duration>,
/// Date de l'action
pub created_at: DateTime<Utc>,
/// 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<RwLock<HashMap<Uuid, Room>>>,
/// Messages par salon
messages: Arc<RwLock<HashMap<Uuid, Vec<ChatMessage>>>>,
/// Actions de modération
moderation_actions: Arc<RwLock<Vec<ModerationAction>>>,
/// Métriques Prometheus
metrics: Option<Arc<PrometheusMetrics>>,
}
impl ChatManager {
/// Crée un nouveau gestionnaire de chat
pub fn new(metrics: Option<Arc<PrometheusMetrics>>) -> 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<Uuid> {
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<Uuid> {
// 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<usize>,
offset: Option<usize>,
) -> Result<Vec<ChatMessage>> {
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<Uuid> {
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::<usize>();
let total_members = rooms.values().map(|room| room.members.len()).sum::<usize>();
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());
}
}