lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 5f066309ff7ffc40c167168b583de2ded52d1794
parent b798d74b381ed24e9b54293280564748368daa0e
Author: triesap <tyson@radroots.org>
Date:   Sat,  4 Oct 2025 20:39:07 +0100

net-core: add structured error handling to Nostr client connect logic and extend profile management

Diffstat:
Mcrates/net-core/src/net.rs | 2+-
Mcrates/net-core/src/nostr_client.rs | 145++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
2 files changed, 112 insertions(+), 35 deletions(-)

diff --git a/crates/net-core/src/net.rs b/crates/net-core/src/net.rs @@ -114,7 +114,7 @@ impl Net { self.nostr = Some(NostrClientManager::new(keys.clone(), rt.handle().clone())); } if let Some(n) = &self.nostr { - n.connect(); + n.connect()?; } } Ok(()) diff --git a/crates/net-core/src/nostr_client.rs b/crates/net-core/src/nostr_client.rs @@ -1,18 +1,15 @@ -#[cfg(feature = "nostr-client")] +#![cfg(feature = "nostr-client")] + use std::collections::HashMap; -#[cfg(feature = "nostr-client")] use std::sync::{Arc, Mutex}; -#[cfg(feature = "nostr-client")] use nostr_sdk::prelude::*; -#[cfg(feature = "nostr-client")] use radroots_events::profile::models::RadrootsProfile; -#[cfg(feature = "nostr-client")] use tokio::runtime::Handle; -#[cfg(feature = "nostr-client")] use tracing::{error, info}; -#[cfg(feature = "nostr-client")] +use crate::error::{NetError, Result}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Light { Red, @@ -20,7 +17,6 @@ pub enum Light { Green, } -#[cfg(feature = "nostr-client")] #[derive(Debug, Clone)] pub struct NostrConnectionSnapshot { pub light: Light, @@ -29,13 +25,11 @@ pub struct NostrConnectionSnapshot { pub last_error: Option<String>, } -#[cfg(feature = "nostr-client")] #[derive(Clone)] pub struct NostrClientManager { inner: Arc<Inner>, } -#[cfg(feature = "nostr-client")] struct Inner { client: Client, relays: Arc<Mutex<Vec<String>>>, @@ -44,7 +38,6 @@ struct Inner { rt: Handle, } -#[cfg(feature = "nostr-client")] impl NostrClientManager { pub fn new(keys: nostr::Keys, rt: Handle) -> Self { let monitor = Monitor::new(2048); @@ -71,37 +64,38 @@ impl NostrClientManager { } } - pub fn connect(&self) { + pub fn connect(&self) -> Result<()> { let inner = self.inner.clone(); - let rt = inner.rt.clone(); - let inner_for_task = inner.clone(); - rt.spawn(async move { - let urls = { - let g = inner_for_task.relays.lock().ok(); - g.map(|v| v.clone()).unwrap_or_default() - }; - if urls.is_empty() { - info!("no relays configured; using default wss://relay.damus.io"); + let urls = { + let g = inner.relays.lock().ok(); + g.map(|v| v.clone()).unwrap_or_default() + }; + + if urls.is_empty() { + if let Ok(mut e) = inner.last_error.lock() { + *e = Some("no relays configured".to_string()); } - let effective = if urls.is_empty() { - vec!["wss://relay.damus.io".to_string()] - } else { - urls - }; + return Err(NetError::Msg("no relays configured".into())); + } - for u in &effective { + let inner_for_task = inner.clone(); + let rt = inner.rt.clone(); + rt.spawn(async move { + for u in &urls { match inner_for_task.client.add_relay(u.as_str()).await { Ok(_) => {} Err(e) => { - *inner_for_task.last_error.lock().unwrap() = - Some(format!("add_relay {u}: {e}")); - error!("add_relay failed for {u}: {e}"); + if let Ok(mut last) = inner_for_task.last_error.lock() { + *last = Some(format!("add_relay {}: {}", u, e)); + } + error!("add_relay failed for {}: {}", u, e); } } } - inner_for_task.client.connect().await; }); + + Ok(()) } pub fn snapshot(&self) -> NostrConnectionSnapshot { @@ -112,6 +106,7 @@ impl NostrClientManager { .ok() .map(|g| g.clone()) .unwrap_or_default(); + let mut connected = 0usize; let mut connecting = 0usize; for (_url, st) in map.iter() { @@ -121,6 +116,7 @@ impl NostrClientManager { _ => {} } } + let light = if connected > 0 { Light::Green } else if connecting > 0 { @@ -128,6 +124,7 @@ impl NostrClientManager { } else { Light::Red }; + let last_error = self.inner.last_error.lock().ok().and_then(|e| e.clone()); NostrConnectionSnapshot { light, @@ -140,17 +137,19 @@ impl NostrClientManager { pub async fn fetch_profile_kind0( &self, author: nostr::PublicKey, - ) -> Result<Option<RadrootsProfile>, String> { + ) -> Result<Option<RadrootsProfile>> { let filter = Filter::new() .authors(vec![author]) .kind(Kind::Metadata) .limit(1); + let events = self .inner .client .fetch_events(filter, std::time::Duration::from_secs(5)) .await - .map_err(|e| e.to_string())?; + .map_err(|e| NetError::Msg(e.to_string()))?; + if let Some(ev) = events.into_iter().next() { if let Ok(p) = serde_json::from_str::<RadrootsProfile>(&ev.content) { return Ok(Some(p)); @@ -170,11 +169,89 @@ impl NostrClientManager { }; return Ok(Some(p)); } - return Err("failed to parse kind:0 metadata content".to_string()); + return Err(NetError::Msg( + "failed to parse kind:0 metadata content".to_string(), + )); } + Ok(None) } + pub fn fetch_profile_kind0_blocking( + &self, + author: nostr::PublicKey, + ) -> Result<Option<RadrootsProfile>> { + let rt = self.inner.rt.clone(); + let inner_for_task = self.inner.clone(); + rt.block_on(async move { + let filter = Filter::new() + .authors(vec![author]) + .kind(Kind::Metadata) + .limit(1); + let events = inner_for_task + .client + .fetch_events(filter, std::time::Duration::from_secs(5)) + .await + .map_err(|e| NetError::Msg(e.to_string()))?; + if let Some(ev) = events.into_iter().next() { + if let Ok(p) = serde_json::from_str::<RadrootsProfile>(&ev.content) { + return Ok(Some(p)); + } + if let Ok(md) = serde_json::from_str::<nostr::Metadata>(&ev.content) { + let p = RadrootsProfile { + name: md.name.unwrap_or_default(), + display_name: md.display_name, + nip05: md.nip05, + about: md.about, + website: md.website.map(|u| u.to_string()), + picture: md.picture.map(|u| u.to_string()), + banner: md.banner.map(|u| u.to_string()), + lud06: md.lud06, + lud16: md.lud16, + bot: None, + }; + return Ok(Some(p)); + } + return Err(NetError::Msg( + "failed to parse kind:0 metadata content".to_string(), + )); + } + Ok(None) + }) + } + + pub fn set_profile_kind0_blocking( + &self, + name: Option<String>, + display_name: Option<String>, + nip05: Option<String>, + about: Option<String>, + ) -> Result<String> { + let rt = self.inner.rt.clone(); + let inner_for_task = self.inner.clone(); + rt.block_on(async move { + let mut md = nostr::Metadata::new(); + if let Some(v) = name { + md = md.name(v); + } + if let Some(v) = display_name { + md = md.display_name(v); + } + if let Some(v) = nip05 { + md = md.nip05(v); + } + if let Some(v) = about { + md = md.about(v); + } + inner_for_task + .client + .set_metadata(&md) + .await + .map_err(|e| NetError::Msg(e.to_string()))?; + Ok("ok".to_string()) + }) + } + fn spawn_status_watcher(&self) { let inner = self.inner.clone(); let rt = inner.rt.clone();