use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
use uuid::Uuid;
use super::rbac::{Permission, Role, RoleAssignment};
pub type TeamId = Uuid;
pub type MemberId = Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamWorkspace {
pub id: TeamId,
pub name: String,
pub description: Option<String>,
pub avatar_url: Option<String>,
pub owner_id: MemberId,
pub settings: TeamSettings,
pub members: Vec<TeamMember>,
pub shared_sessions: Vec<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamMember {
pub user_id: MemberId,
pub display_name: String,
pub email: String,
pub avatar_url: Option<String>,
pub role: Role,
pub custom_permissions: Option<Vec<Permission>>,
pub joined_at: DateTime<Utc>,
pub last_active: Option<DateTime<Utc>>,
pub status: MemberStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemberStatus {
Active,
Invited,
Deactivated,
Left,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamSettings {
pub default_session_visibility: SessionVisibility,
pub allow_external_sharing: bool,
pub default_member_role: Role,
pub require_join_approval: bool,
pub enable_realtime_collaboration: bool,
pub session_retention_days: u32,
pub max_members: u32,
}
impl Default for TeamSettings {
fn default() -> Self {
Self {
default_session_visibility: SessionVisibility::TeamOnly,
allow_external_sharing: false,
default_member_role: Role::Member,
require_join_approval: true,
enable_realtime_collaboration: true,
session_retention_days: 0,
max_members: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionVisibility {
Private,
TeamOnly,
Public,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamInvitation {
pub id: Uuid,
pub team_id: TeamId,
pub inviter_id: MemberId,
pub invitee_email: String,
pub role: Role,
pub message: Option<String>,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub status: InvitationStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InvitationStatus {
Pending,
Accepted,
Declined,
Expired,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PresenceInfo {
pub user_id: MemberId,
pub display_name: String,
pub current_session: Option<String>,
pub cursor_position: Option<CursorPosition>,
pub status: PresenceStatus,
pub last_heartbeat: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CursorPosition {
pub session_id: String,
pub message_index: usize,
pub offset: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PresenceStatus {
Online,
Away,
Busy,
Offline,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CollaborationEvent {
MemberJoined {
user_id: MemberId,
session_id: String,
},
MemberLeft {
user_id: MemberId,
session_id: String,
},
CursorMoved {
user_id: MemberId,
position: CursorPosition,
},
ContentChanged {
user_id: MemberId,
session_id: String,
change_type: ContentChangeType,
},
PresenceUpdated {
user_id: MemberId,
status: PresenceStatus,
},
SessionShared {
user_id: MemberId,
session_id: String,
},
CommentAdded {
user_id: MemberId,
session_id: String,
comment_id: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContentChangeType {
MessageAdded,
MessageEdited,
MessageDeleted,
TagsUpdated,
TitleChanged,
Annotated,
}
pub struct TeamManager {
teams: Arc<RwLock<HashMap<TeamId, TeamWorkspace>>>,
user_teams: Arc<RwLock<HashMap<MemberId, Vec<TeamId>>>>,
presences: Arc<RwLock<HashMap<TeamId, Vec<PresenceInfo>>>>,
event_tx: broadcast::Sender<(TeamId, CollaborationEvent)>,
invitations: Arc<RwLock<HashMap<Uuid, TeamInvitation>>>,
}
impl TeamManager {
pub fn new() -> Self {
let (event_tx, _) = broadcast::channel(1000);
Self {
teams: Arc::new(RwLock::new(HashMap::new())),
user_teams: Arc::new(RwLock::new(HashMap::new())),
presences: Arc::new(RwLock::new(HashMap::new())),
event_tx,
invitations: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn create_team(
&self,
name: String,
description: Option<String>,
owner_id: MemberId,
owner_name: String,
owner_email: String,
) -> TeamWorkspace {
let team_id = Uuid::new_v4();
let now = Utc::now();
let owner_member = TeamMember {
user_id: owner_id,
display_name: owner_name,
email: owner_email,
avatar_url: None,
role: Role::Owner,
custom_permissions: None,
joined_at: now,
last_active: Some(now),
status: MemberStatus::Active,
};
let team = TeamWorkspace {
id: team_id,
name,
description,
avatar_url: None,
owner_id,
settings: TeamSettings::default(),
members: vec![owner_member],
shared_sessions: vec![],
created_at: now,
updated_at: now,
};
self.teams.write().await.insert(team_id, team.clone());
self.user_teams
.write()
.await
.entry(owner_id)
.or_default()
.push(team_id);
team
}
pub async fn get_team(&self, team_id: TeamId) -> Option<TeamWorkspace> {
self.teams.read().await.get(&team_id).cloned()
}
pub async fn get_user_teams(&self, user_id: MemberId) -> Vec<TeamWorkspace> {
let user_teams = self.user_teams.read().await;
let teams = self.teams.read().await;
user_teams
.get(&user_id)
.map(|team_ids| {
team_ids
.iter()
.filter_map(|id| teams.get(id).cloned())
.collect()
})
.unwrap_or_default()
}
pub async fn update_team_settings(
&self,
team_id: TeamId,
settings: TeamSettings,
) -> Option<TeamWorkspace> {
let mut teams = self.teams.write().await;
if let Some(team) = teams.get_mut(&team_id) {
team.settings = settings;
team.updated_at = Utc::now();
Some(team.clone())
} else {
None
}
}
pub async fn invite_member(
&self,
team_id: TeamId,
inviter_id: MemberId,
invitee_email: String,
role: Role,
message: Option<String>,
) -> Option<TeamInvitation> {
let teams = self.teams.read().await;
if !teams.contains_key(&team_id) {
return None;
}
let invitation = TeamInvitation {
id: Uuid::new_v4(),
team_id,
inviter_id,
invitee_email,
role,
message,
expires_at: Utc::now() + chrono::Duration::days(7),
created_at: Utc::now(),
status: InvitationStatus::Pending,
};
self.invitations
.write()
.await
.insert(invitation.id, invitation.clone());
Some(invitation)
}
pub async fn accept_invitation(
&self,
invitation_id: Uuid,
user_id: MemberId,
display_name: String,
email: String,
) -> Result<TeamWorkspace, String> {
let mut invitations = self.invitations.write().await;
let invitation = invitations
.get_mut(&invitation_id)
.ok_or("Invitation not found")?;
if invitation.status != InvitationStatus::Pending {
return Err("Invitation is no longer pending".to_string());
}
if invitation.expires_at < Utc::now() {
invitation.status = InvitationStatus::Expired;
return Err("Invitation has expired".to_string());
}
invitation.status = InvitationStatus::Accepted;
let mut teams = self.teams.write().await;
let team = teams
.get_mut(&invitation.team_id)
.ok_or("Team not found")?;
let now = Utc::now();
let member = TeamMember {
user_id,
display_name,
email,
avatar_url: None,
role: invitation.role,
custom_permissions: None,
joined_at: now,
last_active: Some(now),
status: MemberStatus::Active,
};
team.members.push(member);
team.updated_at = now;
self.user_teams
.write()
.await
.entry(user_id)
.or_default()
.push(invitation.team_id);
Ok(team.clone())
}
pub async fn remove_member(
&self,
team_id: TeamId,
member_id: MemberId,
) -> Result<(), String> {
let mut teams = self.teams.write().await;
let team = teams.get_mut(&team_id).ok_or("Team not found")?;
if team.owner_id == member_id {
return Err("Cannot remove team owner".to_string());
}
team.members.retain(|m| m.user_id != member_id);
team.updated_at = Utc::now();
if let Some(user_teams) = self.user_teams.write().await.get_mut(&member_id) {
user_teams.retain(|id| *id != team_id);
}
Ok(())
}
pub async fn update_member_role(
&self,
team_id: TeamId,
member_id: MemberId,
new_role: Role,
) -> Result<(), String> {
let mut teams = self.teams.write().await;
let team = teams.get_mut(&team_id).ok_or("Team not found")?;
if team.owner_id == member_id && new_role != Role::Owner {
return Err("Cannot change owner's role".to_string());
}
if let Some(member) = team.members.iter_mut().find(|m| m.user_id == member_id) {
member.role = new_role;
team.updated_at = Utc::now();
Ok(())
} else {
Err("Member not found".to_string())
}
}
pub async fn share_session(
&self,
team_id: TeamId,
session_id: String,
sharer_id: MemberId,
) -> Result<(), String> {
let mut teams = self.teams.write().await;
let team = teams.get_mut(&team_id).ok_or("Team not found")?;
if !team.shared_sessions.contains(&session_id) {
team.shared_sessions.push(session_id.clone());
team.updated_at = Utc::now();
let _ = self.event_tx.send((
team_id,
CollaborationEvent::SessionShared {
user_id: sharer_id,
session_id,
},
));
}
Ok(())
}
pub async fn update_presence(&self, team_id: TeamId, presence: PresenceInfo) {
let mut presences = self.presences.write().await;
let team_presences = presences.entry(team_id).or_default();
if let Some(existing) = team_presences
.iter_mut()
.find(|p| p.user_id == presence.user_id)
{
*existing = presence.clone();
} else {
team_presences.push(presence.clone());
}
let _ = self.event_tx.send((
team_id,
CollaborationEvent::PresenceUpdated {
user_id: presence.user_id,
status: presence.status,
},
));
}
pub async fn get_presences(&self, team_id: TeamId) -> Vec<PresenceInfo> {
self.presences
.read()
.await
.get(&team_id)
.cloned()
.unwrap_or_default()
}
pub fn subscribe(&self) -> broadcast::Receiver<(TeamId, CollaborationEvent)> {
self.event_tx.subscribe()
}
pub async fn broadcast_event(&self, team_id: TeamId, event: CollaborationEvent) {
let _ = self.event_tx.send((team_id, event));
}
pub async fn delete_team(&self, team_id: TeamId, requester_id: MemberId) -> Result<(), String> {
let teams = self.teams.read().await;
let team = teams.get(&team_id).ok_or("Team not found")?;
if team.owner_id != requester_id {
return Err("Only the owner can delete the team".to_string());
}
drop(teams);
self.teams.write().await.remove(&team_id);
let mut user_teams = self.user_teams.write().await;
for team_ids in user_teams.values_mut() {
team_ids.retain(|id| *id != team_id);
}
self.presences.write().await.remove(&team_id);
Ok(())
}
}
impl Default for TeamManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_team() {
let manager = TeamManager::new();
let owner_id = Uuid::new_v4();
let team = manager
.create_team(
"Test Team".to_string(),
Some("A test team".to_string()),
owner_id,
"Owner".to_string(),
"owner@example.com".to_string(),
)
.await;
assert_eq!(team.name, "Test Team");
assert_eq!(team.owner_id, owner_id);
assert_eq!(team.members.len(), 1);
assert_eq!(team.members[0].role, Role::Owner);
}
#[tokio::test]
async fn test_invite_and_accept() {
let manager = TeamManager::new();
let owner_id = Uuid::new_v4();
let member_id = Uuid::new_v4();
let team = manager
.create_team(
"Test Team".to_string(),
None,
owner_id,
"Owner".to_string(),
"owner@example.com".to_string(),
)
.await;
let invitation = manager
.invite_member(
team.id,
owner_id,
"member@example.com".to_string(),
Role::Member,
None,
)
.await
.unwrap();
let updated_team = manager
.accept_invitation(
invitation.id,
member_id,
"Member".to_string(),
"member@example.com".to_string(),
)
.await
.unwrap();
assert_eq!(updated_team.members.len(), 2);
}
}