use db::conn::sea_orm::*; use db::space::spaces as space_entity; use time::OffsetDateTime; use uuid::Uuid; use crate::error::{SpaceError, SpaceResult}; use crate::features::authz; use crate::features::SpaceService; use super::types::{ CreateSpaceCmd, SpaceAllowJoinLevel, SpacePage, SpaceView, UpdateSpaceCmd, }; pub async fn create_space( svc: &SpaceService, caller: Uuid, cmd: CreateSpaceCmd, ) -> SpaceResult { let name = cmd.name.trim(); if name.is_empty() || name.len() > svc.config.max_space_name_len { return Err(SpaceError::BadRequest(format!( "invalid space name (length 1..={})", svc.config.max_space_name_len ))); } if let Some(desc) = cmd.description.as_deref() && desc.len() > svc.config.max_space_desc_len { return Err(SpaceError::BadRequest("description too long".into())); } let writer = svc.db.writer(); let now = OffsetDateTime::now_utc(); let id = Uuid::new_v4(); let active: space_entity::ActiveModel = space_entity::ActiveModel::builder() .set_id(id) .set_lower_name(name.to_lowercase()) .set_name(name.to_string()) .set_description(cmd.description) .set_owner_id(caller) .set_avatar_url(cmd.avatar_url) .set_public(cmd.public) .set_is_active(true) .set_allow_join(cmd.allow_join) .set_allow_invite(cmd.allow_invite) .set_invite_sort_url_hash(None) .set_member_count(0) .set_banner_url(None) .set_banner_width(None) .set_banner_height(None) .set_invite_banner_url(None) .set_invite_banner_width(None) .set_invite_banner_height(None) .set_embed_splash_url(None) .set_embed_splash_width(None) .set_embed_splash_height(None) .set_welcome_message(cmd.welcome_message) .set_allow_join_level(space_entity::AllowJoinLevel::from( cmd.allow_join_level.unwrap_or(SpaceAllowJoinLevel::None), )) .set_member_must_enable_2fa(cmd.member_must_enable_2fa) .set_created_at(now) .set_updated_at(now) .into(); let saved = active.insert(&writer).await?; Ok(saved.into()) } pub async fn get_space( svc: &SpaceService, caller: Option, id: Uuid, ) -> SpaceResult { let db = svc.db.reader(); let space = space_entity::Entity::find_by_id(id) .one(&db) .await? .ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?; if !space.is_active { return Err(SpaceError::NotFound(format!("space {id}"))); } if space.public { return Ok(space.into()); } let uid = caller.ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?; match authz::effective_permissions(&db, id, uid).await { Ok(_) => Ok(space.into()), Err(SpaceError::Forbidden(_)) => Err(SpaceError::NotFound(format!("space {id}"))), Err(e) => Err(e), } } pub async fn update_space( svc: &SpaceService, caller: Uuid, id: Uuid, cmd: UpdateSpaceCmd, ) -> SpaceResult { let writer = svc.db.writer(); let txn = writer.begin().await?; let space = space_entity::Entity::find_by_id(id) .one(&txn) .await? .ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?; if space.owner_id != caller { return Err(SpaceError::Forbidden("only the owner can update the space".into())); } let mut active: space_entity::ActiveModel = space.into(); if let Some(name) = cmd.name { let name = name.trim(); if name.is_empty() || name.len() > svc.config.max_space_name_len { return Err(SpaceError::BadRequest("invalid space name".into())); } active.name = Set(name.to_string()); active.lower_name = Set(name.to_lowercase()); } if let Some(desc) = cmd.description { if desc.as_deref().is_some_and(|d| d.len() > svc.config.max_space_desc_len) { return Err(SpaceError::BadRequest("description too long".into())); } active.description = Set(desc); } if let Some(avatar) = cmd.avatar_url { active.avatar_url = Set(avatar); } if let Some(public) = cmd.public { active.public = Set(public); } if let Some(allow_join) = cmd.allow_join { active.allow_join = Set(allow_join); } if let Some(allow_invite) = cmd.allow_invite { active.allow_invite = Set(allow_invite); } if let Some(welcome) = cmd.welcome_message { if welcome.as_deref().is_some_and(|w| w.len() > svc.config.max_space_desc_len) { return Err(SpaceError::BadRequest("welcome message too long".into())); } active.welcome_message = Set(welcome); } if let Some(level) = cmd.allow_join_level { active.allow_join_level = Set(level.into()); } if let Some(twofa) = cmd.member_must_enable_2fa { active.member_must_enable_2fa = Set(twofa); } active.updated_at = Set(OffsetDateTime::now_utc()); let saved = active.update(&txn).await?; txn.commit().await?; Ok(saved.into()) } pub async fn delete_space(svc: &SpaceService, caller: Uuid, id: Uuid) -> SpaceResult<()> { let writer = svc.db.writer(); let txn = writer.begin().await?; let space = space_entity::Entity::find_by_id(id) .one(&txn) .await? .ok_or_else(|| SpaceError::NotFound(format!("space {id}")))?; if space.owner_id != caller { return Err(SpaceError::Forbidden("only the owner can delete the space".into())); } let mut active: space_entity::ActiveModel = space.into(); active.is_active = Set(false); active.updated_at = Set(OffsetDateTime::now_utc()); active.update(&txn).await?; txn.commit().await?; Ok(()) } pub async fn list_spaces( svc: &SpaceService, caller: Uuid, limit: u64, offset: u64, ) -> SpaceResult { let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1) as u64; let db = svc.db.reader(); let (items, total) = futures::join!( space_entity::Entity::find() .filter(space_entity::Column::OwnerId.eq(caller)) .limit(Some(limit)) .offset(Some(offset)) .all(&db), space_entity::Entity::find() .filter(space_entity::Column::OwnerId.eq(caller)) .count(&db), ); let items = items?; let total = total? as i64; Ok(SpacePage { items: items.into_iter().map(Into::into).collect(), total, }) }