gitdataai/lib/channel/rooms.rs
2026-05-30 01:38:40 +08:00

222 lines
5.9 KiB
Rust

use cache::AppCache;
use db::{AppDatabase, sqlx};
use model::room::RoomMessageModel;
use serde::Serialize;
use uuid::Uuid;
use crate::{ChannelBusConfig, ChannelResult};
pub(crate) const RM_COLUMNS: &str =
"id, room, seq, thread, parent, author, content, content_type, pinned, \
system_type, metadata, edited_at, created_at, updated_at, deleted_at";
pub(crate) fn room_socket_name(room: Uuid) -> String {
format!("room:{room}")
}
pub(crate) fn user_rooms_cache_key(user: Uuid) -> String {
format!("channel:user:{user}:rooms")
}
#[derive(Debug, Serialize)]
pub struct RoomListItem {
pub id: Uuid,
pub name: String,
pub topic: Option<String>,
pub room_type: String,
pub is_private: bool,
pub category: Option<Uuid>,
pub workspace_id: Uuid,
}
#[derive(Debug, Serialize)]
pub struct CategoryListItem {
pub id: Uuid,
pub name: String,
pub position: i32,
}
pub async fn user_rooms_for_api(
db: &AppDatabase,
cache: &AppCache,
config: &ChannelBusConfig,
user: Uuid,
) -> ChannelResult<Vec<RoomListItem>> {
let room_ids = user_rooms(db, cache, config, user).await?;
if room_ids.is_empty() {
return Ok(Vec::new());
}
let rows = sqlx::query_as::<_, (Uuid, String, Option<String>, String, bool, Option<Uuid>, Uuid)>(
"SELECT id, name, topic, room_type, is_private, parent, wk \
FROM room \
WHERE id = ANY($1) AND deleted_at IS NULL AND is_archived = false \
ORDER BY name",
)
.bind(&room_ids)
.fetch_all(db.reader())
.await?;
Ok(rows
.into_iter()
.map(|(id, name, topic, room_type, is_private, category, workspace_id)| RoomListItem {
id,
name,
topic,
room_type,
is_private,
category,
workspace_id,
})
.collect())
}
pub async fn user_categories_for_api(
db: &AppDatabase,
cache: &AppCache,
config: &ChannelBusConfig,
user: Uuid,
) -> ChannelResult<Vec<CategoryListItem>> {
let room_ids = user_rooms(db, cache, config, user).await?;
if room_ids.is_empty() {
return Ok(Vec::new());
}
let wk_rows = sqlx::query_as::<_, (Uuid,)>(
"SELECT DISTINCT wk FROM room WHERE id = ANY($1) AND deleted_at IS NULL",
)
.bind(&room_ids)
.fetch_all(db.reader())
.await?;
let wk_ids: Vec<Uuid> = wk_rows.into_iter().map(|r| r.0).collect();
if wk_ids.is_empty() {
return Ok(Vec::new());
}
let rows = sqlx::query_as::<_, (Uuid, String, i32)>(
"SELECT id, name, position FROM room_category WHERE wk = ANY($1) ORDER BY position, name",
)
.bind(&wk_ids)
.fetch_all(db.reader())
.await?;
Ok(rows
.into_iter()
.map(|(id, name, position)| CategoryListItem { id, name, position })
.collect())
}
pub(crate) async fn user_rooms(
db: &AppDatabase,
cache: &AppCache,
config: &ChannelBusConfig,
user: Uuid,
) -> ChannelResult<Vec<Uuid>> {
let key = user_rooms_cache_key(user);
if let Some(rooms) = cache.get::<Vec<Uuid>>(&key).await? {
return Ok(rooms);
}
let rooms = load_user_rooms(db, user).await?;
cache_set_with_ttl(cache, &key, &rooms, config.room_cache_ttl_hint).await?;
Ok(rooms)
}
pub(crate) async fn refresh_user_rooms_cache(
db: &AppDatabase,
cache: &AppCache,
config: &ChannelBusConfig,
user: Uuid,
) -> ChannelResult<Vec<Uuid>> {
let key = user_rooms_cache_key(user);
cache.remove(&key).await?;
let rooms = load_user_rooms(db, user).await?;
cache_set_with_ttl(cache, &key, &rooms, config.room_cache_ttl_hint).await?;
Ok(rooms)
}
async fn cache_set_with_ttl<T>(
cache: &AppCache,
key: &str,
value: &T,
ttl: Option<std::time::Duration>,
) -> ChannelResult<()>
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
cache.set(key, value).await?;
if let Some(ttl) = ttl {
if let Some(cluster) = &cache.cluster {
cluster.expire(key, ttl).await?;
}
}
Ok(())
}
pub(crate) async fn active_workspace_users(
db: &AppDatabase,
wk: Uuid,
) -> ChannelResult<Vec<Uuid>> {
let rows = sqlx::query_as::<_, (Uuid,)>(
"SELECT \"user\" FROM wk_member WHERE wk = $1 AND leave_at IS NULL",
)
.bind(wk)
.fetch_all(db.reader())
.await?;
Ok(rows.into_iter().map(|row| row.0).collect())
}
pub(crate) async fn room_workspace(
db: &AppDatabase,
room: Uuid,
) -> ChannelResult<Option<Uuid>> {
let row = sqlx::query_as::<_, (Uuid,)>("SELECT wk FROM room WHERE id = $1")
.bind(room)
.fetch_optional(db.reader())
.await?;
Ok(row.map(|row| row.0))
}
pub(crate) async fn catchup_messages(
db: &AppDatabase,
config: &ChannelBusConfig,
room: Uuid,
after_seq: i64,
) -> ChannelResult<Vec<RoomMessageModel>> {
let rows = sqlx::query_as::<_, RoomMessageModel>(
db::sqlx::AssertSqlSafe(format!(
"SELECT {RM_COLUMNS} FROM room_message \
WHERE room = $1 AND seq > $2 AND deleted_at IS NULL \
ORDER BY seq ASC \
LIMIT $3"
)),
)
.bind(room)
.bind(after_seq)
.bind(config.catchup_limit)
.fetch_all(db.reader())
.await?;
Ok(rows)
}
async fn load_user_rooms(
db: &AppDatabase,
user: Uuid,
) -> ChannelResult<Vec<Uuid>> {
let rows = sqlx::query_as::<_, (Uuid,)>(
"SELECT r.id \
FROM room r \
INNER JOIN wk_member wm ON wm.wk = r.wk \
WHERE wm.\"user\" = $1 \
AND wm.leave_at IS NULL \
AND r.deleted_at IS NULL \
AND r.is_archived = false \
AND (r.is_private = false \
OR EXISTS ( \
SELECT 1 FROM room_permission_overwrite po \
WHERE po.room = r.id AND po.target_id = $1 \
)) \
ORDER BY r.id",
)
.bind(user)
.fetch_all(db.reader())
.await?;
Ok(rows.into_iter().map(|row| row.0).collect())
}