Skip to main content

hashtree_client/
config.rs

1use anyhow::{ensure, Result};
2use hashtree_config::Config;
3use std::time::Duration;
4
5#[derive(Clone)]
6pub struct ClientConfig {
7    pub daemon_url: Option<String>,
8    pub local_only: bool,
9    pub relays: Vec<String>,
10    pub read_servers: Vec<String>,
11    pub resolve_window: Duration,
12    pub request_timeout: Duration,
13}
14
15fn flag(name: &str) -> Option<bool> {
16    std::env::var(name).ok().map(|v| {
17        !matches!(
18            v.trim().to_ascii_lowercase().as_str(),
19            "0" | "false" | "no" | "off"
20        )
21    })
22}
23
24impl ClientConfig {
25    /// Read the shared config without creating an identity or writing defaults.
26    pub fn from_env() -> Result<Self> {
27        let path = hashtree_config::get_config_path();
28        let config: Config = match std::fs::read_to_string(path) {
29            Ok(text) => toml::from_str(&text)?,
30            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Config::default(),
31            Err(e) => return Err(e.into()),
32        };
33        let local_only = flag("HTREE_LOCAL_DAEMON_ONLY").unwrap_or(false);
34        let prefer_local = local_only || flag("HTREE_PREFER_LOCAL_DAEMON").unwrap_or(true);
35        let port = config
36            .server
37            .bind_address
38            .rsplit_once(':')
39            .and_then(|(_, p)| p.parse::<u16>().ok())
40            .unwrap_or(8080);
41        let daemon_url = prefer_local.then(|| {
42            std::env::var("HTREE_DAEMON_URL").unwrap_or_else(|_| format!("http://127.0.0.1:{port}"))
43        });
44        if let Some(value) = &daemon_url {
45            let url = reqwest::Url::parse(value)?;
46            ensure!(
47                url.scheme() == "http"
48                    && url.username().is_empty()
49                    && url.password().is_none()
50                    && url.query().is_none()
51                    && url.fragment().is_none()
52                    && url.path() == "/"
53                    && url.host_str().is_some_and(|h| h == "localhost"
54                        || h.parse::<std::net::IpAddr>()
55                            .is_ok_and(|ip| ip.is_loopback())),
56                "HTREE_DAEMON_URL must be a loopback HTTP origin"
57            );
58        }
59        let relays = std::env::var("NOSTR_RELAYS")
60            .ok()
61            .map(|v| {
62                v.split(',')
63                    .map(str::trim)
64                    .filter(|v| !v.is_empty())
65                    .map(str::to_string)
66                    .collect()
67            })
68            .unwrap_or(config.nostr.relays);
69        let mut read_servers = config.blossom.read_servers;
70        read_servers.extend(config.blossom.servers);
71        Ok(Self {
72            daemon_url: daemon_url.map(|s| s.trim_end_matches('/').to_string()),
73            local_only,
74            relays,
75            read_servers,
76            resolve_window: Duration::from_secs(3),
77            request_timeout: Duration::from_secs(30),
78        })
79    }
80}