Skip to content
Clone
a06673b
207 lines 6.9 KB Rust
use db::conn::sea_orm::*;
use db::space::space_members as member_entity;
use db::space::spaces as space_entity;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::error::{SpaceError, SpaceResult};
use crate::features::SpaceService;
use super::types::{AddMemberCmd, SpaceMemberPage, SpaceMemberView, UpdateMemberCmd};
async fn space(caller: Uuid, space_id: Uuid, svc: &SpaceService) -> SpaceResult<space_entity::Model> {
let db = svc.db.reader();
let space = space_entity::Entity::find_by_id(space_id)
.one(&db)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("space {space_id}")))?;
if space.owner_id == caller {
Ok(space)
} else {
Err(SpaceError::Forbidden(
"only the owner can manage members".to_string(),
))
}
}
pub async fn add_member(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
cmd: AddMemberCmd,
) -> SpaceResult<SpaceMemberView> {
space(caller, space_id, svc).await?;
let db = svc.db.writer();
let now = OffsetDateTime::now_utc();
let existing = member_entity::Entity::find_by_id((space_id, cmd.user_id))
.one(&db)
.await?;
if existing.is_some() {
return Err(SpaceError::Conflict(format!(
"user {} is already a member of space {space_id}",
cmd.user_id
)));
}
let mute_until = parse_mute_until(cmd.mute_until.as_deref())?;
let active: member_entity::ActiveModel = member_entity::ActiveModel::builder()
.set_space_id(space_id)
.set_user_id(cmd.user_id)
.set_alias_name(cmd.alias_name)
.set_alias_avatar_url(cmd.alias_avatar_url)
.set_status(member_entity::MemberStatus::Active)
.set_invited_at(Option::<OffsetDateTime>::None)
.set_invited_by(Option::<Uuid>::None)
.set_joined_at(now)
.set_created_at(now)
.set_updated_at(Option::<OffsetDateTime>::Some(now))
.set_message_notifications(member_entity::MessageNotifications::from(
cmd.message_notifications.unwrap_or_default(),
))
.set_muted(cmd.muted.unwrap_or(false))
.set_mute_until(mute_until)
.set_mobile_push(cmd.mobile_push.unwrap_or(false))
.set_suppress_everyone(cmd.suppress_everyone.unwrap_or(false))
.set_suppress_roles(cmd.suppress_roles.unwrap_or(false))
.set_hide_muted_channels(cmd.hide_muted_channels.unwrap_or(false))
.into();
let saved: member_entity::Model = active.insert(&db).await?;
Ok(saved.into())
}
pub async fn get_member(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
user_id: Uuid,
) -> SpaceResult<SpaceMemberView> {
space(caller, space_id, svc).await?;
let db = svc.db.reader();
let m = member_entity::Entity::find_by_id((space_id, user_id))
.one(&db)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in space {space_id}")))?;
Ok(m.into())
}
fn parse_mute_until(s: Option<&str>) -> SpaceResult<Option<OffsetDateTime>> {
match s {
None => Ok(None),
Some("") => Ok(None),
Some(v) => OffsetDateTime::parse(v, &time::format_description::well_known::Rfc3339)
.map(Some)
.map_err(|_| SpaceError::BadRequest("invalid mute_until timestamp".to_string())),
}
}
pub async fn update_member(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
user_id: Uuid,
cmd: UpdateMemberCmd,
) -> SpaceResult<SpaceMemberView> {
space(caller, space_id, svc).await?;
let db = svc.db.writer();
let txn = db.begin().await?;
let m = member_entity::Entity::find_by_id((space_id, user_id))
.one(&txn)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in space {space_id}")))?;
let mut active: member_entity::ActiveModel = m.into();
if let Some(v) = cmd.alias_name {
match v {
None => active.alias_name = Set(None),
Some(s) => {
if s.chars().count() > svc.config.max_alias_name_len {
return Err(SpaceError::BadRequest(format!(
"alias_name exceeds {} characters",
svc.config.max_alias_name_len
)));
}
active.alias_name = Set(Some(s));
}
}
}
if let Some(v) = cmd.alias_avatar_url {
active.alias_avatar_url = Set(v);
}
if let Some(v) = cmd.message_notifications {
active.message_notifications = Set(v.into());
}
if let Some(v) = cmd.muted {
active.muted = Set(v);
}
if let Some(v) = cmd.mute_until {
active.mute_until = Set(parse_mute_until(v.as_deref())?);
}
if let Some(v) = cmd.mobile_push {
active.mobile_push = Set(v);
}
if let Some(v) = cmd.suppress_everyone {
active.suppress_everyone = Set(v);
}
if let Some(v) = cmd.suppress_roles {
active.suppress_roles = Set(v);
}
if let Some(v) = cmd.hide_muted_channels {
active.hide_muted_channels = Set(v);
}
active.updated_at = Set(Some(OffsetDateTime::now_utc()));
let saved = active.update(&txn).await?;
txn.commit().await?;
Ok(saved.into())
}
pub async fn remove_member(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
user_id: Uuid,
) -> SpaceResult<()> {
space(caller, space_id, svc).await?;
if caller == user_id {
return Err(SpaceError::BadRequest(
"owner cannot remove themselves".to_string(),
));
}
let db = svc.db.writer();
let txn = db.begin().await?;
let m = member_entity::Entity::find_by_id((space_id, user_id))
.one(&txn)
.await?
.ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in space {space_id}")))?;
let mut active: member_entity::ActiveModel = m.into();
active.status = Set(member_entity::MemberStatus::Removed);
active.left_at = Set(Some(OffsetDateTime::now_utc()));
active.updated_at = Set(Some(OffsetDateTime::now_utc()));
active.update(&txn).await?;
txn.commit().await?;
Ok(())
}
pub async fn list_members(
svc: &SpaceService,
caller: Uuid,
space_id: Uuid,
limit: u64,
offset: u64,
) -> SpaceResult<SpaceMemberPage> {
space(caller, space_id, svc).await?;
let db = svc.db.reader();
let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1) as u64;
let items = member_entity::Entity::find()
.filter(member_entity::Column::SpaceId.eq(space_id))
.order_by_asc(member_entity::Column::JoinedAt)
.limit(Some(limit))
.offset(Some(offset))
.all(&db)
.await?;
let total = member_entity::Entity::find()
.filter(member_entity::Column::SpaceId.eq(space_id))
.count(&db)
.await?;
Ok(SpaceMemberPage {
items: items.into_iter().map(SpaceMemberView::from).collect(),
total: total as i64,
})
}