feat(chat-server): add C2.1 WebRTC call signaling (CallOffer, CallAnswer, ICECandidate, CallHangup, CallReject)
This commit is contained in:
parent
ea730665bb
commit
2c0614fa3a
3 changed files with 203 additions and 0 deletions
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue