use serde::{Deserialize, Serialize}; use sqlx::types::chrono::{DateTime, Utc}; use sqlx::{Postgres, Pool}; use std::collections::HashMap; use tracing::{debug, info, instrument}; /// Émoji de réaction supporté #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ReactionEmoji { Like, Love, Haha, Wow, Sad, Angry, } impl ReactionEmoji { pub fn as_str(&self) -> &'static str { match self { ReactionEmoji::Like => "👍", ReactionEmoji::Love => "❤️", ReactionEmoji::Haha => "😂", ReactionEmoji::Wow => "😮", ReactionEmoji::Sad => "😢", ReactionEmoji::Angry => "😠", } } pub fn from_str(emoji: &str) -> Option { match emoji { "👍" => Some(ReactionEmoji::Like), "❤️" => Some(ReactionEmoji::Love), "😂" => Some(ReactionEmoji::Haha), "😮" => Some(ReactionEmoji::Wow), "😢" => Some(ReactionEmoji::Sad), "😠" => Some(ReactionEmoji::Angry), _ => None, } } } /// Représente une réaction sur un message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MessageReaction { pub message_id: i64, pub user_id: i64, pub emoji: ReactionEmoji, pub created_at: DateTime, } /// Manager pour gérer les réactions sur les messages pub struct ReactionsManager { pool: Pool, } impl ReactionsManager { pub fn new(pool: Pool) -> Self { Self { pool } } /// Ajouter une réaction à un message #[instrument(skip(self))] pub async fn add_reaction( &self, message_id: i64, user_id: i64, emoji: ReactionEmoji, ) -> Result<(), sqlx::Error> { // Vérifier si l'utilisateur a déjà réagi à ce message let existing: Option<(i64,)> = sqlx::query_as( "SELECT id FROM message_reactions WHERE message_id = $1 AND user_id = $2" ) .bind(message_id) .bind(user_id) .fetch_optional(&self.pool) .await?; if let Some(_) = existing { // L'utilisateur a déjà réagi, supprimer la réaction existante sqlx::query( "DELETE FROM message_reactions WHERE message_id = $1 AND user_id = $2" ) .bind(message_id) .bind(user_id) .execute(&self.pool) .await?; debug!( message_id = message_id, user_id = user_id, "Existing reaction removed" ); } // Ajouter la nouvelle réaction sqlx::query( "INSERT INTO message_reactions (message_id, user_id, emoji, created_at) VALUES ($1, $2, $3, NOW())" ) .bind(message_id) .bind(user_id) .bind(emoji.as_str()) .execute(&self.pool) .await?; info!( message_id = message_id, user_id = user_id, emoji = %emoji.as_str(), "Reaction added to message" ); Ok(()) } /// Retirer une réaction d'un message #[instrument(skip(self))] pub async fn remove_reaction( &self, message_id: i64, user_id: i64, ) -> Result<(), sqlx::Error> { sqlx::query( "DELETE FROM message_reactions WHERE message_id = $1 AND user_id = $2" ) .bind(message_id) .bind(user_id) .execute(&self.pool) .await?; info!( message_id = message_id, user_id = user_id, "Reaction removed from message" ); Ok(()) } /// Obtenir toutes les réactions d'un message #[instrument(skip(self))] pub async fn get_message_reactions( &self, message_id: i64, ) -> Result>, sqlx::Error> { let reactions: Vec<(String, i64)> = sqlx::query_as( "SELECT emoji, user_id FROM message_reactions WHERE message_id = $1" ) .bind(message_id) .fetch_all(&self.pool) .await?; let mut result = HashMap::new(); for (emoji_str, user_id) in reactions { if let Some(emoji) = ReactionEmoji::from_str(&emoji_str) { result.entry(emoji).or_insert_with(Vec::new).push(user_id); } } Ok(result) } /// Obtenir le nombre de réactions par émoji pour un message #[instrument(skip(self))] pub async fn get_reaction_counts( &self, message_id: i64, ) -> Result, sqlx::Error> { let reactions = self.get_message_reactions(message_id).await?; let counts: HashMap = reactions .iter() .map(|(emoji, users)| (emoji.clone(), users.len())) .collect(); Ok(counts) } /// Obtenir les réactions d'un user pour tous les messages d'une conversation #[instrument(skip(self))] pub async fn get_user_reactions_in_conversation( &self, conversation_id: i64, user_id: i64, ) -> Result, sqlx::Error> { let reactions: Vec<(i64, String)> = sqlx::query_as( "SELECT mr.message_id, mr.emoji FROM message_reactions mr JOIN messages m ON m.id = mr.message_id WHERE m.conversation_id = $1 AND mr.user_id = $2" ) .bind(conversation_id) .bind(user_id) .fetch_all(&self.pool) .await?; let mut result = HashMap::new(); for (message_id, emoji_str) in reactions { if let Some(emoji) = ReactionEmoji::from_str(&emoji_str) { result.insert(message_id, emoji); } } Ok(result) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_reaction_emoji_conversion() { assert_eq!(ReactionEmoji::Like.as_str(), "👍"); assert_eq!(ReactionEmoji::from_str("👍"), Some(ReactionEmoji::Like)); assert_eq!(ReactionEmoji::from_str("invalid"), None); } #[tokio::test] async fn test_reactions_manager() { // Note: Ces tests nécessitent une base de données de test // Pour l'instant, on teste juste que le code compile assert!(true); } }