main
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<Space>,
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<Uuid>) -> Option<String> {
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::Model, ChatError> {
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<RoomContext, ChatError> {
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<RoomContext, ChatError> {
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<bool, ChatError> {
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<bool, ChatError> {
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<bool, ChatError> {
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<Space, ChatError> {
Ok(service.participants.access.by_slug(space_name).await?)
}
pub async fn require_space_member(
_service: &ChatService,
space: &Space,
uid: i64,
) -> Result<Member, ChatError> {
Ok(space.require_member(uid).await?)
}
pub async fn require_room_writable(
service: &ChatService,
room_id: Uuid,
uid: i64,
) -> Result<RoomContext, ChatError> {
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<RoomContext, ChatError> {
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)
}