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#[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 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 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 #[must_use]
70 pub const fn logger(&self) -> &Option<NetworkLogger> {
71 &self.logger
72 }
73
74 pub fn set_logger(&mut self, logger: NetworkLogger) {
76 self.logger = Some(logger);
77 }
78
79 #[must_use]
81 pub const fn database(&self) -> &NodeDatabase {
82 &self.db
83 }
84
85 #[must_use]
87 pub const fn local_node(&self) -> &iroh::PublicKey {
88 &self.local_node
89 }
90
91 pub fn create(&mut self, name: &str, options: NetworkOptions) -> Result<NetworkId> {
97 self.create_with_doc_ticket(name, options, None)
98 }
99
100 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}