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}