use models::space::space_members::{self, MemberStatus, MessageNotifications}; use models::space::spaces; use sea_orm::sea_query::{Expr, ExprTrait}; use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, Set}; use time::OffsetDateTime; use uuid::Uuid; use kenauth::proto; fn timestamp(value: &Option) -> Option { let value = value.as_ref()?; OffsetDateTime::from_unix_timestamp(value.seconds) .ok()? .replace_nanosecond(value.nanos.clamp(0, 999_999_999) as u32) .ok() } fn space_status(value: i32) -> proto::v1::OrganizationStatus { proto::v1::OrganizationStatus::try_from(value).unwrap_or(proto::v1::OrganizationStatus::Active) } fn join_policy(value: i32) -> &'static str { match proto::v1::OrganizationJoinPolicy::try_from(value) .unwrap_or(proto::v1::OrganizationJoinPolicy::InviteOnly) { proto::v1::OrganizationJoinPolicy::Open => "open", proto::v1::OrganizationJoinPolicy::Approval => "approval", proto::v1::OrganizationJoinPolicy::Private => "private", _ => "invite_only", } } fn notifications(value: &str) -> MessageNotifications { match value.trim() { "mentions" => MessageNotifications::Mentions, "nothing" => MessageNotifications::Nothing, _ => MessageNotifications::All, } } fn non_empty(value: &str) -> Option { let value = value.trim(); if value.is_empty() { None } else { Some(value.to_string()) } } pub async fn upsert_space( db: &C, info: &proto::v1::OrganizationInfo, ) -> Result { let id = Uuid::parse_str(info.id.trim()) .map_err(|_| sea_orm::DbErr::Custom("organization id is not a uuid".into()))?; let now = OffsetDateTime::now_utc(); let policy = join_policy(info.join_policy); let active = space_status(info.status) == proto::v1::OrganizationStatus::Active; let existing = spaces::Entity::find_by_id(id).one(db).await?; let row = match existing { Some(row) => { let mut active_model: spaces::ActiveModel = row.into(); active_model.lower_name = Set(info.slug.clone()); active_model.name = Set(info.name.clone()); active_model.description = Set(non_empty(&info.description)); active_model.avatar_url = Set(non_empty(&info.avatar_url)); active_model.owner_id = Set(info.owner_user_id); active_model.is_private = Set(policy == "private"); active_model.is_active = Set(active); active_model.allow_join = Set(info.allow_join); active_model.nums_members = Set(info.member_count as i64); active_model.welcome_message = Set(non_empty(&info.welcome_message)); active_model.member_must_enable_2fa = Set(info.member_must_enable_2fa); active_model.join_policy = Set(policy.to_string()); active_model.require_email_verified = Set(info.require_email_verified); active_model.require_mfa = Set(info.require_mfa); active_model.version = Set(info.version); active_model.updated_at = Set(now); active_model.update(db).await? } None => { spaces::ActiveModel { id: Set(id), lower_name: Set(info.slug.clone()), name: Set(info.name.clone()), description: Set(non_empty(&info.description)), owner_id: Set(info.owner_user_id), avatar_url: Set(non_empty(&info.avatar_url)), is_private: Set(policy == "private"), is_active: Set(active), allow_join: Set(info.allow_join), allow_invite: Set(true), invite_sort_url_hash: Set(None), nums_members: Set(info.member_count as i64), banner_url: Set(None), banner_width: Set(None), banner_height: Set(None), invite_banner_url: Set(None), invite_banner_width: Set(None), invite_banner_height: Set(None), embed_splash_url: Set(None), embed_splash_width: Set(None), embed_splash_height: Set(None), welcome_message: Set(non_empty(&info.welcome_message)), allow_join_level: Set(models::space::spaces::AllowJoinLevel::None), member_must_enable_2fa: Set(info.member_must_enable_2fa), welcome_object: Set(None), join_policy: Set(policy.to_string()), require_email_verified: Set(info.require_email_verified), require_mfa: Set(info.require_mfa), version: Set(info.version), created_at: Set(timestamp(&info.created_at).unwrap_or(now)), updated_at: Set(timestamp(&info.updated_at).unwrap_or(now)), } .insert(db) .await? } }; Ok(row) } pub async fn remove_space( db: &C, organization_id: Uuid, ) -> Result<(), sea_orm::DbErr> { spaces::Entity::delete_by_id(organization_id).exec(db).await?; Ok(()) } pub async fn upsert_member( db: &C, organization_id: Uuid, member: &proto::v1::OrganizationMemberInfo, ) -> Result { let now = OffsetDateTime::now_utc(); let status = if member.banned_at.is_some() { MemberStatus::Banned } else if member.left_at.is_some() { MemberStatus::Left } else { MemberStatus::Active }; let invited_by = (member.invited_by > 0).then_some(member.invited_by); let existing = space_members::Entity::find_by_id((organization_id, member.user_id)) .one(db) .await?; let row = match existing { Some(row) => { let mut active: space_members::ActiveModel = row.into(); active.status = Set(status); active.alias_name = Set(non_empty(&member.alias_name)); active.alias_avatar_url = Set(non_empty(&member.alias_avatar_url)); active.message_notifications = Set(notifications(&member.message_notifications)); active.muted = Set(member.muted); active.mute_until = Set(timestamp(&member.mute_until)); active.mobile_push = Set(member.mobile_push); active.suppress_everyone = Set(member.suppress_everyone); active.suppress_roles = Set(member.suppress_roles); active.hide_muted_channels = Set(member.hide_muted_channels); active.banned_at = Set(timestamp(&member.banned_at)); active.ban_reason = Set(non_empty(&member.ban_reason)); active.ban_expires_at = Set(timestamp(&member.ban_expires_at)); active.ban_allow_speak = Set(member.ban_allow_speak); active.ban_allow_push_repo = Set(member.ban_allow_push_repo); active.ban_allow_comment_any = Set(member.ban_allow_comment_any); active.left_at = Set(timestamp(&member.left_at)); active.invited_by = Set(invited_by); active.last_active_at = Set(timestamp(&member.last_active_at)); active.updated_at = Set(Some(now)); active.update(db).await? } None => { space_members::ActiveModel { space_id: Set(organization_id), user_id: Set(member.user_id), alias_name: Set(non_empty(&member.alias_name)), alias_avatar_url: Set(non_empty(&member.alias_avatar_url)), status: Set(status), last_active_at: Set(timestamp(&member.last_active_at)), left_at: Set(timestamp(&member.left_at)), invited_at: Set(None), invited_by: Set(invited_by), joined_at: Set(timestamp(&member.joined_at).unwrap_or(now)), created_at: Set(now), updated_at: Set(Some(now)), message_notifications: Set(notifications(&member.message_notifications)), muted: Set(member.muted), mute_until: Set(timestamp(&member.mute_until)), mobile_push: Set(member.mobile_push), suppress_everyone: Set(member.suppress_everyone), suppress_roles: Set(member.suppress_roles), hide_muted_channels: Set(member.hide_muted_channels), banned_at: Set(timestamp(&member.banned_at)), ban_reason: Set(non_empty(&member.ban_reason)), ban_expires_at: Set(timestamp(&member.ban_expires_at)), ban_allow_speak: Set(member.ban_allow_speak), ban_allow_push_repo: Set(member.ban_allow_push_repo), ban_allow_comment_any: Set(member.ban_allow_comment_any), } .insert(db) .await? } }; Ok(row) } pub async fn remove_member( db: &C, organization_id: Uuid, user_id: i64, ) -> Result<(), sea_orm::DbErr> { space_members::Entity::delete_by_id((organization_id, user_id)) .exec(db) .await?; Ok(()) } pub async fn bump_members( db: &C, organization_id: Uuid, delta: i32, ) -> Result<(), sea_orm::DbErr> { spaces::Entity::update_many() .col_expr( spaces::Column::NumsMembers, Expr::col(spaces::Column::NumsMembers).add(delta), ) .filter(spaces::Column::Id.eq(organization_id)) .exec(db) .await?; Ok(()) }