1use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::fs;
8use std::path::PathBuf;
9
10pub const DEFAULT_READ_SERVERS: &[&str] = &[
12 "https://cdn.iris.to",
13 "https://hashtree.iris.to",
14];
15
16pub const DEFAULT_WRITE_SERVERS: &[&str] = &[
18 "https://upload.iris.to",
19];
20
21pub const DEFAULT_RELAYS: &[&str] = &[
23 "wss://temp.iris.to",
24 "wss://relay.damus.io",
25 "wss://nos.lol",
26 "wss://relay.primal.net",
27];
28
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
31pub struct Config {
32 #[serde(default)]
33 pub server: ServerConfig,
34 #[serde(default)]
35 pub storage: StorageConfig,
36 #[serde(default)]
37 pub nostr: NostrConfig,
38 #[serde(default)]
39 pub blossom: BlossomConfig,
40 #[serde(default)]
41 pub sync: SyncConfig,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ServerConfig {
47 #[serde(default = "default_bind_address")]
48 pub bind_address: String,
49 #[serde(default = "default_true")]
50 pub enable_auth: bool,
51 #[serde(default)]
52 pub public_writes: bool,
53 #[serde(default)]
54 pub enable_webrtc: bool,
55 #[serde(default)]
56 pub stun_port: u16,
57}
58
59impl Default for ServerConfig {
60 fn default() -> Self {
61 Self {
62 bind_address: default_bind_address(),
63 enable_auth: true,
64 public_writes: false,
65 enable_webrtc: false,
66 stun_port: 0,
67 }
68 }
69}
70
71fn default_bind_address() -> String {
72 "127.0.0.1:8080".to_string()
73}
74
75fn default_true() -> bool {
76 true
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct StorageConfig {
82 #[serde(default = "default_data_dir")]
83 pub data_dir: String,
84 #[serde(default = "default_max_size_gb")]
85 pub max_size_gb: u64,
86 #[serde(default)]
87 pub s3: Option<S3Config>,
88}
89
90impl Default for StorageConfig {
91 fn default() -> Self {
92 Self {
93 data_dir: default_data_dir(),
94 max_size_gb: default_max_size_gb(),
95 s3: None,
96 }
97 }
98}
99
100fn default_data_dir() -> String {
101 get_hashtree_dir()
102 .join("data")
103 .to_string_lossy()
104 .to_string()
105}
106
107fn default_max_size_gb() -> u64 {
108 10
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct S3Config {
114 pub endpoint: String,
115 pub bucket: String,
116 pub region: String,
117 #[serde(default)]
118 pub prefix: Option<String>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct NostrConfig {
124 #[serde(default = "default_relays")]
125 pub relays: Vec<String>,
126 #[serde(default)]
127 pub allowed_npubs: Vec<String>,
128}
129
130impl Default for NostrConfig {
131 fn default() -> Self {
132 Self {
133 relays: default_relays(),
134 allowed_npubs: vec![],
135 }
136 }
137}
138
139fn default_relays() -> Vec<String> {
140 DEFAULT_RELAYS.iter().map(|s| s.to_string()).collect()
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct BlossomConfig {
146 #[serde(default)]
148 pub servers: Vec<String>,
149 #[serde(default = "default_read_servers")]
151 pub read_servers: Vec<String>,
152 #[serde(default = "default_write_servers")]
154 pub write_servers: Vec<String>,
155 #[serde(default = "default_max_upload_mb")]
157 pub max_upload_mb: u64,
158 #[serde(default)]
160 pub force_upload: bool,
161}
162
163impl Default for BlossomConfig {
164 fn default() -> Self {
165 Self {
166 servers: vec![],
167 read_servers: default_read_servers(),
168 write_servers: default_write_servers(),
169 max_upload_mb: default_max_upload_mb(),
170 force_upload: false,
171 }
172 }
173}
174
175fn default_read_servers() -> Vec<String> {
176 DEFAULT_READ_SERVERS.iter().map(|s| s.to_string()).collect()
177}
178
179fn default_write_servers() -> Vec<String> {
180 DEFAULT_WRITE_SERVERS.iter().map(|s| s.to_string()).collect()
181}
182
183fn default_max_upload_mb() -> u64 {
184 100
185}
186
187impl BlossomConfig {
188 pub fn all_read_servers(&self) -> Vec<String> {
190 let mut servers = self.servers.clone();
191 servers.extend(self.read_servers.clone());
192 servers.sort();
193 servers.dedup();
194 servers
195 }
196
197 pub fn all_write_servers(&self) -> Vec<String> {
199 let mut servers = self.servers.clone();
200 servers.extend(self.write_servers.clone());
201 servers.sort();
202 servers.dedup();
203 servers
204 }
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct SyncConfig {
210 #[serde(default)]
211 pub enabled: bool,
212 #[serde(default = "default_true")]
213 pub sync_own: bool,
214 #[serde(default)]
215 pub sync_followed: bool,
216 #[serde(default = "default_max_concurrent")]
217 pub max_concurrent: usize,
218 #[serde(default = "default_webrtc_timeout_ms")]
219 pub webrtc_timeout_ms: u64,
220 #[serde(default = "default_blossom_timeout_ms")]
221 pub blossom_timeout_ms: u64,
222}
223
224impl Default for SyncConfig {
225 fn default() -> Self {
226 Self {
227 enabled: false,
228 sync_own: true,
229 sync_followed: false,
230 max_concurrent: default_max_concurrent(),
231 webrtc_timeout_ms: default_webrtc_timeout_ms(),
232 blossom_timeout_ms: default_blossom_timeout_ms(),
233 }
234 }
235}
236
237fn default_max_concurrent() -> usize {
238 4
239}
240
241fn default_webrtc_timeout_ms() -> u64 {
242 5000
243}
244
245fn default_blossom_timeout_ms() -> u64 {
246 10000
247}
248
249impl Config {
250 pub fn load() -> Result<Self> {
252 let config_path = get_config_path();
253
254 if config_path.exists() {
255 let content = fs::read_to_string(&config_path)
256 .context("Failed to read config file")?;
257 toml::from_str(&content).context("Failed to parse config file")
258 } else {
259 let config = Config::default();
260 config.save()?;
261 Ok(config)
262 }
263 }
264
265 pub fn load_or_default() -> Self {
267 Self::load().unwrap_or_default()
268 }
269
270 pub fn save(&self) -> Result<()> {
272 let config_path = get_config_path();
273
274 if let Some(parent) = config_path.parent() {
275 fs::create_dir_all(parent)?;
276 }
277
278 let content = toml::to_string_pretty(self)?;
279 fs::write(&config_path, content)?;
280
281 Ok(())
282 }
283}
284
285pub fn get_hashtree_dir() -> PathBuf {
287 if let Ok(dir) = std::env::var("HTREE_CONFIG_DIR") {
288 return PathBuf::from(dir);
289 }
290 dirs::home_dir()
291 .unwrap_or_else(|| PathBuf::from("."))
292 .join(".hashtree")
293}
294
295pub fn get_config_path() -> PathBuf {
297 get_hashtree_dir().join("config.toml")
298}
299
300pub fn get_keys_path() -> PathBuf {
302 get_hashtree_dir().join("keys")
303}
304
305pub fn get_auth_cookie_path() -> PathBuf {
307 get_hashtree_dir().join("auth.cookie")
308}
309
310pub fn get_data_dir() -> PathBuf {
313 if let Ok(dir) = std::env::var("HTREE_DATA_DIR") {
314 return PathBuf::from(dir);
315 }
316 let config = Config::load_or_default();
317 PathBuf::from(&config.storage.data_dir)
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_default_config() {
326 let config = Config::default();
327 assert!(!config.blossom.read_servers.is_empty());
328 assert!(!config.blossom.write_servers.is_empty());
329 assert!(!config.nostr.relays.is_empty());
330 }
331
332 #[test]
333 fn test_parse_empty_config() {
334 let config: Config = toml::from_str("").unwrap();
335 assert!(!config.blossom.read_servers.is_empty());
336 }
337
338 #[test]
339 fn test_parse_partial_config() {
340 let toml = r#"
341[blossom]
342write_servers = ["https://custom.server"]
343"#;
344 let config: Config = toml::from_str(toml).unwrap();
345 assert_eq!(config.blossom.write_servers, vec!["https://custom.server"]);
346 assert!(!config.blossom.read_servers.is_empty());
347 }
348
349 #[test]
350 fn test_all_servers() {
351 let mut config = BlossomConfig::default();
352 config.servers = vec!["https://legacy.server".to_string()];
353
354 let read = config.all_read_servers();
355 assert!(read.contains(&"https://legacy.server".to_string()));
356 assert!(read.contains(&"https://cdn.iris.to".to_string()));
357
358 let write = config.all_write_servers();
359 assert!(write.contains(&"https://legacy.server".to_string()));
360 assert!(write.contains(&"https://upload.iris.to".to_string()));
361 }
362}