265 lines
No EOL
8.2 KiB
Rust
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
|
|
}
|
|
}
|