diff --git a/bindings/nostr-ffi/src/event/id.rs b/bindings/nostr-ffi/src/event/id.rs index 97434af75..a44aca3df 100644 --- a/bindings/nostr-ffi/src/event/id.rs +++ b/bindings/nostr-ffi/src/event/id.rs @@ -13,7 +13,7 @@ use super::Kind; use crate::error::Result; use crate::{PublicKey, Tag, Timestamp}; -#[derive(Object)] +#[derive(PartialEq, Eq, Hash, Object)] pub struct EventId { inner: nostr::EventId, } diff --git a/bindings/nostr-sdk-ffi/src/client/mod.rs b/bindings/nostr-sdk-ffi/src/client/mod.rs index 081cc145e..dd3938512 100644 --- a/bindings/nostr-sdk-ffi/src/client/mod.rs +++ b/bindings/nostr-sdk-ffi/src/client/mod.rs @@ -3,7 +3,6 @@ // Distributed under the MIT software license use std::collections::HashMap; -use std::fmt::Debug; use std::ops::Deref; use std::sync::Arc; use std::time::Duration; @@ -11,7 +10,7 @@ use std::time::Duration; use async_utility::thread; use nostr_ffi::{ ClientMessage, Event, EventBuilder, EventId, FileMetadata, Filter, Metadata, PublicKey, - RelayMessage, Timestamp, + Timestamp, }; use nostr_sdk::client::Client as ClientSdk; use nostr_sdk::pool::RelayPoolNotification as RelayPoolNotificationSdk; @@ -29,7 +28,7 @@ pub use self::signer::NostrSigner; use self::zapper::{ZapDetails, ZapEntity}; use crate::error::Result; use crate::relay::options::{NegentropyOptions, SubscribeAutoCloseOptions}; -use crate::{NostrDatabase, Relay}; +use crate::{HandleNotification, NostrDatabase, Relay}; #[derive(Object)] pub struct Client { @@ -529,9 +528,3 @@ impl Client { Ok(()) } } - -#[uniffi::export(callback_interface)] -pub trait HandleNotification: Send + Sync + Debug { - fn handle_msg(&self, relay_url: String, msg: Arc); - fn handle(&self, relay_url: String, subscription_id: String, event: Arc); -} diff --git a/bindings/nostr-sdk-ffi/src/error.rs b/bindings/nostr-sdk-ffi/src/error.rs index dc0564a21..fb9164cf6 100644 --- a/bindings/nostr-sdk-ffi/src/error.rs +++ b/bindings/nostr-sdk-ffi/src/error.rs @@ -50,6 +50,12 @@ impl From for NostrSdkError { } } +impl From for NostrSdkError { + fn from(e: nostr_sdk::pool::pool::Error) -> NostrSdkError { + Self::Generic(e.to_string()) + } +} + impl From for NostrSdkError { fn from(e: AddrParseError) -> NostrSdkError { Self::Generic(e.to_string()) diff --git a/bindings/nostr-sdk-ffi/src/lib.rs b/bindings/nostr-sdk-ffi/src/lib.rs index 86eb86280..671571507 100644 --- a/bindings/nostr-sdk-ffi/src/lib.rs +++ b/bindings/nostr-sdk-ffi/src/lib.rs @@ -10,7 +10,9 @@ pub mod client; pub mod database; pub mod error; pub mod logger; +pub mod notifications; pub mod nwc; +pub mod pool; pub mod profile; pub mod relay; @@ -18,10 +20,11 @@ trait FromResult: Sized { fn from_result(_: T) -> error::Result; } -pub use crate::client::{Client, ClientBuilder, HandleNotification, Options}; -pub use crate::database::NostrDatabase; -pub use crate::error::NostrSdkError; -pub use crate::logger::{init_logger, LogLevel}; -pub use crate::relay::{Relay, RelayConnectionStats, RelayStatus}; +pub use self::client::{Client, ClientBuilder, Options}; +pub use self::database::NostrDatabase; +pub use self::error::NostrSdkError; +pub use self::logger::{init_logger, LogLevel}; +pub use self::notifications::HandleNotification; +pub use self::relay::{Relay, RelayConnectionStats, RelayStatus}; uniffi::setup_scaffolding!("nostr_sdk"); diff --git a/bindings/nostr-sdk-ffi/src/notifications.rs b/bindings/nostr-sdk-ffi/src/notifications.rs new file mode 100644 index 000000000..2b271b3cc --- /dev/null +++ b/bindings/nostr-sdk-ffi/src/notifications.rs @@ -0,0 +1,14 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +use std::fmt::Debug; +use std::sync::Arc; + +use nostr_ffi::{Event, RelayMessage}; + +#[uniffi::export(callback_interface)] +pub trait HandleNotification: Send + Sync + Debug { + fn handle_msg(&self, relay_url: String, msg: Arc); + fn handle(&self, relay_url: String, subscription_id: String, event: Arc); +} diff --git a/bindings/nostr-sdk-ffi/src/pool/mod.rs b/bindings/nostr-sdk-ffi/src/pool/mod.rs new file mode 100644 index 000000000..d0da3bf14 --- /dev/null +++ b/bindings/nostr-sdk-ffi/src/pool/mod.rs @@ -0,0 +1,415 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::Arc; +use std::time::Duration; + +use async_utility::thread; +use nostr_ffi::{ClientMessage, Event, EventId, Filter, Timestamp}; +use nostr_sdk::database::DynNostrDatabase; +use nostr_sdk::{block_on, spawn_blocking, RelayPoolOptions, SubscriptionId}; +use uniffi::Object; + +use crate::error::Result; +use crate::relay::options::{FilterOptions, NegentropyOptions}; +use crate::relay::{RelayOptions, RelaySendOptions, SubscribeOptions}; +use crate::{HandleNotification, NostrDatabase, Relay}; + +#[derive(Object)] +pub struct RelayPool { + inner: nostr_sdk::RelayPool, +} + +#[uniffi::export] +impl RelayPool { + /// Create new `RelayPool` with `in-memory` database + #[uniffi::constructor] + pub fn new() -> Self { + Self { + inner: nostr_sdk::RelayPool::new(RelayPoolOptions::default()), + } + } + + /// Create new `RelayPool` with `custom` database + #[uniffi::constructor] + pub fn with_database(database: &NostrDatabase) -> Self { + let database: Arc = database.into(); + Self { + inner: nostr_sdk::RelayPool::with_database(RelayPoolOptions::default(), database), + } + } + + /// Stop + /// + /// Call `connect` to re-start relays connections + pub fn stop(&self) -> Result<()> { + block_on(async move { Ok(self.inner.stop().await?) }) + } + + /// Completely shutdown pool + pub fn shutdown(&self) -> Result<()> { + block_on(async move { Ok(self.inner.clone().shutdown().await?) }) + } + + /// Get database + pub fn database(&self) -> Arc { + Arc::new(self.inner.database().into()) + } + + /// Get relays + pub fn relays(&self) -> HashMap> { + block_on(async move { + self.inner + .relays() + .await + .into_iter() + .map(|(u, r)| (u.to_string(), Arc::new(r.into()))) + .collect() + }) + } + + /// Get relay + pub fn relay(&self, url: String) -> Result> { + block_on(async move { Ok(Arc::new(self.inner.relay(url).await?.into())) }) + } + + pub fn add_relay(&self, url: String, opts: &RelayOptions) -> Result { + block_on(async move { Ok(self.inner.add_relay(url, opts.deref().clone()).await?) }) + } + + pub fn remove_relay(&self, url: String) -> Result<()> { + block_on(async move { Ok(self.inner.remove_relay(url).await?) }) + } + + pub fn remove_all_relay(&self) -> Result<()> { + block_on(async move { Ok(self.inner.remove_all_relays().await?) }) + } + + /// Connect to all added relays and keep connection alive + pub fn connect(&self, connection_timeout: Option) { + block_on(async move { self.inner.connect(connection_timeout).await }) + } + + /// Disconnect from all relays + pub fn disconnect(&self) -> Result<()> { + block_on(async move { Ok(self.inner.disconnect().await?) }) + } + + /// Connect to relay + pub fn connect_relay(&self, url: String, connection_timeout: Option) -> Result<()> { + block_on(async move { Ok(self.inner.connect_relay(url, connection_timeout).await?) }) + } + + /// Get subscriptions + pub fn subscriptions(&self) -> HashMap>> { + block_on(async move { + self.inner + .subscriptions() + .await + .into_iter() + .map(|(id, filters)| { + ( + id.to_string(), + filters.into_iter().map(|f| Arc::new(f.into())).collect(), + ) + }) + .collect() + }) + } + + /// Get filters by subscription ID + pub fn subscription(&self, id: String) -> Option>> { + block_on(async move { + let id = SubscriptionId::new(id); + self.inner + .subscription(&id) + .await + .map(|f| f.into_iter().map(|f| Arc::new(f.into())).collect()) + }) + } + + /// Send client message to all connected relays + pub fn send_msg(&self, msg: Arc, opts: Arc) -> Result<()> { + block_on(async move { + Ok(self + .inner + .send_msg(msg.as_ref().deref().clone(), **opts) + .await?) + }) + } + + /// Send multiple client messages at once to all connected relays + pub fn batch_msg(&self, msgs: Vec>, opts: &RelaySendOptions) -> Result<()> { + let msgs = msgs + .into_iter() + .map(|msg| msg.as_ref().deref().clone()) + .collect(); + block_on(async move { Ok(self.inner.batch_msg(msgs, **opts).await?) }) + } + + /// Send client message to specific relays + /// + /// Note: **the relays must already be added!** + pub fn send_msg_to( + &self, + urls: Vec, + msg: Arc, + opts: Arc, + ) -> Result<()> { + block_on(async move { + Ok(self + .inner + .send_msg_to(urls, msg.as_ref().deref().clone(), **opts) + .await?) + }) + } + + /// Send multiple client messages at once to specific relays + /// + /// Note: **the relays must already be added!** + pub fn batch_msg_to( + &self, + urls: Vec, + msgs: Vec>, + opts: &RelaySendOptions, + ) -> Result<()> { + let msgs = msgs + .into_iter() + .map(|msg| msg.as_ref().deref().clone()) + .collect(); + block_on(async move { Ok(self.inner.batch_msg_to(urls, msgs, **opts).await?) }) + } + + /// Send event to **all connected relays** and wait for `OK` message + pub fn send_event(&self, event: &Event, opts: &RelaySendOptions) -> Result> { + block_on(async move { + Ok(Arc::new( + self.inner + .send_event(event.deref().clone(), **opts) + .await? + .into(), + )) + }) + } + + /// Send multiple `Event` at once to **all connected relays** and wait for `OK` message + pub fn batch_event(&self, events: Vec>, opts: &RelaySendOptions) -> Result<()> { + let events = events + .into_iter() + .map(|e| e.as_ref().deref().clone()) + .collect(); + block_on(async move { Ok(self.inner.batch_event(events, **opts).await?) }) + } + + /// Send event to **specific relays** and wait for `OK` message + pub fn send_event_to( + &self, + urls: Vec, + event: &Event, + opts: &RelaySendOptions, + ) -> Result> { + block_on(async move { + Ok(Arc::new( + self.inner + .send_event_to(urls, event.deref().clone(), **opts) + .await? + .into(), + )) + }) + } + + /// Send multiple events at once to **specific relays** and wait for `OK` message + pub fn batch_event_to( + &self, + urls: Vec, + events: Vec>, + opts: &RelaySendOptions, + ) -> Result<()> { + let events = events + .into_iter() + .map(|e| e.as_ref().deref().clone()) + .collect(); + block_on(async move { Ok(self.inner.batch_event_to(urls, events, **opts).await?) }) + } + + /// Subscribe to filters + /// + /// ### Auto-closing subscription + /// + /// It's possible to automatically close a subscription by configuring the `SubscribeOptions`. + /// + /// Note: auto-closing subscriptions aren't saved in subscriptions map! + pub fn subscribe(&self, filters: Vec>, opts: &SubscribeOptions) -> String { + block_on(async move { + self.inner + .subscribe( + filters + .into_iter() + .map(|f| f.as_ref().deref().clone()) + .collect(), + **opts, + ) + .await + .to_string() + }) + } + + /// Subscribe with custom subscription ID + /// + /// ### Auto-closing subscription + /// + /// It's possible to automatically close a subscription by configuring the `SubscribeOptions`. + /// + /// Note: auto-closing subscriptions aren't saved in subscriptions map! + pub fn subscribe_with_id( + &self, + id: String, + filters: Vec>, + opts: &SubscribeOptions, + ) { + block_on(async move { + self.inner + .subscribe_with_id( + SubscriptionId::new(id), + filters + .into_iter() + .map(|f| f.as_ref().deref().clone()) + .collect(), + **opts, + ) + .await + }) + } + + /// Unsubscribe + pub fn unsubscribe(&self, id: String, opts: Arc) { + block_on(async move { + self.inner + .unsubscribe(SubscriptionId::new(id), **opts) + .await + }) + } + + /// Unsubscribe from all subscriptions + pub fn unsubscribe_all(&self, opts: Arc) { + block_on(async move { self.inner.unsubscribe_all(**opts).await }) + } + + /// Get events of filters + /// + /// Get events both from **local database** and **relays** + pub fn get_events_of( + &self, + filters: Vec>, + timeout: Duration, + opts: FilterOptions, + ) -> Result>> { + block_on(async move { + let filters = filters + .into_iter() + .map(|f| f.as_ref().deref().clone()) + .collect(); + Ok(self + .inner + .get_events_of(filters, timeout, opts.into()) + .await? + .into_iter() + .map(|e| Arc::new(e.into())) + .collect()) + }) + } + + /// Get events of filters from **specific relays** + /// + /// Get events both from **local database** and **relays** + /// + /// If no relay is specified, will be queried only the database. + pub fn get_events_from( + &self, + urls: Vec, + filters: Vec>, + timeout: Duration, + opts: FilterOptions, + ) -> Result>> { + block_on(async move { + let filters = filters + .into_iter() + .map(|f| f.as_ref().deref().clone()) + .collect(); + Ok(self + .inner + .get_events_from(urls, filters, timeout, opts.into()) + .await? + .into_iter() + .map(|e| Arc::new(e.into())) + .collect()) + }) + } + + /// Negentropy reconciliation + /// + /// Use events stored in database + pub fn reconcile(&self, filter: &Filter, opts: &NegentropyOptions) -> Result<()> { + block_on(async move { Ok(self.inner.reconcile(filter.deref().clone(), **opts).await?) }) + } + + /// Negentropy reconciliation with custom items + pub fn reconcile_with_items( + &self, + filter: &Filter, + items: HashMap, Arc>, + opts: &NegentropyOptions, + ) -> Result<()> { + block_on(async move { + let items = items.into_iter().map(|(k, v)| (**k, **v)).collect(); + Ok(self + .inner + .reconcile_with_items(filter.deref().clone(), items, **opts) + .await?) + }) + } + + /// Handle relay pool notifications + pub fn handle_notifications( + self: Arc, + handler: Box, + ) -> Result<()> { + thread::spawn(async move { + let handler = Arc::new(handler); + self.inner + .handle_notifications(|notification| async { + match notification { + nostr_sdk::RelayPoolNotification::Message { relay_url, message } => { + let h = handler.clone(); + let _ = spawn_blocking(move || { + h.handle_msg(relay_url.to_string(), Arc::new(message.into())) + }) + .await; + } + nostr_sdk::RelayPoolNotification::Event { + relay_url, + subscription_id, + event, + } => { + let h = handler.clone(); + let _ = spawn_blocking(move || { + h.handle( + relay_url.to_string(), + subscription_id.to_string(), + Arc::new((*event).into()), + ) + }) + .await; + } + _ => (), + } + Ok(false) + }) + .await + })?; + Ok(()) + } +} diff --git a/bindings/nostr-sdk-ffi/src/relay/mod.rs b/bindings/nostr-sdk-ffi/src/relay/mod.rs index 83b28af3d..c13457d54 100644 --- a/bindings/nostr-sdk-ffi/src/relay/mod.rs +++ b/bindings/nostr-sdk-ffi/src/relay/mod.rs @@ -7,7 +7,7 @@ use std::ops::Deref; use std::sync::Arc; use std::time::Duration; -use nostr_ffi::{ClientMessage, Event, EventId, Filter, RelayInformationDocument}; +use nostr_ffi::{ClientMessage, Event, EventId, Filter, RelayInformationDocument, Timestamp}; use nostr_sdk::database::DynNostrDatabase; use nostr_sdk::{block_on, pool, FilterOptions, SubscriptionId, Url}; use uniffi::Object; @@ -145,7 +145,7 @@ impl Relay { /// Connect to relay and keep alive connection pub fn connect(&self, connection_timeout: Option) { - block_on(self.inner.connect(connection_timeout)) + block_on(async move { self.inner.connect(connection_timeout).await }) } /// Disconnect from relay and set status to 'Stopped' @@ -307,6 +307,22 @@ impl Relay { block_on(async move { Ok(self.inner.reconcile(filter.deref().clone(), **opts).await?) }) } + /// Negentropy reconciliation with custom items + pub fn reconcile_with_items( + &self, + filter: &Filter, + items: HashMap, Arc>, + opts: &NegentropyOptions, + ) -> Result<()> { + block_on(async move { + let items = items.into_iter().map(|(k, v)| (**k, **v)).collect(); + Ok(self + .inner + .reconcile_with_items(filter.deref().clone(), items, **opts) + .await?) + }) + } + // TODO: add reconcile_with_items /// Check if relay support negentropy protocol