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::WsHandler; use super::WsOutEvent; impl WsHandler { pub(super) async fn room_get( bus: &ChannelBus, user_id: Uuid, room: Uuid, ) -> ChannelResult> { Self::ensure_room_access(bus, user_id, room).await?; let row = db::sqlx::query_as::<_, model::channel::ChannelModel>( "SELECT id, wk, parent, name, topic, room_type, position, \ is_private, is_archived, ai_enabled, 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.channel_type, "is_private": row.is_private, "is_archived": row.is_archived, "ai_enabled": row.ai_enabled, "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, ai_enabled: Option, channel_type: Option, ) -> ChannelResult> { 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 ai = ai_enabled.unwrap_or(false); let ctype = channel_type.unwrap_or_else(|| "channel".to_string()); let row = db::sqlx::query_as::<_, model::channel::ChannelModel>( "INSERT INTO room (wk, parent, name, room_type, is_private, ai_enabled, created_by, created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7, now(), now()) \ RETURNING id, wk, parent, name, topic, room_type, position, \ is_private, is_archived, ai_enabled, created_by, created_at, updated_at, deleted_at", ) .bind(workspace) .bind(category) .bind(&room_name) .bind(&ctype) .bind(is_private) .bind(ai) .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, public: Option, category: Option, ai_enabled: Option, ) -> ChannelResult> { Self::ensure_room_access(bus, user_id, room).await?; let old = db::sqlx::query_as::<_, model::channel::ChannelModel>( "SELECT id, wk, parent, name, topic, room_type, position, \ is_private, is_archived, ai_enabled, 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 new_ai = ai_enabled.unwrap_or(old.ai_enabled); let row = db::sqlx::query_as::<_, model::channel::ChannelModel>( "UPDATE room SET name = $2, is_private = $3, parent = $4, ai_enabled = $5, updated_at = now() \ WHERE id = $1 AND deleted_at IS NULL \ RETURNING id, wk, parent, name, topic, room_type, position, \ is_private, is_archived, ai_enabled, created_by, created_at, updated_at, deleted_at", ) .bind(room) .bind(&new_name) .bind(new_private) .bind(new_category) .bind(new_ai) .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 || new_ai != old.ai_enabled { 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(Some(WsOutEvent::Response { request_id: Uuid::nil(), data: serde_json::json!({ "id": row.id, "name": row.name, "is_private": row.is_private, "ai_enabled": row.ai_enabled, "parent": row.parent, }), })) } pub(super) async fn room_delete( bus: &ChannelBus, user_id: Uuid, room: Uuid, ) -> ChannelResult> { Self::ensure_room_access(bus, user_id, room).await?; let old = db::sqlx::query_as::<_, model::channel::ChannelModel>( "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::channel::ChannelModel>( "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> { Self::ensure_room_access(bus, user_id, room).await?; 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> { Self::ensure_room_access(bus, user_id, room).await?; 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, })) } }