use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
use uuid::Uuid;
use super::workspace::{MemberId, TeamId};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActivityEvent {
pub id: Uuid,
pub team_id: TeamId,
pub actor_id: MemberId,
pub actor_name: String,
pub event_type: EventType,
pub details: EventDetails,
pub timestamp: DateTime<Utc>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
TeamCreated,
TeamUpdated,
TeamDeleted,
SettingsChanged,
MemberInvited,
MemberJoined,
MemberLeft,
MemberRemoved,
RoleChanged,
SessionCreated,
SessionUpdated,
SessionDeleted,
SessionShared,
SessionArchived,
SessionExported,
CommentAdded,
CommentEdited,
CommentDeleted,
AnnotationAdded,
PermissionGranted,
PermissionRevoked,
AccessDenied,
SuspiciousActivity,
WebhookTriggered,
IntegrationConnected,
IntegrationDisconnected,
HarvestCompleted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EventDetails {
Team {
team_name: String,
changes: Option<HashMap<String, ChangeValue>>,
},
Member {
member_id: MemberId,
member_name: String,
member_email: Option<String>,
role: Option<String>,
previous_role: Option<String>,
},
Session {
session_id: String,
session_title: String,
provider: Option<String>,
},
Comment {
comment_id: String,
session_id: String,
content_preview: Option<String>,
},
Permission {
permission: String,
target_id: Option<String>,
target_type: Option<String>,
},
Integration {
integration_name: String,
integration_type: String,
},
Generic {
message: String,
metadata: Option<HashMap<String, String>>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangeValue {
pub old: Option<String>,
pub new: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Notification {
pub id: Uuid,
pub user_id: MemberId,
pub team_id: TeamId,
pub notification_type: NotificationType,
pub title: String,
pub message: String,
pub event_id: Option<Uuid>,
pub action_url: Option<String>,
pub read: bool,
pub created_at: DateTime<Utc>,
pub read_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NotificationType {
Invitation,
Mention,
SessionShare,
Comment,
RoleChange,
SecurityAlert,
Announcement,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationPreferences {
pub user_id: MemberId,
pub email_enabled: bool,
pub push_enabled: bool,
pub in_app_enabled: bool,
pub enabled_types: HashMap<NotificationType, bool>,
pub quiet_hours_start: Option<u8>,
pub quiet_hours_end: Option<u8>,
}
impl Default for NotificationPreferences {
fn default() -> Self {
let mut enabled_types = HashMap::new();
enabled_types.insert(NotificationType::Invitation, true);
enabled_types.insert(NotificationType::Mention, true);
enabled_types.insert(NotificationType::SessionShare, true);
enabled_types.insert(NotificationType::Comment, true);
enabled_types.insert(NotificationType::RoleChange, true);
enabled_types.insert(NotificationType::SecurityAlert, true);
enabled_types.insert(NotificationType::Announcement, true);
Self {
user_id: Uuid::nil(),
email_enabled: true,
push_enabled: true,
in_app_enabled: true,
enabled_types,
quiet_hours_start: None,
quiet_hours_end: None,
}
}
}
pub struct ActivityManager {
activities: Arc<RwLock<HashMap<TeamId, VecDeque<ActivityEvent>>>>,
max_events_per_team: usize,
event_tx: broadcast::Sender<ActivityEvent>,
notifications: Arc<RwLock<HashMap<MemberId, Vec<Notification>>>>,
preferences: Arc<RwLock<HashMap<MemberId, NotificationPreferences>>>,
}
impl ActivityManager {
pub fn new() -> Self {
let (event_tx, _) = broadcast::channel(1000);
Self {
activities: Arc::new(RwLock::new(HashMap::new())),
max_events_per_team: 10000,
event_tx,
notifications: Arc::new(RwLock::new(HashMap::new())),
preferences: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn record_event(&self, event: ActivityEvent) {
let team_id = event.team_id;
let mut activities = self.activities.write().await;
let team_events = activities.entry(team_id).or_insert_with(VecDeque::new);
if team_events.len() >= self.max_events_per_team {
team_events.pop_front();
}
team_events.push_back(event.clone());
let _ = self.event_tx.send(event);
}
pub async fn get_activities(
&self,
team_id: TeamId,
limit: usize,
offset: usize,
) -> Vec<ActivityEvent> {
let activities = self.activities.read().await;
activities
.get(&team_id)
.map(|events| {
events
.iter()
.rev()
.skip(offset)
.take(limit)
.cloned()
.collect()
})
.unwrap_or_default()
}
pub async fn get_activities_by_type(
&self,
team_id: TeamId,
event_type: EventType,
limit: usize,
) -> Vec<ActivityEvent> {
let activities = self.activities.read().await;
activities
.get(&team_id)
.map(|events| {
events
.iter()
.rev()
.filter(|e| e.event_type == event_type)
.take(limit)
.cloned()
.collect()
})
.unwrap_or_default()
}
pub async fn get_activities_by_actor(
&self,
team_id: TeamId,
actor_id: MemberId,
limit: usize,
) -> Vec<ActivityEvent> {
let activities = self.activities.read().await;
activities
.get(&team_id)
.map(|events| {
events
.iter()
.rev()
.filter(|e| e.actor_id == actor_id)
.take(limit)
.cloned()
.collect()
})
.unwrap_or_default()
}
pub async fn get_activities_in_range(
&self,
team_id: TeamId,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Vec<ActivityEvent> {
let activities = self.activities.read().await;
activities
.get(&team_id)
.map(|events| {
events
.iter()
.filter(|e| e.timestamp >= start && e.timestamp <= end)
.cloned()
.collect()
})
.unwrap_or_default()
}
pub fn subscribe(&self) -> broadcast::Receiver<ActivityEvent> {
self.event_tx.subscribe()
}
pub async fn create_notification(&self, notification: Notification) {
let user_id = notification.user_id;
let prefs = self.preferences.read().await;
if let Some(user_prefs) = prefs.get(&user_id) {
if !user_prefs.in_app_enabled {
return;
}
if let Some(enabled) = user_prefs
.enabled_types
.get(¬ification.notification_type)
{
if !enabled {
return;
}
}
}
let mut notifications = self.notifications.write().await;
notifications.entry(user_id).or_default().push(notification);
}
pub async fn get_unread_notifications(&self, user_id: MemberId) -> Vec<Notification> {
let notifications = self.notifications.read().await;
notifications
.get(&user_id)
.map(|notifs| notifs.iter().filter(|n| !n.read).cloned().collect())
.unwrap_or_default()
}
pub async fn get_notifications(
&self,
user_id: MemberId,
limit: usize,
offset: usize,
) -> Vec<Notification> {
let notifications = self.notifications.read().await;
notifications
.get(&user_id)
.map(|notifs| {
notifs
.iter()
.rev()
.skip(offset)
.take(limit)
.cloned()
.collect()
})
.unwrap_or_default()
}
pub async fn mark_as_read(&self, user_id: MemberId, notification_id: Uuid) {
let mut notifications = self.notifications.write().await;
if let Some(user_notifs) = notifications.get_mut(&user_id) {
if let Some(notif) = user_notifs.iter_mut().find(|n| n.id == notification_id) {
notif.read = true;
notif.read_at = Some(Utc::now());
}
}
}
pub async fn mark_all_as_read(&self, user_id: MemberId) {
let mut notifications = self.notifications.write().await;
if let Some(user_notifs) = notifications.get_mut(&user_id) {
let now = Utc::now();
for notif in user_notifs.iter_mut() {
if !notif.read {
notif.read = true;
notif.read_at = Some(now);
}
}
}
}
pub async fn delete_notification(&self, user_id: MemberId, notification_id: Uuid) {
let mut notifications = self.notifications.write().await;
if let Some(user_notifs) = notifications.get_mut(&user_id) {
user_notifs.retain(|n| n.id != notification_id);
}
}
pub async fn update_preferences(&self, preferences: NotificationPreferences) {
self.preferences
.write()
.await
.insert(preferences.user_id, preferences);
}
pub async fn get_preferences(&self, user_id: MemberId) -> NotificationPreferences {
self.preferences
.read()
.await
.get(&user_id)
.cloned()
.unwrap_or_else(|| {
let mut prefs = NotificationPreferences::default();
prefs.user_id = user_id;
prefs
})
}
pub async fn get_unread_count(&self, user_id: MemberId) -> usize {
let notifications = self.notifications.read().await;
notifications
.get(&user_id)
.map(|notifs| notifs.iter().filter(|n| !n.read).count())
.unwrap_or(0)
}
}
impl Default for ActivityManager {
fn default() -> Self {
Self::new()
}
}
pub fn team_event(
team_id: TeamId,
actor_id: MemberId,
actor_name: String,
event_type: EventType,
team_name: String,
changes: Option<HashMap<String, ChangeValue>>,
) -> ActivityEvent {
ActivityEvent {
id: Uuid::new_v4(),
team_id,
actor_id,
actor_name,
event_type,
details: EventDetails::Team { team_name, changes },
timestamp: Utc::now(),
ip_address: None,
user_agent: None,
}
}
pub fn member_event(
team_id: TeamId,
actor_id: MemberId,
actor_name: String,
event_type: EventType,
member_id: MemberId,
member_name: String,
member_email: Option<String>,
role: Option<String>,
previous_role: Option<String>,
) -> ActivityEvent {
ActivityEvent {
id: Uuid::new_v4(),
team_id,
actor_id,
actor_name,
event_type,
details: EventDetails::Member {
member_id,
member_name,
member_email,
role,
previous_role,
},
timestamp: Utc::now(),
ip_address: None,
user_agent: None,
}
}
pub fn session_event(
team_id: TeamId,
actor_id: MemberId,
actor_name: String,
event_type: EventType,
session_id: String,
session_title: String,
provider: Option<String>,
) -> ActivityEvent {
ActivityEvent {
id: Uuid::new_v4(),
team_id,
actor_id,
actor_name,
event_type,
details: EventDetails::Session {
session_id,
session_title,
provider,
},
timestamp: Utc::now(),
ip_address: None,
user_agent: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_record_and_get_activities() {
let manager = ActivityManager::new();
let team_id = Uuid::new_v4();
let actor_id = Uuid::new_v4();
let event = team_event(
team_id,
actor_id,
"Test User".to_string(),
EventType::TeamCreated,
"Test Team".to_string(),
None,
);
manager.record_event(event.clone()).await;
let activities = manager.get_activities(team_id, 10, 0).await;
assert_eq!(activities.len(), 1);
assert_eq!(activities[0].event_type, EventType::TeamCreated);
}
#[tokio::test]
async fn test_notifications() {
let manager = ActivityManager::new();
let user_id = Uuid::new_v4();
let team_id = Uuid::new_v4();
let notification = Notification {
id: Uuid::new_v4(),
user_id,
team_id,
notification_type: NotificationType::Invitation,
title: "Team Invitation".to_string(),
message: "You have been invited to join a team".to_string(),
event_id: None,
action_url: None,
read: false,
created_at: Utc::now(),
read_at: None,
};
manager.create_notification(notification.clone()).await;
let unread = manager.get_unread_notifications(user_id).await;
assert_eq!(unread.len(), 1);
manager.mark_as_read(user_id, notification.id).await;
let unread = manager.get_unread_notifications(user_id).await;
assert_eq!(unread.len(), 0);
}
}