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 if self.networks.contains_key(&ticket.network_id) {
159 return Err(SyncwebError::InvalidConfig(format!(
160 "already joined network {}",
161 ticket.network_id,
162 )));
163 }
164 let mut members = ticket.members;
165 members.insert(self.local_node);
166 let network = Network {
167 id: ticket.network_id,
168 name: ticket.name,
169 label: ticket.label,
170 topic: network_topic(ticket.network_id),
171 owner: ticket.owner,
172 members,
173 folders: ticket.folders,
174 shared_secret: ticket.shared_secret,
175 doc_ticket: ticket.doc_ticket,
176 };
177 let id = network.id;
178 self.db.create_network(&network)?;
179 self.networks.insert(id, network);
180 self.log_event(
181 &id,
182 NetworkEventType::MemberAdded,
183 Some(&self.local_node),
184 Some("joined via ticket"),
185 );
186 self.sync_member_keys();
187 self.log_event(&id, NetworkEventType::TicketAccepted, None, Some("network joined"));
188 Ok(id)
189 }
190
191 pub fn leave(&mut self, id: NetworkId) -> Result<()> {
197 self.log_event(
198 &id,
199 NetworkEventType::MemberRemoved,
200 Some(&self.local_node),
201 Some("left network"),
202 );
203 self.networks
204 .remove(&id)
205 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))?;
206 self.sync_member_keys();
207 self.db.delete_network(id)
208 }
209
210 pub fn invite(&mut self, id: NetworkId, device: PublicKey) -> Result<NetworkTicket> {
217 let network = self.network_mut_as_owner(id)?;
218 network.members.insert(device);
219 let ticket = NetworkTicket {
220 network_id: network.id,
221 name: network.name.clone(),
222 label: network.label.clone(),
223 owner: network.owner,
224 invited_node: Some(device),
225 members: network.members.clone(),
226 folders: network.folders.clone(),
227 shared_secret: network.shared_secret,
228 doc_ticket: network.doc_ticket.clone(),
229 };
230 self.db.add_member(id, device)?;
231 self.sync_member_keys();
232 self.log_event(&id, NetworkEventType::MemberAdded, Some(&device), Some("invited"));
233 self.log_event(
234 &id,
235 NetworkEventType::TicketCreated,
236 None,
237 Some("device-bound ticket generated"),
238 );
239 Ok(ticket)
240 }
241
242 pub fn invite_any(&self, id: NetworkId) -> Result<NetworkTicket> {
248 let network = self.network_as_owner(id)?;
249 self.log_event(
250 &id,
251 NetworkEventType::TicketCreated,
252 None,
253 Some("open ticket generated"),
254 );
255 Ok(NetworkTicket {
256 network_id: network.id,
257 name: network.name.clone(),
258 label: network.label.clone(),
259 owner: network.owner,
260 invited_node: None,
261 members: network.members.clone(),
262 folders: network.folders.clone(),
263 shared_secret: network.shared_secret,
264 doc_ticket: network.doc_ticket.clone(),
265 })
266 }
267
268 pub fn kick(&mut self, id: NetworkId, device: &PublicKey) -> Result<()> {
275 let network = self.network_mut_as_owner(id)?;
276 if network.owner == *device {
277 return Err(SyncwebError::InvalidConfig(
278 "the network owner cannot be kicked".to_owned(),
279 ));
280 }
281 if !network.members.remove(device) {
282 return Err(SyncwebError::InvalidConfig("device is not a network member".to_owned()));
283 }
284 self.db.remove_member(id, device)?;
285 self.sync_member_keys();
286 self.log_event(&id, NetworkEventType::MemberRemoved, Some(device), Some("kicked"));
287 Ok(())
288 }
289
290 pub fn add_folder(&mut self, id: NetworkId, folder: NamespaceId) -> Result<()> {
296 self.network_mut(id)?.folders.insert(folder);
297 self.log_event(
298 &id,
299 NetworkEventType::FolderAdded,
300 None,
301 Some(&format!("folder {folder}")),
302 );
303 self.db.add_folder_to_network(id, folder)
304 }
305
306 pub fn remove_folder(&mut self, id: NetworkId, folder: NamespaceId) -> Result<()> {
312 self.network_mut(id)?.folders.remove(&folder);
313 self.log_event(
314 &id,
315 NetworkEventType::FolderRemoved,
316 None,
317 Some(&format!("folder {folder}")),
318 );
319 self.db.remove_folder_from_network(id, folder)
320 }
321
322 pub async fn subscribe(&self, id: NetworkId, gossip: &GossipService) -> Result<GossipTopic> {
332 let network = self
333 .networks
334 .get(&id)
335 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))?;
336 if !network.is_member(&self.local_node) {
337 return Err(SyncwebError::InvalidConfig(
338 "local device is not a member of this network".to_owned(),
339 ));
340 }
341 let mut bootstrap = network
342 .members
343 .iter()
344 .copied()
345 .filter(|member| *member != self.local_node)
346 .collect::<Vec<_>>();
347 bootstrap.sort_unstable();
348 gossip.subscribe(network.topic, bootstrap).await
349 }
350
351 pub fn topic(&self, id: NetworkId) -> Result<TopicId> {
357 self.networks
358 .get(&id)
359 .map(|network| network.topic)
360 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))
361 }
362
363 #[must_use]
364 pub fn list(&self) -> Vec<&Network> {
365 let mut networks = self.networks.values().collect::<Vec<_>>();
366 networks.sort_by(|left, right| left.name.cmp(&right.name));
367 networks
368 }
369
370 #[must_use]
371 pub fn get(&self, id: &NetworkId) -> Option<&Network> {
372 self.networks.get(id)
373 }
374
375 #[must_use]
376 pub fn get_by_name(&self, name: &str) -> Option<&Network> {
377 self.networks.values().find(|network| network.name == name)
378 }
379
380 pub fn can_access_folder(&self, namespace_id: &NamespaceId) -> Result<bool> {
386 self.db
387 .can_access_folder(&namespace_id.to_string(), &self.local_node.to_string())
388 }
389
390 pub fn networks_for_folder(&self, namespace_id: &NamespaceId) -> Result<Vec<String>> {
396 self.db.networks_for_folder(&namespace_id.to_string())
397 }
398
399 pub fn network_for_folder(&self, namespace_id: &NamespaceId) -> Result<Option<String>> {
405 Ok(self
406 .db
407 .networks_for_folder(&namespace_id.to_string())?
408 .into_iter()
409 .next())
410 }
411
412 pub fn folders_for_network(&self, network_id: &NetworkId) -> Result<Vec<NamespaceId>> {
418 let folders = self.db.folders_for_network(&network_id.to_string())?;
419 folders
420 .into_iter()
421 .map(|f| {
422 f.parse()
423 .map_err(|error| SyncwebError::operation("invalid namespace in database", error))
424 })
425 .collect()
426 }
427
428 pub fn members_of_network(&self, network_id: &NetworkId) -> Result<Vec<iroh::PublicKey>> {
434 let members = self.db.members_of_network(&network_id.to_string())?;
435 members
436 .into_iter()
437 .map(|m| {
438 m.parse()
439 .map_err(|error| SyncwebError::operation("invalid member key in database", error))
440 })
441 .collect()
442 }
443
444 fn sync_member_keys(&self) {
446 let keys: HashSet<iroh::PublicKey> = self.networks.values().flat_map(|n| n.members.iter().copied()).collect();
447 if let Ok(mut guard) = self.member_keys.write() {
448 *guard = keys;
449 }
450 }
451
452 fn log_event(
453 &self,
454 network_id: &NetworkId,
455 event: NetworkEventType,
456 peer: Option<&iroh::PublicKey>,
457 details: Option<&str>,
458 ) {
459 if let Some(ref logger) = self.logger {
460 let peer_str = peer.map(std::string::ToString::to_string);
461 let _ = logger.record_event(&network_id.to_string(), event, peer_str.as_deref(), details);
462 }
463 }
464
465 fn network_mut(&mut self, id: NetworkId) -> Result<&mut Network> {
466 self.networks
467 .get_mut(&id)
468 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))
469 }
470
471 fn network_as_owner(&self, id: NetworkId) -> Result<&Network> {
472 let network = self
473 .networks
474 .get(&id)
475 .ok_or_else(|| SyncwebError::FolderNotFound(format!("network {id}")))?;
476 if network.owner != self.local_node {
477 return Err(SyncwebError::InvalidConfig(
478 "only the network owner can manage invitations".to_owned(),
479 ));
480 }
481 Ok(network)
482 }
483
484 fn network_mut_as_owner(&mut self, id: NetworkId) -> Result<&mut Network> {
485 self.network_as_owner(id)?;
486 self.network_mut(id)
487 }
488}