veza/veza-chat-server/src/typing_indicator.rs

179 lines
5.7 KiB
Rust

use chrono::{Duration, Utc};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, instrument, warn};
use uuid::Uuid;
/// Représente un changement de statut typing pour un utilisateur
#[derive(Debug, Clone)]
pub struct TypingStatusChange {
pub user_id: Uuid,
pub conversation_id: Uuid,
pub is_typing: bool,
}
/// Manager pour gérer les typing indicators
pub struct TypingIndicatorManager {
/// Map de conversation ID vers map de user ID vers timestamp de dernière activité
typing_users: Arc<RwLock<HashMap<Uuid, HashMap<Uuid, chrono::DateTime<Utc>>>>>,
/// Durée après laquelle un user n'est plus considéré comme "en train de taper"
timeout_duration: Duration,
}
impl TypingIndicatorManager {
pub fn new() -> Self {
Self {
typing_users: Arc::new(RwLock::new(HashMap::new())),
timeout_duration: Duration::seconds(3),
}
}
/// Marquer qu'un user est en train de taper dans une conversation
#[instrument(skip(self))]
pub async fn user_started_typing(&self, user_id: Uuid, conversation_id: Uuid) {
let mut typing = self.typing_users.write().await;
let conversation_typing = typing.entry(conversation_id).or_insert_with(HashMap::new);
conversation_typing.insert(user_id, Utc::now());
info!(
user_id = %user_id,
conversation_id = %conversation_id,
"User started typing"
);
}
/// Retirer un user de la liste des users en train de taper
#[instrument(skip(self))]
pub async fn user_stopped_typing(&self, user_id: Uuid, conversation_id: Uuid) {
let mut typing = self.typing_users.write().await;
if let Some(conversation_typing) = typing.get_mut(&conversation_id) {
conversation_typing.remove(&user_id);
info!(
user_id = %user_id,
conversation_id = %conversation_id,
"User stopped typing"
);
}
}
/// Obtenir la liste des users en train de taper dans une conversation
pub async fn get_typing_users(&self, conversation_id: Uuid) -> Vec<Uuid> {
let typing = self.typing_users.read().await;
if let Some(conversation_typing) = typing.get(&conversation_id) {
let now = Utc::now();
let mut active_users = Vec::new();
for (user_id, last_activity) in conversation_typing.iter() {
let elapsed = now.signed_duration_since(*last_activity);
if elapsed < self.timeout_duration {
active_users.push(*user_id);
}
}
active_users
} else {
Vec::new()
}
}
/// Détecter les utilisateurs dont le timeout a expiré et les retirer
/// Retourne la liste des changements de statut (is_typing = false)
#[instrument(skip(self))]
pub async fn monitor_timeouts(&self) -> Vec<TypingStatusChange> {
let mut typing = self.typing_users.write().await;
let now = Utc::now();
let mut expired_changes = Vec::new();
for (conversation_id, conversation_typing) in typing.iter_mut() {
let mut expired_users = Vec::new();
for (user_id, last_activity) in conversation_typing.iter() {
let elapsed = now.signed_duration_since(*last_activity);
if elapsed >= self.timeout_duration {
expired_users.push(*user_id);
}
}
// Retirer les utilisateurs expirés et créer les changements de statut
for user_id in expired_users {
conversation_typing.remove(&user_id);
expired_changes.push(TypingStatusChange {
user_id,
conversation_id: *conversation_id,
is_typing: false,
});
debug!(
user_id = %user_id,
conversation_id = %conversation_id,
"User typing timeout expired"
);
}
}
// Retirer les conversations vides
typing.retain(|_conversation_id, users| !users.is_empty());
if !expired_changes.is_empty() {
debug!(
count = expired_changes.len(),
"Detected expired typing indicators"
);
}
expired_changes
}
/// Nettoyer les users expirés de manière périodique (méthode legacy, utiliser monitor_timeouts)
#[deprecated(note = "Use monitor_timeouts() instead")]
pub async fn cleanup_expired(&self) {
let _ = self.monitor_timeouts().await;
}
}
impl Default for TypingIndicatorManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_typing_indicator_manager() {
let manager = TypingIndicatorManager::new();
let conv1 = Uuid::new_v4();
let user1 = Uuid::new_v4();
let user2 = Uuid::new_v4();
// Test user_started_typing
manager.user_started_typing(user1, conv1).await;
manager.user_started_typing(user2, conv1).await;
let typing_users = manager.get_typing_users(conv1).await;
assert!(typing_users.contains(&user1));
assert!(typing_users.contains(&user2));
// Test user_stopped_typing
manager.user_stopped_typing(user1, conv1).await;
let typing_users = manager.get_typing_users(conv1).await;
assert!(!typing_users.contains(&user1));
assert!(typing_users.contains(&user2));
// Test monitor_timeouts
let expired = manager.monitor_timeouts().await;
assert!(expired.is_empty()); // Pas encore expiré
}
}