enlace 0.2.2

Encrypted mailbox and latest-value slot fan-out.
Documentation
use std::net::SocketAddr;
#[cfg(feature = "dht")]
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

use ed25519_dalek::{SigningKey, VerifyingKey};
use url::Url;

use crate::TransportKind;
use crate::dedup;
use crate::state::State;
use crate::transports::Transport;

pub const DEFAULT_MAX_PLAINTEXT_BYTES: usize = 65_536;
pub const DEFAULT_LONG_POLL_SECS: u32 = 25;
pub const DEFAULT_REPUBLISH_INTERVAL: Duration = Duration::from_mins(30);
pub const DEFAULT_DHT_WATCH_POLL_INTERVAL: Duration = Duration::from_secs(10);
pub const DEFAULT_DHT_BOOTSTRAP_CACHE_TTL: Duration = Duration::from_hours(24);
pub const DEFAULT_DHT_BOOTSTRAP_CACHE_MAX_PEERS: usize = 64;
pub const DEFAULT_PKARR_RELAYS: &[&str] = &[
    "https://relay.pkarr.org",
    "https://pkarr.pubky.org",
    "https://pkarr.pubky.app",
];
pub const DEFAULT_IROH_MAX_MESSAGE_BYTES: usize = DEFAULT_MAX_PLAINTEXT_BYTES + 4096;
pub const DEFAULT_IROH_MAX_STREAMS_PER_PEER: u32 = 32;
pub const DEFAULT_IROH_MAX_CONNS_PER_PEER: u32 = 4;

pub struct Config {
    #[cfg(feature = "http")]
    pub http: Option<HttpConfig>,
    #[cfg(feature = "pkarr")]
    pub pkarr: Option<PkarrConfig>,
    #[cfg(feature = "dht")]
    pub dht: Option<DhtConfig>,
    #[cfg(feature = "iroh")]
    pub iroh: Option<IrohConfig>,
    pub signing: Option<SigningKey>,
    pub trusted: Vec<VerifyingKey>,
    pub dedup_buffer: usize,
    pub max_plaintext_bytes: usize,
    pub state: State,
    pub transports: Vec<ConfiguredTransport>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            #[cfg(feature = "http")]
            http: None,
            #[cfg(feature = "pkarr")]
            pkarr: None,
            #[cfg(feature = "dht")]
            dht: None,
            #[cfg(feature = "iroh")]
            iroh: None,
            signing: None,
            trusted: Vec::new(),
            dedup_buffer: dedup::DEFAULT_CAPACITY,
            max_plaintext_bytes: DEFAULT_MAX_PLAINTEXT_BYTES,
            state: State::memory(),
            transports: Vec::new(),
        }
    }
}

impl Config {
    pub(crate) fn transport_count(&self) -> usize {
        let count = self.transports.len();
        #[cfg(feature = "http")]
        let count = count + usize::from(self.http.is_some());
        #[cfg(feature = "pkarr")]
        let count = count + usize::from(self.pkarr.is_some());
        #[cfg(feature = "dht")]
        let count = count + usize::from(self.dht.is_some());
        #[cfg(feature = "iroh")]
        let count = count + usize::from(self.iroh.is_some());
        count
    }
}

#[derive(Clone)]
pub struct ConfiguredTransport {
    pub kind: TransportKind,
    pub transport: Arc<dyn Transport>,
}

impl ConfiguredTransport {
    #[must_use]
    pub fn new(kind: TransportKind, transport: Arc<dyn Transport>) -> Self {
        Self { kind, transport }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(feature = "http")]
pub struct BasicAuth {
    pub username: String,
    pub password: String,
}

#[derive(Clone)]
#[cfg(feature = "http")]
pub struct HttpConfig {
    pub url: Url,
    pub skip_verify: bool,
    pub auth: Option<BasicAuth>,
    pub long_poll_secs: u32,
}

#[cfg(feature = "http")]
impl HttpConfig {
    #[must_use]
    pub fn new(url: Url) -> Self {
        Self {
            url,
            skip_verify: false,
            auth: None,
            long_poll_secs: DEFAULT_LONG_POLL_SECS,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(feature = "pkarr")]
pub struct PkarrConfig {
    pub resolvers: Vec<String>,
    pub republish_interval: Duration,
}

#[cfg(feature = "pkarr")]
impl Default for PkarrConfig {
    fn default() -> Self {
        Self {
            resolvers: DEFAULT_PKARR_RELAYS
                .iter()
                .map(|url| (*url).to_owned())
                .collect(),
            republish_interval: DEFAULT_REPUBLISH_INTERVAL,
        }
    }
}

#[cfg(feature = "pkarr")]
impl PkarrConfig {
    #[must_use]
    pub fn effective_resolvers(&self) -> Vec<String> {
        if self.resolvers.is_empty() {
            DEFAULT_PKARR_RELAYS
                .iter()
                .map(|url| (*url).to_owned())
                .collect()
        } else {
            self.resolvers.clone()
        }
    }
}

#[cfg(all(test, any(feature = "pkarr", feature = "dht", feature = "iroh")))]
mod tests {
    use super::*;

    #[test]
    #[cfg(feature = "pkarr")]
    fn pkarr_default_resolvers_are_public_relays() {
        let config = PkarrConfig::default();

        assert_eq!(config.resolvers, config.effective_resolvers());
        assert_eq!(config.resolvers.len(), DEFAULT_PKARR_RELAYS.len());
        for resolver in config.resolvers {
            assert!(resolver.starts_with("https://"));
        }
    }

    #[test]
    #[cfg(feature = "pkarr")]
    fn pkarr_empty_resolvers_fall_back_to_defaults() {
        let config = PkarrConfig {
            resolvers: Vec::new(),
            ..PkarrConfig::default()
        };

        assert_eq!(
            config.effective_resolvers(),
            DEFAULT_PKARR_RELAYS
                .iter()
                .map(|url| (*url).to_owned())
                .collect::<Vec<_>>()
        );
    }

    #[test]
    #[cfg(feature = "pkarr")]
    fn pkarr_configured_resolvers_are_preserved() {
        let config = PkarrConfig {
            resolvers: vec!["http://127.0.0.1:8080".to_owned()],
            ..PkarrConfig::default()
        };

        assert_eq!(config.effective_resolvers(), config.resolvers);
    }

    #[test]
    #[cfg(feature = "dht")]
    fn dht_bootstrap_cache_defaults_are_bounded() {
        let config = DhtBootstrapCacheConfig::new("bootstrap.txt".into());

        assert_eq!(config.ttl, DEFAULT_DHT_BOOTSTRAP_CACHE_TTL);
        assert_eq!(config.max_peers, DEFAULT_DHT_BOOTSTRAP_CACHE_MAX_PEERS);
    }

    #[test]
    #[cfg(feature = "iroh")]
    fn iroh_defaults_match_realtime_transport_shape() {
        let config = IrohConfig::default();

        assert_eq!(config.relay_mode, IrohRelayMode::Default);
        assert!(config.bind_addrs.is_empty());
        assert!(config.peers.is_empty());
        assert_eq!(config.max_message_bytes, DEFAULT_IROH_MAX_MESSAGE_BYTES);
        assert_eq!(
            config.max_streams_per_peer,
            DEFAULT_IROH_MAX_STREAMS_PER_PEER
        );
        assert_eq!(config.max_conns_per_peer, DEFAULT_IROH_MAX_CONNS_PER_PEER);
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(feature = "dht")]
pub struct DhtConfig {
    pub bootstrap: Vec<SocketAddr>,
    pub watch_poll_interval: Duration,
    pub bootstrap_cache: Option<DhtBootstrapCacheConfig>,
}

#[cfg(feature = "dht")]
impl Default for DhtConfig {
    fn default() -> Self {
        Self {
            bootstrap: Vec::new(),
            watch_poll_interval: DEFAULT_DHT_WATCH_POLL_INTERVAL,
            bootstrap_cache: None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(feature = "dht")]
pub struct DhtBootstrapCacheConfig {
    pub path: PathBuf,
    pub ttl: Duration,
    pub max_peers: usize,
}

#[cfg(feature = "dht")]
impl DhtBootstrapCacheConfig {
    #[must_use]
    pub fn new(path: PathBuf) -> Self {
        Self {
            path,
            ttl: DEFAULT_DHT_BOOTSTRAP_CACHE_TTL,
            max_peers: DEFAULT_DHT_BOOTSTRAP_CACHE_MAX_PEERS,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(feature = "iroh")]
pub struct IrohConfig {
    pub relay_mode: IrohRelayMode,
    pub bind_addrs: Vec<SocketAddr>,
    pub peers: Vec<IrohEndpointAddr>,
    pub max_message_bytes: usize,
    pub max_streams_per_peer: u32,
    pub max_conns_per_peer: u32,
}

#[cfg(feature = "iroh")]
impl Default for IrohConfig {
    fn default() -> Self {
        Self {
            relay_mode: IrohRelayMode::Default,
            bind_addrs: Vec::new(),
            peers: Vec::new(),
            max_message_bytes: DEFAULT_IROH_MAX_MESSAGE_BYTES,
            max_streams_per_peer: DEFAULT_IROH_MAX_STREAMS_PER_PEER,
            max_conns_per_peer: DEFAULT_IROH_MAX_CONNS_PER_PEER,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(feature = "iroh")]
pub enum IrohRelayMode {
    Default,
    Custom(Vec<Url>),
    Disabled,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IrohEndpointAddr {
    pub endpoint_id: [u8; 32],
    pub relay_urls: Vec<Url>,
    pub direct_addrs: Vec<SocketAddr>,
}