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