use chrono::Utc; use uuid::Uuid; use crate::event::{RoomInfo, UserInfo, thread}; use crate::{ChannelBus, ChannelError, ChannelResult}; use super::WsOutEvent; use super::WsHandler; impl WsHandler { pub(super) async fn thread_create( bus: &ChannelBus, user_id: Uuid, room: Uuid, parent: i64, ) -> ChannelResult> { Self::ensure_room_access(bus, user_id, room).await?; // Look up the message UUID from seq + room let parent_id: Option<(Uuid,)> = db::sqlx::query_as( "SELECT id FROM room_message WHERE room = $1 AND seq = $2 AND deleted_at IS NULL", ) .bind(room) .bind(parent) .fetch_optional(bus.inner.db.reader()) .await?; let parent_msg_id = parent_id.ok_or(ChannelError::RoomNotFound)?.0; let seq = bus.inner.seq.seq(room).await?; let row = db::sqlx::query_as::<_, model::room::RoomThreadModel>( "INSERT INTO room_thread (room, seq, starter_message, title, created_by, created_at, updated_at) \ VALUES ($1, $2, $3, '', $4, now(), now()) \ RETURNING id, room, seq, starter_message, title, created_by, archived, locked, \ last_message_at, created_at, updated_at, archived_at", ) .bind(room) .bind(seq) .bind(parent_msg_id) // UUID of the starter message .bind(user_id) .fetch_one(bus.inner.db.writer()) .await?; let tc_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room)); let created_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)); let data = thread::ThreadCreatedService { id: row.id, room: tc_room, parent, created_by, participants: serde_json::Value::Null, created_at: row.created_at, }; bus.publish_room_event(room, "thread.created", &data).await?; Ok(Some(WsOutEvent::ThreadCreated { room: data.room.clone(), data })) } pub(super) async fn thread_resolve( bus: &ChannelBus, user_id: Uuid, thread_id: Uuid, ) -> ChannelResult> { let existing: (Uuid,) = db::sqlx::query_as( "SELECT room FROM room_thread WHERE id = $1", ) .bind(thread_id) .fetch_optional(bus.inner.db.reader()) .await? .ok_or(ChannelError::RoomNotFound)?; Self::ensure_room_access(bus, user_id, existing.0).await?; let row = db::sqlx::query_as::<_, model::room::RoomThreadModel>( "UPDATE room_thread SET locked = true, updated_at = now() \ WHERE id = $1 \ RETURNING id, room, seq, starter_message, title, created_by, archived, locked, \ last_message_at, created_at, updated_at, archived_at", ) .bind(thread_id) .fetch_one(bus.inner.db.writer()) .await?; let tr_room = bus.lookup_room(row.room).await.unwrap_or_else(|_| RoomInfo::unknown(row.room)); let resolved_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)); let data = thread::ThreadResolvedService { id: row.id, room: tr_room, resolved_by, resolved_at: Utc::now(), }; bus.publish_room_event(row.room, "thread.resolved", &data).await?; Ok(Some(WsOutEvent::ThreadResolved { room: data.room.clone(), data })) } pub(super) async fn thread_archive( bus: &ChannelBus, user_id: Uuid, thread_id: Uuid, ) -> ChannelResult> { let existing: (Uuid,) = db::sqlx::query_as( "SELECT room FROM room_thread WHERE id = $1", ) .bind(thread_id) .fetch_optional(bus.inner.db.reader()) .await? .ok_or(ChannelError::RoomNotFound)?; Self::ensure_room_access(bus, user_id, existing.0).await?; let row = db::sqlx::query_as::<_, model::room::RoomThreadModel>( "UPDATE room_thread SET archived = true, archived_at = now(), updated_at = now() \ WHERE id = $1 \ RETURNING id, room, seq, starter_message, title, created_by, archived, locked, \ last_message_at, created_at, updated_at, archived_at", ) .bind(thread_id) .fetch_one(bus.inner.db.writer()) .await?; let ta_room = bus.lookup_room(row.room).await.unwrap_or_else(|_| RoomInfo::unknown(row.room)); let archived_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)); let data = thread::ThreadArchivedService { id: row.id, room: ta_room, archived_by, archived_at: Utc::now(), }; bus.publish_room_event(row.room, "thread.archived", &data).await?; Ok(Some(WsOutEvent::ThreadArchived { room: data.room.clone(), data })) } }