ricecoder_sessions/
share.rs

1//! Session sharing functionality
2
3use chrono::{DateTime, Duration, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8use crate::{Session, SessionError, SessionResult};
9
10/// Service for managing session sharing
11#[derive(Debug, Clone)]
12pub struct ShareService {
13    /// In-memory store of active shares
14    shares: std::sync::Arc<std::sync::Mutex<HashMap<String, SessionShare>>>,
15}
16
17impl ShareService {
18    /// Create a new share service
19    pub fn new() -> Self {
20        Self {
21            shares: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
22        }
23    }
24
25    /// Generate a share link for a session with optional expiration
26    pub fn generate_share_link(
27        &self,
28        session_id: &str,
29        permissions: SharePermissions,
30        expires_in: Option<Duration>,
31    ) -> SessionResult<SessionShare> {
32        let share_id = Uuid::new_v4().to_string();
33        let now = Utc::now();
34        let expires_at = expires_in.map(|duration| now + duration);
35
36        let share = SessionShare {
37            id: share_id.clone(),
38            session_id: session_id.to_string(),
39            created_at: now,
40            expires_at,
41            permissions,
42        };
43
44        // Store the share
45        let mut shares = self
46            .shares
47            .lock()
48            .map_err(|e| SessionError::StorageError(format!("Failed to lock shares: {}", e)))?;
49        shares.insert(share_id.clone(), share.clone());
50
51        Ok(share)
52    }
53
54    /// Get a share by ID, checking expiration
55    pub fn get_share(&self, share_id: &str) -> SessionResult<SessionShare> {
56        let shares = self
57            .shares
58            .lock()
59            .map_err(|e| SessionError::StorageError(format!("Failed to lock shares: {}", e)))?;
60
61        let share = shares
62            .get(share_id)
63            .ok_or_else(|| SessionError::ShareNotFound(share_id.to_string()))?;
64
65        // Check if share has expired
66        if let Some(expires_at) = share.expires_at {
67            if Utc::now() > expires_at {
68                return Err(SessionError::ShareExpired(share_id.to_string()));
69            }
70        }
71
72        Ok(share.clone())
73    }
74
75    /// Create a read-only view of a session based on share permissions
76    pub fn create_shared_session_view(
77        &self,
78        session: &Session,
79        permissions: &SharePermissions,
80    ) -> Session {
81        let mut shared_session = session.clone();
82
83        // Apply permissions
84        if !permissions.include_history {
85            shared_session.history.clear();
86        }
87
88        if !permissions.include_context {
89            shared_session.context.files.clear();
90            shared_session.context.custom.clear();
91        }
92
93        shared_session
94    }
95
96    /// Import a shared session, creating a new local session
97    pub fn import_shared_session(
98        &self,
99        share_id: &str,
100        shared_session: &Session,
101    ) -> SessionResult<Session> {
102        // Verify the share exists and hasn't expired
103        let _share = self.get_share(share_id)?;
104
105        // Create a new local session with the shared session's data
106        let mut imported_session = shared_session.clone();
107
108        // Generate a new ID for the imported session
109        imported_session.id = Uuid::new_v4().to_string();
110
111        // Update timestamps
112        let now = Utc::now();
113        imported_session.created_at = now;
114        imported_session.updated_at = now;
115
116        Ok(imported_session)
117    }
118
119    /// Revoke a share by ID
120    pub fn revoke_share(&self, share_id: &str) -> SessionResult<()> {
121        let mut shares = self
122            .shares
123            .lock()
124            .map_err(|e| SessionError::StorageError(format!("Failed to lock shares: {}", e)))?;
125
126        shares
127            .remove(share_id)
128            .ok_or_else(|| SessionError::ShareNotFound(share_id.to_string()))?;
129
130        Ok(())
131    }
132
133    /// List all active shares
134    pub fn list_shares(&self) -> SessionResult<Vec<SessionShare>> {
135        let shares = self
136            .shares
137            .lock()
138            .map_err(|e| SessionError::StorageError(format!("Failed to lock shares: {}", e)))?;
139
140        let now = Utc::now();
141        let active_shares: Vec<SessionShare> = shares
142            .values()
143            .filter(|share| {
144                // Include only non-expired shares
145                share.expires_at.is_none() || share.expires_at.is_some_and(|exp| now <= exp)
146            })
147            .cloned()
148            .collect();
149
150        Ok(active_shares)
151    }
152
153    /// Clean up expired shares
154    pub fn cleanup_expired_shares(&self) -> SessionResult<usize> {
155        let mut shares = self
156            .shares
157            .lock()
158            .map_err(|e| SessionError::StorageError(format!("Failed to lock shares: {}", e)))?;
159
160        let now = Utc::now();
161        let initial_count = shares.len();
162
163        shares.retain(|_, share| {
164            share.expires_at.is_none() || share.expires_at.is_some_and(|exp| now <= exp)
165        });
166
167        Ok(initial_count - shares.len())
168    }
169
170    /// List all active shares for a specific session
171    pub fn list_shares_for_session(&self, session_id: &str) -> SessionResult<Vec<SessionShare>> {
172        let shares = self
173            .shares
174            .lock()
175            .map_err(|e| SessionError::StorageError(format!("Failed to lock shares: {}", e)))?;
176
177        let now = Utc::now();
178        let session_shares: Vec<SessionShare> = shares
179            .values()
180            .filter(|share| {
181                // Filter by session_id and include only non-expired shares
182                share.session_id == session_id
183                    && (share.expires_at.is_none()
184                        || share.expires_at.is_some_and(|exp| now <= exp))
185            })
186            .cloned()
187            .collect();
188
189        Ok(session_shares)
190    }
191
192    /// Invalidate all shares for a session when the session is deleted
193    pub fn invalidate_session_shares(&self, session_id: &str) -> SessionResult<usize> {
194        let mut shares = self
195            .shares
196            .lock()
197            .map_err(|e| SessionError::StorageError(format!("Failed to lock shares: {}", e)))?;
198
199        let initial_count = shares.len();
200
201        // Remove all shares associated with this session
202        shares.retain(|_, share| share.session_id != session_id);
203
204        Ok(initial_count - shares.len())
205    }
206}
207
208impl Default for ShareService {
209    fn default() -> Self {
210        Self::new()
211    }
212}
213
214/// A shared session
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct SessionShare {
217    /// Unique identifier for the share
218    pub id: String,
219    /// ID of the shared session
220    pub session_id: String,
221    /// When the share was created
222    pub created_at: DateTime<Utc>,
223    /// When the share expires (if applicable)
224    pub expires_at: Option<DateTime<Utc>>,
225    /// Share permissions
226    pub permissions: SharePermissions,
227}
228
229/// Permissions for a shared session
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct SharePermissions {
232    /// Whether the share is read-only
233    pub read_only: bool,
234    /// Whether to include conversation history
235    pub include_history: bool,
236    /// Whether to include session context
237    pub include_context: bool,
238}