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://relay.damus.io",
24 "wss://relay.snort.social",
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}
159
160impl Default for BlossomConfig {
161 fn default() -> Self {
162 Self {
163 servers: vec![],
164 read_servers: default_read_servers(),
165 write_servers: default_write_servers(),
166 max_upload_mb: default_max_upload_mb(),
167 }
168 }
169}
170
171fn default_read_servers() -> Vec<String> {
172 DEFAULT_READ_SERVERS.iter().map(|s| s.to_string()).collect()
173}
174
175fn default_write_servers() -> Vec<String> {
176 DEFAULT_WRITE_SERVERS.iter().map(|s| s.to_string()).collect()
177}
178
179fn default_max_upload_mb() -> u64 {
180 100
181}
182
183impl BlossomConfig {
184 pub fn all_read_servers(&self) -> Vec<String> {
186 let mut servers = self.servers.clone();
187 servers.extend(self.read_servers.clone());
188 servers.sort();
189 servers.dedup();
190 servers
191 }
192
193 pub fn all_write_servers(&self) -> Vec<String> {
195 let mut servers = self.servers.clone();
196 servers.extend(self.write_servers.clone());
197 servers.sort();
198 servers.dedup();
199 servers
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct SyncConfig {
206 #[serde(default)]
207 pub enabled: bool,
208 #[serde(default = "default_true")]
209 pub sync_own: bool,
210 #[serde(default)]
211 pub sync_followed: bool,
212 #[serde(default = "default_max_concurrent")]
213 pub max_concurrent: usize,
214 #[serde(default = "default_webrtc_timeout_ms")]
215 pub webrtc_timeout_ms: u64,
216 #[serde(default = "default_blossom_timeout_ms")]
217 pub blossom_timeout_ms: u64,
218}
219
220impl Default for SyncConfig {
221 fn default() -> Self {
222 Self {
223 enabled: false,
224 sync_own: true,
225 sync_followed: false,
226 max_concurrent: default_max_concurrent(),
227 webrtc_timeout_ms: default_webrtc_timeout_ms(),
228 blossom_timeout_ms: default_blossom_timeout_ms(),
229 }
230 }
231}
232
233fn default_max_concurrent() -> usize {
234 4
235}
236
237fn default_webrtc_timeout_ms() -> u64 {
238 5000
239}
240
241fn default_blossom_timeout_ms() -> u64 {
242 10000
243}
244
245impl Config {
246 pub fn load() -> Result<Self> {
248 let config_path = get_config_path();
249
250 if config_path.exists() {
251 let content = fs::read_to_string(&config_path)
252 .context("Failed to read config file")?;
253 toml::from_str(&content).context("Failed to parse config file")
254 } else {
255 let config = Config::default();
256 config.save()?;
257 Ok(config)
258 }
259 }
260
261 pub fn load_or_default() -> Self {
263 Self::load().unwrap_or_default()
264 }
265
266 pub fn save(&self) -> Result<()> {
268 let config_path = get_config_path();
269
270 if let Some(parent) = config_path.parent() {
271 fs::create_dir_all(parent)?;
272 }
273
274 let content = toml::to_string_pretty(self)?;
275 fs::write(&config_path, content)?;
276
277 Ok(())
278 }
279}
280
281pub fn get_hashtree_dir() -> PathBuf {
283 if let Ok(dir) = std::env::var("HTREE_CONFIG_DIR") {
284 return PathBuf::from(dir);
285 }
286 dirs::home_dir()
287 .unwrap_or_else(|| PathBuf::from("."))
288 .join(".hashtree")
289}
290
291pub fn get_config_path() -> PathBuf {
293 get_hashtree_dir().join("config.toml")
294}
295
296pub fn get_keys_path() -> PathBuf {
298 get_hashtree_dir().join("keys")
299}
300
301pub fn get_auth_cookie_path() -> PathBuf {
303 get_hashtree_dir().join("auth.cookie")
304}
305
306pub fn get_data_dir() -> PathBuf {
309 if let Ok(dir) = std::env::var("HTREE_DATA_DIR") {
310 return PathBuf::from(dir);
311 }
312 let config = Config::load_or_default();
313 PathBuf::from(&config.storage.data_dir)
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_default_config() {
322 let config = Config::default();
323 assert!(!config.blossom.read_servers.is_empty());
324 assert!(!config.blossom.write_servers.is_empty());
325 assert!(!config.nostr.relays.is_empty());
326 }
327
328 #[test]
329 fn test_parse_empty_config() {
330 let config: Config = toml::from_str("").unwrap();
331 assert!(!config.blossom.read_servers.is_empty());
332 }
333
334 #[test]
335 fn test_parse_partial_config() {
336 let toml = r#"
337[blossom]
338write_servers = ["https://custom.server"]
339"#;
340 let config: Config = toml::from_str(toml).unwrap();
341 assert_eq!(config.blossom.write_servers, vec!["https://custom.server"]);
342 assert!(!config.blossom.read_servers.is_empty());
343 }
344
345 #[test]
346 fn test_all_servers() {
347 let mut config = BlossomConfig::default();
348 config.servers = vec!["https://legacy.server".to_string()];
349
350 let read = config.all_read_servers();
351 assert!(read.contains(&"https://legacy.server".to_string()));
352 assert!(read.contains(&"https://cdn.iris.to".to_string()));
353
354 let write = config.all_write_servers();
355 assert!(write.contains(&"https://legacy.server".to_string()));
356 assert!(write.contains(&"https://upload.iris.to".to_string()));
357 }
358}