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

265 lines
No EOL
8.2 KiB
Rust

use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use serde::{Serialize, Deserialize};
use serde_json::json;
use crate::error::Result;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum UserStatus {
Online,
Away,
Busy,
Invisible,
Offline,
}
#[derive(Debug, Clone, Serialize)]
pub struct UserPresence {
pub user_id: i32,
pub username: String,
pub status: UserStatus,
#[serde(skip)] // Skip Instant car non sérialisable
pub last_seen: Instant,
pub status_message: Option<String>,
pub current_room: Option<String>,
}
impl UserPresence {
pub fn new(user_id: i32, username: String) -> Self {
Self {
user_id,
username,
status: UserStatus::Online,
last_seen: Instant::now(),
status_message: None,
current_room: None,
}
}
pub fn update_activity(&mut self) {
self.last_seen = Instant::now();
if self.status == UserStatus::Away {
self.status = UserStatus::Online;
}
}
pub fn set_away_if_inactive(&mut self, away_threshold: Duration) {
if self.status == UserStatus::Online && self.last_seen.elapsed() > away_threshold {
self.status = UserStatus::Away;
}
}
}
/// Gestionnaire de présence des utilisateurs
pub struct PresenceManager {
users: Arc<RwLock<HashMap<i32, UserPresence>>>,
away_threshold: Duration,
}
impl Default for PresenceManager {
fn default() -> Self {
Self::new()
}
}
impl PresenceManager {
pub fn new() -> Self {
Self {
users: Arc::new(RwLock::new(HashMap::new())),
away_threshold: Duration::from_secs(300), // 5 minutes
}
}
/// Enregistre un utilisateur comme en ligne
pub async fn user_online(&self, user_id: i32, username: String) {
let mut users = self.users.write().await;
let presence = UserPresence::new(user_id, username.clone());
tracing::info!(user_id = %user_id, username = %username, "👋 Utilisateur en ligne");
users.insert(user_id, presence);
}
/// Marque un utilisateur comme hors ligne
pub async fn user_offline(&self, user_id: i32) {
let mut users = self.users.write().await;
if let Some(mut presence) = users.remove(&user_id) {
presence.status = UserStatus::Offline;
tracing::info!(user_id = %user_id, username = %presence.username, "👋 Utilisateur hors ligne");
}
}
/// Met à jour l'activité d'un utilisateur
pub async fn update_user_activity(&self, user_id: i32) {
let mut users = self.users.write().await;
if let Some(presence) = users.get_mut(&user_id) {
presence.update_activity();
}
}
/// Change le statut d'un utilisateur
pub async fn set_user_status(&self, user_id: i32, status: UserStatus, message: Option<String>) -> Result<()> {
let mut users = self.users.write().await;
if let Some(presence) = users.get_mut(&user_id) {
presence.status = status.clone();
presence.status_message = message.clone();
tracing::info!(
user_id = %user_id,
username = %presence.username,
status = ?status,
message = ?message,
"📊 Statut utilisateur mis à jour"
);
}
Ok(())
}
/// Met à jour le salon actuel d'un utilisateur
pub async fn set_user_room(&self, user_id: i32, room: Option<String>) {
let mut users = self.users.write().await;
if let Some(presence) = users.get_mut(&user_id) {
presence.current_room = room.clone();
tracing::debug!(user_id = %user_id, room = ?room, "🏠 Salon actuel mis à jour");
}
}
/// Obtient la présence d'un utilisateur
pub async fn get_user_presence(&self, user_id: i32) -> Option<UserPresence> {
let users = self.users.read().await;
users.get(&user_id).cloned()
}
/// Obtient la liste des utilisateurs en ligne dans un salon
pub async fn get_room_users(&self, room: &str) -> Vec<UserPresence> {
let users = self.users.read().await;
users.values()
.filter(|presence| {
presence.current_room.as_deref() == Some(room) &&
presence.status != UserStatus::Offline &&
presence.status != UserStatus::Invisible
})
.cloned()
.collect()
}
/// Obtient tous les utilisateurs en ligne
pub async fn get_online_users(&self) -> Vec<UserPresence> {
let users = self.users.read().await;
users.values()
.filter(|presence| {
presence.status != UserStatus::Offline &&
presence.status != UserStatus::Invisible
})
.cloned()
.collect()
}
/// Nettoie les utilisateurs inactifs (les marque comme "away")
pub async fn cleanup_inactive_users(&self) {
let mut users = self.users.write().await;
let mut updated_users = Vec::new();
for presence in users.values_mut() {
let old_status = presence.status.clone();
presence.set_away_if_inactive(self.away_threshold);
if old_status != presence.status {
updated_users.push(presence.clone());
}
}
if !updated_users.is_empty() {
tracing::info!(count = %updated_users.len(), "😴 Utilisateurs marqués comme inactifs");
}
}
/// Génère un événement de présence pour diffusion
pub fn create_presence_event(&self, presence: &UserPresence, event_type: &str) -> serde_json::Value {
json!({
"type": "presence_update",
"data": {
"event": event_type,
"user_id": presence.user_id,
"username": presence.username,
"status": presence.status,
"status_message": presence.status_message,
"current_room": presence.current_room
}
})
}
}
/// Système de notifications push
pub struct NotificationManager {
// Ici on pourrait intégrer avec des services comme Firebase, Apple Push, etc.
}
impl Default for NotificationManager {
fn default() -> Self {
Self::new()
}
}
impl NotificationManager {
pub fn new() -> Self {
Self {}
}
/// Envoie une notification push à un utilisateur
pub async fn send_push_notification(
&self,
user_id: i32,
title: &str,
body: &str,
_data: Option<serde_json::Value>
) -> Result<()> {
// Implémentation des notifications push
tracing::info!(
user_id = %user_id,
title = %title,
body = %body,
"📱 Notification push envoyée"
);
// TODO: Intégrer avec Firebase Cloud Messaging, Apple Push Notification, etc.
Ok(())
}
/// Notification pour un nouveau message direct
pub async fn notify_new_dm(&self, to_user: i32, from_username: &str, preview: &str) -> Result<()> {
let title = format!("Nouveau message de {}", from_username);
let body = if preview.len() > 50 {
format!("{}...", &preview[..47])
} else {
preview.to_string()
};
self.send_push_notification(
to_user,
&title,
&body,
Some(json!({"type": "dm", "from": from_username}))
).await
}
/// Notification pour mention dans un salon
pub async fn notify_room_mention(&self, user_id: i32, room: &str, from_username: &str, message: &str) -> Result<()> {
let title = format!("Mention dans #{}", room);
let body = format!("{}: {}", from_username,
if message.len() > 50 {
format!("{}...", &message[..47])
} else {
message.to_string()
}
);
self.send_push_notification(
user_id,
&title,
&body,
Some(json!({"type": "mention", "room": room, "from": from_username}))
).await
}
}