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

229 lines
6.3 KiB
Rust

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<Self> {
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<Utc>,
}
/// Manager pour gérer les réactions sur les messages
pub struct ReactionsManager {
pool: Pool<Postgres>,
}
impl ReactionsManager {
pub fn new(pool: Pool<Postgres>) -> 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<HashMap<ReactionEmoji, Vec<i64>>, 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<HashMap<ReactionEmoji, usize>, sqlx::Error> {
let reactions = self.get_message_reactions(message_id).await?;
let counts: HashMap<ReactionEmoji, usize> = 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<HashMap<i64, ReactionEmoji>, 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);
}
}