Skip to content
Clone
ef0b83a
137 lines 4.0 KB Rust
use models::room::rooms::{self, RoomKind};
use models::room::{room_bans, room_members};
use models::space::{space_members, spaces};
use sea_orm::EntityTrait;
use space::share::authz::{MESSAGE, effective_permissions};
use uuid::Uuid;
use crate::errors::ChatError;
use crate::services::service::ChatService;
pub struct RoomContext {
pub room: rooms::Model,
pub space: Option<spaces::Model>,
pub is_dm: bool,
}
impl RoomContext {
pub fn is_thread_room(&self) -> bool {
matches!(self.room.kind, RoomKind::Thread)
}
}
/// 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?;
space::authority::space_by_id(&service.state, 0, space_id)
.await
.ok()
.map(|space| space.slug)
}
pub async fn lookup_room(service: &ChatService, room_id: Uuid) -> Result<rooms::Model, ChatError> {
rooms::Entity::find_by_id(room_id)
.one(service.db().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(space::authority::space_by_id(&service.state, 0, 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
}
pub async fn is_room_member(
service: &ChatService,
room_id: Uuid,
uid: i64,
) -> Result<bool, ChatError> {
Ok(room_members::Entity::find_by_id((room_id, uid))
.one(service.db().read())
.await?
.is_some_and(|m| m.left_at.is_none()))
}
pub async fn lookup_space_by_name(
service: &ChatService,
space_name: &str,
) -> Result<spaces::Model, ChatError> {
Ok(space::share::lookup::find_active_space_by_name(&service.state, 0, space_name).await?)
}
pub async fn require_space_member(
service: &ChatService,
space: &spaces::Model,
uid: i64,
) -> Result<space_members::Model, ChatError> {
space::authority::require_member(&service.state, space.id, uid)
.await
.map_err(|_| ChatError::Forbidden)
}
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 {
let perms = effective_permissions(&service.state, space, uid).await?;
if !perms.has(MESSAGE) {
return Err(ChatError::Forbidden);
}
}
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?;
if let Some(ban) = room_bans::Entity::find_by_id((room_id, uid))
.one(service.db().read())
.await?
{
let now = time::OffsetDateTime::now_utc();
if ban.expires_at.is_none() || ban.expires_at > Some(now) {
return Err(ChatError::Forbidden);
}
}
match &ctx.space {
Some(space) => {
require_space_member(service, space, uid).await?;
if ctx.room.is_private
&& ctx.room.owner_id != uid
&& !is_room_member(service, room_id, uid).await?
{
return Err(ChatError::Forbidden);
}
}
None => {
if !is_room_member(service, room_id, uid).await? {
return Err(ChatError::Forbidden);
}
}
}
Ok(ctx)
}