1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, VecDeque};
10use std::sync::Arc;
11use tokio::sync::{broadcast, RwLock};
12use uuid::Uuid;
13
14use super::workspace::{MemberId, TeamId};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ActivityEvent {
23 pub id: Uuid,
25 pub team_id: TeamId,
27 pub actor_id: MemberId,
29 pub actor_name: String,
31 pub event_type: EventType,
33 pub details: EventDetails,
35 pub timestamp: DateTime<Utc>,
37 pub ip_address: Option<String>,
39 pub user_agent: Option<String>,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum EventType {
47 TeamCreated,
49 TeamUpdated,
50 TeamDeleted,
51 SettingsChanged,
52
53 MemberInvited,
55 MemberJoined,
56 MemberLeft,
57 MemberRemoved,
58 RoleChanged,
59
60 SessionCreated,
62 SessionUpdated,
63 SessionDeleted,
64 SessionShared,
65 SessionArchived,
66 SessionExported,
67
68 CommentAdded,
70 CommentEdited,
71 CommentDeleted,
72 AnnotationAdded,
73
74 PermissionGranted,
76 PermissionRevoked,
77 AccessDenied,
78 SuspiciousActivity,
79
80 WebhookTriggered,
82 IntegrationConnected,
83 IntegrationDisconnected,
84 HarvestCompleted,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(tag = "type", rename_all = "snake_case")]
90pub enum EventDetails {
91 Team {
93 team_name: String,
94 changes: Option<HashMap<String, ChangeValue>>,
95 },
96 Member {
98 member_id: MemberId,
99 member_name: String,
100 member_email: Option<String>,
101 role: Option<String>,
102 previous_role: Option<String>,
103 },
104 Session {
106 session_id: String,
107 session_title: String,
108 provider: Option<String>,
109 },
110 Comment {
112 comment_id: String,
113 session_id: String,
114 content_preview: Option<String>,
115 },
116 Permission {
118 permission: String,
119 target_id: Option<String>,
120 target_type: Option<String>,
121 },
122 Integration {
124 integration_name: String,
125 integration_type: String,
126 },
127 Generic {
129 message: String,
130 metadata: Option<HashMap<String, String>>,
131 },
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ChangeValue {
137 pub old: Option<String>,
138 pub new: Option<String>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct Notification {
148 pub id: Uuid,
150 pub user_id: MemberId,
152 pub team_id: TeamId,
154 pub notification_type: NotificationType,
156 pub title: String,
158 pub message: String,
160 pub event_id: Option<Uuid>,
162 pub action_url: Option<String>,
164 pub read: bool,
166 pub created_at: DateTime<Utc>,
168 pub read_at: Option<DateTime<Utc>>,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175pub enum NotificationType {
176 Invitation,
178 Mention,
180 SessionShare,
182 Comment,
184 RoleChange,
186 SecurityAlert,
188 Announcement,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct NotificationPreferences {
195 pub user_id: MemberId,
197 pub email_enabled: bool,
199 pub push_enabled: bool,
201 pub in_app_enabled: bool,
203 pub enabled_types: HashMap<NotificationType, bool>,
205 pub quiet_hours_start: Option<u8>,
207 pub quiet_hours_end: Option<u8>,
209}
210
211impl Default for NotificationPreferences {
212 fn default() -> Self {
213 let mut enabled_types = HashMap::new();
214 enabled_types.insert(NotificationType::Invitation, true);
215 enabled_types.insert(NotificationType::Mention, true);
216 enabled_types.insert(NotificationType::SessionShare, true);
217 enabled_types.insert(NotificationType::Comment, true);
218 enabled_types.insert(NotificationType::RoleChange, true);
219 enabled_types.insert(NotificationType::SecurityAlert, true);
220 enabled_types.insert(NotificationType::Announcement, true);
221
222 Self {
223 user_id: Uuid::nil(),
224 email_enabled: true,
225 push_enabled: true,
226 in_app_enabled: true,
227 enabled_types,
228 quiet_hours_start: None,
229 quiet_hours_end: None,
230 }
231 }
232}
233
234pub struct ActivityManager {
240 activities: Arc<RwLock<HashMap<TeamId, VecDeque<ActivityEvent>>>>,
242 max_events_per_team: usize,
244 event_tx: broadcast::Sender<ActivityEvent>,
246 notifications: Arc<RwLock<HashMap<MemberId, Vec<Notification>>>>,
248 preferences: Arc<RwLock<HashMap<MemberId, NotificationPreferences>>>,
250}
251
252impl ActivityManager {
253 pub fn new() -> Self {
255 let (event_tx, _) = broadcast::channel(1000);
256 Self {
257 activities: Arc::new(RwLock::new(HashMap::new())),
258 max_events_per_team: 10000,
259 event_tx,
260 notifications: Arc::new(RwLock::new(HashMap::new())),
261 preferences: Arc::new(RwLock::new(HashMap::new())),
262 }
263 }
264
265 pub async fn record_event(&self, event: ActivityEvent) {
267 let team_id = event.team_id;
268
269 let mut activities = self.activities.write().await;
271 let team_events = activities.entry(team_id).or_insert_with(VecDeque::new);
272
273 if team_events.len() >= self.max_events_per_team {
275 team_events.pop_front();
276 }
277 team_events.push_back(event.clone());
278
279 let _ = self.event_tx.send(event);
281 }
282
283 pub async fn get_activities(
285 &self,
286 team_id: TeamId,
287 limit: usize,
288 offset: usize,
289 ) -> Vec<ActivityEvent> {
290 let activities = self.activities.read().await;
291 activities
292 .get(&team_id)
293 .map(|events| {
294 events
295 .iter()
296 .rev()
297 .skip(offset)
298 .take(limit)
299 .cloned()
300 .collect()
301 })
302 .unwrap_or_default()
303 }
304
305 pub async fn get_activities_by_type(
307 &self,
308 team_id: TeamId,
309 event_type: EventType,
310 limit: usize,
311 ) -> Vec<ActivityEvent> {
312 let activities = self.activities.read().await;
313 activities
314 .get(&team_id)
315 .map(|events| {
316 events
317 .iter()
318 .rev()
319 .filter(|e| e.event_type == event_type)
320 .take(limit)
321 .cloned()
322 .collect()
323 })
324 .unwrap_or_default()
325 }
326
327 pub async fn get_activities_by_actor(
329 &self,
330 team_id: TeamId,
331 actor_id: MemberId,
332 limit: usize,
333 ) -> Vec<ActivityEvent> {
334 let activities = self.activities.read().await;
335 activities
336 .get(&team_id)
337 .map(|events| {
338 events
339 .iter()
340 .rev()
341 .filter(|e| e.actor_id == actor_id)
342 .take(limit)
343 .cloned()
344 .collect()
345 })
346 .unwrap_or_default()
347 }
348
349 pub async fn get_activities_in_range(
351 &self,
352 team_id: TeamId,
353 start: DateTime<Utc>,
354 end: DateTime<Utc>,
355 ) -> Vec<ActivityEvent> {
356 let activities = self.activities.read().await;
357 activities
358 .get(&team_id)
359 .map(|events| {
360 events
361 .iter()
362 .filter(|e| e.timestamp >= start && e.timestamp <= end)
363 .cloned()
364 .collect()
365 })
366 .unwrap_or_default()
367 }
368
369 pub fn subscribe(&self) -> broadcast::Receiver<ActivityEvent> {
371 self.event_tx.subscribe()
372 }
373
374 pub async fn create_notification(&self, notification: Notification) {
376 let user_id = notification.user_id;
377
378 let prefs = self.preferences.read().await;
380 if let Some(user_prefs) = prefs.get(&user_id) {
381 if !user_prefs.in_app_enabled {
382 return;
383 }
384 if let Some(enabled) = user_prefs
385 .enabled_types
386 .get(¬ification.notification_type)
387 {
388 if !enabled {
389 return;
390 }
391 }
392 }
393
394 let mut notifications = self.notifications.write().await;
396 notifications.entry(user_id).or_default().push(notification);
397 }
398
399 pub async fn get_unread_notifications(&self, user_id: MemberId) -> Vec<Notification> {
401 let notifications = self.notifications.read().await;
402 notifications
403 .get(&user_id)
404 .map(|notifs| notifs.iter().filter(|n| !n.read).cloned().collect())
405 .unwrap_or_default()
406 }
407
408 pub async fn get_notifications(
410 &self,
411 user_id: MemberId,
412 limit: usize,
413 offset: usize,
414 ) -> Vec<Notification> {
415 let notifications = self.notifications.read().await;
416 notifications
417 .get(&user_id)
418 .map(|notifs| {
419 notifs
420 .iter()
421 .rev()
422 .skip(offset)
423 .take(limit)
424 .cloned()
425 .collect()
426 })
427 .unwrap_or_default()
428 }
429
430 pub async fn mark_as_read(&self, user_id: MemberId, notification_id: Uuid) {
432 let mut notifications = self.notifications.write().await;
433 if let Some(user_notifs) = notifications.get_mut(&user_id) {
434 if let Some(notif) = user_notifs.iter_mut().find(|n| n.id == notification_id) {
435 notif.read = true;
436 notif.read_at = Some(Utc::now());
437 }
438 }
439 }
440
441 pub async fn mark_all_as_read(&self, user_id: MemberId) {
443 let mut notifications = self.notifications.write().await;
444 if let Some(user_notifs) = notifications.get_mut(&user_id) {
445 let now = Utc::now();
446 for notif in user_notifs.iter_mut() {
447 if !notif.read {
448 notif.read = true;
449 notif.read_at = Some(now);
450 }
451 }
452 }
453 }
454
455 pub async fn delete_notification(&self, user_id: MemberId, notification_id: Uuid) {
457 let mut notifications = self.notifications.write().await;
458 if let Some(user_notifs) = notifications.get_mut(&user_id) {
459 user_notifs.retain(|n| n.id != notification_id);
460 }
461 }
462
463 pub async fn update_preferences(&self, preferences: NotificationPreferences) {
465 self.preferences
466 .write()
467 .await
468 .insert(preferences.user_id, preferences);
469 }
470
471 pub async fn get_preferences(&self, user_id: MemberId) -> NotificationPreferences {
473 self.preferences
474 .read()
475 .await
476 .get(&user_id)
477 .cloned()
478 .unwrap_or_else(|| {
479 let mut prefs = NotificationPreferences::default();
480 prefs.user_id = user_id;
481 prefs
482 })
483 }
484
485 pub async fn get_unread_count(&self, user_id: MemberId) -> usize {
487 let notifications = self.notifications.read().await;
488 notifications
489 .get(&user_id)
490 .map(|notifs| notifs.iter().filter(|n| !n.read).count())
491 .unwrap_or(0)
492 }
493}
494
495impl Default for ActivityManager {
496 fn default() -> Self {
497 Self::new()
498 }
499}
500
501pub fn team_event(
507 team_id: TeamId,
508 actor_id: MemberId,
509 actor_name: String,
510 event_type: EventType,
511 team_name: String,
512 changes: Option<HashMap<String, ChangeValue>>,
513) -> ActivityEvent {
514 ActivityEvent {
515 id: Uuid::new_v4(),
516 team_id,
517 actor_id,
518 actor_name,
519 event_type,
520 details: EventDetails::Team { team_name, changes },
521 timestamp: Utc::now(),
522 ip_address: None,
523 user_agent: None,
524 }
525}
526
527pub fn member_event(
529 team_id: TeamId,
530 actor_id: MemberId,
531 actor_name: String,
532 event_type: EventType,
533 member_id: MemberId,
534 member_name: String,
535 member_email: Option<String>,
536 role: Option<String>,
537 previous_role: Option<String>,
538) -> ActivityEvent {
539 ActivityEvent {
540 id: Uuid::new_v4(),
541 team_id,
542 actor_id,
543 actor_name,
544 event_type,
545 details: EventDetails::Member {
546 member_id,
547 member_name,
548 member_email,
549 role,
550 previous_role,
551 },
552 timestamp: Utc::now(),
553 ip_address: None,
554 user_agent: None,
555 }
556}
557
558pub fn session_event(
560 team_id: TeamId,
561 actor_id: MemberId,
562 actor_name: String,
563 event_type: EventType,
564 session_id: String,
565 session_title: String,
566 provider: Option<String>,
567) -> ActivityEvent {
568 ActivityEvent {
569 id: Uuid::new_v4(),
570 team_id,
571 actor_id,
572 actor_name,
573 event_type,
574 details: EventDetails::Session {
575 session_id,
576 session_title,
577 provider,
578 },
579 timestamp: Utc::now(),
580 ip_address: None,
581 user_agent: None,
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[tokio::test]
590 async fn test_record_and_get_activities() {
591 let manager = ActivityManager::new();
592 let team_id = Uuid::new_v4();
593 let actor_id = Uuid::new_v4();
594
595 let event = team_event(
596 team_id,
597 actor_id,
598 "Test User".to_string(),
599 EventType::TeamCreated,
600 "Test Team".to_string(),
601 None,
602 );
603
604 manager.record_event(event.clone()).await;
605
606 let activities = manager.get_activities(team_id, 10, 0).await;
607 assert_eq!(activities.len(), 1);
608 assert_eq!(activities[0].event_type, EventType::TeamCreated);
609 }
610
611 #[tokio::test]
612 async fn test_notifications() {
613 let manager = ActivityManager::new();
614 let user_id = Uuid::new_v4();
615 let team_id = Uuid::new_v4();
616
617 let notification = Notification {
618 id: Uuid::new_v4(),
619 user_id,
620 team_id,
621 notification_type: NotificationType::Invitation,
622 title: "Team Invitation".to_string(),
623 message: "You have been invited to join a team".to_string(),
624 event_id: None,
625 action_url: None,
626 read: false,
627 created_at: Utc::now(),
628 read_at: None,
629 };
630
631 manager.create_notification(notification.clone()).await;
632
633 let unread = manager.get_unread_notifications(user_id).await;
634 assert_eq!(unread.len(), 1);
635
636 manager.mark_as_read(user_id, notification.id).await;
637
638 let unread = manager.get_unread_notifications(user_id).await;
639 assert_eq!(unread.len(), 0);
640 }
641}