//! Reaction event handlers on `MessageService`. //! //! Toggle semantics: sending the same reaction again removes it. use std::sync::Arc; use uuid::Uuid; use crate::ImksError; use crate::socket::socket::Socket; use super::message::MessageService; impl MessageService { /// Handle `reaction:add` — toggle (add or remove) a reaction, then broadcast. pub async fn toggle_reaction( &self, socket: Arc, data: &serde_json::Value, ) -> crate::ImksResult<()> { let user_id = self.user_id(&socket)?; let (message_id, content) = self.parse_reaction_payload(data)?; let message = self .repo .get(message_id) .await? .ok_or_else(|| ImksError::NotFound(format!("message {message_id}")))?; let channel_id = message.channel_id; let channel_id_str = channel_id.to_string(); let user_id_str = user_id.to_string(); self.ensure_readable(&channel_id_str, &user_id_str).await?; self.ensure_member(&channel_id_str, &user_id_str).await?; let action = if self .repo .add_reaction(message_id, channel_id, user_id, &content) .await? .is_some() { tracing::info!(%message_id, %user_id, %content, "Reaction added"); "add" } else { self.repo .remove_reaction(message_id, user_id, &content) .await?; tracing::info!(%message_id, %user_id, %content, "Reaction removed"); "remove" }; if let Some(ns) = self.namespaces.get_namespace(&socket.namespace) { ns.emit_to_room( &channel_id.to_string(), "reaction:updated", serde_json::json!({ "message_id": message_id.to_string(), "channel_id": channel_id.to_string(), "user_id": user_id.to_string(), "content": content, "action": action, }), ) .await; } Ok(()) } /// Handle `reaction:remove` — explicitly remove a reaction. pub async fn remove_reaction( &self, socket: Arc, data: &serde_json::Value, ) -> crate::ImksResult<()> { let user_id = self.user_id(&socket)?; let (message_id, content) = self.parse_reaction_payload(data)?; let message = self .repo .get(message_id) .await? .ok_or_else(|| ImksError::NotFound(format!("message {message_id}")))?; let channel_id = message.channel_id; let channel_id_str = channel_id.to_string(); let user_id_str = user_id.to_string(); self.ensure_readable(&channel_id_str, &user_id_str).await?; self.ensure_member(&channel_id_str, &user_id_str).await?; self.repo .remove_reaction(message_id, user_id, &content) .await?; if let Some(ns) = self.namespaces.get_namespace(&socket.namespace) { ns.emit_to_room( &channel_id.to_string(), "reaction:updated", serde_json::json!({ "message_id": message_id.to_string(), "channel_id": channel_id.to_string(), "user_id": user_id.to_string(), "content": content, "action": "remove", }), ) .await; } Ok(()) } fn parse_reaction_payload( &self, data: &serde_json::Value, ) -> crate::ImksResult<(Uuid, String)> { let arr = data .as_array() .and_then(|a| a.first()) .ok_or_else(|| ImksError::InvalidInput("Expected [payload] array".into()))?; let content: String = Self::parse_field(arr, "content")?; if content.trim().is_empty() { return Err(ImksError::InvalidInput( "Reaction content cannot be empty".into(), )); } Ok((Self::parse_field(arr, "message_id")?, content)) } }