1use anyhow::{Context, Result};
2use nostr::nips::nip19::{FromBech32, ToBech32};
3use nostr::{Keys, SecretKey};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Config {
10 #[serde(default)]
11 pub server: ServerConfig,
12 #[serde(default)]
13 pub storage: StorageConfig,
14 #[serde(default)]
15 pub nostr: NostrConfig,
16 #[serde(default)]
17 pub blossom: BlossomConfig,
18 #[serde(default)]
19 pub sync: SyncConfig,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ServerConfig {
24 #[serde(default = "default_bind_address")]
25 pub bind_address: String,
26 #[serde(default = "default_enable_auth")]
27 pub enable_auth: bool,
28 #[serde(default = "default_stun_port")]
30 pub stun_port: u16,
31 #[serde(default = "default_enable_webrtc")]
33 pub enable_webrtc: bool,
34 #[serde(default = "default_public_writes")]
37 pub public_writes: bool,
38}
39
40fn default_public_writes() -> bool {
41 true
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct StorageConfig {
46 #[serde(default = "default_data_dir")]
47 pub data_dir: String,
48 #[serde(default = "default_max_size_gb")]
49 pub max_size_gb: u64,
50 #[serde(default)]
52 pub s3: Option<S3Config>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct S3Config {
58 pub endpoint: String,
60 pub bucket: String,
62 #[serde(default)]
64 pub prefix: Option<String>,
65 #[serde(default = "default_s3_region")]
67 pub region: String,
68 #[serde(default)]
70 pub access_key: Option<String>,
71 #[serde(default)]
73 pub secret_key: Option<String>,
74 #[serde(default)]
76 pub public_url: Option<String>,
77}
78
79fn default_s3_region() -> String {
80 "auto".to_string()
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct NostrConfig {
85 #[serde(default = "default_relays")]
86 pub relays: Vec<String>,
87 #[serde(default)]
89 pub allowed_npubs: Vec<String>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct BlossomConfig {
94 #[serde(default)]
96 pub servers: Vec<String>,
97 #[serde(default = "default_read_servers")]
99 pub read_servers: Vec<String>,
100 #[serde(default = "default_write_servers")]
102 pub write_servers: Vec<String>,
103 #[serde(default = "default_max_upload_mb")]
105 pub max_upload_mb: u64,
106}
107
108fn default_read_servers() -> Vec<String> {
109 vec!["https://cdn.iris.to".to_string(), "https://hashtree.iris.to".to_string()]
110}
111
112fn default_write_servers() -> Vec<String> {
113 vec!["https://hashtree.iris.to".to_string()]
114}
115
116fn default_max_upload_mb() -> u64 {
117 5
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct SyncConfig {
122 #[serde(default = "default_sync_enabled")]
124 pub enabled: bool,
125 #[serde(default = "default_sync_own")]
127 pub sync_own: bool,
128 #[serde(default = "default_sync_followed")]
130 pub sync_followed: bool,
131 #[serde(default = "default_max_concurrent")]
133 pub max_concurrent: usize,
134 #[serde(default = "default_webrtc_timeout_ms")]
136 pub webrtc_timeout_ms: u64,
137 #[serde(default = "default_blossom_timeout_ms")]
139 pub blossom_timeout_ms: u64,
140}
141
142
143fn default_sync_enabled() -> bool {
144 true
145}
146
147fn default_sync_own() -> bool {
148 true
149}
150
151fn default_sync_followed() -> bool {
152 true
153}
154
155fn default_max_concurrent() -> usize {
156 3
157}
158
159fn default_webrtc_timeout_ms() -> u64 {
160 2000
161}
162
163fn default_blossom_timeout_ms() -> u64 {
164 10000
165}
166
167fn default_relays() -> Vec<String> {
168 vec![
169 "wss://relay.damus.io".to_string(),
170 "wss://relay.snort.social".to_string(),
171 "wss://nos.lol".to_string(),
172 "wss://temp.iris.to".to_string(),
173 ]
174}
175
176fn default_bind_address() -> String {
177 "127.0.0.1:8080".to_string()
178}
179
180fn default_enable_auth() -> bool {
181 true
182}
183
184fn default_stun_port() -> u16 {
185 3478 }
187
188fn default_enable_webrtc() -> bool {
189 true
190}
191
192fn default_data_dir() -> String {
193 get_hashtree_dir()
194 .join("data")
195 .to_string_lossy()
196 .to_string()
197}
198
199fn default_max_size_gb() -> u64 {
200 10
201}
202
203impl Default for ServerConfig {
204 fn default() -> Self {
205 Self {
206 bind_address: default_bind_address(),
207 enable_auth: default_enable_auth(),
208 stun_port: default_stun_port(),
209 enable_webrtc: default_enable_webrtc(),
210 public_writes: default_public_writes(),
211 }
212 }
213}
214
215impl Default for StorageConfig {
216 fn default() -> Self {
217 Self {
218 data_dir: default_data_dir(),
219 max_size_gb: default_max_size_gb(),
220 s3: None,
221 }
222 }
223}
224
225impl Default for NostrConfig {
226 fn default() -> Self {
227 Self {
228 relays: default_relays(),
229 allowed_npubs: Vec::new(),
230 }
231 }
232}
233
234impl Default for BlossomConfig {
235 fn default() -> Self {
236 Self {
237 servers: Vec::new(),
238 read_servers: default_read_servers(),
239 write_servers: default_write_servers(),
240 max_upload_mb: default_max_upload_mb(),
241 }
242 }
243}
244
245impl Default for SyncConfig {
246 fn default() -> Self {
247 Self {
248 enabled: default_sync_enabled(),
249 sync_own: default_sync_own(),
250 sync_followed: default_sync_followed(),
251 max_concurrent: default_max_concurrent(),
252 webrtc_timeout_ms: default_webrtc_timeout_ms(),
253 blossom_timeout_ms: default_blossom_timeout_ms(),
254 }
255 }
256}
257
258impl Default for Config {
259 fn default() -> Self {
260 Self {
261 server: ServerConfig::default(),
262 storage: StorageConfig::default(),
263 nostr: NostrConfig::default(),
264 blossom: BlossomConfig::default(),
265 sync: SyncConfig::default(),
266 }
267 }
268}
269
270impl Config {
271 pub fn load() -> Result<Self> {
273 let config_path = get_config_path();
274
275 if config_path.exists() {
276 let content = fs::read_to_string(&config_path)
277 .context("Failed to read config file")?;
278 toml::from_str(&content).context("Failed to parse config file")
279 } else {
280 let config = Config::default();
281 config.save()?;
282 Ok(config)
283 }
284 }
285
286 pub fn save(&self) -> Result<()> {
288 let config_path = get_config_path();
289
290 if let Some(parent) = config_path.parent() {
292 fs::create_dir_all(parent)?;
293 }
294
295 let content = toml::to_string_pretty(self)?;
296 fs::write(&config_path, content)?;
297
298 Ok(())
299 }
300}
301
302pub fn get_hashtree_dir() -> PathBuf {
304 dirs::home_dir()
305 .unwrap_or_else(|| PathBuf::from("."))
306 .join(".hashtree")
307}
308
309pub fn get_config_path() -> PathBuf {
311 get_hashtree_dir().join("config.toml")
312}
313
314pub fn get_auth_cookie_path() -> PathBuf {
316 get_hashtree_dir().join("auth.cookie")
317}
318
319pub fn get_nsec_path() -> PathBuf {
321 get_hashtree_dir().join("nsec")
322}
323
324pub fn ensure_auth_cookie() -> Result<(String, String)> {
326 let cookie_path = get_auth_cookie_path();
327
328 if cookie_path.exists() {
329 read_auth_cookie()
330 } else {
331 generate_auth_cookie()
332 }
333}
334
335pub fn read_auth_cookie() -> Result<(String, String)> {
337 let cookie_path = get_auth_cookie_path();
338 let content = fs::read_to_string(&cookie_path)
339 .context("Failed to read auth cookie")?;
340
341 let parts: Vec<&str> = content.trim().split(':').collect();
342 if parts.len() != 2 {
343 anyhow::bail!("Invalid auth cookie format");
344 }
345
346 Ok((parts[0].to_string(), parts[1].to_string()))
347}
348
349pub fn ensure_nsec() -> Result<(Keys, bool)> {
352 let nsec_path = get_nsec_path();
353
354 if nsec_path.exists() {
355 let nsec_str = fs::read_to_string(&nsec_path)
356 .context("Failed to read nsec file")?;
357 let nsec_str = nsec_str.trim();
358 let secret_key = SecretKey::from_bech32(nsec_str)
359 .context("Invalid nsec format")?;
360 let keys = Keys::new(secret_key);
361 Ok((keys, false))
362 } else {
363 let keys = generate_nsec()?;
364 Ok((keys, true))
365 }
366}
367
368pub fn read_nsec() -> Result<Keys> {
370 let nsec_path = get_nsec_path();
371 let nsec_str = fs::read_to_string(&nsec_path)
372 .context("Failed to read nsec file")?;
373 let nsec_str = nsec_str.trim();
374 let secret_key = SecretKey::from_bech32(nsec_str)
375 .context("Invalid nsec format")?;
376 Ok(Keys::new(secret_key))
377}
378
379pub fn ensure_nsec_string() -> Result<(String, bool)> {
382 let nsec_path = get_nsec_path();
383
384 if nsec_path.exists() {
385 let nsec_str = fs::read_to_string(&nsec_path)
386 .context("Failed to read nsec file")?;
387 Ok((nsec_str.trim().to_string(), false))
388 } else {
389 let keys = generate_nsec()?;
390 let nsec = keys.secret_key().to_bech32()
391 .context("Failed to encode nsec")?;
392 Ok((nsec, true))
393 }
394}
395
396pub fn generate_nsec() -> Result<Keys> {
398 let nsec_path = get_nsec_path();
399
400 if let Some(parent) = nsec_path.parent() {
402 fs::create_dir_all(parent)?;
403 }
404
405 let keys = Keys::generate();
407 let nsec = keys.secret_key().to_bech32()
408 .context("Failed to encode nsec")?;
409
410 fs::write(&nsec_path, &nsec)?;
412
413 #[cfg(unix)]
415 {
416 use std::os::unix::fs::PermissionsExt;
417 let perms = fs::Permissions::from_mode(0o600);
418 fs::set_permissions(&nsec_path, perms)?;
419 }
420
421 Ok(keys)
422}
423
424pub fn pubkey_bytes(keys: &Keys) -> [u8; 32] {
426 keys.public_key().to_bytes()
427}
428
429pub fn parse_npub(npub: &str) -> Result<[u8; 32]> {
431 use nostr::PublicKey;
432 let pk = PublicKey::from_bech32(npub)
433 .context("Invalid npub format")?;
434 Ok(pk.to_bytes())
435}
436
437pub fn generate_auth_cookie() -> Result<(String, String)> {
439 use rand::Rng;
440
441 let cookie_path = get_auth_cookie_path();
442
443 if let Some(parent) = cookie_path.parent() {
445 fs::create_dir_all(parent)?;
446 }
447
448 let mut rng = rand::thread_rng();
450 let username = format!("htree_{}", rng.gen::<u32>());
451 let password: String = (0..32)
452 .map(|_| {
453 let idx = rng.gen_range(0..62);
454 match idx {
455 0..=25 => (b'a' + idx) as char,
456 26..=51 => (b'A' + (idx - 26)) as char,
457 _ => (b'0' + (idx - 52)) as char,
458 }
459 })
460 .collect();
461
462 let content = format!("{}:{}", username, password);
464 fs::write(&cookie_path, content)?;
465
466 #[cfg(unix)]
468 {
469 use std::os::unix::fs::PermissionsExt;
470 let perms = fs::Permissions::from_mode(0o600);
471 fs::set_permissions(&cookie_path, perms)?;
472 }
473
474 Ok((username, password))
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480 use tempfile::TempDir;
481
482 #[test]
483 fn test_config_default() {
484 let config = Config::default();
485 assert_eq!(config.server.bind_address, "127.0.0.1:8080");
486 assert_eq!(config.server.enable_auth, true);
487 assert_eq!(config.storage.max_size_gb, 10);
488 }
489
490 #[test]
491 fn test_auth_cookie_generation() -> Result<()> {
492 let temp_dir = TempDir::new()?;
493
494 std::env::set_var("HOME", temp_dir.path());
496
497 let (username, password) = generate_auth_cookie()?;
498
499 assert!(username.starts_with("htree_"));
500 assert_eq!(password.len(), 32);
501
502 let cookie_path = get_auth_cookie_path();
504 assert!(cookie_path.exists());
505
506 let (u2, p2) = read_auth_cookie()?;
508 assert_eq!(username, u2);
509 assert_eq!(password, p2);
510
511 Ok(())
512 }
513}