Skip to main content

radroots_net_core/
net.rs

1use serde::Serialize;
2use std::sync::{Arc, Mutex, MutexGuard};
3
4use crate::error::{NetError, Result};
5#[cfg(feature = "nostr-client")]
6use crate::nostr_client::{NostrClientManager, NostrConnectionSnapshot};
7#[cfg(feature = "nostr-client")]
8use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager;
9
10#[derive(Debug, Clone, Serialize)]
11pub struct BuildInfo {
12    pub crate_name: &'static str,
13    pub crate_version: &'static str,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub rustc: Option<&'static str>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub profile: Option<&'static str>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub git_sha: Option<&'static str>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub build_time_unix: Option<u64>,
22}
23
24#[derive(Debug, Clone, Serialize)]
25pub struct NetInfo {
26    pub build: BuildInfo,
27}
28
29pub struct Net {
30    pub info: NetInfo,
31    pub config: crate::config::NetConfig,
32
33    #[cfg(feature = "nostr-client")]
34    pub accounts: RadrootsNostrAccountsManager,
35
36    #[cfg(feature = "nostr-client")]
37    pub nostr: Option<NostrClientManager>,
38
39    #[cfg(feature = "rt")]
40    pub rt: Option<tokio::runtime::Runtime>,
41}
42
43impl Net {
44    pub fn new(cfg: crate::config::NetConfig) -> Self {
45        Self {
46            info: NetInfo {
47                build: BuildInfo {
48                    crate_name: env!("CARGO_PKG_NAME"),
49                    crate_version: env!("CARGO_PKG_VERSION"),
50                    rustc: option_env!("RUSTC_VERSION"),
51                    profile: option_env!("PROFILE"),
52                    git_sha: option_env!("GIT_HASH"),
53                    build_time_unix: option_env!("BUILD_TIME_UNIX").and_then(|s| s.parse().ok()),
54                },
55            },
56            config: cfg,
57            #[cfg(feature = "nostr-client")]
58            accounts: RadrootsNostrAccountsManager::new_in_memory(),
59            #[cfg(feature = "nostr-client")]
60            nostr: None,
61            #[cfg(feature = "rt")]
62            rt: None,
63        }
64    }
65
66    #[cfg(feature = "rt")]
67    pub fn init_managed_runtime(&mut self, worker_threads: Option<usize>) -> Result<()> {
68        if self.rt.is_some() {
69            return Ok(());
70        }
71
72        let threads = worker_threads.unwrap_or_else(|| {
73            std::thread::available_parallelism()
74                .map(|n| n.get())
75                .unwrap_or(1)
76                .max(1)
77        });
78
79        let rt = tokio::runtime::Builder::new_multi_thread()
80            .worker_threads(threads)
81            .enable_all()
82            .build()
83            .map_err(|e| NetError::msg(format!("failed to build tokio runtime: {e}")))?;
84
85        self.rt = Some(rt);
86        Ok(())
87    }
88
89    #[cfg(feature = "nostr-client")]
90    pub fn nostr_set_default_relays(&mut self, urls: &[String]) -> Result<()> {
91        if self.nostr.is_none() {
92            let keys = self.selected_nostr_keys().ok_or(NetError::MissingKey)?;
93            let rt = self
94                .rt
95                .as_ref()
96                .ok_or_else(|| NetError::msg("tokio runtime missing"))?;
97            self.nostr = Some(NostrClientManager::new(keys, rt.handle().clone()));
98        }
99        if let Some(n) = &self.nostr {
100            n.set_relays(urls);
101        }
102        Ok(())
103    }
104
105    #[cfg(feature = "nostr-client")]
106    pub fn nostr_connect_if_key_present(&mut self) -> Result<()> {
107        let Some(keys) = self.selected_nostr_keys() else {
108            return Ok(());
109        };
110        let rt = self
111            .rt
112            .as_ref()
113            .ok_or_else(|| NetError::msg("tokio runtime missing"))?;
114        if self.nostr.is_none() {
115            self.nostr = Some(NostrClientManager::new(keys, rt.handle().clone()));
116        }
117        if let Some(n) = &self.nostr {
118            n.connect()?;
119        }
120        Ok(())
121    }
122
123    #[cfg(feature = "nostr-client")]
124    pub fn nostr_connection_snapshot(&self) -> Option<NostrConnectionSnapshot> {
125        self.nostr.as_ref().map(|n| n.snapshot())
126    }
127
128    #[cfg(feature = "nostr-client")]
129    pub fn selected_nostr_keys(&self) -> Option<radroots_nostr::prelude::RadrootsNostrKeys> {
130        self.accounts
131            .selected_signing_identity()
132            .ok()
133            .flatten()
134            .map(|identity| identity.into_keys())
135    }
136}
137
138#[derive(Clone)]
139pub struct NetHandle(Arc<Mutex<Net>>);
140
141impl NetHandle {
142    pub fn from_inner(inner: Net) -> Self {
143        Self(Arc::new(Mutex::new(inner)))
144    }
145
146    pub fn lock(&self) -> Result<MutexGuard<'_, Net>> {
147        self.0.lock().map_err(|_| NetError::Poisoned)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use crate::builder::NetBuilder;
154
155    #[test]
156    fn builds_minimal() {
157        let cfg = crate::config::NetConfig::default();
158        let handle = NetBuilder::new().config(cfg).build();
159        assert!(handle.is_ok());
160    }
161
162    #[test]
163    fn lock_is_ok() {
164        let cfg = crate::config::NetConfig::default();
165        let handle = NetBuilder::new().config(cfg).build().unwrap();
166        let guard = handle.lock();
167        assert!(guard.is_ok());
168    }
169
170    #[cfg(feature = "rt")]
171    #[test]
172    fn builds_with_managed_rt() {
173        let cfg = crate::config::NetConfig::default();
174        let handle = crate::builder::NetBuilder::new()
175            .config(cfg)
176            .manage_runtime(true)
177            .build()
178            .expect("build with runtime");
179
180        let rt_present = handle.lock().unwrap().rt.is_some();
181        assert!(rt_present);
182    }
183
184    #[cfg(feature = "nostr-client")]
185    #[test]
186    fn selected_nostr_keys_reflects_selected_signing_account() {
187        let cfg = crate::config::NetConfig::default();
188        let net = crate::Net::new(cfg);
189        assert!(net.selected_nostr_keys().is_none());
190
191        net.accounts
192            .generate_identity(Some("primary".into()), true)
193            .expect("generate account");
194        assert!(net.selected_nostr_keys().is_some());
195    }
196}