gitdataai/lib/channel/http/handler/thread.rs

228 lines
8.0 KiB
Rust

use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::event::{RoomInfo, UserInfo, thread};
use crate::{ChannelBus, ChannelError, ChannelResult};
use super::WsHandler;
use super::WsOutEvent;
/// Helper struct for thread_list JOIN query result
#[derive(db::sqlx::FromRow)]
#[allow(dead_code)]
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::channel::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::channel::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::channel::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,
}))
}
}