use chrono::Utc; use uuid::Uuid; use crate::event::{RoomInfo, UserInfo, WorkspaceInfo, invite}; use crate::{ChannelBus, ChannelError, ChannelResult}; use super::WsOutEvent; use super::WsHandler; impl WsHandler { pub(super) async fn invite_create( bus: &ChannelBus, user_id: Uuid, workspace: Uuid, _room: Option, _max_uses: Option, _expires_at: Option>, ) -> ChannelResult> { Self::ensure_workspace_member(bus, user_id, workspace).await?; let invite_id = Uuid::now_v7(); let code = Uuid::now_v7().to_string(); let id_key = format!("invite:id:{}", invite_id); let code_key = format!("invite:code:{}", code); let meta = serde_json::json!({ "workspace": workspace, "created_by": user_id, "room": _room, "max_uses": _max_uses, "expires_at": _expires_at, }); bus.inner.cache.set(&id_key, &meta.to_string()).await?; bus.inner.cache.set(&code_key, &invite_id.to_string()).await?; let inv_room = match _room { Some(r) => Some(bus.lookup_room(r).await.unwrap_or_else(|_| RoomInfo::unknown(r))), None => None, }; let data = invite::InviteCreatedService { id: invite_id, workspace: bus.lookup_workspace(workspace).await.unwrap_or_else(|_| WorkspaceInfo::unknown(workspace)), room: inv_room, inviter: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)), invitee: None, code, max_uses: _max_uses, expires_at: _expires_at, created_at: Utc::now(), }; Ok(Some(WsOutEvent::InviteCreated { data })) } pub(super) async fn invite_accept( bus: &ChannelBus, user_id: Uuid, code: String, ) -> ChannelResult> { let code_key = format!("invite:code:{}", code); let invite_id_str: Option = bus.inner.cache.get(&code_key).await?; let invite_id = invite_id_str .as_deref() .and_then(|s| Uuid::parse_str(s).ok()) .ok_or(ChannelError::RoomNotFound)?; let id_key = format!("invite:id:{}", invite_id); let stored: Option = bus.inner.cache.get(&id_key).await?; let meta: serde_json::Value = stored .as_deref() .and_then(|s| serde_json::from_str(s).ok()) .ok_or(ChannelError::RoomNotFound)?; let wk = meta["workspace"] .as_str() .and_then(|s| Uuid::parse_str(s).ok()) .ok_or(ChannelError::RoomNotFound)?; db::sqlx::query( "INSERT INTO wk_member (wk, \"user\", owner, admin, join_at) \ VALUES ($1, $2, false, false, now()) \ ON CONFLICT DO NOTHING", ) .bind(wk) .bind(user_id) .execute(bus.inner.db.writer()) .await?; db::sqlx::query( "INSERT INTO wk_apply_join (wk, \"user\", status, created_at, updated_at) \ VALUES ($1, $2, 'accepted', now(), now())", ) .bind(wk) .bind(user_id) .execute(bus.inner.db.writer()) .await?; bus.inner.cache.remove(&code_key).await?; bus.inner.cache.remove(&id_key).await?; let data = invite::InviteAcceptedService { id: Uuid::now_v7(), workspace: bus.lookup_workspace(wk).await.unwrap_or_else(|_| WorkspaceInfo::unknown(wk)), room: None, user: bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)), accepted_at: Utc::now(), }; bus.workspace_changed(wk).await?; Ok(Some(WsOutEvent::InviteAccepted { data })) } pub(super) async fn invite_revoke( bus: &ChannelBus, _user_id: Uuid, id: Uuid, ) -> ChannelResult> { let id_key = format!("invite:id:{}", id); let stored: Option = bus.inner.cache.get(&id_key).await?; let meta: serde_json::Value = stored .as_deref() .and_then(|s| serde_json::from_str(s).ok()) .ok_or(ChannelError::RoomNotFound)?; let created_by = meta["created_by"] .as_str() .and_then(|s| Uuid::parse_str(s).ok()) .ok_or(ChannelError::RoomNotFound)?; if created_by != _user_id { return Err(ChannelError::AccessDenied); } bus.inner.cache.remove(&id_key).await?; Ok(None) } }