Skip to main content

syncweb_core/net/
network_manager.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::{Arc, RwLock};
3
4use iroh::PublicKey;
5use iroh_docs::NamespaceId;
6
7use crate::storage::node_db::NodeDatabase;
8use crate::{Result, SyncwebError};
9
10use super::network::{Network, NetworkId, NetworkOptions, NetworkTicket};
11use super::network_log::{NetworkEventType, NetworkLogger};
12
13/// Persistent manager for network membership and folder associations.
14#[derive(Clone, Debug)]
15pub struct NetworkManager {
16    db: NodeDatabase,
17    local_node: PublicKey,
18    networks: HashMap<NetworkId, Network>,
19    logger: Option<NetworkLogger>,
20    member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
21}
22
23impl NetworkManager {
24    /// Open the network database.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error if existing state cannot be read.
29    pub fn new(
30        db: NodeDatabase,
31        local_node: PublicKey,
32        member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
33    ) -> Result<Self> {
34        let networks = db
35            .list_networks()?
36            .into_iter()
37            .map(|network| (network.id, network))
38            .collect();
39        let mgr = Self {
40            db,
41            local_node,
42            networks,
43            logger: None,
44            member_keys,
45        };
46        mgr.sync_member_keys();
47        Ok(mgr)
48    }
49
50    /// Open the network database with a network logger.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if existing state cannot be read.
55    pub fn with_logger(
56        db: NodeDatabase,
57        local_node: PublicKey,
58        logger: NetworkLogger,
59        member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
60    ) -> Result<Self> {
61        let mut mgr = Self::new(db, local_node, member_keys)?;
62        mgr.logger = Some(logger);
63        Ok(mgr)
64    }
65
66    /// Return a reference to the network logger, if set.
67    #[must_use]
68    pub const fn logger(&self) -> &Option<NetworkLogger> {
69        &self.logger
70    }
71
72    /// Set the network logger after construction.
73    pub fn set_logger(&mut self, logger: NetworkLogger) {
74        self.logger = Some(logger);
75    }
76
77    /// Return a reference to the underlying database.
78    #[must_use]
79    pub const fn database(&self) -> &NodeDatabase {
80        &self.db
81    }
82
83    /// Return the local node's public key.
84    #[must_use]
85    pub const fn local_node(&self) -> &iroh::PublicKey {
86        &self.local_node
87    }
88
89    /// Create and persist a network owned by the local node.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error for an empty/duplicate name or failed persistence.
94    pub fn create(&mut self, name: &str, options: NetworkOptions) -> Result<NetworkId> {
95        self.create_with_doc_ticket(name, options, None)
96    }
97
98    /// Create a network with an optional membership doc ticket.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error for an empty/duplicate name or failed persistence.
103    pub fn create_with_doc_ticket(
104        &mut self,
105        name: &str,
106        options: NetworkOptions,
107        doc_ticket: Option<String>,
108    ) -> Result<NetworkId> {
109        let normalized = name.trim();
110        if normalized.is_empty() {
111            return Err(SyncwebError::InvalidConfig("network name cannot be empty".to_owned()));
112        }
113        let id = NetworkId::from_name(normalized);
114        if self.networks.contains_key(&id) {
115            return Err(SyncwebError::InvalidConfig(format!(
116                "network {normalized:?} already exists"
117            )));
118        }
119        let mut network = Network::new(normalized, self.local_node, options);
120        network.doc_ticket = doc_ticket;
121        self.db.create_network(&network)?;
122        self.networks.insert(id, network.clone());
123        self.log_event(
124            &id,
125            NetworkEventType::MemberAdded,
126            Some(&self.local_node),
127            Some("created"),
128        );
129        if network.is_invite_only() {
130            self.log_event(
131                &id,
132                NetworkEventType::TicketCreated,
133                None,
134                Some("invite-only network created"),
135            );
136        }
137        Ok(id)
138    }
139
140    /// Join and persist the network represented by a ticket.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if the ticket is for another device or persistence fails.
145    pub fn join(&mut self, ticket: NetworkTicket) -> Result<NetworkId> {
146        if ticket.name.trim().is_empty() || NetworkId::from_name(ticket.name.trim()) != ticket.network_id {
147            return Err(SyncwebError::InvalidTicket(
148                "network ticket ID does not match its network name".to_owned(),
149            ));
150        }
151        if ticket.invited_node.is_some_and(|invited| invited != self.local_node) {
152            return Err(SyncwebError::InvalidTicket(
153                "network ticket was issued for another device".to_owned(),
154            ));
155        }
156        if self.networks.contains_key(&ticket.network_id) {
157            return Err(SyncwebError::InvalidConfig(format!(
158                "already joined network {}",
159                ticket.network_id,
160            )));
161        }
162        let mut members = ticket.members;
163        members.insert(self.local_node);
164        let network = Network {
165            id: ticket.network_id,
166            name: ticket.name,
167            label: ticket.label,
168            owner: ticket.owner,
169            members,
170            folders: ticket.folders,
171            shared_secret: ticket.shared_secret,
172            doc_ticket: ticket.doc_ticket,
173        };
174        let id = network.id;
175        self.db.create_network(&network)?;
176        self.networks.insert(id, network);
177        self.log_event(
178            &id,
179            NetworkEventType::MemberAdded,
180            Some(&self.local_node),
181            Some("joined via ticket"),
182        );
183        self.sync_member_keys();
184        self.log_event(&id, NetworkEventType::TicketAccepted, None, Some("network joined"));
185        Ok(id)
186    }
187
188    /// Leave a network and remove its local state.
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the network does not exist or persistence fails.
193    pub fn leave(&mut self, id: NetworkId) -> Result<()> {
194        self.log_event(
195            &id,
196            NetworkEventType::MemberRemoved,
197            Some(&self.local_node),
198            Some("left network"),
199        );
200        self.networks
201            .remove(&id)
202            .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))?;
203        self.sync_member_keys();
204        self.db.delete_network(id)
205    }
206
207    /// Generate a device-bound invitation and add the device as a member.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if the network does not exist, the local node is not
212    /// its owner, or persistence fails.
213    pub fn invite(&mut self, id: NetworkId, device: PublicKey) -> Result<NetworkTicket> {
214        let network = self.network_mut_as_owner(id)?;
215        network.members.insert(device);
216        let ticket = NetworkTicket {
217            network_id: network.id,
218            name: network.name.clone(),
219            label: network.label.clone(),
220            owner: network.owner,
221            invited_node: Some(device),
222            members: network.members.clone(),
223            folders: network.folders.clone(),
224            shared_secret: network.shared_secret,
225            doc_ticket: network.doc_ticket.clone(),
226        };
227        self.db.add_member(id, device)?;
228        self.sync_member_keys();
229        self.log_event(&id, NetworkEventType::MemberAdded, Some(&device), Some("invited"));
230        self.log_event(
231            &id,
232            NetworkEventType::TicketCreated,
233            None,
234            Some("device-bound ticket generated"),
235        );
236        Ok(ticket)
237    }
238
239    /// Generate an invitation usable by any device.
240    ///
241    /// # Errors
242    ///
243    /// Returns an error if the network does not exist or the local node is not its owner.
244    pub fn invite_any(&self, id: NetworkId) -> Result<NetworkTicket> {
245        let network = self.network_as_owner(id)?;
246        self.log_event(
247            &id,
248            NetworkEventType::TicketCreated,
249            None,
250            Some("open ticket generated"),
251        );
252        Ok(NetworkTicket {
253            network_id: network.id,
254            name: network.name.clone(),
255            label: network.label.clone(),
256            owner: network.owner,
257            invited_node: None,
258            members: network.members.clone(),
259            folders: network.folders.clone(),
260            shared_secret: network.shared_secret,
261            doc_ticket: network.doc_ticket.clone(),
262        })
263    }
264
265    /// Remove a member from a locally owned network.
266    ///
267    /// # Errors
268    ///
269    /// Returns an error if the network does not exist, authorization fails,
270    /// the owner is targeted, or persistence fails.
271    pub fn kick(&mut self, id: NetworkId, device: &PublicKey) -> Result<()> {
272        let network = self.network_mut_as_owner(id)?;
273        if network.owner == *device {
274            return Err(SyncwebError::InvalidConfig(
275                "the network owner cannot be kicked".to_owned(),
276            ));
277        }
278        if !network.members.remove(device) {
279            return Err(SyncwebError::InvalidConfig("device is not a network member".to_owned()));
280        }
281        self.db.remove_member(id, device)?;
282        self.sync_member_keys();
283        self.log_event(&id, NetworkEventType::MemberRemoved, Some(device), Some("kicked"));
284        Ok(())
285    }
286
287    /// Set (or clear) the membership-doc ticket on a network.
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if the network does not exist or persistence fails.
292    pub fn set_doc_ticket(&mut self, id: NetworkId, doc_ticket: &Option<String>) -> Result<()> {
293        let network = self
294            .networks
295            .get_mut(&id)
296            .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))?;
297        network.doc_ticket.clone_from(doc_ticket);
298        self.db.set_network_doc_ticket(id, doc_ticket)
299    }
300
301    /// Associate a folder with a network.
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if the network does not exist or persistence fails.
306    pub fn add_folder(&mut self, id: NetworkId, folder: NamespaceId) -> Result<()> {
307        self.network_mut(id)?.folders.insert(folder);
308        self.log_event(
309            &id,
310            NetworkEventType::FolderAdded,
311            None,
312            Some(&format!("folder {folder}")),
313        );
314        self.db.add_folder_to_network(id, folder)
315    }
316
317    /// Remove a folder association without changing the folder itself.
318    ///
319    /// # Errors
320    ///
321    /// Returns an error if the network does not exist or persistence fails.
322    pub fn remove_folder(&mut self, id: NetworkId, folder: NamespaceId) -> Result<()> {
323        self.network_mut(id)?.folders.remove(&folder);
324        self.log_event(
325            &id,
326            NetworkEventType::FolderRemoved,
327            None,
328            Some(&format!("folder {folder}")),
329        );
330        self.db.remove_folder_from_network(id, folder)
331    }
332
333    #[must_use]
334    pub fn list(&self) -> Vec<&Network> {
335        let mut networks = self.networks.values().collect::<Vec<_>>();
336        networks.sort_by(|left, right| left.name.cmp(&right.name));
337        networks
338    }
339
340    #[must_use]
341    pub fn get(&self, id: &NetworkId) -> Option<&Network> {
342        self.networks.get(id)
343    }
344
345    #[must_use]
346    pub fn get_by_name(&self, name: &str) -> Option<&Network> {
347        self.networks.values().find(|network| network.name == name)
348    }
349
350    /// Return the network IDs (as strings) that contain a given folder.
351    ///
352    /// # Errors
353    ///
354    /// Returns an error if the database query fails.
355    pub fn networks_for_folder(&self, namespace_id: &NamespaceId) -> Result<Vec<String>> {
356        self.db.networks_for_folder(&namespace_id.to_string())
357    }
358
359    /// Return the first network ID that contains a given folder, if any.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if the database query fails.
364    pub fn network_for_folder(&self, namespace_id: &NamespaceId) -> Result<Option<String>> {
365        Ok(self
366            .db
367            .networks_for_folder(&namespace_id.to_string())?
368            .into_iter()
369            .next())
370    }
371
372    /// List all folder namespace IDs associated with a network.
373    ///
374    /// # Errors
375    ///
376    /// Returns an error if the database query fails.
377    pub fn folders_for_network(&self, network_id: &NetworkId) -> Result<Vec<NamespaceId>> {
378        let folders = self.db.folders_for_network(&network_id.to_string())?;
379        folders
380            .into_iter()
381            .map(|f| {
382                f.parse()
383                    .map_err(|error| SyncwebError::operation("invalid namespace in database", error))
384            })
385            .collect()
386    }
387
388    /// List all members of a network.
389    ///
390    /// # Errors
391    ///
392    /// Returns an error if the database query fails.
393    pub fn members_of_network(&self, network_id: &NetworkId) -> Result<Vec<iroh::PublicKey>> {
394        let members = self.db.members_of_network(&network_id.to_string())?;
395        members
396            .into_iter()
397            .map(|m| {
398                m.parse()
399                    .map_err(|error| SyncwebError::operation("invalid member key in database", error))
400            })
401            .collect()
402    }
403
404    /// Sync the `member_keys` set from all networks.
405    fn sync_member_keys(&self) {
406        let keys: HashSet<iroh::PublicKey> = self.networks.values().flat_map(|n| n.members.iter().copied()).collect();
407        if let Ok(mut guard) = self.member_keys.write() {
408            *guard = keys;
409        }
410    }
411
412    fn log_event(
413        &self,
414        network_id: &NetworkId,
415        event: NetworkEventType,
416        peer: Option<&iroh::PublicKey>,
417        details: Option<&str>,
418    ) {
419        if let Some(ref logger) = self.logger {
420            let peer_str = peer.map(std::string::ToString::to_string);
421            let _ = logger.record_event(&network_id.to_string(), event, peer_str.as_deref(), details);
422        }
423    }
424
425    fn network_mut(&mut self, id: NetworkId) -> Result<&mut Network> {
426        self.networks
427            .get_mut(&id)
428            .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))
429    }
430
431    fn network_as_owner(&self, id: NetworkId) -> Result<&Network> {
432        let network = self
433            .networks
434            .get(&id)
435            .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))?;
436        if network.owner != self.local_node {
437            return Err(SyncwebError::InvalidConfig(
438                "only the network owner can manage invitations".to_owned(),
439            ));
440        }
441        Ok(network)
442    }
443
444    fn network_mut_as_owner(&mut self, id: NetworkId) -> Result<&mut Network> {
445        self.network_as_owner(id)?;
446        self.network_mut(id)
447    }
448}