commit 2969de4edb2eb70fecd5288d048d2ffdaf2730ac
parent 107e8a4f7248720cff7e87d3f865f84d644ef7f0
Author: triesap <tyson@radroots.org>
Date: Sun, 4 Jan 2026 16:28:31 +0000
nostr: add client options and filter tag helper
- Introduce configurable RadrootsNostrClientOptions builder
- Support proxy and connection tuning outside wasm targets
- Add validated single-letter filter tag helper
- Extend error enum for client config and filter tags
Diffstat:
5 files changed, 115 insertions(+), 1 deletion(-)
diff --git a/nostr/Cargo.toml b/nostr/Cargo.toml
@@ -9,6 +9,7 @@ license.workspace = true
[features]
default = ["std"]
std = []
+os-rng = ["nostr/os-rng"]
client = ["std", "dep:nostr-sdk", "dep:radroots-identity"]
codec = ["dep:radroots-events", "dep:radroots-events-codec", "radroots-events-codec/nostr"]
events = ["dep:radroots-events", "radroots-events/std", "radroots-events/serde"]
diff --git a/nostr/src/client.rs b/nostr/src/client.rs
@@ -3,8 +3,10 @@
use core::ops::Deref;
use core::time::Duration;
use std::collections::HashMap;
+#[cfg(not(target_arch = "wasm32"))]
+use std::net::SocketAddr;
-use nostr_sdk::Client;
+use nostr_sdk::{Client, ClientBuilder, ClientOptions};
use radroots_identity::RadrootsIdentity;
use crate::error::RadrootsNostrError;
@@ -28,6 +30,79 @@ pub struct RadrootsNostrClient {
inner: Client,
}
+#[derive(Debug, Clone, Default)]
+pub struct RadrootsNostrClientOptions {
+ automatic_authentication: Option<bool>,
+ max_avg_latency_ms: Option<u64>,
+ verify_subscriptions: Option<bool>,
+ ban_relay_on_mismatch: Option<bool>,
+ #[cfg(not(target_arch = "wasm32"))]
+ proxy: Option<SocketAddr>,
+}
+
+impl RadrootsNostrClientOptions {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn automatic_authentication(mut self, enabled: bool) -> Self {
+ self.automatic_authentication = Some(enabled);
+ self
+ }
+
+ pub fn max_avg_latency_ms(mut self, max_ms: u64) -> Self {
+ self.max_avg_latency_ms = Some(max_ms);
+ self
+ }
+
+ pub fn verify_subscriptions(mut self, enabled: bool) -> Self {
+ self.verify_subscriptions = Some(enabled);
+ self
+ }
+
+ pub fn ban_relay_on_mismatch(mut self, enabled: bool) -> Self {
+ self.ban_relay_on_mismatch = Some(enabled);
+ self
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn proxy_addr(mut self, addr: SocketAddr) -> Self {
+ self.proxy = Some(addr);
+ self
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn proxy_str(mut self, addr: &str) -> Result<Self, RadrootsNostrError> {
+ let parsed: SocketAddr = addr
+ .parse()
+ .map_err(|err| RadrootsNostrError::ClientConfigError(err.to_string()))?;
+ self.proxy = Some(parsed);
+ Ok(self)
+ }
+
+ fn to_client_options(&self) -> Result<ClientOptions, RadrootsNostrError> {
+ let mut opts = ClientOptions::new();
+ if let Some(enabled) = self.automatic_authentication {
+ opts = opts.automatic_authentication(enabled);
+ }
+ if let Some(max_ms) = self.max_avg_latency_ms {
+ opts = opts.max_avg_latency(Duration::from_millis(max_ms));
+ }
+ if let Some(enabled) = self.verify_subscriptions {
+ opts = opts.verify_subscriptions(enabled);
+ }
+ if let Some(enabled) = self.ban_relay_on_mismatch {
+ opts = opts.ban_relay_on_mismatch(enabled);
+ }
+ #[cfg(not(target_arch = "wasm32"))]
+ if let Some(proxy) = self.proxy {
+ let connection = nostr_sdk::client::options::Connection::new().proxy(proxy);
+ opts = opts.connection(connection);
+ }
+ Ok(opts)
+ }
+}
+
impl RadrootsNostrClient {
pub fn new(keys: RadrootsNostrKeys) -> Self {
Self {
@@ -35,6 +110,15 @@ impl RadrootsNostrClient {
}
}
+ pub fn from_keys_with_options(
+ keys: RadrootsNostrKeys,
+ options: RadrootsNostrClientOptions,
+ ) -> Result<Self, RadrootsNostrError> {
+ let opts = options.to_client_options()?;
+ let inner = ClientBuilder::new().signer(keys).opts(opts).build();
+ Ok(Self { inner })
+ }
+
pub fn new_with_monitor(keys: RadrootsNostrKeys, monitor: RadrootsNostrMonitor) -> Self {
let inner = Client::builder().signer(keys).monitor(monitor).build();
Self { inner }
diff --git a/nostr/src/error.rs b/nostr/src/error.rs
@@ -10,6 +10,10 @@ pub enum RadrootsNostrError {
#[error("Database error: {0}")]
DatabaseError(#[from] nostr_sdk::prelude::DatabaseError),
+ #[cfg(feature = "client")]
+ #[error("Client configuration error: {0}")]
+ ClientConfigError(String),
+
#[error("Event error: {0}")]
EventError(#[from] nostr::event::Error),
@@ -22,6 +26,9 @@ pub enum RadrootsNostrError {
#[error("Key error: {0}")]
KeyError(#[from] nostr::key::Error),
+ #[error("Filter tag error: {0}")]
+ FilterTagError(String),
+
#[cfg(feature = "codec")]
#[error("Profile encode error: {0}")]
ProfileEncodeError(#[from] radroots_events_codec::profile::error::ProfileEncodeError),
diff --git a/nostr/src/filter.rs b/nostr/src/filter.rs
@@ -1,5 +1,25 @@
+use crate::error::RadrootsNostrError;
use crate::types::{RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrTimestamp};
+pub fn radroots_nostr_filter_tag(
+ filter: RadrootsNostrFilter,
+ tag: &str,
+ values: Vec<String>,
+) -> Result<RadrootsNostrFilter, RadrootsNostrError> {
+ let mut chars = tag.chars();
+ let tag_char = chars
+ .next()
+ .ok_or_else(|| RadrootsNostrError::FilterTagError("tag is empty".to_string()))?;
+ if chars.next().is_some() {
+ return Err(RadrootsNostrError::FilterTagError(
+ "tag must be a single letter".to_string(),
+ ));
+ }
+ let tag_key = nostr::filter::SingleLetterTag::from_char(tag_char)
+ .map_err(|err| RadrootsNostrError::FilterTagError(err.to_string()))?;
+ Ok(filter.custom_tags(tag_key, values))
+}
+
pub fn radroots_nostr_kind(kind: u16) -> RadrootsNostrKind {
RadrootsNostrKind::Custom(kind)
}
diff --git a/nostr/src/lib.rs b/nostr/src/lib.rs
@@ -43,6 +43,7 @@ pub mod prelude {
pub use crate::client::{
radroots_nostr_fetch_event_by_id,
radroots_nostr_send_event,
+ RadrootsNostrClientOptions,
RadrootsNostrClient,
};
@@ -50,6 +51,7 @@ pub mod prelude {
pub use crate::filter::{
radroots_nostr_filter_kind,
radroots_nostr_filter_new_events,
+ radroots_nostr_filter_tag,
radroots_nostr_kind,
};