zoe_client/client/
builder.rs

1use super::ClientSecret;
2use crate::error::Result;
3use crate::services::MultiRelayMessageManager;
4use crate::services::blob_store::MultiRelayBlobService;
5use crate::{Client, ClientError, FileStorage, SessionManager};
6use async_broadcast;
7use eyeball::SharedObservable;
8use rand::Rng;
9use std::collections::BTreeMap;
10use std::net::SocketAddr;
11use std::path::PathBuf;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14use zoe_app_primitives::{CompressionConfig, RelayAddress};
15use zoe_client_storage::{SqliteMessageStorage, StorageConfig};
16use zoe_wire_protocol::{KeyPair, VerifyingKey};
17
18#[cfg(feature = "frb-api")]
19use flutter_rust_bridge::frb;
20
21#[derive(Default)]
22#[cfg_attr(feature = "frb-api", frb(opaque))]
23pub struct ClientBuilder {
24    media_storage_dir: Option<PathBuf>,
25    inner_keypair: Option<Arc<KeyPair>>,
26    servers: Option<Vec<RelayAddress>>,
27    db_storage_dir: Option<PathBuf>,
28    encryption_key: Option<[u8; 32]>,
29    autoconnect: bool,
30}
31
32impl Client {
33    /// Create a new ClientBuilder for constructing a Client
34    #[cfg_attr(feature = "frb-api", frb)]
35    pub fn builder() -> ClientBuilder {
36        ClientBuilder::default()
37    }
38}
39
40#[cfg_attr(feature = "frb-api", frb)]
41impl ClientBuilder {
42    pub fn client_secret(&mut self, secret: ClientSecret) {
43        self.servers.get_or_insert_default().extend(secret.servers);
44        self.inner_keypair = Some(secret.inner_keypair);
45        self.encryption_key = Some(secret.encryption_key);
46    }
47
48    pub fn media_storage_dir(&mut self, media_storage_dir: String) {
49        self.media_storage_dir_pathbuf(PathBuf::from(media_storage_dir));
50    }
51
52    pub fn servers(&mut self, servers: Vec<RelayAddress>) {
53        self.servers = Some(servers);
54    }
55
56    pub fn server_info(&mut self, server_public_key: VerifyingKey, server_addr: SocketAddr) {
57        self.servers.get_or_insert_default().push(
58            RelayAddress::new(server_public_key)
59                .with_address(server_addr.into())
60                .with_name("Legacy Server".to_string()),
61        );
62    }
63
64    /// Set the storage database path (convenience method)
65    pub fn db_storage_dir(&mut self, path: String) {
66        self.db_storage_dir_pathbuf(PathBuf::from(path));
67    }
68
69    /// Set the encryption key for storage
70    pub fn encryption_key(&mut self, key: [u8; 32]) {
71        self.encryption_key = Some(key);
72    }
73
74    /// Enable or disable automatic connection to server during build
75    ///
76    /// When autoconnect is true (default for backward compatibility), the client
77    /// will require server information and connect immediately during build().
78    /// When autoconnect is false, the client starts in offline mode and can
79    /// connect to relays later using add_relay().
80    pub fn autoconnect(&mut self, autoconnect: bool) {
81        self.autoconnect = autoconnect;
82    }
83
84    pub async fn build(self) -> Result<Client> {
85        let Some(media_storage_dir) = self.media_storage_dir else {
86            return Err(ClientError::BuildError(
87                "Media storage dir is required".to_string(),
88            ));
89        };
90
91        let Some(db_storage_dir) = self.db_storage_dir else {
92            return Err(ClientError::BuildError(
93                "DB storage dir is required".to_string(),
94            ));
95        };
96
97        // Server info is only required when autoconnect is true (default behavior)
98        if self.autoconnect {
99            if self.servers.is_none() || self.servers.as_ref().unwrap().is_empty() {
100                return Err(ClientError::BuildError(
101                    "At least one server is required when autoconnect is enabled".to_string(),
102                ));
103            }
104        }
105
106        let encryption_key = match self.encryption_key {
107            Some(encryption_key) => encryption_key,
108            None => rand::rngs::OsRng::default().r#gen(),
109        };
110
111        let key_pair = if let Some(key_pair) = self.inner_keypair {
112            key_pair
113        } else {
114            Arc::new(KeyPair::generate(&mut rand::rngs::OsRng))
115        };
116
117        let user_id = key_pair.id();
118        let user_id_hex = hex::encode(user_id);
119
120        // Create storage
121        let fs_path = media_storage_dir.to_path_buf().join(&user_id_hex);
122        let storage_config = StorageConfig {
123            database_path: db_storage_dir.join(&user_id_hex).join("db.sqlite"),
124            max_query_limit: None,
125            enable_wal_mode: true, // Enable WAL mode for better concurrent access
126            cache_size_kb: Some(32 * 1024), // 32MB cache for better performance
127        };
128
129        let storage = Arc::new(
130            SqliteMessageStorage::new(storage_config, &encryption_key)
131                .await
132                .map_err(|e| ClientError::BuildError(format!("Failed to create storage: {}", e)))?,
133        );
134
135        // Initialize broadcast channel for relay status updates
136        let (relay_status_sender, relay_status_rx) = async_broadcast::broadcast(1000);
137        let relay_status_keeper = relay_status_rx.deactivate();
138        // Offline mode: use multi-relay services
139        let message_manager = Arc::new(MultiRelayMessageManager::new(Arc::clone(&storage)));
140        let blob_service = Arc::new(MultiRelayBlobService::new(Arc::clone(&storage)));
141        let session_manager = Arc::new(
142            SessionManager::builder(Arc::clone(&storage), message_manager.clone())
143                .client_keypair(key_pair.clone())
144                .build()
145                .await
146                .map_err(|e| {
147                    ClientError::BuildError(format!("Failed to create session manager: {}", e))
148                })?,
149        );
150
151        let fs =
152            FileStorage::new(&fs_path, blob_service.clone(), CompressionConfig::default()).await?;
153
154        let servers = self.servers.clone().unwrap_or_default();
155        let client_secret = ClientSecret {
156            inner_keypair: key_pair,
157            servers: servers.clone(),
158            encryption_key: encryption_key,
159        };
160
161        // Initialize observable state for client secret
162        let client_secret_observable = SharedObservable::new(client_secret.clone());
163
164        let client = Client {
165            client_secret: Arc::new(client_secret),
166            fs: Arc::new(fs),
167            storage: Arc::clone(&storage),
168            message_manager,
169            blob_service,
170            relay_connections: Arc::new(RwLock::new(BTreeMap::new())),
171            relay_info: Arc::new(RwLock::new(BTreeMap::new())),
172            encryption_key,
173            client_secret_observable,
174            relay_status_sender: Arc::new(relay_status_sender),
175            _relay_status_keeper: Arc::new(relay_status_keeper),
176            connection_monitors: Arc::new(RwLock::new(BTreeMap::new())),
177            session_manager,
178        };
179
180        // If autoconnect is enabled, add the first server immediately
181        if self.autoconnect {
182            for server in servers {
183                // add all servers in background
184                let relay_address = server.clone();
185                let _handle = client.add_relay_background(relay_address);
186            }
187        }
188
189        Ok(client)
190    }
191}
192
193#[cfg_attr(feature = "frb-api", frb(ignore))]
194// non Flutter Rust Bridge API methods
195impl ClientBuilder {
196    pub fn media_storage_dir_pathbuf(&mut self, media_storage_dir: PathBuf) {
197        self.media_storage_dir = Some(PathBuf::from(media_storage_dir));
198    }
199
200    pub fn inner_keypair(&mut self, inner_keypair: KeyPair) {
201        self.inner_keypair = Some(Arc::new(inner_keypair));
202    }
203
204    pub fn db_storage_dir_pathbuf(&mut self, path: PathBuf) {
205        self.db_storage_dir = Some(path);
206    }
207}