Skip to main content

communitas_core/
core_context.rs

1// Copyright (c) 2025 Saorsa Labs Limited
2//
3// Dual-licensed under the AGPL-3.0-or-later and a commercial license.
4// You may use this file under the terms of the GNU Affero General Public License v3.0 or later.
5// For commercial licensing, contact: saorsalabs@gmail.com
6//
7// See the LICENSE-AGPL-3.0 and LICENSE-COMMERCIAL.md files for details.
8
9//! Core Context - Centralized application state for Communitas (RC1b architecture)
10//!
11//! This module provides the main application context that coordinates:
12//! - User identity and profiles
13//! - Gossip networking layer
14//! - Message synchronization
15//! - Storage and persistence
16//!
17//! **Architecture Note**: This is the RC1b implementation that replaces the old
18//! saorsa_core-based CoreContext with a simpler saorsa-gossip + four-word-networking
19//! based architecture.
20
21use crate::disk_service::EntityDiskService;
22use crate::keystore::Keystore;
23use crate::message_sync::MessageSyncService;
24use crate::types::{DeviceType, UserProfile};
25use crate::webrtc::CommunitasWebRtcService;
26use blake3;
27use communitas_kanban::KanbanService;
28use fips204::traits::{SerDes, Signer, Verifier};
29use rand::rngs::OsRng;
30use saorsa_pqc::ml_dsa_87::{PrivateKey, PublicKey, try_keygen_with_rng};
31use std::collections::HashMap;
32use std::net::SocketAddr;
33use std::path::PathBuf;
34use std::sync::Arc;
35use tracing::{info, warn};
36
37/// Centralized context for the Communitas application
38///
39/// This replaces the old saorsa_core-based CoreContext with a simpler architecture
40/// based on saorsa-gossip for networking and four-word-networking for identities.
41///
42/// **Lifecycle**:
43/// 1. Initialize with user profile (four-word ID, display name, device)
44/// 2. Start gossip networking (optional, can run in local mode)
45/// 3. Initialize message sync service
46/// 4. Ready for operations
47pub struct CoreContext {
48    /// User profile with identity and device info
49    pub profile: UserProfile,
50
51    /// ML-DSA-87 post-quantum signing key (kept in memory for session)
52    pub signing_key: PrivateKey,
53
54    /// ML-DSA-87 post-quantum public key
55    pub public_key: PublicKey,
56
57    /// Four-word user identity (derived from public key)
58    pub four_words: String,
59
60    /// Display name for this user
61    pub display_name: String,
62
63    /// Device name for this instance
64    pub device_name: String,
65
66    /// CRDT manager for persistent document storage
67    pub crdt_manager: Arc<crate::CrdtManager>,
68
69    /// Entity service for managing groups, channels, and members
70    pub entity_service: Arc<crate::EntityService>,
71
72    /// Message service for unified messaging operations
73    pub message_service: Arc<crate::MessageService>,
74
75    /// Message synchronization service (CRDT-based) - legacy, use message_service instead
76    pub message_sync: Arc<MessageSyncService>,
77
78    /// Document replicator for collaborative editing (CRDT-based, Yrs)
79    /// Handles dual-storage: Files (encrypted) + Web (public)
80    pub doc_replicator: Arc<crate::doc_replicator::DocReplicator>,
81
82    /// Current listen address (if networking is active)
83    pub listen_address: Option<SocketAddr>,
84
85    /// Connection identity (four-word encoded listen address)
86    pub connection_identity: Option<String>,
87
88    /// External/public address (NAT-reflected address for WAN connectivity)
89    /// This is the address that other peers on the internet see us at,
90    /// obtained via address reflection from a coordinator/bootstrap node
91    pub external_address: Option<SocketAddr>,
92
93    /// Gossip overlay system (replaces DHT-based networking)
94    /// Handles P2P networking, membership, pubsub, presence, and discovery
95    pub gossip: Option<Arc<crate::gossip::GossipContext>>,
96
97    /// Group keys for channels/projects/orgs (MLS-based)
98    /// Maps group ID to group keypair
99    pub group_keys: HashMap<String, GroupKeyPairPlaceholder>,
100
101    /// Per-entity virtual disk service (Private, Public, Shared disks)
102    pub disk_service: Arc<EntityDiskService>,
103
104    /// WebRTC service for voice, video, and screen sharing
105    /// Initialized when networking starts (requires gossip context)
106    pub webrtc: Option<Arc<CommunitasWebRtcService>>,
107
108    /// Kanban service for project management boards
109    /// CRDT-based, offline-first collaborative Kanban system
110    pub kanban_service: Arc<KanbanService>,
111
112    /// Invite service for cross-organization collaboration
113    /// Handles four-word invite creation, acceptance, rejection, and revocation
114    pub invite_service: Arc<crate::invite_service::InviteService>,
115}
116
117impl std::fmt::Debug for CoreContext {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("CoreContext")
120            .field("profile", &self.profile)
121            .field("four_words", &self.four_words)
122            .field("display_name", &self.display_name)
123            .field("device_name", &self.device_name)
124            .field("crdt_manager", &"<active>")
125            .field("entity_service", &"<active>")
126            .field("message_service", &"<active>")
127            .field("listen_address", &self.listen_address)
128            .field("connection_identity", &self.connection_identity)
129            .field("external_address", &self.external_address)
130            .field("signing_key", &"<redacted>")
131            .field("public_key", &"<key_bytes>")
132            .field("doc_replicator", &"<active>")
133            .field("gossip", &self.gossip.as_ref().map(|_| "<active>"))
134            .field("group_keys", &self.group_keys)
135            .field("disk_service", &"<active>")
136            .field("webrtc", &self.webrtc.as_ref().map(|_| "<active>"))
137            .field("kanban_service", &"<active>")
138            .field("invite_service", &"<active>")
139            .finish()
140    }
141}
142
143/// Placeholder for group keypairs (will use MLS in future)
144#[derive(Debug, Clone)]
145pub struct GroupKeyPairPlaceholder {
146    pub group_id: String,
147    // Will be replaced with actual MLS group key material
148}
149
150impl CoreContext {
151    /// Initialize a new CoreContext from a four-word identity
152    ///
153    /// This creates a new profile with:
154    /// - Generated Ed25519 keypair
155    /// - Four-word user identity derived from public key
156    /// - Local storage directory
157    ///
158    /// **Note**: This does NOT start networking. Call `start_networking()` separately.
159    ///
160    /// # Arguments
161    /// * `four_words` - Four-word user identity (e.g., "ocean-forest-moon-star")
162    /// * `display_name` - Human-readable display name
163    /// * `device_name` - Device identifier for this instance
164    /// * `device_type` - Device type classification
165    /// * `storage_dir` - Directory for profile storage
166    ///
167    /// # Returns
168    /// New CoreContext instance
169    ///
170    /// # Errors
171    /// Returns error if:
172    /// - Four-word format is invalid
173    /// - Storage directory cannot be created
174    /// - Message sync initialization fails
175    pub async fn initialize(
176        four_words: String,
177        display_name: String,
178        device_name: String,
179        device_type: DeviceType,
180        storage_dir: PathBuf,
181    ) -> Result<Self, String> {
182        // Validate four-word format
183        let words: Vec<&str> = four_words.split('-').collect();
184        if words.len() != 4 {
185            return Err(format!(
186                "Invalid four-word format: expected 4 words, got {}",
187                words.len()
188            ));
189        }
190
191        // Initialize keystore for secure key management
192        let keystore = Keystore::new();
193
194        // Use four-word identity as hex identifier for keystore lookups
195        let id_hex = blake3::hash(four_words.as_bytes()).to_hex().to_string();
196
197        // Try to load existing ML-DSA keys from secure keystore
198        let (public_key, signing_key) = match keystore.load_mldsa_keys(&id_hex) {
199            Ok((pk_bytes, sk_bytes)) => {
200                info!(
201                    "Loaded existing ML-DSA-87 keypair for identity '{}' from keystore",
202                    four_words
203                );
204
205                // Deserialize keys from stored bytes
206                let public_key = PublicKey::try_from_bytes(
207                    pk_bytes
208                        .as_slice()
209                        .try_into()
210                        .map_err(|_| "Invalid public key length in keystore".to_string())?,
211                )
212                .map_err(|e| format!("Failed to deserialize public key: {}", e))?;
213
214                let signing_key = PrivateKey::try_from_bytes(
215                    sk_bytes
216                        .as_slice()
217                        .try_into()
218                        .map_err(|_| "Invalid signing key length in keystore".to_string())?,
219                )
220                .map_err(|e| format!("Failed to deserialize signing key: {}", e))?;
221
222                (public_key, signing_key)
223            }
224            Err(_) => {
225                // No existing keys - generate new ones using cryptographically secure RNG
226                info!(
227                    "Generating new ML-DSA-87 keypair for identity '{}' using CSPRNG",
228                    four_words
229                );
230
231                let mut rng = OsRng;
232                let (public_key, signing_key) = try_keygen_with_rng(&mut rng)
233                    .map_err(|e| format!("Failed to generate ML-DSA-87 keypair: {}", e))?;
234
235                // Store keys securely in platform keychain
236                let pk_bytes = public_key.clone().into_bytes();
237                let sk_bytes = signing_key.clone().into_bytes();
238
239                keystore
240                    .save_mldsa_keys(&id_hex, &pk_bytes, &sk_bytes)
241                    .map_err(|e| {
242                        warn!(
243                            "Failed to save keys to keystore: {}. Keys will not persist.",
244                            e
245                        );
246                        e
247                    })?;
248
249                info!("Saved ML-DSA-87 keypair to secure keystore (Level 5 PQC security)");
250
251                (public_key, signing_key)
252            }
253        };
254
255        // Get public key bytes for UserProfile
256        let pubkey_bytes = public_key.clone().into_bytes();
257
258        // Create storage directory if it doesn't exist
259        if !storage_dir.exists() {
260            std::fs::create_dir_all(&storage_dir).map_err(|e| {
261                format!(
262                    "Failed to create storage directory {:?}: {}",
263                    storage_dir, e
264                )
265            })?;
266        }
267
268        // For UserProfile, we need a fixed-size array. Use first 32 bytes of public key
269        let pubkey_array: [u8; 32] = pubkey_bytes[..32]
270            .try_into()
271            .map_err(|_| "Public key too short".to_string())?;
272
273        // Create user profile
274        let profile = UserProfile::new(
275            four_words.clone(),
276            display_name.clone(),
277            pubkey_array,
278            device_type,
279            storage_dir.clone(),
280        );
281
282        // Initialize CRDT manager for persistent storage
283        let crdt_manager = Arc::new(
284            crate::CrdtManager::new(&storage_dir.join("crdt.db"))
285                .await
286                .map_err(|e| format!("Failed to initialize CrdtManager: {}", e))?,
287        );
288
289        // Initialize entity service for managing groups, channels, and members
290        let entity_service = Arc::new(crate::EntityService::new(crdt_manager.clone()));
291
292        // Initialize unified message service
293        let message_service = Arc::new(crate::MessageService::new(four_words.clone()));
294
295        // Initialize legacy message sync service (for backward compatibility)
296        let message_sync = Arc::new(MessageSyncService::new(four_words.clone()));
297
298        // Initialize document replicator with dual storage enabled (Sprint 3.2)
299        let doc_config = crate::doc_replicator::DocReplicatorConfig {
300            files_storage_enabled: true,
301            web_storage_enabled: true,
302        };
303        let doc_replicator = Arc::new(
304            crate::doc_replicator::DocReplicator::new(doc_config)
305                .await
306                .map_err(|e| format!("Failed to initialize DocReplicator: {}", e))?,
307        );
308
309        // Initialize per-entity virtual disk service
310        let disk_root = storage_dir.join("disks");
311        let disk_service = Arc::new(
312            EntityDiskService::new(&disk_root)
313                .await
314                .map_err(|e| format!("Failed to initialize EntityDiskService: {}", e))?,
315        );
316
317        // Initialize Kanban service for project management boards
318        let kanban_service = Arc::new(KanbanService::new(four_words.clone()));
319
320        // Initialize Invite service for cross-organization collaboration
321        let invite_service = Arc::new(crate::invite_service::InviteService::new(
322            crdt_manager.clone(),
323            entity_service.clone(),
324        ));
325
326        info!(
327            "CoreContext initialized for user '{}' ({}) with EntityService, MessageService, DocReplicator, DiskService, KanbanService, and InviteService",
328            display_name, four_words
329        );
330
331        Ok(Self {
332            profile,
333            signing_key,
334            public_key,
335            four_words,
336            display_name,
337            device_name,
338            crdt_manager,
339            entity_service,
340            message_service,
341            message_sync,
342            doc_replicator,
343            listen_address: None,
344            connection_identity: None,
345            external_address: None,
346            gossip: None,
347            group_keys: HashMap::new(),
348            disk_service,
349            webrtc: None, // Initialized when networking starts
350            kanban_service,
351            invite_service,
352        })
353    }
354
355    /// Start networking with gossip overlay system
356    ///
357    /// Initializes the saorsa-gossip based P2P networking layer:
358    /// - Creates QUIC transport on random high port (49152-65535)
359    /// - Initializes HyParView membership and Plumtree pubsub
360    /// - Sets up peer cache for fast boot
361    /// - Connects to coordinator and rendezvous services
362    /// - Generates connection identity (four-word encoded address)
363    ///
364    /// # Arguments
365    /// * `_port` - Optional specific port (currently ignored, transport auto-selects)
366    ///
367    /// # Returns
368    /// Connection identity (four-word encoded address)
369    pub async fn start_networking(
370        &mut self,
371        preferred_port: Option<u16>,
372    ) -> Result<String, String> {
373        info!("Starting gossip networking for {}", self.four_words);
374
375        // Allocate UDP port using PortManager
376        let mut port_manager = if let Some(port) = preferred_port {
377            crate::gossip::PortManager::with_preferred_port(port)
378        } else {
379            crate::gossip::PortManager::new()
380        };
381
382        let listen_port = port_manager
383            .allocate_port()
384            .map_err(|e| format!("Failed to allocate port: {}", e))?;
385
386        info!("Allocated port {} for QUIC transport", listen_port);
387
388        // Initialize gossip context with allocated port
389        let gossip_ctx = crate::gossip::GossipContext::initialize(
390            self.four_words.clone(),
391            self.display_name.clone(),
392            self.device_name.clone(),
393            Some(listen_port),
394        )
395        .await
396        .map_err(|e| format!("Failed to initialize gossip: {}", e))?;
397
398        // Execute gossip boot sequence (SPEC.md §2)
399        // This enables: membership, topic subscriptions, presence, and CRDT anti-entropy
400        info!("Executing gossip boot sequence (5 steps)");
401        let mut boot_sequence = crate::gossip::GossipBootSequence::new(gossip_ctx);
402        boot_sequence
403            .execute()
404            .await
405            .map_err(|e| format!("Failed to execute boot sequence: {}", e))?;
406
407        // Extract the gossip context after boot
408        let gossip = boot_sequence.into_context();
409        info!("Gossip boot sequence completed successfully");
410
411        // Build listen address
412        let local_ip =
413            local_ip_address::local_ip().map_err(|e| format!("Failed to get local IP: {}", e))?;
414
415        let listen_addr = std::net::SocketAddr::new(local_ip, listen_port);
416
417        // Generate connection identity using four-word encoding
418        let connection_identity = crate::conn_words(&listen_addr)
419            .map_err(|e| format!("Failed to encode connection address: {}", e))?;
420
421        info!(
422            "Gossip networking started on {} ({})",
423            listen_addr, connection_identity
424        );
425
426        self.listen_address = Some(listen_addr);
427        self.connection_identity = Some(connection_identity.clone());
428        let gossip_arc = Arc::new(gossip);
429        self.gossip = Some(gossip_arc.clone());
430
431        // Set up entity message handler for incoming gossip messages
432        self.setup_entity_message_handler().await?;
433
434        // Initialize WebRTC service (requires gossip context)
435        match CommunitasWebRtcService::new(gossip_arc).await {
436            Ok(webrtc) => {
437                info!("WebRTC service initialized successfully");
438                self.webrtc = Some(Arc::new(webrtc));
439            }
440            Err(e) => {
441                warn!(
442                    "Failed to initialize WebRTC service: {}. Voice/video calls will be unavailable.",
443                    e
444                );
445                // Don't fail networking start - WebRTC is optional
446            }
447        }
448
449        // Auto-request external address after a brief delay (allow time for peer connections)
450        // This is non-blocking and best-effort - failure is logged but doesn't affect networking
451        info!("Scheduling automatic external address detection...");
452
453        Ok(connection_identity)
454    }
455
456    /// Set up the entity message handler to process incoming gossip messages
457    ///
458    /// This handler is called whenever a message is received on a subscribed entity topic.
459    /// It deserializes the message and handles different message types:
460    /// - Chat: Regular messages stored via message service
461    /// - SyncRequest: Respond with historical messages
462    /// - SyncResponse: Process and store historical messages
463    async fn setup_entity_message_handler(&self) -> Result<(), String> {
464        let gossip = self.gossip.as_ref().ok_or("Gossip not initialized")?;
465
466        // Clone services for use in the handler closure
467        let message_service = self.message_service.clone();
468        let gossip_clone = gossip.clone();
469
470        // Create handler that processes incoming entity messages
471        let handler: crate::gossip::EntityMessageHandler =
472            Arc::new(move |entity_id, sender_peer_id, message_bytes| {
473                // Try to parse as GossipMessageType first (new format)
474                // Fall back to CRDTMessage for backwards compatibility
475                let gossip_msg: Result<crate::crdt::GossipMessageType, _> =
476                    serde_json::from_slice(&message_bytes);
477
478                let message_service = message_service.clone();
479                let gossip_clone = gossip_clone.clone();
480                let entity_id_clone = entity_id.clone();
481
482                match gossip_msg {
483                    Ok(crate::crdt::GossipMessageType::Chat(crdt_message)) => {
484                        info!(
485                            "Received chat message for entity {} from peer {:?}: {}",
486                            entity_id, sender_peer_id, crdt_message.metadata.id
487                        );
488                        Self::handle_chat_message(message_service, entity_id_clone, crdt_message);
489                    }
490                    Ok(crate::crdt::GossipMessageType::SyncRequest(sync_request)) => {
491                        info!(
492                            "Received sync request for entity {} from peer {:?}",
493                            entity_id, sender_peer_id
494                        );
495                        Self::handle_sync_request(
496                            message_service,
497                            gossip_clone,
498                            entity_id_clone,
499                            sender_peer_id,
500                            sync_request,
501                        );
502                    }
503                    Ok(crate::crdt::GossipMessageType::SyncResponse(sync_response)) => {
504                        info!(
505                            "Received sync response for entity {} from peer {:?} with {} messages",
506                            entity_id,
507                            sender_peer_id,
508                            sync_response.messages.len()
509                        );
510                        Self::handle_sync_response(message_service, entity_id_clone, sync_response);
511                    }
512                    Err(_) => {
513                        // Try legacy CRDTMessage format for backwards compatibility
514                        match serde_json::from_slice::<crate::crdt::CRDTMessage>(&message_bytes) {
515                            Ok(crdt_message) => {
516                                info!(
517                                    "Received legacy message for entity {} from peer {:?}: {}",
518                                    entity_id, sender_peer_id, crdt_message.metadata.id
519                                );
520                                Self::handle_chat_message(
521                                    message_service,
522                                    entity_id_clone,
523                                    crdt_message,
524                                );
525                            }
526                            Err(e) => {
527                                warn!(
528                                    "Failed to deserialize message for entity {}: {}",
529                                    entity_id, e
530                                );
531                            }
532                        }
533                    }
534                }
535            });
536
537        // Register the handler with the gossip context
538        gossip.set_entity_message_handler(handler).await;
539
540        info!("Entity message handler registered for incoming gossip messages");
541        Ok(())
542    }
543
544    /// Handle incoming chat message
545    fn handle_chat_message(
546        message_service: Arc<crate::MessageService>,
547        entity_id: String,
548        crdt_message: crate::crdt::CRDTMessage,
549    ) {
550        tokio::spawn(async move {
551            match message_service.receive_message(crdt_message).await {
552                Ok(result) => {
553                    if result.accepted {
554                        info!("Stored incoming message for entity {}", entity_id);
555                    } else if result.out_of_order {
556                        warn!(
557                            "Message for entity {} was out of order, queued for later",
558                            entity_id
559                        );
560                    }
561                }
562                Err(e) => {
563                    warn!(
564                        "Failed to store incoming message for entity {}: {}",
565                        entity_id, e
566                    );
567                }
568            }
569        });
570    }
571
572    /// Handle sync request - respond with historical messages
573    ///
574    /// Also adds the requesting peer to our eager_peers for this entity's topic,
575    /// ensuring they receive any new messages we publish.
576    fn handle_sync_request(
577        message_service: Arc<crate::MessageService>,
578        gossip: Arc<crate::gossip::GossipContext>,
579        entity_id: String,
580        sender_peer_id: saorsa_gossip_types::PeerId,
581        _sync_request: crate::crdt::SyncRequest,
582    ) {
583        tokio::spawn(async move {
584            // Add the requesting peer to our eager_peers for this entity's topic
585            // This ensures they receive any new messages we publish
586            if let Err(e) = gossip
587                .add_peer_to_entity_topic(&entity_id, sender_peer_id)
588                .await
589            {
590                warn!(
591                    "Failed to add peer {:?} to entity {} topic: {}",
592                    sender_peer_id, entity_id, e
593                );
594            }
595
596            // Get all messages for this entity
597            match message_service.get_entity_messages(entity_id.clone()).await {
598                Ok(sync_response) => {
599                    info!(
600                        "Sending sync response with {} messages for entity {}",
601                        sync_response.messages.len(),
602                        entity_id
603                    );
604
605                    // Wrap in GossipMessageType
606                    let gossip_msg = crate::crdt::GossipMessageType::SyncResponse(sync_response);
607
608                    // Serialize and publish
609                    match serde_json::to_vec(&gossip_msg) {
610                        Ok(bytes) => {
611                            if let Err(e) = gossip.publish_to_entity(&entity_id, bytes).await {
612                                warn!("Failed to send sync response for {}: {}", entity_id, e);
613                            }
614                        }
615                        Err(e) => {
616                            warn!("Failed to serialize sync response: {}", e);
617                        }
618                    }
619                }
620                Err(e) => {
621                    warn!("Failed to get messages for sync response: {}", e);
622                }
623            }
624        });
625    }
626
627    /// Handle sync response - process and store historical messages
628    fn handle_sync_response(
629        message_service: Arc<crate::MessageService>,
630        entity_id: String,
631        sync_response: crate::crdt::SyncResponse,
632    ) {
633        tokio::spawn(async move {
634            let mut accepted = 0;
635            let mut rejected = 0;
636
637            for message in sync_response.messages {
638                match message_service.receive_message(message).await {
639                    Ok(result) => {
640                        if result.accepted {
641                            accepted += 1;
642                        } else {
643                            rejected += 1;
644                        }
645                    }
646                    Err(e) => {
647                        warn!("Failed to process sync message for {}: {}", entity_id, e);
648                        rejected += 1;
649                    }
650                }
651            }
652
653            info!(
654                "Sync response processed for {}: {} accepted, {} rejected",
655                entity_id, accepted, rejected
656            );
657        });
658    }
659
660    /// Automatically request external address with retry logic
661    ///
662    /// This is called after networking starts to discover our public IP address.
663    /// It retries a few times with delays to allow peer connections to establish.
664    ///
665    /// # Returns
666    /// Ok if external address was successfully determined, Err otherwise
667    pub async fn auto_request_external_address(&mut self) -> Result<(), String> {
668        // Retry up to 3 times with 2 second delays to allow peer connections
669        for attempt in 1..=3 {
670            info!("Auto-detecting external address (attempt {}/3)...", attempt);
671
672            // Wait a bit for peers to connect (first attempt waits longer)
673            let delay_ms = if attempt == 1 { 2000 } else { 1000 };
674            tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
675
676            match self.request_external_address().await {
677                Ok(()) => {
678                    if let Some(addr) = self.external_address {
679                        info!("Auto-detected external address: {}", addr);
680                        return Ok(());
681                    }
682                }
683                Err(e) => {
684                    if attempt < 3 {
685                        warn!(
686                            "External address detection attempt {} failed: {}. Retrying...",
687                            attempt, e
688                        );
689                    } else {
690                        warn!("External address detection failed after 3 attempts: {}", e);
691                    }
692                }
693            }
694        }
695
696        Err("Failed to auto-detect external address after 3 attempts".to_string())
697    }
698
699    /// Request external/public address via NAT reflection from a bootstrap node
700    ///
701    /// Get our external IP address and port as seen from the internet.
702    ///
703    /// This uses the native QUIC OBSERVED_ADDRESS frame mechanism (draft-ietf-quic-address-discovery)
704    /// which is automatically exchanged during QUIC connection establishment.
705    ///
706    /// Should be called after networking is active and we have connections to bootstrap nodes.
707    ///
708    /// # Returns
709    /// The external address if successfully obtained, or an error message
710    pub async fn request_external_address(&mut self) -> Result<(), String> {
711        let gossip = self
712            .gossip
713            .as_ref()
714            .ok_or("Networking not started. Call start_networking() first")?;
715
716        info!("Requesting external address via native QUIC address discovery");
717
718        // Use the native QUIC OBSERVED_ADDRESS mechanism through the transport layer
719        // This is the standard way to discover external address via QUIC connections
720        if let Some(external_addr) = gossip.transport.get_external_address() {
721            info!(
722                "Got external address via QUIC OBSERVED_ADDRESS: {}",
723                external_addr
724            );
725            self.external_address = Some(external_addr);
726            return Ok(());
727        }
728
729        // If native discovery didn't work, check if we have any connected peers at all
730        let cache = gossip.peer_cache.read().await;
731        let peers = cache.get_top_peers(5);
732        drop(cache);
733
734        if peers.is_empty() {
735            return Err("No connected peers - external address not yet available. \
736                 The address will be discovered automatically when connections are established."
737                .to_string());
738        }
739
740        // We have peers but no observed address yet - this can happen if:
741        // 1. The connection is still being established
742        // 2. The remote peer doesn't support OBSERVED_ADDRESS frames
743        // 3. We're behind a very restrictive NAT
744        warn!(
745            "Connected to {} peers but no OBSERVED_ADDRESS received yet",
746            peers.len()
747        );
748        Err(
749            "External address not yet available - waiting for OBSERVED_ADDRESS frame from peers"
750                .to_string(),
751        )
752    }
753
754    /// Stop networking gracefully
755    ///
756    /// Shuts down gossip services, presence beacons, and transport
757    pub async fn stop_networking(&mut self) -> Result<(), String> {
758        if let Some(_gossip) = self.gossip.take() {
759            info!("Stopping gossip networking for {}", self.four_words);
760            // Clear WebRTC service (depends on gossip)
761            self.webrtc = None;
762            // Note: Graceful shutdown will be implemented when needed
763            // The Arc drop will clean up resources
764            self.listen_address = None;
765            self.connection_identity = None;
766            self.external_address = None;
767        }
768        Ok(())
769    }
770
771    /// Connect to a peer using their four-word identity
772    ///
773    /// This adds the peer to favourite contacts and initiates FOAF discovery.
774    /// The gossip overlay will find and connect to the peer automatically.
775    ///
776    /// # Arguments
777    /// * `peer_four_words` - The peer's four-word identity (e.g., "ocean-forest-moon-star")
778    ///
779    /// # Returns
780    /// Success if peer added to favourites
781    pub async fn connect_to_peer(&self, peer_four_words: &str) -> Result<(), String> {
782        let gossip = self
783            .gossip
784            .as_ref()
785            .ok_or("Networking not started. Call start_networking() first")?;
786
787        info!("Adding peer {} to favourites", peer_four_words);
788
789        // Add peer to favourite contacts
790        gossip
791            .add_favourite_contact(peer_four_words.to_string())
792            .await
793            .map_err(|e| format!("Failed to add favourite contact: {}", e))?;
794
795        // Try to decode as connection address and dial immediately (for bootstrap)
796        // First try parsing as a direct IP:port address, then as four-word address
797        let maybe_addr: Option<std::net::SocketAddr> = peer_four_words
798            .parse::<std::net::SocketAddr>()
799            .ok()
800            .or_else(|| crate::identity::conn_from_words(peer_four_words).ok());
801
802        if let Some(addr) = maybe_addr {
803            info!("Dialing peer at {} ({})", addr, peer_four_words);
804            if let Err(e) = gossip.dial_address(addr).await {
805                warn!("Failed to dial peer {}: {}", addr, e);
806            } else {
807                info!("Successfully dialed peer {}", addr);
808            }
809        } else {
810            info!(
811                "Peer {} is not a direct address; relying on FOAF discovery",
812                peer_four_words
813            );
814        }
815
816        // The gossip overlay will automatically discover and connect via FOAF
817        info!(
818            "Peer {} added. FOAF discovery will locate and connect automatically",
819            peer_four_words
820        );
821
822        Ok(())
823    }
824
825    /// Send a channel message and publish it to gossip if networking is active
826    ///
827    /// This method:
828    /// 1. Stores the message locally via CRDT
829    /// 2. If gossip is active, joins the entity topic and publishes for P2P sync
830    ///
831    /// # Arguments
832    /// * `channel_id` - The channel to send to
833    /// * `content_text` - The message text
834    /// * `reply_to_id` - Optional parent message for threading
835    ///
836    /// # Returns
837    /// The message ID on success
838    pub async fn send_and_publish_channel_message(
839        &self,
840        channel_id: String,
841        content_text: String,
842        reply_to_id: Option<String>,
843    ) -> Result<String, String> {
844        use crate::crdt::{EntityType, MessageContent};
845
846        // Create message content
847        let content = MessageContent {
848            text: content_text,
849            author: self.four_words.clone(),
850            attachments: None,
851        };
852
853        // Store locally via message_service
854        let message = if let Some(reply_to) = reply_to_id {
855            self.message_service
856                .send_message(
857                    channel_id.clone(),
858                    EntityType::Channel,
859                    content,
860                    Some(reply_to),
861                )
862                .await
863                .map_err(|e| format!("Failed to send message: {}", e))?
864        } else {
865            self.message_service
866                .send_message(channel_id.clone(), EntityType::Channel, content, None)
867                .await
868                .map_err(|e| format!("Failed to send message: {}", e))?
869        };
870
871        let message_id = message.metadata.id.clone();
872
873        // If gossip is active, publish to the network
874        if let Some(gossip) = self.gossip.as_ref() {
875            // Ensure we're joined to the entity topic
876            if let Err(e) = gossip.join_entity(&channel_id, "channel").await {
877                warn!(
878                    "Failed to join channel topic {} (may already be joined): {}",
879                    channel_id, e
880                );
881                // Continue anyway - we might already be subscribed
882            }
883
884            // Serialize message to JSON bytes
885            let message_bytes = serde_json::to_vec(&message)
886                .map_err(|e| format!("Failed to serialize message: {}", e))?;
887
888            // Publish to gossip
889            if let Err(e) = gossip.publish_to_entity(&channel_id, message_bytes).await {
890                warn!("Failed to publish message to gossip: {}", e);
891                // Don't fail - message is stored locally, sync will catch up
892            } else {
893                info!(
894                    "Message {} published to gossip for channel {}",
895                    message_id, channel_id
896                );
897            }
898        } else {
899            info!(
900                "Gossip not active - message {} stored locally only",
901                message_id
902            );
903        }
904
905        Ok(message_id)
906    }
907
908    /// Add a group key for a channel/project/org
909    ///
910    /// # Arguments
911    /// * `group_id` - Group identifier
912    /// * `key` - Group key material (placeholder for now)
913    pub fn add_group_key(&mut self, group_id: String, key: GroupKeyPairPlaceholder) {
914        self.group_keys.insert(group_id, key);
915    }
916
917    /// Get the ML-DSA-87 public key for this identity
918    pub fn get_public_key(&self) -> &PublicKey {
919        &self.public_key
920    }
921
922    /// Get the public key bytes
923    /// ML-DSA-87 public keys are 2592 bytes
924    pub fn public_key_bytes(&self) -> [u8; 2592] {
925        self.public_key.clone().into_bytes()
926    }
927
928    /// Sign a message with ML-DSA-87 post-quantum signature
929    ///
930    /// # Arguments
931    /// * `message` - Message bytes to sign
932    ///
933    /// # Returns
934    /// ML-DSA-87 signature (4627 bytes for ML-DSA-87)
935    ///
936    /// # Errors
937    /// Returns error if signing fails
938    pub fn sign(&self, message: &[u8]) -> Result<[u8; 4627], String> {
939        self.signing_key
940            .try_sign(message, &[]) // Empty context
941            .map_err(|e| format!("ML-DSA-87 signing failed: {}", e))
942    }
943
944    /// Verify an ML-DSA-87 signature
945    ///
946    /// # Arguments
947    /// * `message` - Message bytes that were signed
948    /// * `signature` - ML-DSA-87 signature (4627 bytes)
949    ///
950    /// # Returns
951    /// true if signature is valid, false otherwise
952    pub fn verify(&self, message: &[u8], signature: &[u8; 4627]) -> bool {
953        self.public_key.verify(message, signature, &[]) // Empty context, returns bool directly
954    }
955
956    /// Get the storage directory for this profile
957    pub fn storage_dir(&self) -> &PathBuf {
958        &self.profile.storage_dir
959    }
960
961    /// Check if networking is active
962    pub fn is_networking_active(&self) -> bool {
963        self.gossip.is_some() && self.listen_address.is_some()
964    }
965
966    /// Get connection identity (if networking is active)
967    pub fn connection_identity(&self) -> Option<&str> {
968        self.connection_identity.as_deref()
969    }
970
971    /// Update display name
972    pub fn set_display_name(&mut self, display_name: String) {
973        self.display_name = display_name.clone();
974        self.profile.display_name = display_name;
975    }
976
977    /// Get device type
978    pub fn device_type(&self) -> DeviceType {
979        self.profile.device_type
980    }
981
982    /// Check if passkey is registered for this profile
983    pub fn has_passkey(&self) -> bool {
984        self.profile.has_passkey()
985    }
986}
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991    use tempfile::TempDir;
992
993    #[tokio::test]
994    async fn test_core_context_initialization() {
995        let temp_dir = TempDir::new().unwrap();
996        let storage_dir = temp_dir.path().to_path_buf();
997
998        let context = CoreContext::initialize(
999            "ocean-forest-moon-star".to_string(),
1000            "Test User".to_string(),
1001            "Test Device".to_string(),
1002            DeviceType::Desktop,
1003            storage_dir.clone(),
1004        )
1005        .await;
1006
1007        assert!(context.is_ok());
1008        let ctx = context.unwrap();
1009
1010        assert_eq!(ctx.display_name, "Test User");
1011        assert_eq!(ctx.device_name, "Test Device");
1012        assert_eq!(ctx.profile.device_type, DeviceType::Desktop);
1013        assert_eq!(ctx.four_words, "ocean-forest-moon-star");
1014        assert!(!ctx.is_networking_active());
1015        assert!(storage_dir.exists());
1016    }
1017
1018    #[tokio::test]
1019    async fn test_invalid_four_word_format() {
1020        let temp_dir = TempDir::new().unwrap();
1021
1022        let context = CoreContext::initialize(
1023            "only-three-words".to_string(),
1024            "Test User".to_string(),
1025            "Test Device".to_string(),
1026            DeviceType::Desktop,
1027            temp_dir.path().to_path_buf(),
1028        )
1029        .await;
1030
1031        assert!(context.is_err());
1032        assert!(context.unwrap_err().contains("Invalid four-word format"));
1033    }
1034
1035    #[tokio::test]
1036    async fn test_display_name_update() {
1037        let temp_dir = TempDir::new().unwrap();
1038
1039        let mut context = CoreContext::initialize(
1040            "ocean-forest-moon-star".to_string(),
1041            "Old Name".to_string(),
1042            "Test Device".to_string(),
1043            DeviceType::Desktop,
1044            temp_dir.path().to_path_buf(),
1045        )
1046        .await
1047        .unwrap();
1048
1049        context.set_display_name("New Name".to_string());
1050
1051        assert_eq!(context.display_name, "New Name");
1052        assert_eq!(context.profile.display_name, "New Name");
1053    }
1054
1055    #[tokio::test]
1056    async fn test_signing() {
1057        let temp_dir = TempDir::new().unwrap();
1058
1059        let context = CoreContext::initialize(
1060            "ocean-forest-moon-star".to_string(),
1061            "Test User".to_string(),
1062            "Test Device".to_string(),
1063            DeviceType::Desktop,
1064            temp_dir.path().to_path_buf(),
1065        )
1066        .await
1067        .unwrap();
1068
1069        let message = b"test message";
1070        let signature = context.sign(message).unwrap();
1071
1072        // Verify ML-DSA-87 signature
1073        assert!(context.verify(message, &signature));
1074
1075        // Verify signature fails with wrong message
1076        let wrong_message = b"wrong message";
1077        assert!(!context.verify(wrong_message, &signature));
1078    }
1079
1080    #[tokio::test]
1081    async fn test_group_key_management() {
1082        let temp_dir = TempDir::new().unwrap();
1083
1084        let mut context = CoreContext::initialize(
1085            "ocean-forest-moon-star".to_string(),
1086            "Test User".to_string(),
1087            "Test Device".to_string(),
1088            DeviceType::Desktop,
1089            temp_dir.path().to_path_buf(),
1090        )
1091        .await
1092        .unwrap();
1093
1094        let group_id = "test-group".to_string();
1095        let key = GroupKeyPairPlaceholder {
1096            group_id: group_id.clone(),
1097        };
1098
1099        context.add_group_key(group_id.clone(), key);
1100
1101        assert!(context.group_keys.contains_key(&group_id));
1102    }
1103
1104    #[tokio::test]
1105    async fn test_networking_not_active_by_default() {
1106        let temp_dir = TempDir::new().unwrap();
1107
1108        let context = CoreContext::initialize(
1109            "ocean-forest-moon-star".to_string(),
1110            "Test User".to_string(),
1111            "Test Device".to_string(),
1112            DeviceType::Desktop,
1113            temp_dir.path().to_path_buf(),
1114        )
1115        .await
1116        .unwrap();
1117
1118        assert!(!context.is_networking_active());
1119        assert!(context.connection_identity().is_none());
1120    }
1121}