use models::room::room_members; use models::room::rooms::{self}; use sea_orm::EntityTrait; use space::{Member, Space}; use uuid::Uuid; use crate::errors::ChatError; use crate::services::service::ChatService; pub struct RoomContext { pub room: rooms::Model, pub space: Option, pub is_dm: bool, } impl RoomContext {} /// Route slug (`lower_name`) of the space owning `space_id`, or `None` for a /// DM. Notification payloads carry it so the inbox can deep-link straight to /// `/app/{slug}/chat/{room}` without a second round trip. pub async fn space_slug_for(service: &ChatService, space_id: Option) -> Option { let space_id = space_id?; service .participants .access .by_id(space_id) .await .ok() .map(|space| space.slug().to_owned()) } pub async fn lookup_room(service: &ChatService, room_id: Uuid) -> Result { rooms::Entity::find_by_id(room_id) .one(service.database.read()) .await? .filter(|r| r.is_active) .ok_or(ChatError::NotFound) } pub async fn load_room_context( service: &ChatService, room: rooms::Model, ) -> Result { let is_dm = room.space_id.is_none(); let space = match room.space_id { Some(space_id) => Some(service.participants.access.by_id(space_id).await?), None => None, }; Ok(RoomContext { room, space, is_dm }) } pub async fn lookup_room_context( service: &ChatService, room_id: Uuid, ) -> Result { let room = lookup_room(service, room_id).await?; load_room_context(service, room).await } /// 空间频道对空间成员开放;私密频道与 DM 需要 `room_members` 行,owner 总是可读。 async fn room_visible( service: &ChatService, room: &rooms::Model, space: Option<&Space>, uid: i64, ) -> Result { if room.space_id.is_some() && space.is_none() { return Ok(false); } if space.is_some() && (!room.is_private || room.owner_id == uid) { return Ok(true); } is_room_member_row(service, room.id, uid).await } async fn is_room_member_row( service: &ChatService, room_id: Uuid, uid: i64, ) -> Result { Ok(room_members::Entity::find_by_id((room_id, uid)) .one(service.database.read()) .await? .is_some_and(|m| m.left_at.is_none())) } pub async fn is_room_member( service: &ChatService, room_id: Uuid, uid: i64, ) -> Result { let ctx = lookup_room_context(service, room_id).await?; room_visible(service, &ctx.room, ctx.space.as_ref(), uid).await } pub async fn lookup_space_by_name( service: &ChatService, space_name: &str, ) -> Result { Ok(service.participants.access.by_slug(space_name).await?) } pub async fn require_space_member( _service: &ChatService, space: &Space, uid: i64, ) -> Result { Ok(space.require_member(uid).await?) } pub async fn require_room_writable( service: &ChatService, room_id: Uuid, uid: i64, ) -> Result { let ctx = require_room_readable(service, room_id, uid).await?; if let Some(space) = &ctx.space { space::require(space, uid, space::Capability::Message).await?; } Ok(ctx) } pub async fn require_room_readable( service: &ChatService, room_id: Uuid, uid: i64, ) -> Result { let ctx = lookup_room_context(service, room_id).await?; match &ctx.space { Some(space) => { require_space_member(service, space, uid).await?; if !room_visible(service, &ctx.room, Some(space), uid).await? { return Err(ChatError::Forbidden); } } None => { if !room_visible(service, &ctx.room, None, uid).await? { return Err(ChatError::Forbidden); } } } Ok(ctx) }