diff --git a/veza-chat-server/src/security/rate_limiter.rs b/veza-chat-server/src/security/rate_limiter.rs index ebf60cd3e..eaf3e3c84 100644 --- a/veza-chat-server/src/security/rate_limiter.rs +++ b/veza-chat-server/src/security/rate_limiter.rs @@ -36,6 +36,7 @@ pub enum RateLimitAction { Typing, JoinConversation, SearchMessages, + CallSignaling, } impl RateLimitAction { @@ -70,6 +71,10 @@ impl RateLimitAction { max_requests: 15, window: Duration::from_secs(60), }, + Self::CallSignaling => RateLimitConfig { + max_requests: 60, + window: Duration::from_secs(60), + }, } } @@ -82,6 +87,7 @@ impl RateLimitAction { Self::Typing => "typing", Self::JoinConversation => "join", Self::SearchMessages => "search", + Self::CallSignaling => "callsig", } } } diff --git a/veza-chat-server/src/websocket/handler.rs b/veza-chat-server/src/websocket/handler.rs index 153636541..b162cdfd4 100644 --- a/veza-chat-server/src/websocket/handler.rs +++ b/veza-chat-server/src/websocket/handler.rs @@ -293,6 +293,13 @@ async fn handle_incoming_message( IncomingMessage::SearchMessages { .. } => { Some(crate::security::RateLimitAction::SearchMessages) } + IncomingMessage::CallOffer { .. } + | IncomingMessage::CallAnswer { .. } + | IncomingMessage::ICECandidate { .. } + | IncomingMessage::CallHangup { .. } + | IncomingMessage::CallReject { .. } => { + Some(crate::security::RateLimitAction::CallSignaling) + } // Ping, MarkAsRead, Delivered, LeaveConversation, FetchHistory, SyncMessages // are not rate-limited _ => None, @@ -1097,6 +1104,120 @@ async fn handle_incoming_message( conversation_id, message_count ); } + IncomingMessage::CallOffer { + conversation_id, + target_user_id, + sdp, + call_type, + } => { + let sender_uuid = Uuid::parse_str(&claims.user_id) + .map_err(|e| ChatError::validation_error(&format!("Invalid user UUID: {}", e)))?; + state + .permission_service + .can_send_message(sender_uuid, conversation_id) + .await + .map_err(|e| { + warn!( + user_id = %sender_uuid, + conversation_id = %conversation_id, + error = %e, + "Permission refusée pour CallOffer" + ); + e + })?; + let msg = OutgoingMessage::CallOffer { + conversation_id, + caller_user_id: sender_uuid, + sdp: sdp.clone(), + call_type: call_type.clone(), + }; + state + .ws_manager + .send_to_user(&target_user_id.to_string(), msg, Some(client.id)) + .await?; + info!( + "📞 CallOffer relayé de {} vers {} (conversation: {})", + claims.username, target_user_id, conversation_id + ); + } + IncomingMessage::CallAnswer { + conversation_id, + caller_user_id, + sdp, + } => { + let msg = OutgoingMessage::CallAnswer { + conversation_id, + target_user_id: caller_user_id, + sdp: sdp.clone(), + }; + state + .ws_manager + .send_to_user(&caller_user_id.to_string(), msg, Some(client.id)) + .await?; + info!( + "📞 CallAnswer relayé vers {} (conversation: {})", + caller_user_id, conversation_id + ); + } + IncomingMessage::ICECandidate { + conversation_id, + target_user_id, + candidate, + } => { + let sender_uuid = Uuid::parse_str(&claims.user_id) + .map_err(|e| ChatError::validation_error(&format!("Invalid user UUID: {}", e)))?; + let msg = OutgoingMessage::ICECandidate { + conversation_id, + from_user_id: sender_uuid, + candidate: candidate.clone(), + }; + state + .ws_manager + .send_to_user(&target_user_id.to_string(), msg, Some(client.id)) + .await?; + debug!( + "📞 ICECandidate relayé de {} vers {} (conversation: {})", + claims.username, target_user_id, conversation_id + ); + } + IncomingMessage::CallHangup { + conversation_id, + target_user_id, + } => { + let sender_uuid = Uuid::parse_str(&claims.user_id) + .map_err(|e| ChatError::validation_error(&format!("Invalid user UUID: {}", e)))?; + let msg = OutgoingMessage::CallHangup { + conversation_id, + user_id: sender_uuid, + }; + state + .ws_manager + .send_to_user(&target_user_id.to_string(), msg, Some(client.id)) + .await?; + info!( + "📞 CallHangup relayé de {} vers {} (conversation: {})", + claims.username, target_user_id, conversation_id + ); + } + IncomingMessage::CallReject { + conversation_id, + caller_user_id, + } => { + let sender_uuid = Uuid::parse_str(&claims.user_id) + .map_err(|e| ChatError::validation_error(&format!("Invalid user UUID: {}", e)))?; + let msg = OutgoingMessage::CallRejected { + conversation_id, + user_id: sender_uuid, + }; + state + .ws_manager + .send_to_user(&caller_user_id.to_string(), msg, Some(client.id)) + .await?; + info!( + "📞 CallReject relayé vers {} (conversation: {})", + caller_user_id, conversation_id + ); + } IncomingMessage::Ping => { debug!("🏓 Ping WebSocket reçu"); client.send_message(OutgoingMessage::Pong).await?; diff --git a/veza-chat-server/src/websocket/mod.rs b/veza-chat-server/src/websocket/mod.rs index a4b39a323..0554703cd 100644 --- a/veza-chat-server/src/websocket/mod.rs +++ b/veza-chat-server/src/websocket/mod.rs @@ -89,6 +89,35 @@ pub enum IncomingMessage { conversation_id: Uuid, since: chrono::DateTime, }, + /// Initier un appel WebRTC + CallOffer { + conversation_id: Uuid, + target_user_id: Uuid, + sdp: String, + call_type: String, // "audio" | "video" + }, + /// Accepter un appel + CallAnswer { + conversation_id: Uuid, + caller_user_id: Uuid, + sdp: String, + }, + /// Échanger un candidat ICE + ICECandidate { + conversation_id: Uuid, + target_user_id: Uuid, + candidate: String, + }, + /// Raccrocher + CallHangup { + conversation_id: Uuid, + target_user_id: Uuid, + }, + /// Refuser un appel + CallReject { + conversation_id: Uuid, + caller_user_id: Uuid, + }, /// Ping de connexion Ping, } @@ -187,6 +216,35 @@ pub enum OutgoingMessage { ActionConfirmed { action: String, success: bool }, /// Erreur Error { message: String }, + /// Appel WebRTC — offre + CallOffer { + conversation_id: Uuid, + caller_user_id: Uuid, + sdp: String, + call_type: String, + }, + /// Appel WebRTC — réponse + CallAnswer { + conversation_id: Uuid, + target_user_id: Uuid, + sdp: String, + }, + /// Appel WebRTC — candidat ICE + ICECandidate { + conversation_id: Uuid, + from_user_id: Uuid, + candidate: String, + }, + /// Appel WebRTC — raccrocher + CallHangup { + conversation_id: Uuid, + user_id: Uuid, + }, + /// Appel WebRTC — refusé + CallRejected { + conversation_id: Uuid, + user_id: Uuid, + }, /// Pong de connexion Pong, } @@ -283,4 +341,22 @@ impl WebSocketManager { let clients = self.clients.read().await; clients.iter().find(|c| c.id == client_id).cloned() } + + /// Envoie un message à un utilisateur spécifique (1-to-1, pour appels) + /// exclude_client_id : ne pas envoyer au client qui a initié (éviter echo) + pub async fn send_to_user( + &self, + user_id: &str, + message: OutgoingMessage, + exclude_client_id: Option, + ) -> Result<()> { + let clients = self.clients.read().await; + for client in clients.iter() { + if client.user_id == user_id && Some(client.id) != exclude_client_id { + let _ = client.send_message(message.clone()).await; + return Ok(()); + } + } + Ok(()) + } }