feat(chat-server): add C2.1 WebRTC call signaling (CallOffer, CallAnswer, ICECandidate, CallHangup, CallReject)

This commit is contained in:
senke 2026-02-22 03:42:47 +01:00
parent ea730665bb
commit 2c0614fa3a
3 changed files with 203 additions and 0 deletions

View file

@ -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",
}
}
}

View file

@ -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?;

View file

@ -89,6 +89,35 @@ pub enum IncomingMessage {
conversation_id: Uuid,
since: chrono::DateTime<chrono::Utc>,
},
/// 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<Uuid>,
) -> 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(())
}
}