use crate::RoomEventType; use crate::RoomUserStateResponse; use crate::RoomUserStateUpdateDndRequest; use crate::error::RoomError; use crate::service::RoomService; use crate::ws_context::WsUserContext; use chrono::Utc; use db::database::AppDatabase; use models::rooms::{room_access, room_user_state}; use sea_orm::*; use uuid::Uuid; /// Grant explicit access to a private room. /// For public rooms this is a no-op (project membership suffices). pub async fn grant_room_access( db: &AppDatabase, room_id: Uuid, target_user_id: Uuid, granted_by: Uuid, ) -> Result<(), RoomError> { let existing = room_access::Entity::find_by_id((room_id, target_user_id)) .one(db) .await?; if existing.is_some() { return Ok(()); } let _created = room_access::ActiveModel { room: Set(room_id), user: Set(target_user_id), granted_by: Set(granted_by), granted_at: Set(Utc::now()), } .insert(db) .await?; Ok(()) } /// Revoke explicit access from a private room. pub async fn revoke_room_access( db: &AppDatabase, room_id: Uuid, target_user_id: Uuid, ) -> Result<(), RoomError> { room_access::Entity::delete_by_id((room_id, target_user_id)) .exec(db) .await?; Ok(()) } impl RoomService { /// Grant access to a private room (creates room_access row). /// Public rooms don't need explicit access — this is a no-op. pub async fn room_access_grant( &self, room_id: Uuid, target_user_id: Uuid, ctx: &WsUserContext, ) -> Result<(), RoomError> { let room = self.find_room_or_404(room_id).await?; self.require_room_admin(room_id, ctx.user_id).await?; if room.public { // Public rooms don't need explicit access grants return Err(RoomError::BadRequest( "public rooms do not need explicit access grants".to_string(), )); } grant_room_access(&self.db, room_id, target_user_id, ctx.user_id).await?; // Ensure user state exists (for last_read_seq tracking etc.) let _ = crate::service::access::get_or_create_room_user_state( &self.db, room_id, target_user_id, ) .await; Ok(()) } /// Revoke access from a private room. pub async fn room_access_revoke( &self, room_id: Uuid, target_user_id: Uuid, ctx: &WsUserContext, ) -> Result<(), RoomError> { self.require_room_admin(room_id, ctx.user_id).await?; if target_user_id == ctx.user_id { return Err(RoomError::BadRequest( "cannot revoke your own access".to_string(), )); } revoke_room_access(&self.db, room_id, target_user_id).await?; Ok(()) } /// Update read position (last_read_seq) for a room. pub async fn room_user_state_update_read_seq( &self, room_id: Uuid, last_read_seq: i64, ctx: &WsUserContext, ) -> Result { self.require_room_access(room_id, ctx.user_id).await?; let state = crate::service::access::get_or_create_room_user_state(&self.db, room_id, ctx.user_id) .await?; let mut active: room_user_state::ActiveModel = state.into(); active.last_read_seq = Set(Some(last_read_seq)); let updated = active.update(&self.db).await?; let room = self.find_room_or_404(room_id).await?; self.invalidate_room_list_cache_for_user(room.project, ctx.user_id) .await; self.publish_room_event( room.project, RoomEventType::ReadReceipt, Some(room_id), None, Some(ctx.user_id), Some(last_read_seq), ) .await; Ok(RoomUserStateResponse { room: updated.room, user: updated.user, last_read_seq: updated.last_read_seq, do_not_disturb: updated.do_not_disturb, dnd_start_hour: updated.dnd_start_hour, dnd_end_hour: updated.dnd_end_hour, joined_at: updated.joined_at, }) } /// Update DND settings for a room. pub async fn room_user_state_update_dnd( &self, room_id: Uuid, request: RoomUserStateUpdateDndRequest, ctx: &WsUserContext, ) -> Result { self.require_room_access(room_id, ctx.user_id).await?; let state = crate::service::access::get_or_create_room_user_state(&self.db, room_id, ctx.user_id) .await?; let mut active: room_user_state::ActiveModel = state.into(); if let Some(dnd) = request.do_not_disturb { active.do_not_disturb = Set(dnd); } if let Some(start) = request.dnd_start_hour { if !(0..=23).contains(&start) { return Err(RoomError::BadRequest("dnd_start_hour must be 0-23".into())); } active.dnd_start_hour = Set(Some(start)); } if let Some(end) = request.dnd_end_hour { if !(0..=23).contains(&end) { return Err(RoomError::BadRequest("dnd_end_hour must be 0-23".into())); } active.dnd_end_hour = Set(Some(end)); } let updated = active.update(&self.db).await?; Ok(RoomUserStateResponse { room: updated.room, user: updated.user, last_read_seq: updated.last_read_seq, do_not_disturb: updated.do_not_disturb, dnd_start_hour: updated.dnd_start_hour, dnd_end_hour: updated.dnd_end_hour, joined_at: updated.joined_at, }) } }