1use std::{
2 collections::HashSet,
3 fmt::{Display, Formatter},
4 str::FromStr,
5};
6
7use iroh::PublicKey;
8use iroh_docs::NamespaceId;
9use serde::{Deserialize, Serialize};
10
11use crate::{Result, SyncwebError};
12
13#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub struct NetworkId([u8; 32]);
16
17impl NetworkId {
18 #[must_use]
19 pub fn from_name(name: &str) -> Self {
20 let mut hasher = blake3::Hasher::new();
21 hasher.update(b"network:");
22 hasher.update(name.as_bytes());
23 Self(*hasher.finalize().as_bytes())
24 }
25
26 #[must_use]
27 pub const fn as_bytes(&self) -> &[u8; 32] {
28 &self.0
29 }
30}
31
32impl Display for NetworkId {
33 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
34 formatter.write_str(&hex::encode(self.0))
35 }
36}
37
38impl FromStr for NetworkId {
39 type Err = SyncwebError;
40
41 fn from_str(value: &str) -> Result<Self> {
42 let bytes =
43 hex::decode(value).map_err(|error| SyncwebError::InvalidConfig(format!("invalid network ID: {error}")))?;
44 let array: [u8; 32] = bytes
45 .try_into()
46 .map_err(|_length_error| SyncwebError::InvalidConfig("network ID must contain 32 bytes".to_owned()))?;
47 Ok(Self(array))
48 }
49}
50
51#[derive(Clone, Debug, Default, Eq, PartialEq)]
53#[non_exhaustive]
54pub struct NetworkOptions {
55 pub label: String,
56 pub invite_only: bool,
57}
58
59impl NetworkOptions {
60 #[must_use]
61 pub fn with_label(mut self, label: impl Into<String>) -> Self {
62 self.label = label.into();
63 self
64 }
65
66 #[must_use]
67 pub const fn invite_only(mut self, invite_only: bool) -> Self {
68 self.invite_only = invite_only;
69 self
70 }
71}
72
73#[derive(Clone, Debug)]
75#[non_exhaustive]
76pub struct Network {
77 pub id: NetworkId,
78 pub name: String,
79 pub label: String,
80 pub owner: PublicKey,
81 pub members: HashSet<PublicKey>,
82 pub folders: HashSet<NamespaceId>,
83 pub(crate) shared_secret: Option<[u8; 32]>,
84 pub doc_ticket: Option<String>,
85}
86
87impl Network {
88 #[must_use]
89 pub fn new(name_arg: impl Into<String>, owner: PublicKey, options: NetworkOptions) -> Self {
90 let name = name_arg.into().trim().to_owned();
91 let id = NetworkId::from_name(&name);
92 let mut members = HashSet::new();
93 members.insert(owner);
94 Self {
95 id,
96 name,
97 label: options.label,
98 owner,
99 members,
100 folders: HashSet::new(),
101 shared_secret: options.invite_only.then(rand::random),
102 doc_ticket: None,
103 }
104 }
105
106 #[must_use]
107 pub fn is_member(&self, node_id: &PublicKey) -> bool {
108 self.members.contains(node_id)
109 }
110
111 #[must_use]
112 pub const fn is_invite_only(&self) -> bool {
113 self.shared_secret.is_some()
114 }
115}
116
117#[derive(Clone, Debug, Eq, PartialEq)]
119pub struct NetworkTicket {
120 pub network_id: NetworkId,
121 pub name: String,
122 pub label: String,
123 pub owner: PublicKey,
124 pub invited_node: Option<PublicKey>,
125 pub members: HashSet<PublicKey>,
126 pub folders: HashSet<NamespaceId>,
127 pub(crate) shared_secret: Option<[u8; 32]>,
128 pub doc_ticket: Option<String>,
129}
130
131impl NetworkTicket {
132 #[must_use]
133 pub const fn is_invite_only(&self) -> bool {
134 self.shared_secret.is_some()
135 }
136}
137
138impl Display for NetworkTicket {
139 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
140 let wire = TicketWire::from(self);
141 let bytes = serde_json::to_vec(&wire).map_err(|_serialization_error| std::fmt::Error)?;
142 write!(formatter, "syncweb://network/{}", hex::encode(bytes))
143 }
144}
145
146impl FromStr for NetworkTicket {
147 type Err = SyncwebError;
148
149 fn from_str(value: &str) -> Result<Self> {
150 let encoded = value.strip_prefix("syncweb://network/").ok_or_else(|| {
151 SyncwebError::InvalidTicket("network ticket must start with syncweb://network/".to_owned())
152 })?;
153 let bytes = hex::decode(encoded)
154 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid network ticket encoding: {error}")))?;
155 let wire: TicketWire = serde_json::from_slice(&bytes)
156 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid network ticket: {error}")))?;
157 wire.try_into()
158 }
159}
160
161#[derive(Deserialize, Serialize)]
162struct TicketWire {
163 network_id: String,
164 name: String,
165 label: String,
166 owner: String,
167 invited_node: Option<String>,
168 members: Vec<String>,
169 folders: Vec<String>,
170 shared_secret: Option<String>,
171 #[serde(default)]
172 doc_ticket: Option<String>,
173}
174
175impl From<&NetworkTicket> for TicketWire {
176 fn from(ticket: &NetworkTicket) -> Self {
177 let mut members = ticket.members.iter().map(ToString::to_string).collect::<Vec<_>>();
178 members.sort_unstable();
179 let mut folders = ticket.folders.iter().map(ToString::to_string).collect::<Vec<_>>();
180 folders.sort_unstable();
181 Self {
182 network_id: ticket.network_id.to_string(),
183 name: ticket.name.clone(),
184 label: ticket.label.clone(),
185 owner: ticket.owner.to_string(),
186 invited_node: ticket.invited_node.map(|node| node.to_string()),
187 members,
188 folders,
189 shared_secret: ticket.shared_secret.map(hex::encode),
190 doc_ticket: ticket.doc_ticket.clone(),
191 }
192 }
193}
194
195impl TryFrom<TicketWire> for NetworkTicket {
196 type Error = SyncwebError;
197
198 fn try_from(wire: TicketWire) -> Result<Self> {
199 if wire.name.trim().is_empty() {
200 return Err(SyncwebError::InvalidTicket("network name cannot be empty".to_owned()));
201 }
202 let network_id: NetworkId = wire.network_id.parse()?;
203 if network_id != NetworkId::from_name(wire.name.trim()) {
204 return Err(SyncwebError::InvalidTicket(
205 "network ticket ID does not match its network name".to_owned(),
206 ));
207 }
208 let members = wire
209 .members
210 .into_iter()
211 .map(|member| parse_public_key(&member))
212 .collect::<Result<HashSet<_>>>()?;
213 let folders = wire
214 .folders
215 .into_iter()
216 .map(|folder| {
217 folder
218 .parse()
219 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid folder namespace: {error}")))
220 })
221 .collect::<Result<HashSet<_>>>()?;
222 let shared_secret = wire
223 .shared_secret
224 .map(|secret| {
225 let bytes = hex::decode(secret)
226 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid network secret: {error}")))?;
227 bytes.try_into().map_err(|_length_error| {
228 SyncwebError::InvalidTicket("network secret must contain 32 bytes".to_owned())
229 })
230 })
231 .transpose()?;
232 Ok(Self {
233 network_id,
234 name: wire.name,
235 label: wire.label,
236 owner: parse_public_key(&wire.owner)?,
237 invited_node: wire.invited_node.map(|node| parse_public_key(&node)).transpose()?,
238 members,
239 folders,
240 shared_secret,
241 doc_ticket: wire.doc_ticket,
242 })
243 }
244}
245
246pub(crate) fn parse_public_key(value: &str) -> Result<PublicKey> {
247 value
248 .parse()
249 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid node ID: {error}")))
250}