use db::channel::channel_members as member_entity; use db::channel::channels as channel_entity; use db::conn::sea_orm::*; use db::space::space_members as space_member_entity; 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::{AddChannelMemberCmd, ChannelMemberPage, ChannelMemberView, UpdateChannelMemberCmd}; async fn member_space( svc: &SpaceService, caller: Uuid, space_id: Uuid, ) -> SpaceResult { 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}")))?; let member = space_member_entity::Entity::find_by_id((space_id, caller)).one(&db).await?; let active = member .map(|m| m.status == space_member_entity::MemberStatus::Active) .unwrap_or(false); if !active { return Err(SpaceError::Forbidden( "caller is not an active member of this space".to_string(), )); } Ok(space) } fn parse_ts(s: Option<&str>) -> SpaceResult> { match s { None | Some("") => Ok(None), Some(v) => OffsetDateTime::parse(v, &time::format_description::well_known::Rfc3339) .map(Some) .map_err(|_| SpaceError::BadRequest("invalid timestamp".to_string())), } } async fn channel_in_space( db: &C, space_id: Uuid, channel_id: Uuid, ) -> SpaceResult where C: ConnectionTrait, { let channel = channel_entity::Entity::find_by_id(channel_id) .one(db) .await? .ok_or_else(|| SpaceError::NotFound(format!("channel {channel_id}")))?; if channel.space_id != Some(space_id) { return Err(SpaceError::NotFound(format!("channel {channel_id}"))); } Ok(channel) } pub async fn add_channel_member( svc: &SpaceService, caller: Uuid, space_id: Uuid, channel_id: Uuid, cmd: AddChannelMemberCmd, ) -> SpaceResult { authz::require_perm(svc, caller, space_id, authz::MANAGE_CHANNELS).await?; let db = svc.db.writer(); channel_in_space(&db, space_id, channel_id).await?; let exists = member_entity::Entity::find_by_id((channel_id, cmd.user_id)) .one(&db) .await?; if exists.is_some() { return Err(SpaceError::Conflict(format!( "user {} is already a member of channel {channel_id}", cmd.user_id ))); } let now = OffsetDateTime::now_utc(); let active: member_entity::ActiveModel = member_entity::ActiveModel::builder() .set_channel_id(channel_id) .set_user_id(cmd.user_id) .set_role(member_entity::ChannelMemberRole::from(cmd.role.unwrap_or_default())) .set_muted(cmd.muted.unwrap_or(false)) .set_muted_until(None) .set_last_read_message_id(None) .set_joined_at(now) .into(); let saved = active.insert(&db).await?; Ok(saved.into()) } pub async fn get_channel_member( svc: &SpaceService, caller: Uuid, space_id: Uuid, channel_id: Uuid, user_id: Uuid, ) -> SpaceResult { member_space(svc, caller, space_id).await?; let db = svc.db.reader(); let member = member_entity::Entity::find_by_id((channel_id, user_id)) .one(&db) .await? .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in channel {channel_id}")))?; Ok(member.into()) } pub async fn update_channel_member( svc: &SpaceService, caller: Uuid, space_id: Uuid, channel_id: Uuid, user_id: Uuid, cmd: UpdateChannelMemberCmd, ) -> SpaceResult { authz::require_perm(svc, caller, space_id, authz::MANAGE_CHANNELS).await?; let db = svc.db.writer(); let txn = db.begin().await?; channel_in_space(&txn, space_id, channel_id).await?; let member = member_entity::Entity::find_by_id((channel_id, user_id)) .one(&txn) .await? .ok_or_else(|| SpaceError::NotFound(format!("member {user_id} in channel {channel_id}")))?; let mut active: member_entity::ActiveModel = member.into(); if let Some(role) = cmd.role { active.role = Set(role.into()); } if let Some(muted) = cmd.muted { active.muted = Set(muted); } if let Some(mute_until) = cmd.muted_until { active.muted_until = Set(parse_ts(mute_until.as_deref())?); } let saved = active.update(&txn).await?; txn.commit().await?; Ok(saved.into()) } pub async fn remove_channel_member( svc: &SpaceService, caller: Uuid, space_id: Uuid, channel_id: Uuid, user_id: Uuid, ) -> SpaceResult<()> { authz::require_perm(svc, caller, space_id, authz::MANAGE_CHANNELS).await?; let db = svc.db.writer(); let res = member_entity::Entity::delete_by_id((channel_id, user_id)).exec(&db).await?; if res.rows_affected == 0 { return Err(SpaceError::NotFound(format!( "member {user_id} in channel {channel_id}" ))); } Ok(()) } pub async fn list_channel_members( svc: &SpaceService, caller: Uuid, space_id: Uuid, channel_id: Uuid, limit: u64, offset: u64, ) -> SpaceResult { member_space(svc, caller, space_id).await?; let db = svc.db.reader(); let limit = std::cmp::max(std::cmp::min(limit, svc.config.max_pagination), 1); let items = member_entity::Entity::find() .filter(member_entity::Column::ChannelId.eq(channel_id)) .order_by_asc(member_entity::Column::JoinedAt) .limit(Some(limit)) .offset(Some(offset)) .all(&db) .await? .into_iter() .map(ChannelMemberView::from) .collect(); let total = member_entity::Entity::find() .filter(member_entity::Column::ChannelId.eq(channel_id)) .count(&db) .await?; Ok(ChannelMemberPage { items, total: total as i64 }) }