use std::collections::HashMap; use uuid::Uuid; use crate::event::{RoomInfo, UserInfo, message, reaction}; use crate::{ChannelBus, ChannelError, ChannelResult}; use super::WsHandler; impl WsHandler { pub(super) async fn ensure_room_access( bus: &ChannelBus, user_id: Uuid, room: Uuid, ) -> ChannelResult<()> { let rooms = crate::rooms::user_rooms( &bus.inner.db, &bus.inner.cache, &bus.inner.config, user_id, ) .await?; if rooms.contains(&room) { Ok(()) } else { Err(ChannelError::AccessDenied) } } pub(super) async fn ensure_message_in_room( bus: &ChannelBus, room: Uuid, message: Uuid, ) -> ChannelResult<()> { let exists: Option<(Uuid,)> = db::sqlx::query_as( "SELECT id FROM room_message WHERE id = $1 AND room = $2 AND deleted_at IS NULL", ) .bind(message) .bind(room) .fetch_optional(bus.inner.db.reader()) .await?; exists.map(|_| ()).ok_or(ChannelError::RoomNotFound) } pub(super) async fn reaction_groups_for_messages( bus: &ChannelBus, user_id: Uuid, message_ids: &[Uuid], ) -> ChannelResult>> { if message_ids.is_empty() { return Ok(HashMap::new()); } let rows = db::sqlx::query_as::<_, (Uuid, String, Uuid)>( "SELECT message, reaction, \"user\" FROM room_reaction \ WHERE message = ANY($1) ORDER BY created_at ASC", ) .bind(message_ids) .fetch_all(bus.inner.db.reader()) .await?; let user_ids: Vec = rows.iter().map(|(_, _, user)| *user).collect(); let users = bus.lookup_users(&user_ids).await.unwrap_or_default(); let mut grouped: HashMap< Uuid, HashMap, > = HashMap::new(); for (message_id, emoji, reactor) in rows { let group = grouped .entry(message_id) .or_default() .entry(emoji.clone()) .or_insert_with(|| reaction::ReactionGroup { emoji: emoji.clone(), count: 0, reacted_by_me: false, users: Vec::new(), }); group.count += 1; group.reacted_by_me |= reactor == user_id; group.users.push( users .get(&reactor) .cloned() .unwrap_or_else(|| UserInfo::unknown(reactor)), ); } Ok(grouped .into_iter() .map(|(message_id, groups)| { (message_id, groups.into_values().collect::>()) }) .collect()) } pub(super) async fn ensure_workspace_member( bus: &ChannelBus, user_id: Uuid, wk: Uuid, ) -> ChannelResult<()> { let row: Option<(Uuid,)> = db::sqlx::query_as( "SELECT wk FROM wk_member WHERE wk = $1 AND \"user\" = $2 AND leave_at IS NULL", ) .bind(wk) .bind(user_id) .fetch_optional(bus.inner.db.reader()) .await?; row.map(|_| ()).ok_or(ChannelError::AccessDenied) } #[allow(dead_code)] pub(super) fn missed_message_data( m: crate::MissedMessage, ) -> message::MessageNewService { message::MessageNewService { id: m.message_id, seq: m.seq, room: RoomInfo::unknown(m.room_id), sender_type: "user".to_string(), sender: UserInfo::unknown(m.sender_id), thread: None, in_reply_to: None, content: m.content, content_type: "text".to_string(), pinned: false, system_type: None, metadata: serde_json::Value::Null, thinking_content: None, thinking_is_chunked: None, send_at: m.send_at, reactions: vec![], } } #[allow(dead_code)] pub(super) fn message_data( m: model::room::RoomMessageModel, ) -> message::MessageNewService { message::MessageNewService { id: m.id, seq: m.seq, room: RoomInfo::unknown(m.room), sender_type: "user".to_string(), sender: UserInfo::unknown(m.author), thread: m.thread, in_reply_to: m.parent, content: m.content, content_type: m.content_type, pinned: m.pinned, system_type: m.system_type, metadata: m.metadata, thinking_content: None, thinking_is_chunked: None, send_at: m.created_at, reactions: vec![], } } }