195 lines
7.5 KiB
Rust
195 lines
7.5 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use uuid::Uuid;
|
|
|
|
use crate::event::{RoomInfo, UserInfo, thread};
|
|
use crate::{ChannelBus, ChannelError, ChannelResult};
|
|
|
|
use super::WsOutEvent;
|
|
use super::WsHandler;
|
|
|
|
/// Helper struct for thread_list JOIN query result
|
|
#[derive(db::sqlx::FromRow)]
|
|
struct ThreadListRow {
|
|
id: Uuid,
|
|
room: Uuid,
|
|
seq: i64,
|
|
starter_message: Option<Uuid>,
|
|
title: String,
|
|
created_by: Uuid,
|
|
archived: bool,
|
|
locked: bool,
|
|
last_message_at: Option<DateTime<Utc>>,
|
|
created_at: DateTime<Utc>,
|
|
updated_at: DateTime<Utc>,
|
|
archived_at: Option<DateTime<Utc>>,
|
|
parent_seq: i64,
|
|
}
|
|
|
|
impl WsHandler {
|
|
pub(super) async fn thread_list(
|
|
bus: &ChannelBus,
|
|
user_id: Uuid,
|
|
room: Uuid,
|
|
) -> ChannelResult<Option<WsOutEvent>> {
|
|
Self::ensure_room_access(bus, user_id, room).await?;
|
|
|
|
// Join room_thread with room_message to get the parent message's seq
|
|
let rows = db::sqlx::query_as::<_, ThreadListRow>(
|
|
"SELECT rt.id, rt.room, rt.seq, rt.starter_message, rt.title, rt.created_by, \
|
|
rt.archived, rt.locked, rt.last_message_at, rt.created_at, rt.updated_at, rt.archived_at, \
|
|
COALESCE(rm.seq, 0) as parent_seq \
|
|
FROM room_thread rt \
|
|
LEFT JOIN room_message rm ON rm.id = rt.starter_message \
|
|
WHERE rt.room = $1 ORDER BY rt.last_message_at DESC NULLS LAST",
|
|
)
|
|
.bind(room)
|
|
.fetch_all(bus.inner.db.reader())
|
|
.await?;
|
|
|
|
let mut items = Vec::new();
|
|
for row in rows {
|
|
let tc_room = bus.lookup_room(row.room).await
|
|
.unwrap_or_else(|_| RoomInfo::unknown(row.room));
|
|
let created_by = bus.lookup_user(row.created_by).await
|
|
.unwrap_or_else(|_| UserInfo::unknown(row.created_by));
|
|
// Get last message preview
|
|
let preview: Option<(String,)> = db::sqlx::query_as(
|
|
"SELECT content FROM room_message \
|
|
WHERE thread = $1 AND deleted_at IS NULL \
|
|
ORDER BY seq DESC LIMIT 1",
|
|
)
|
|
.bind(row.id)
|
|
.fetch_optional(bus.inner.db.reader())
|
|
.await?;
|
|
items.push(thread::ThreadListItem {
|
|
id: row.id,
|
|
room: tc_room,
|
|
seq: row.seq,
|
|
parent_seq: row.parent_seq,
|
|
title: row.title,
|
|
created_by,
|
|
archived: row.archived,
|
|
locked: row.locked,
|
|
last_message_at: row.last_message_at,
|
|
last_message_preview: preview.map(|p| p.0),
|
|
created_at: row.created_at,
|
|
});
|
|
}
|
|
|
|
Ok(Some(WsOutEvent::ThreadList {
|
|
data: thread::ThreadListService { threads: items },
|
|
}))
|
|
}
|
|
|
|
pub(super) async fn thread_create(
|
|
bus: &ChannelBus,
|
|
user_id: Uuid,
|
|
room: Uuid,
|
|
parent: i64,
|
|
) -> ChannelResult<Option<WsOutEvent>> {
|
|
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<Option<WsOutEvent>> {
|
|
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<Option<WsOutEvent>> {
|
|
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 }))
|
|
}
|
|
}
|