Skip to content
Clone
main
162 lines 5.4 KB Rust
use models::chat::{messages, reactions};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::errors::ChatError;
use crate::services::service::ChatService;
use crate::share::lookup;
use crate::share::realtime::RoomEvent;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ReactionAggDto {
pub emoji: String,
pub count: i64,
pub me: bool,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct ReactionUserDto {
pub emoji: String,
pub user: api::users::UserDto,
pub created_at: OffsetDateTime,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct ReactionCmd {
pub emoji: String,
}
impl ChatService {
pub async fn add_reaction(
&self,
caller: i64,
room_id: Uuid,
message_id: Uuid,
cmd: ReactionCmd,
) -> Result<Vec<ReactionAggDto>, ChatError> {
let emoji = cmd.emoji.trim().to_string();
if emoji.is_empty() || emoji.chars().count() > self.settings.chat.max_reaction_emoji_len {
return Err(ChatError::BadRequest("invalid emoji".to_string()));
}
let _ctx = lookup::require_room_readable(self, room_id, caller).await?;
let message = messages::Entity::find_by_id(message_id)
.filter(messages::Column::RoomId.eq(room_id))
.one(self.database.read())
.await?
.filter(|m| m.deleted_at.is_none())
.ok_or(ChatError::NotFound)?;
let _ = message;
let now = OffsetDateTime::now_utc();
let result = reactions::Entity::insert(reactions::ActiveModel {
message_id: Set(message_id),
emoji: Set(emoji.clone()),
user_id: Set(caller),
created_at: Set(now),
})
.exec(self.database.write())
.await;
match result {
Ok(_) => {}
Err(err) => {
let msg = err.to_string().to_lowercase();
if !(msg.contains("unique") || msg.contains("duplicate")) {
return Err(ChatError::from(err));
}
}
}
self.publish_reactions(room_id, message_id, caller).await
}
pub async fn remove_reaction(
&self,
caller: i64,
room_id: Uuid,
message_id: Uuid,
cmd: ReactionCmd,
) -> Result<Vec<ReactionAggDto>, ChatError> {
let emoji = cmd.emoji.trim().to_string();
let _ctx = lookup::require_room_readable(self, room_id, caller).await?;
reactions::Entity::delete_many()
.filter(reactions::Column::MessageId.eq(message_id))
.filter(reactions::Column::Emoji.eq(&emoji))
.filter(reactions::Column::UserId.eq(caller))
.exec(self.database.write())
.await?;
self.publish_reactions(room_id, message_id, caller).await
}
pub async fn list_message_reactions(
&self,
caller: i64,
room_id: Uuid,
message_id: Uuid,
) -> Result<Vec<ReactionUserDto>, ChatError> {
let _ctx = lookup::require_room_readable(self, room_id, caller).await?;
let rows = reactions::Entity::find()
.filter(reactions::Column::MessageId.eq(message_id))
.order_by_asc(reactions::Column::Emoji)
.order_by_asc(reactions::Column::CreatedAt)
.all(self.database.read())
.await?;
let uids: Vec<i64> = rows.iter().map(|r| r.user_id).collect();
let user_map = self.audience.load_user_briefs(&uids).await?;
Ok(rows
.into_iter()
.filter_map(|r| {
user_map.get(&r.user_id).map(|u| ReactionUserDto {
emoji: r.emoji,
user: u.clone(),
created_at: r.created_at,
})
})
.collect())
}
async fn publish_reactions(
&self,
room_id: Uuid,
message_id: Uuid,
viewer: i64,
) -> Result<Vec<ReactionAggDto>, ChatError> {
let rows = reactions::Entity::find()
.filter(reactions::Column::MessageId.eq(message_id))
.all(self.database.read())
.await?;
let mut agg: std::collections::HashMap<String, (i64, bool)> =
std::collections::HashMap::new();
for r in rows {
let entry = agg.entry(r.emoji).or_insert((0, false));
entry.0 += 1;
if r.user_id == viewer {
entry.1 = true;
}
}
let mut result: Vec<ReactionAggDto> = agg
.clone()
.into_iter()
.map(|(emoji, (count, me))| ReactionAggDto { emoji, count, me })
.collect();
result.sort_by(|a, b| a.emoji.cmp(&b.emoji));
// The room-wide event must be viewer-neutral: `me` is per-recipient
// state and only belongs in the HTTP response above.
let shared: Vec<serde_json::Value> = agg
.into_iter()
.map(|(emoji, (count, _))| serde_json::json!({ "emoji": emoji, "count": count }))
.collect();
let value = serde_json::to_value(&shared)?;
let event = RoomEvent::ReactionUpdated {
room_id,
user_id: viewer,
message_id,
reactions: value,
};
self.audience.realtime.publish_room(&event).await;
Ok(result)
}
}