1use anyhow::{Context, Result};
2use nostr::nips::nip19::{FromBech32, ToBech32};
3use nostr::{Keys, SecretKey};
4use serde::{Deserialize, Serialize};
5use std::fs;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9 #[serde(default)]
10 pub server: ServerConfig,
11 #[serde(default)]
12 pub storage: StorageConfig,
13 #[serde(default)]
14 pub nostr: NostrConfig,
15 #[serde(default)]
16 pub blossom: BlossomConfig,
17 #[serde(default)]
18 pub sync: SyncConfig,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ServerConfig {
23 #[serde(default = "default_bind_address")]
24 pub bind_address: String,
25 #[serde(default = "default_enable_auth")]
26 pub enable_auth: bool,
27 #[serde(default = "default_stun_port")]
29 pub stun_port: u16,
30 #[serde(default = "default_enable_webrtc")]
32 pub enable_webrtc: bool,
33 #[serde(default = "default_public_writes")]
36 pub public_writes: bool,
37 #[serde(default = "default_socialgraph_snapshot_public")]
39 pub socialgraph_snapshot_public: bool,
40}
41
42fn default_public_writes() -> bool {
43 true
44}
45
46fn default_socialgraph_snapshot_public() -> bool {
47 false
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct StorageConfig {
52 #[serde(default = "default_data_dir")]
53 pub data_dir: String,
54 #[serde(default = "default_max_size_gb")]
55 pub max_size_gb: u64,
56 #[serde(default)]
58 pub s3: Option<S3Config>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct S3Config {
64 pub endpoint: String,
66 pub bucket: String,
68 #[serde(default)]
70 pub prefix: Option<String>,
71 #[serde(default = "default_s3_region")]
73 pub region: String,
74 #[serde(default)]
76 pub access_key: Option<String>,
77 #[serde(default)]
79 pub secret_key: Option<String>,
80 #[serde(default)]
82 pub public_url: Option<String>,
83}
84
85fn default_s3_region() -> String {
86 "auto".to_string()
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct NostrConfig {
91 #[serde(default = "default_relays")]
92 pub relays: Vec<String>,
93 #[serde(default)]
95 pub allowed_npubs: Vec<String>,
96 #[serde(default)]
98 pub socialgraph_root: Option<String>,
99 #[serde(default = "default_crawl_depth")]
101 pub crawl_depth: u32,
102 #[serde(default = "default_max_write_distance")]
104 pub max_write_distance: u32,
105 #[serde(default = "default_nostr_db_max_size_gb")]
107 pub db_max_size_gb: u64,
108 #[serde(default = "default_nostr_spambox_max_size_gb")]
111 pub spambox_max_size_gb: u64,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct BlossomConfig {
116 #[serde(default)]
118 pub servers: Vec<String>,
119 #[serde(default = "default_read_servers")]
121 pub read_servers: Vec<String>,
122 #[serde(default = "default_write_servers")]
124 pub write_servers: Vec<String>,
125 #[serde(default = "default_max_upload_mb")]
127 pub max_upload_mb: u64,
128}
129
130fn default_read_servers() -> Vec<String> {
132 vec![
133 "https://cdn.iris.to".to_string(),
134 "https://hashtree.iris.to".to_string(),
135 ]
136}
137
138fn default_write_servers() -> Vec<String> {
139 vec!["https://upload.iris.to".to_string()]
140}
141
142fn default_max_upload_mb() -> u64 {
143 5
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct SyncConfig {
148 #[serde(default = "default_sync_enabled")]
150 pub enabled: bool,
151 #[serde(default = "default_sync_own")]
153 pub sync_own: bool,
154 #[serde(default = "default_sync_followed")]
156 pub sync_followed: bool,
157 #[serde(default = "default_max_concurrent")]
159 pub max_concurrent: usize,
160 #[serde(default = "default_webrtc_timeout_ms")]
162 pub webrtc_timeout_ms: u64,
163 #[serde(default = "default_blossom_timeout_ms")]
165 pub blossom_timeout_ms: u64,
166}
167
168fn default_sync_enabled() -> bool {
169 true
170}
171
172fn default_sync_own() -> bool {
173 true
174}
175
176fn default_sync_followed() -> bool {
177 true
178}
179
180fn default_max_concurrent() -> usize {
181 3
182}
183
184fn default_webrtc_timeout_ms() -> u64 {
185 2000
186}
187
188fn default_blossom_timeout_ms() -> u64 {
189 10000
190}
191
192fn default_crawl_depth() -> u32 {
193 2
194}
195
196fn default_max_write_distance() -> u32 {
197 3
198}
199
200fn default_nostr_db_max_size_gb() -> u64 {
201 10
202}
203
204fn default_nostr_spambox_max_size_gb() -> u64 {
205 1
206}
207
208fn default_relays() -> Vec<String> {
209 vec![
210 "wss://relay.damus.io".to_string(),
211 "wss://relay.snort.social".to_string(),
212 "wss://nos.lol".to_string(),
213 "wss://temp.iris.to".to_string(),
214 ]
215}
216
217fn default_bind_address() -> String {
218 "127.0.0.1:8080".to_string()
219}
220
221fn default_enable_auth() -> bool {
222 true
223}
224
225fn default_stun_port() -> u16 {
226 3478 }
228
229fn default_enable_webrtc() -> bool {
230 true
231}
232
233fn default_data_dir() -> String {
234 hashtree_config::get_hashtree_dir()
235 .join("data")
236 .to_string_lossy()
237 .to_string()
238}
239
240fn default_max_size_gb() -> u64 {
241 10
242}
243
244impl Default for ServerConfig {
245 fn default() -> Self {
246 Self {
247 bind_address: default_bind_address(),
248 enable_auth: default_enable_auth(),
249 stun_port: default_stun_port(),
250 enable_webrtc: default_enable_webrtc(),
251 public_writes: default_public_writes(),
252 socialgraph_snapshot_public: default_socialgraph_snapshot_public(),
253 }
254 }
255}
256
257impl Default for StorageConfig {
258 fn default() -> Self {
259 Self {
260 data_dir: default_data_dir(),
261 max_size_gb: default_max_size_gb(),
262 s3: None,
263 }
264 }
265}
266
267impl Default for NostrConfig {
268 fn default() -> Self {
269 Self {
270 relays: default_relays(),
271 allowed_npubs: Vec::new(),
272 socialgraph_root: None,
273 crawl_depth: default_crawl_depth(),
274 max_write_distance: default_max_write_distance(),
275 db_max_size_gb: default_nostr_db_max_size_gb(),
276 spambox_max_size_gb: default_nostr_spambox_max_size_gb(),
277 }
278 }
279}
280
281impl Default for BlossomConfig {
282 fn default() -> Self {
283 Self {
284 servers: Vec::new(),
285 read_servers: default_read_servers(),
286 write_servers: default_write_servers(),
287 max_upload_mb: default_max_upload_mb(),
288 }
289 }
290}
291
292impl Default for SyncConfig {
293 fn default() -> Self {
294 Self {
295 enabled: default_sync_enabled(),
296 sync_own: default_sync_own(),
297 sync_followed: default_sync_followed(),
298 max_concurrent: default_max_concurrent(),
299 webrtc_timeout_ms: default_webrtc_timeout_ms(),
300 blossom_timeout_ms: default_blossom_timeout_ms(),
301 }
302 }
303}
304
305impl Default for Config {
306 fn default() -> Self {
307 Self {
308 server: ServerConfig::default(),
309 storage: StorageConfig::default(),
310 nostr: NostrConfig::default(),
311 blossom: BlossomConfig::default(),
312 sync: SyncConfig::default(),
313 }
314 }
315}
316
317impl Config {
318 pub fn load() -> Result<Self> {
320 let config_path = get_config_path();
321
322 if config_path.exists() {
323 let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
324 toml::from_str(&content).context("Failed to parse config file")
325 } else {
326 let config = Config::default();
327 config.save()?;
328 Ok(config)
329 }
330 }
331
332 pub fn save(&self) -> Result<()> {
334 let config_path = get_config_path();
335
336 if let Some(parent) = config_path.parent() {
338 fs::create_dir_all(parent)?;
339 }
340
341 let content = toml::to_string_pretty(self)?;
342 fs::write(&config_path, content)?;
343
344 Ok(())
345 }
346}
347
348pub use hashtree_config::{get_auth_cookie_path, get_config_path, get_hashtree_dir, get_keys_path};
350
351pub fn ensure_auth_cookie() -> Result<(String, String)> {
353 let cookie_path = get_auth_cookie_path();
354
355 if cookie_path.exists() {
356 read_auth_cookie()
357 } else {
358 generate_auth_cookie()
359 }
360}
361
362pub fn read_auth_cookie() -> Result<(String, String)> {
364 let cookie_path = get_auth_cookie_path();
365 let content = fs::read_to_string(&cookie_path).context("Failed to read auth cookie")?;
366
367 let parts: Vec<&str> = content.trim().split(':').collect();
368 if parts.len() != 2 {
369 anyhow::bail!("Invalid auth cookie format");
370 }
371
372 Ok((parts[0].to_string(), parts[1].to_string()))
373}
374
375pub fn ensure_keys() -> Result<(Keys, bool)> {
378 let keys_path = get_keys_path();
379
380 if keys_path.exists() {
381 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
382 let entries = hashtree_config::parse_keys_file(&content);
383 let nsec_str = entries
384 .into_iter()
385 .next()
386 .map(|e| e.secret)
387 .context("Keys file is empty")?;
388 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
389 let keys = Keys::new(secret_key);
390 Ok((keys, false))
391 } else {
392 let keys = generate_keys()?;
393 Ok((keys, true))
394 }
395}
396
397pub fn read_keys() -> Result<Keys> {
399 let keys_path = get_keys_path();
400 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
401 let entries = hashtree_config::parse_keys_file(&content);
402 let nsec_str = entries
403 .into_iter()
404 .next()
405 .map(|e| e.secret)
406 .context("Keys file is empty")?;
407 let secret_key = SecretKey::from_bech32(&nsec_str).context("Invalid nsec format")?;
408 Ok(Keys::new(secret_key))
409}
410
411pub fn ensure_keys_string() -> Result<(String, bool)> {
414 let keys_path = get_keys_path();
415
416 if keys_path.exists() {
417 let content = fs::read_to_string(&keys_path).context("Failed to read keys file")?;
418 let entries = hashtree_config::parse_keys_file(&content);
419 let nsec_str = entries
420 .into_iter()
421 .next()
422 .map(|e| e.secret)
423 .context("Keys file is empty")?;
424 Ok((nsec_str, false))
425 } else {
426 let keys = generate_keys()?;
427 let nsec = keys
428 .secret_key()
429 .to_bech32()
430 .context("Failed to encode nsec")?;
431 Ok((nsec, true))
432 }
433}
434
435pub fn generate_keys() -> Result<Keys> {
437 let keys_path = get_keys_path();
438
439 if let Some(parent) = keys_path.parent() {
441 fs::create_dir_all(parent)?;
442 }
443
444 let keys = Keys::generate();
446 let nsec = keys
447 .secret_key()
448 .to_bech32()
449 .context("Failed to encode nsec")?;
450
451 fs::write(&keys_path, &nsec)?;
453
454 #[cfg(unix)]
456 {
457 use std::os::unix::fs::PermissionsExt;
458 let perms = fs::Permissions::from_mode(0o600);
459 fs::set_permissions(&keys_path, perms)?;
460 }
461
462 Ok(keys)
463}
464
465pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
467 keys.public_key().to_bytes()
468}
469
470pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
472 use nostr::PublicKey;
473 let pk = PublicKey::from_bech32(npub).context("Invalid npub format")?;
474 Ok(pk.to_bytes())
475}
476
477pub fn generate_auth_cookie() -> Result<(String, String)> {
479 use rand::Rng;
480
481 let cookie_path = get_auth_cookie_path();
482
483 if let Some(parent) = cookie_path.parent() {
485 fs::create_dir_all(parent)?;
486 }
487
488 let mut rng = rand::thread_rng();
490 let username = format!("htree_{}", rng.gen::<u32>());
491 let password: String = (0..32)
492 .map(|_| {
493 let idx = rng.gen_range(0..62);
494 match idx {
495 0..=25 => (b'a' + idx) as char,
496 26..=51 => (b'A' + (idx - 26)) as char,
497 _ => (b'0' + (idx - 52)) as char,
498 }
499 })
500 .collect();
501
502 let content = format!("{}:{}", username, password);
504 fs::write(&cookie_path, content)?;
505
506 #[cfg(unix)]
508 {
509 use std::os::unix::fs::PermissionsExt;
510 let perms = fs::Permissions::from_mode(0o600);
511 fs::set_permissions(&cookie_path, perms)?;
512 }
513
514 Ok((username, password))
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520 use tempfile::TempDir;
521
522 #[test]
523 fn test_config_default() {
524 let config = Config::default();
525 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
526 assert_eq!(config.server.enable_auth, true);
527 assert_eq!(config.storage.max_size_gb, 10);
528 assert_eq!(config.nostr.crawl_depth, 2);
529 assert_eq!(config.nostr.max_write_distance, 3);
530 assert_eq!(config.nostr.db_max_size_gb, 10);
531 assert_eq!(config.nostr.spambox_max_size_gb, 1);
532 assert!(config.nostr.socialgraph_root.is_none());
533 assert_eq!(config.server.socialgraph_snapshot_public, false);
534 }
535
536 #[test]
537 fn test_nostr_config_deserialize_with_defaults() {
538 let toml_str = r#"
539[nostr]
540relays = ["wss://relay.damus.io"]
541"#;
542 let config: Config = toml::from_str(toml_str).unwrap();
543 assert_eq!(config.nostr.relays, vec!["wss://relay.damus.io"]);
544 assert_eq!(config.nostr.crawl_depth, 2);
545 assert_eq!(config.nostr.max_write_distance, 3);
546 assert_eq!(config.nostr.db_max_size_gb, 10);
547 assert_eq!(config.nostr.spambox_max_size_gb, 1);
548 assert!(config.nostr.socialgraph_root.is_none());
549 }
550
551 #[test]
552 fn test_nostr_config_deserialize_with_socialgraph() {
553 let toml_str = r#"
554[nostr]
555relays = ["wss://relay.damus.io"]
556socialgraph_root = "npub1test"
557crawl_depth = 3
558max_write_distance = 5
559"#;
560 let config: Config = toml::from_str(toml_str).unwrap();
561 assert_eq!(config.nostr.socialgraph_root, Some("npub1test".to_string()));
562 assert_eq!(config.nostr.crawl_depth, 3);
563 assert_eq!(config.nostr.max_write_distance, 5);
564 assert_eq!(config.nostr.db_max_size_gb, 10);
565 assert_eq!(config.nostr.spambox_max_size_gb, 1);
566 }
567
568 #[test]
569 fn test_auth_cookie_generation() -> Result<()> {
570 let temp_dir = TempDir::new()?;
571
572 std::env::set_var("HOME", temp_dir.path());
574
575 let (username, password) = generate_auth_cookie()?;
576
577 assert!(username.starts_with("htree_"));
578 assert_eq!(password.len(), 32);
579
580 let cookie_path = get_auth_cookie_path();
582 assert!(cookie_path.exists());
583
584 let (u2, p2) = read_auth_cookie()?;
586 assert_eq!(username, u2);
587 assert_eq!(password, p2);
588
589 Ok(())
590 }
591}