Skip to main content

chasm/teams/
activity.rs

1// Copyright (c) 2024-2027 Nervosys LLC
2// SPDX-License-Identifier: AGPL-3.0-only
3//! Team activity feed module
4//!
5//! Tracks and provides team activities, notifications, and audit trail.
6
7use 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// ============================================================================
17// Activity Types
18// ============================================================================
19
20/// Activity event that occurred in a team
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ActivityEvent {
23    /// Unique event ID
24    pub id: Uuid,
25    /// Team ID
26    pub team_id: TeamId,
27    /// Actor who performed the action
28    pub actor_id: MemberId,
29    /// Actor display name
30    pub actor_name: String,
31    /// Event type
32    pub event_type: EventType,
33    /// Event details
34    pub details: EventDetails,
35    /// Event timestamp
36    pub timestamp: DateTime<Utc>,
37    /// IP address (for audit)
38    pub ip_address: Option<String>,
39    /// User agent (for audit)
40    pub user_agent: Option<String>,
41}
42
43/// Type of activity event
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum EventType {
47    // Team events
48    TeamCreated,
49    TeamUpdated,
50    TeamDeleted,
51    SettingsChanged,
52
53    // Member events
54    MemberInvited,
55    MemberJoined,
56    MemberLeft,
57    MemberRemoved,
58    RoleChanged,
59
60    // Session events
61    SessionCreated,
62    SessionUpdated,
63    SessionDeleted,
64    SessionShared,
65    SessionArchived,
66    SessionExported,
67
68    // Collaboration events
69    CommentAdded,
70    CommentEdited,
71    CommentDeleted,
72    AnnotationAdded,
73
74    // Security events
75    PermissionGranted,
76    PermissionRevoked,
77    AccessDenied,
78    SuspiciousActivity,
79
80    // Integration events
81    WebhookTriggered,
82    IntegrationConnected,
83    IntegrationDisconnected,
84    HarvestCompleted,
85}
86
87/// Detailed information about the event
88#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(tag = "type", rename_all = "snake_case")]
90pub enum EventDetails {
91    /// Team-related details
92    Team {
93        team_name: String,
94        changes: Option<HashMap<String, ChangeValue>>,
95    },
96    /// Member-related details
97    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-related details
105    Session {
106        session_id: String,
107        session_title: String,
108        provider: Option<String>,
109    },
110    /// Comment-related details
111    Comment {
112        comment_id: String,
113        session_id: String,
114        content_preview: Option<String>,
115    },
116    /// Permission-related details
117    Permission {
118        permission: String,
119        target_id: Option<String>,
120        target_type: Option<String>,
121    },
122    /// Integration-related details
123    Integration {
124        integration_name: String,
125        integration_type: String,
126    },
127    /// Generic details
128    Generic {
129        message: String,
130        metadata: Option<HashMap<String, String>>,
131    },
132}
133
134/// Value change for audit trail
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ChangeValue {
137    pub old: Option<String>,
138    pub new: Option<String>,
139}
140
141// ============================================================================
142// Notifications
143// ============================================================================
144
145/// Notification for a user
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct Notification {
148    /// Notification ID
149    pub id: Uuid,
150    /// Recipient user ID
151    pub user_id: MemberId,
152    /// Team ID
153    pub team_id: TeamId,
154    /// Notification type
155    pub notification_type: NotificationType,
156    /// Notification title
157    pub title: String,
158    /// Notification message
159    pub message: String,
160    /// Related event ID
161    pub event_id: Option<Uuid>,
162    /// Action URL
163    pub action_url: Option<String>,
164    /// Read status
165    pub read: bool,
166    /// Creation timestamp
167    pub created_at: DateTime<Utc>,
168    /// Read timestamp
169    pub read_at: Option<DateTime<Utc>>,
170}
171
172/// Type of notification
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
174#[serde(rename_all = "snake_case")]
175pub enum NotificationType {
176    /// Team invitation
177    Invitation,
178    /// Mention in comment
179    Mention,
180    /// Session shared with user
181    SessionShare,
182    /// Comment on user's session
183    Comment,
184    /// Role changed
185    RoleChange,
186    /// Security alert
187    SecurityAlert,
188    /// System announcement
189    Announcement,
190}
191
192/// Notification preferences
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct NotificationPreferences {
195    /// User ID
196    pub user_id: MemberId,
197    /// Email notifications enabled
198    pub email_enabled: bool,
199    /// Push notifications enabled
200    pub push_enabled: bool,
201    /// In-app notifications enabled
202    pub in_app_enabled: bool,
203    /// Notification types to receive
204    pub enabled_types: HashMap<NotificationType, bool>,
205    /// Quiet hours start (24h format)
206    pub quiet_hours_start: Option<u8>,
207    /// Quiet hours end (24h format)
208    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
234// ============================================================================
235// Activity Manager
236// ============================================================================
237
238/// Manager for team activity tracking
239pub struct ActivityManager {
240    /// Activity events by team (ring buffer per team)
241    activities: Arc<RwLock<HashMap<TeamId, VecDeque<ActivityEvent>>>>,
242    /// Maximum events per team
243    max_events_per_team: usize,
244    /// Event broadcaster
245    event_tx: broadcast::Sender<ActivityEvent>,
246    /// User notifications
247    notifications: Arc<RwLock<HashMap<MemberId, Vec<Notification>>>>,
248    /// Notification preferences
249    preferences: Arc<RwLock<HashMap<MemberId, NotificationPreferences>>>,
250}
251
252impl ActivityManager {
253    /// Create a new activity manager
254    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    /// Record an activity event
266    pub async fn record_event(&self, event: ActivityEvent) {
267        let team_id = event.team_id;
268
269        // Store event
270        let mut activities = self.activities.write().await;
271        let team_events = activities.entry(team_id).or_insert_with(VecDeque::new);
272
273        // Maintain ring buffer
274        if team_events.len() >= self.max_events_per_team {
275            team_events.pop_front();
276        }
277        team_events.push_back(event.clone());
278
279        // Broadcast event
280        let _ = self.event_tx.send(event);
281    }
282
283    /// Get recent activities for a team
284    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    /// Get activities by type
306    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    /// Get activities by actor
328    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    /// Get activities in a time range
350    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    /// Subscribe to activity events
370    pub fn subscribe(&self) -> broadcast::Receiver<ActivityEvent> {
371        self.event_tx.subscribe()
372    }
373
374    /// Create a notification for a user
375    pub async fn create_notification(&self, notification: Notification) {
376        let user_id = notification.user_id;
377
378        // Check preferences
379        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(&notification.notification_type)
387            {
388                if !enabled {
389                    return;
390                }
391            }
392        }
393
394        // Store notification
395        let mut notifications = self.notifications.write().await;
396        notifications.entry(user_id).or_default().push(notification);
397    }
398
399    /// Get unread notifications for a user
400    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    /// Get all notifications for a user
409    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    /// Mark notification as read
431    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    /// Mark all notifications as read for a user
442    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    /// Delete a notification
456    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    /// Update notification preferences
464    pub async fn update_preferences(&self, preferences: NotificationPreferences) {
465        self.preferences
466            .write()
467            .await
468            .insert(preferences.user_id, preferences);
469    }
470
471    /// Get notification preferences for a user
472    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    /// Get unread notification count for a user
486    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
501// ============================================================================
502// Helper Functions
503// ============================================================================
504
505/// Create a team activity event
506pub 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
527/// Create a member activity event
528pub 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
558/// Create a session activity event
559pub 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}