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

260 lines
11 KiB
Rust

use chrono::Utc;
use uuid::Uuid;
use crate::event::{RoomInfo, UserInfo, WorkspaceInfo, member, rooms};
use crate::{ChannelBus, ChannelError, ChannelResult};
use super::{MAX_ROOM_NAME_LEN};
use super::WsOutEvent;
use super::WsHandler;
impl WsHandler {
pub(super) async fn room_get(
bus: &ChannelBus,
user_id: Uuid,
room: Uuid,
) -> ChannelResult<Option<WsOutEvent>> {
Self::ensure_room_access(bus, user_id, room).await?;
let row = db::sqlx::query_as::<_, model::room::RoomModel>(
"SELECT id, wk, parent, name, topic, room_type, position, \
is_private, is_archived, created_by, created_at, updated_at, deleted_at \
FROM room WHERE id = $1 AND deleted_at IS NULL",
)
.bind(room)
.fetch_one(bus.inner.db.reader())
.await?;
Ok(Some(WsOutEvent::Response {
request_id: Uuid::nil(),
data: serde_json::json!({
"id": row.id,
"wk": row.wk,
"name": row.name,
"topic": row.topic,
"room_type": row.room_type,
"is_private": row.is_private,
"is_archived": row.is_archived,
"parent": row.parent,
"created_by": row.created_by,
"created_at": row.created_at,
}),
}))
}
pub(super) async fn room_create(
bus: &ChannelBus,
user_id: Uuid,
workspace: Uuid,
room_name: String,
public: bool,
category: Option<Uuid>,
) -> ChannelResult<Option<WsOutEvent>> {
if room_name.is_empty() || room_name.len() > MAX_ROOM_NAME_LEN {
return Err(ChannelError::Validation("invalid room name".into()));
}
Self::ensure_workspace_member(bus, user_id, workspace).await?;
let is_private = !public;
let row = db::sqlx::query_as::<_, model::room::RoomModel>(
"INSERT INTO room (wk, parent, name, room_type, is_private, created_by, created_at, updated_at) \
VALUES ($1, $2, $3, 'channel', $4, $5, now(), now()) \
RETURNING id, wk, parent, name, topic, room_type, position, \
is_private, is_archived, created_by, created_at, updated_at, deleted_at",
)
.bind(workspace)
.bind(category)
.bind(&room_name)
.bind(is_private)
.bind(user_id)
.fetch_one(bus.inner.db.writer())
.await?;
db::sqlx::query(
"INSERT INTO room_permission_overwrite \
(room, target_type, target_id, allow_permissions, deny_permissions, created_at, updated_at) \
VALUES ($1, 'user', $2, '', '', now(), now())",
)
.bind(row.id)
.bind(user_id)
.execute(bus.inner.db.writer())
.await?;
let data = rooms::RoomCreatedService {
room: RoomInfo::from_model(&row),
workspace: bus.lookup_workspace(workspace).await.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)),
public,
category,
created_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
created_at: row.created_at,
};
bus.publish_room_event(row.id, "room.created", &data).await?;
bus.room_changed(row.id).await?;
Ok(Some(WsOutEvent::RoomCreated { room: data.room.clone(), data }))
}
pub(super) async fn room_update(
bus: &ChannelBus,
user_id: Uuid,
room: Uuid,
room_name: Option<String>,
public: Option<bool>,
category: Option<Uuid>,
) -> ChannelResult<Option<WsOutEvent>> {
Self::ensure_room_access(bus, user_id, room).await?;
let old = db::sqlx::query_as::<_, model::room::RoomModel>(
"SELECT id, wk, parent, name, topic, room_type, position, \
is_private, is_archived, created_by, created_at, updated_at, deleted_at \
FROM room WHERE id = $1 AND deleted_at IS NULL",
)
.bind(room)
.fetch_one(bus.inner.db.reader())
.await?;
let new_name = room_name.unwrap_or(old.name.clone());
let new_private =
public.map(|p| !p).unwrap_or(old.is_private);
let new_category = category.or(old.parent);
let row = db::sqlx::query_as::<_, model::room::RoomModel>(
"UPDATE room SET name = $2, is_private = $3, parent = $4, updated_at = now() \
WHERE id = $1 AND deleted_at IS NULL \
RETURNING id, wk, parent, name, topic, room_type, position, \
is_private, is_archived, created_by, created_at, updated_at, deleted_at",
)
.bind(room)
.bind(&new_name)
.bind(new_private)
.bind(new_category)
.fetch_one(bus.inner.db.writer())
.await?;
let mut renamed = false;
if new_name != old.name {
let data = rooms::RoomRenamedService {
room: RoomInfo::from_model(&row),
workspace: bus.lookup_workspace(row.wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
old_name: old.name.clone(),
new_name: new_name,
renamed_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
renamed_at: Utc::now(),
};
bus.publish_room_event(room, "room.renamed", &data).await?;
renamed = true;
}
if new_private != old.is_private || new_category != old.parent {
let data = rooms::RoomSettingsUpdatedService {
room: RoomInfo::from_model(&row),
workspace: bus.lookup_workspace(row.wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
slowmode_seconds: None,
nsfw: false,
default_auto_archive_duration: None,
updated_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
updated_at: Utc::now(),
};
bus.publish_room_event(room, "room.settings_updated", &data).await?;
}
bus.room_changed(room).await?;
if renamed {
return Ok(Some(WsOutEvent::RoomRenamed {
room: RoomInfo::from_model(&row),
data: rooms::RoomRenamedService {
room: RoomInfo::from_model(&row),
workspace: bus.lookup_workspace(row.wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
old_name: old.name,
new_name: row.name,
renamed_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
renamed_at: Utc::now(),
},
}));
}
Ok(None)
}
pub(super) async fn room_delete(
bus: &ChannelBus,
user_id: Uuid,
room: Uuid,
) -> ChannelResult<Option<WsOutEvent>> {
Self::ensure_room_access(bus, user_id, room).await?;
let old = db::sqlx::query_as::<_, model::room::RoomModel>(
"SELECT id, wk, parent, name, topic, room_type, position, \
is_private, is_archived, created_by, created_at, updated_at, deleted_at \
FROM room WHERE id = $1 AND deleted_at IS NULL",
)
.bind(room)
.fetch_one(bus.inner.db.reader())
.await?;
if old.created_by != user_id {
return Err(ChannelError::AccessDenied);
}
let row = db::sqlx::query_as::<_, model::room::RoomModel>(
"UPDATE room SET deleted_at = now(), updated_at = now() \
WHERE id = $1 AND deleted_at IS NULL \
RETURNING id, wk, parent, name, topic, room_type, position, \
is_private, is_archived, created_by, created_at, updated_at, deleted_at",
)
.bind(room)
.fetch_one(bus.inner.db.writer())
.await?;
let data = rooms::RoomDeletedService {
room: RoomInfo::from_model(&row),
workspace: bus.lookup_workspace(row.wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(row.wk)),
deleted_by: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)),
deleted_at: Utc::now(),
};
bus.publish_room_event(room, "room.deleted", &data).await?;
bus.room_changed(room).await?;
Ok(Some(WsOutEvent::RoomDeleted { room: data.room.clone(), data }))
}
pub(super) async fn access_grant(
bus: &ChannelBus,
_user_id: Uuid,
room: Uuid,
target_user: Uuid,
) -> ChannelResult<Option<WsOutEvent>> {
db::sqlx::query(
"INSERT INTO room_permission_overwrite \
(room, target_type, target_id, allow_permissions, deny_permissions, created_at, updated_at) \
SELECT $1, 'user', $2, '', '', now(), now() \
WHERE NOT EXISTS (SELECT 1 FROM room_permission_overwrite WHERE room = $1 AND target_type = 'user' AND target_id = $2)",
)
.bind(room)
.bind(target_user)
.execute(bus.inner.db.writer())
.await?;
let mj_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
let mj_user = bus.lookup_user(target_user).await.unwrap_or_else(|_| UserInfo::unknown(target_user));
let data = member::MemberJoinedService {
room: mj_room,
user: mj_user,
project_role: None,
joined_at: Utc::now(),
};
bus.publish_room_event(room, "member.joined", &data).await?;
bus.room_changed(room).await?;
Ok(Some(WsOutEvent::MemberJoined { room: data.room.clone(), data }))
}
pub(super) async fn access_revoke(
bus: &ChannelBus,
user_id: Uuid,
room: Uuid,
target_user: Uuid,
) -> ChannelResult<Option<WsOutEvent>> {
db::sqlx::query(
"DELETE FROM room_permission_overwrite \
WHERE room = $1 AND target_type = 'user' AND target_id = $2",
)
.bind(room)
.bind(target_user)
.execute(bus.inner.db.writer())
.await?;
let mr_room = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room));
let mr_target = bus.lookup_user(target_user).await.unwrap_or_else(|_| UserInfo::unknown(target_user));
let mr_remover = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id));
let data = member::MemberRemovedService {
room: mr_room,
user: mr_target,
removed_by: mr_remover,
removed_at: Utc::now(),
};
bus.publish_room_event(room, "member.removed", &data).await?;
bus.room_changed(room).await?;
Ok(Some(WsOutEvent::MemberRemoved { room: data.room.clone(), data }))
}
}