saorsa_core/chat/
mod.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: saorsalabs@gmail.com
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under these licenses is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
14//! Chat system (Slack-like) with channels, threads, and real-time messaging
15//!
16//! Features:
17//! - Public and private channels with threshold access control
18//! - Direct messages with E2E encryption
19//! - Threaded conversations
20//! - Voice/video calls via WebRTC
21//! - Rich media support
22
23use crate::identity::enhanced::{EnhancedIdentity, OrganizationId};
24use crate::storage::{StorageManager, keys, ttl};
25// Removed unused ThresholdGroup import
26use crate::quantum_crypto::types::GroupId;
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29use std::time::SystemTime;
30use thiserror::Error;
31use uuid::Uuid;
32
33/// Chat errors
34#[derive(Debug, Error)]
35pub enum ChatError {
36    #[error("Storage error: {0}")]
37    StorageError(#[from] crate::storage::StorageError),
38
39    #[error("Channel not found: {0}")]
40    ChannelNotFound(String),
41
42    #[error("Permission denied: {0}")]
43    PermissionDenied(String),
44
45    #[error("Invalid operation: {0}")]
46    InvalidOperation(String),
47}
48
49type Result<T> = std::result::Result<T, ChatError>;
50
51/// Channel identifier
52#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub struct ChannelId(pub String);
54
55impl Default for ChannelId {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl ChannelId {
62    /// Generate new channel ID
63    pub fn new() -> Self {
64        Self(Uuid::new_v4().to_string())
65    }
66}
67
68/// Message identifier
69#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
70pub struct MessageId(pub String);
71
72impl Default for MessageId {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl MessageId {
79    /// Generate new message ID
80    pub fn new() -> Self {
81        Self(Uuid::new_v4().to_string())
82    }
83}
84
85/// Thread identifier
86#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
87pub struct ThreadId(pub String);
88
89/// User ID type
90pub type UserId = String;
91
92/// Channel type
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub enum ChannelType {
95    /// Public channel - anyone in org can join
96    Public,
97
98    /// Private channel - controlled by threshold group
99    Private {
100        access_group: GroupId,
101        visibility: ChannelVisibility,
102    },
103
104    /// Direct message between users
105    Direct { participants: Vec<UserId> },
106
107    /// Group DM with multiple users
108    GroupDirect {
109        participants: Vec<UserId>,
110        name: Option<String>,
111    },
112}
113
114/// Channel visibility for private channels
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub enum ChannelVisibility {
117    /// Visible to all, but join requires approval
118    Listed,
119
120    /// Not visible unless member
121    Hidden,
122
123    /// Invite-only
124    Secret,
125}
126
127/// Channel information
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct Channel {
130    pub id: ChannelId,
131    pub name: String,
132    pub description: String,
133    pub channel_type: ChannelType,
134    pub organization_id: Option<OrganizationId>,
135    pub created_by: UserId,
136    pub created_at: SystemTime,
137    pub members: Vec<ChannelMember>,
138    pub settings: ChannelSettings,
139    pub metadata: HashMap<String, String>,
140}
141
142/// Channel member
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct ChannelMember {
145    pub user_id: UserId,
146    pub role: ChannelRole,
147    pub joined_at: SystemTime,
148    pub last_read: Option<MessageId>,
149    pub notifications: NotificationSettings,
150}
151
152/// Channel role
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
154pub enum ChannelRole {
155    Owner,
156    Admin,
157    Moderator,
158    Member,
159    Guest,
160}
161
162/// Notification settings
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct NotificationSettings {
165    pub all_messages: bool,
166    pub mentions_only: bool,
167    pub muted: bool,
168    pub muted_until: Option<SystemTime>,
169}
170
171impl Default for NotificationSettings {
172    fn default() -> Self {
173        Self {
174            all_messages: true,
175            mentions_only: false,
176            muted: false,
177            muted_until: None,
178        }
179    }
180}
181
182/// Channel settings
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct ChannelSettings {
185    pub allow_threads: bool,
186    pub allow_reactions: bool,
187    pub allow_files: bool,
188    pub allow_voice_video: bool,
189    pub message_retention_days: Option<u32>,
190    pub max_message_length: usize,
191    pub slow_mode_seconds: Option<u32>,
192}
193
194impl Default for ChannelSettings {
195    fn default() -> Self {
196        Self {
197            allow_threads: true,
198            allow_reactions: true,
199            allow_files: true,
200            allow_voice_video: true,
201            message_retention_days: Some(90),
202            max_message_length: 4000,
203            slow_mode_seconds: None,
204        }
205    }
206}
207
208/// Message content
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct Message {
211    pub id: MessageId,
212    pub channel_id: ChannelId,
213    pub thread_id: Option<ThreadId>,
214    pub author: UserId,
215    pub content: MessageContent,
216    pub created_at: SystemTime,
217    pub edited_at: Option<SystemTime>,
218    pub deleted_at: Option<SystemTime>,
219    pub reactions: Vec<Reaction>,
220    pub mentions: Vec<Mention>,
221    pub attachments: Vec<Attachment>,
222    pub reply_to: Option<MessageId>,
223}
224
225/// Message content type
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub enum MessageContent {
228    /// Plain text message
229    Text(String),
230
231    /// Rich text with formatting
232    RichText {
233        text: String,
234        formatting: Vec<TextFormat>,
235    },
236
237    /// System message (join/leave/etc)
238    System(SystemMessage),
239
240    /// Encrypted content (for DMs)
241    Encrypted {
242        ciphertext: Vec<u8>,
243        algorithm: String,
244    },
245}
246
247/// Text formatting
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct TextFormat {
250    pub start: usize,
251    pub end: usize,
252    pub format_type: FormatType,
253}
254
255/// Format type
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub enum FormatType {
258    Bold,
259    Italic,
260    Code,
261    Strike,
262    Link(String),
263    Mention(UserId),
264    ChannelRef(ChannelId),
265}
266
267/// System message type
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub enum SystemMessage {
270    UserJoined(UserId),
271    UserLeft(UserId),
272    UserInvited {
273        inviter: UserId,
274        invitee: UserId,
275    },
276    ChannelRenamed {
277        old_name: String,
278        new_name: String,
279    },
280    ChannelDescriptionChanged,
281    CallStarted {
282        call_id: String,
283    },
284    CallEnded {
285        call_id: String,
286        duration_seconds: u64,
287    },
288}
289
290/// Reaction to a message
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct Reaction {
293    pub emoji: String,
294    pub users: Vec<UserId>,
295}
296
297/// Mention in a message
298#[derive(Debug, Clone, Serialize, Deserialize)]
299pub enum Mention {
300    User(UserId),
301    Channel,
302    Here,
303    Everyone,
304}
305
306/// File attachment
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct Attachment {
309    pub id: String,
310    pub name: String,
311    pub mime_type: String,
312    pub size: u64,
313    pub url: String,
314    pub thumbnail_url: Option<String>,
315    pub metadata: AttachmentMetadata,
316}
317
318/// Attachment metadata
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub enum AttachmentMetadata {
321    Image {
322        width: u32,
323        height: u32,
324    },
325    Video {
326        duration_seconds: u64,
327        width: u32,
328        height: u32,
329    },
330    Audio {
331        duration_seconds: u64,
332    },
333    Document {
334        page_count: Option<u32>,
335    },
336    Other,
337}
338
339/// Thread information
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct Thread {
342    pub id: ThreadId,
343    pub channel_id: ChannelId,
344    pub parent_message_id: MessageId,
345    pub reply_count: u32,
346    pub participant_count: u32,
347    pub last_reply_at: Option<SystemTime>,
348    pub participants: Vec<UserId>,
349}
350
351/// Voice/video call information
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct Call {
354    pub id: String,
355    pub channel_id: ChannelId,
356    pub call_type: CallType,
357    pub started_by: UserId,
358    pub started_at: SystemTime,
359    pub ended_at: Option<SystemTime>,
360    pub participants: Vec<CallParticipant>,
361    pub recording_url: Option<String>,
362}
363
364/// Call type
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub enum CallType {
367    Voice,
368    Video,
369    ScreenShare,
370}
371
372/// Call participant
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct CallParticipant {
375    pub user_id: UserId,
376    pub joined_at: SystemTime,
377    pub left_at: Option<SystemTime>,
378    pub is_muted: bool,
379    pub is_video_on: bool,
380    pub is_screen_sharing: bool,
381}
382
383/// Chat manager
384pub struct ChatManager {
385    storage: StorageManager,
386    identity: EnhancedIdentity,
387}
388
389impl ChatManager {
390    /// Create new chat manager
391    pub fn new(storage: StorageManager, identity: EnhancedIdentity) -> Self {
392        Self { storage, identity }
393    }
394
395    /// Create a new channel
396    pub async fn create_channel(
397        &mut self,
398        name: String,
399        description: String,
400        channel_type: ChannelType,
401        organization_id: Option<OrganizationId>,
402    ) -> Result<Channel> {
403        // Check permissions
404        if let Some(_org_id) = &organization_id {
405            // Verify user has permission to create channels in org
406            // TODO: Implement permission check
407        }
408
409        let channel = Channel {
410            id: ChannelId::new(),
411            name,
412            description,
413            channel_type,
414            organization_id,
415            created_by: self.identity.base_identity.user_id.clone(),
416            created_at: SystemTime::now(),
417            members: vec![ChannelMember {
418                user_id: self.identity.base_identity.user_id.clone(),
419                role: ChannelRole::Owner,
420                joined_at: SystemTime::now(),
421                last_read: None,
422                notifications: NotificationSettings::default(),
423            }],
424            settings: ChannelSettings::default(),
425            metadata: HashMap::new(),
426        };
427
428        // Store channel in DHT
429        let key = keys::chat_channel(&channel.id.0);
430        self.storage
431            .store_encrypted(&key, &channel, ttl::PROFILE, None)
432            .await?;
433
434        // Add to user's channel list
435        self.add_user_channel(&channel.id).await?;
436
437        // If public, add to public channel list
438        if matches!(channel.channel_type, ChannelType::Public) {
439            self.add_public_channel(&channel.id, &channel.name).await?;
440        }
441
442        Ok(channel)
443    }
444
445    /// Send a message
446    pub async fn send_message(
447        &mut self,
448        channel_id: &ChannelId,
449        content: MessageContent,
450        thread_id: Option<ThreadId>,
451        attachments: Vec<Attachment>,
452    ) -> Result<Message> {
453        // Verify user is member of channel
454        let channel = self.get_channel(channel_id).await?;
455        if !channel
456            .members
457            .iter()
458            .any(|m| m.user_id == self.identity.base_identity.user_id)
459        {
460            return Err(ChatError::PermissionDenied(
461                "Not a member of channel".to_string(),
462            ));
463        }
464
465        let message = Message {
466            id: MessageId::new(),
467            channel_id: channel_id.clone(),
468            thread_id,
469            author: self.identity.base_identity.user_id.clone(),
470            content,
471            created_at: SystemTime::now(),
472            edited_at: None,
473            deleted_at: None,
474            reactions: vec![],
475            mentions: vec![], // TODO: Extract mentions from content
476            attachments,
477            reply_to: None,
478        };
479
480        // Store message
481        let msg_key = keys::chat_message(&channel_id.0, &message.id.0);
482        self.storage
483            .store_encrypted(&msg_key, &message, ttl::MESSAGE, None)
484            .await?;
485
486        // Add to message index for pagination
487        let timestamp = message
488            .created_at
489            .duration_since(SystemTime::UNIX_EPOCH)
490            .map_err(|e| ChatError::InvalidOperation(format!("Invalid message timestamp: {}", e)))?
491            .as_secs();
492        let index_key = keys::chat_index(&channel_id.0, timestamp);
493        self.storage
494            .store_encrypted(&index_key, &message.id, ttl::MESSAGE, None)
495            .await?;
496
497        Ok(message)
498    }
499
500    /// Get channel by ID
501    pub async fn get_channel(&self, channel_id: &ChannelId) -> Result<Channel> {
502        let key = keys::chat_channel(&channel_id.0);
503        self.storage
504            .get_encrypted(&key)
505            .await
506            .map_err(|_| ChatError::ChannelNotFound(channel_id.0.clone()))
507    }
508
509    /// Get user's channels
510    pub async fn get_user_channels(&self) -> Result<Vec<ChannelId>> {
511        let key = keys::user_channels(&self.identity.base_identity.user_id);
512        Ok(self
513            .storage
514            .get_encrypted(&key)
515            .await
516            .unwrap_or_else(|_| vec![]))
517    }
518
519    /// Add channel to user's list
520    async fn add_user_channel(&mut self, channel_id: &ChannelId) -> Result<()> {
521        let mut channels = self.get_user_channels().await.unwrap_or_default();
522        if !channels.contains(channel_id) {
523            channels.push(channel_id.clone());
524
525            let key = keys::user_channels(&self.identity.base_identity.user_id);
526            self.storage
527                .store_encrypted(&key, &channels, ttl::PROFILE, None)
528                .await?;
529        }
530        Ok(())
531    }
532
533    /// Add channel to public list
534    async fn add_public_channel(&mut self, channel_id: &ChannelId, name: &str) -> Result<()> {
535        let key = keys::public_channel_list();
536        let mut public_channels: HashMap<String, String> =
537            self.storage.get_public(&key).await.unwrap_or_default();
538
539        public_channels.insert(channel_id.0.clone(), name.to_string());
540
541        self.storage
542            .store_public(&key, &public_channels, ttl::PROFILE)
543            .await?;
544
545        Ok(())
546    }
547
548    /// Create a thread
549    pub async fn create_thread(
550        &mut self,
551        channel_id: &ChannelId,
552        parent_message_id: &MessageId,
553    ) -> Result<Thread> {
554        let thread = Thread {
555            id: ThreadId(Uuid::new_v4().to_string()),
556            channel_id: channel_id.clone(),
557            parent_message_id: parent_message_id.clone(),
558            reply_count: 0,
559            participant_count: 1,
560            last_reply_at: None,
561            participants: vec![self.identity.base_identity.user_id.clone()],
562        };
563
564        // Store thread (in practice, would be part of message structure)
565        Ok(thread)
566    }
567
568    /// Add reaction to message
569    pub async fn add_reaction(
570        &mut self,
571        channel_id: &ChannelId,
572        message_id: &MessageId,
573        emoji: String,
574    ) -> Result<()> {
575        let msg_key = keys::chat_message(&channel_id.0, &message_id.0);
576        let mut message: Message = self.storage.get_encrypted(&msg_key).await?;
577
578        // Find or create reaction
579        let user_id = &self.identity.base_identity.user_id;
580        if let Some(reaction) = message.reactions.iter_mut().find(|r| r.emoji == emoji) {
581            if !reaction.users.contains(user_id) {
582                reaction.users.push(user_id.clone());
583            }
584        } else {
585            message.reactions.push(Reaction {
586                emoji,
587                users: vec![user_id.clone()],
588            });
589        }
590
591        // Store updated message
592        self.storage
593            .store_encrypted(&msg_key, &message, ttl::MESSAGE, None)
594            .await?;
595
596        Ok(())
597    }
598}