field_lib

Cross-platform Rust runtime for Radroots iOS and Android apps
git clone https://radroots.dev/git/field_lib.git
Log | Files | Refs | README | LICENSE

nostr.rs (12132B)


      1 use super::RadrootsRuntime;
      2 use crate::RadrootsAppError;
      3 #[cfg(feature = "nostr-client")]
      4 use tokio::sync::broadcast::error::TryRecvError;
      5 
      6 #[cfg(feature = "nostr-client")]
      7 fn nostr_manager(
      8     runtime: &RadrootsRuntime,
      9 ) -> Result<radroots_net_core::nostr_client::NostrClientManager, RadrootsAppError> {
     10     let guard = runtime
     11         .net
     12         .lock()
     13         .map_err(|err| RadrootsAppError::runtime(format!("{err}")))?;
     14     guard
     15         .nostr
     16         .clone()
     17         .ok_or_else(|| RadrootsAppError::relay("nostr not initialized"))
     18 }
     19 
     20 #[derive(uniffi::Enum, Debug, Clone, Copy, PartialEq, Eq)]
     21 pub enum NostrLight {
     22     Red,
     23     Yellow,
     24     Green,
     25 }
     26 
     27 #[derive(uniffi::Record, Debug, Clone)]
     28 pub struct NostrConnectionStatus {
     29     pub light: NostrLight,
     30     pub connected: u32,
     31     pub connecting: u32,
     32     pub last_error: Option<String>,
     33 }
     34 
     35 #[derive(uniffi::Record, Debug, Clone, Default)]
     36 pub struct NostrProfile {
     37     pub name: Option<String>,
     38     pub display_name: Option<String>,
     39     pub nip05: Option<String>,
     40     pub about: Option<String>,
     41     pub website: Option<String>,
     42     pub picture: Option<String>,
     43     pub banner: Option<String>,
     44     pub lud06: Option<String>,
     45     pub lud16: Option<String>,
     46     pub bot: Option<String>,
     47 }
     48 
     49 #[derive(uniffi::Record, Debug, Clone)]
     50 pub struct NostrProfileEventMetadata {
     51     pub id: String,
     52     pub author: String,
     53     pub published_at: u64,
     54     pub profile: NostrProfile,
     55 }
     56 
     57 #[derive(uniffi::Record, Debug, Clone)]
     58 pub struct NostrEvent {
     59     pub id: String,
     60     pub author: String,
     61     pub created_at: u64,
     62     pub kind: u32,
     63     pub content: String,
     64 }
     65 
     66 #[derive(uniffi::Record, Debug, Clone)]
     67 pub struct NostrPost {
     68     pub content: String,
     69 }
     70 
     71 #[derive(uniffi::Record, Debug, Clone)]
     72 pub struct NostrPostEventMetadata {
     73     pub id: String,
     74     pub author: String,
     75     pub published_at: u64,
     76     pub post: NostrPost,
     77 }
     78 
     79 #[cfg(feature = "nostr-client")]
     80 fn map_post_event_metadata(
     81     event: radroots_events_codec::parsed::RadrootsParsedData<radroots_events::post::RadrootsPost>,
     82 ) -> NostrPostEventMetadata {
     83     NostrPostEventMetadata {
     84         id: event.id,
     85         author: event.author,
     86         published_at: event.published_at as u64,
     87         post: NostrPost {
     88             content: event.data.content,
     89         },
     90     }
     91 }
     92 
     93 #[cfg(feature = "nostr-client")]
     94 fn map_profile_event_metadata(
     95     event: radroots_events_codec::parsed::RadrootsParsedData<
     96         radroots_events_codec::profile::RadrootsProfileData,
     97     >,
     98 ) -> NostrProfileEventMetadata {
     99     NostrProfileEventMetadata {
    100         id: event.id,
    101         author: event.author,
    102         published_at: event.published_at as u64,
    103         profile: NostrProfile {
    104             name: event.data.profile.name.into(),
    105             display_name: event.data.profile.display_name.into(),
    106             nip05: event.data.profile.nip05.into(),
    107             about: event.data.profile.about.into(),
    108             website: event.data.profile.website,
    109             picture: event.data.profile.picture,
    110             banner: event.data.profile.banner,
    111             lud06: event.data.profile.lud06,
    112             lud16: event.data.profile.lud16,
    113             bot: event.data.profile.bot,
    114         },
    115     }
    116 }
    117 
    118 #[cfg_attr(not(coverage_nightly), uniffi::export)]
    119 impl RadrootsRuntime {
    120     pub fn nostr_set_default_relays(&self, relays: Vec<String>) -> Result<(), RadrootsAppError> {
    121         #[cfg(feature = "nostr-client")]
    122         {
    123             let mut guard = match self.net.lock() {
    124                 Ok(guard) => guard,
    125                 Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
    126             };
    127             guard
    128                 .nostr_set_default_relays(&relays)
    129                 .map_err(|e| RadrootsAppError::relay(format!("{e}")))
    130         }
    131         #[cfg(not(feature = "nostr-client"))]
    132         {
    133             let _ = relays;
    134             Err(RadrootsAppError::unsupported("nostr disabled"))
    135         }
    136     }
    137 
    138     pub fn nostr_connect_if_key_present(&self) -> Result<(), RadrootsAppError> {
    139         #[cfg(feature = "nostr-client")]
    140         {
    141             let mut guard = match self.net.lock() {
    142                 Ok(guard) => guard,
    143                 Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
    144             };
    145             guard
    146                 .nostr_connect_if_key_present()
    147                 .map_err(|e| RadrootsAppError::relay(format!("{e}")))
    148         }
    149         #[cfg(not(feature = "nostr-client"))]
    150         {
    151             Err(RadrootsAppError::unsupported("nostr disabled"))
    152         }
    153     }
    154 
    155     pub fn nostr_connection_status(&self) -> NostrConnectionStatus {
    156         #[cfg(feature = "nostr-client")]
    157         {
    158             let guard = self.net.lock();
    159             if let Ok(g) = guard {
    160                 if let Some(s) = g.nostr_connection_snapshot() {
    161                     let light = match s.light {
    162                         radroots_net_core::nostr_client::Light::Green => NostrLight::Green,
    163                         radroots_net_core::nostr_client::Light::Yellow => NostrLight::Yellow,
    164                         radroots_net_core::nostr_client::Light::Red => NostrLight::Red,
    165                     };
    166                     return NostrConnectionStatus {
    167                         light,
    168                         connected: s.connected as u32,
    169                         connecting: s.connecting as u32,
    170                         last_error: s.last_error,
    171                     };
    172                 }
    173             }
    174             NostrConnectionStatus {
    175                 light: NostrLight::Red,
    176                 connected: 0,
    177                 connecting: 0,
    178                 last_error: None,
    179             }
    180         }
    181 
    182         #[cfg(not(feature = "nostr-client"))]
    183         {
    184             NostrConnectionStatus {
    185                 light: NostrLight::Red,
    186                 connected: 0,
    187                 connecting: 0,
    188                 last_error: None,
    189             }
    190         }
    191     }
    192 
    193     pub fn nostr_profile_for_self(
    194         &self,
    195     ) -> Result<Option<NostrProfileEventMetadata>, RadrootsAppError> {
    196         #[cfg(feature = "nostr-client")]
    197         {
    198             let (pk, mgr) = {
    199                 let guard = self
    200                     .net
    201                     .lock()
    202                     .map_err(|err| RadrootsAppError::runtime(format!("{err}")))?;
    203                 let keys = guard.selected_nostr_keys().ok_or_else(|| {
    204                     RadrootsAppError::identity("selected signing identity is not configured")
    205                 })?;
    206                 let pk = keys.public_key();
    207                 let mgr = guard
    208                     .nostr
    209                     .clone()
    210                     .ok_or_else(|| RadrootsAppError::relay("nostr not initialized"))?;
    211                 (pk, mgr)
    212             };
    213             let out = mgr
    214                 .fetch_profile_event_blocking(pk)
    215                 .map_err(|error| RadrootsAppError::relay(error.to_string()))?;
    216             Ok(out.map(map_profile_event_metadata))
    217         }
    218         #[cfg(not(feature = "nostr-client"))]
    219         {
    220             Err(RadrootsAppError::unsupported("nostr disabled"))
    221         }
    222     }
    223 
    224     pub fn nostr_post_profile(
    225         &self,
    226         name: Option<String>,
    227         display_name: Option<String>,
    228         nip05: Option<String>,
    229         about: Option<String>,
    230     ) -> Result<String, RadrootsAppError> {
    231         #[cfg(feature = "nostr-client")]
    232         {
    233             let mgr = nostr_manager(self)?;
    234             mgr.publish_profile_event_blocking(name, display_name, nip05, about)
    235                 .map_err(|e| RadrootsAppError::relay(e.to_string()))
    236         }
    237         #[cfg(not(feature = "nostr-client"))]
    238         {
    239             let _ = (name, display_name, nip05, about);
    240             Err(RadrootsAppError::unsupported("nostr disabled"))
    241         }
    242     }
    243 
    244     pub fn nostr_post_text_note(&self, content: String) -> Result<String, RadrootsAppError> {
    245         #[cfg(feature = "nostr-client")]
    246         {
    247             let mgr = nostr_manager(self)?;
    248             mgr.publish_post_event_blocking(content)
    249                 .map_err(|e| RadrootsAppError::relay(e.to_string()))
    250         }
    251         #[cfg(not(feature = "nostr-client"))]
    252         {
    253             let _ = content;
    254             Err(RadrootsAppError::unsupported("nostr disabled"))
    255         }
    256     }
    257 
    258     pub fn nostr_fetch_text_notes(
    259         &self,
    260         limit: u16,
    261         since_unix: Option<u64>,
    262     ) -> Result<Vec<NostrPostEventMetadata>, RadrootsAppError> {
    263         #[cfg(feature = "nostr-client")]
    264         {
    265             let mgr = nostr_manager(self)?;
    266             let items = mgr
    267                 .fetch_post_events_blocking(limit, since_unix)
    268                 .map_err(|e| RadrootsAppError::relay(e.to_string()))?;
    269             Ok(items.into_iter().map(map_post_event_metadata).collect())
    270         }
    271         #[cfg(not(feature = "nostr-client"))]
    272         {
    273             let _ = (limit, since_unix);
    274             Err(RadrootsAppError::unsupported("nostr disabled"))
    275         }
    276     }
    277 
    278     pub fn nostr_post_reply(
    279         &self,
    280         parent_event_id_hex: String,
    281         parent_author_hex: String,
    282         content: String,
    283         root_event_id_hex: Option<String>,
    284     ) -> Result<String, RadrootsAppError> {
    285         #[cfg(feature = "nostr-client")]
    286         {
    287             let mgr = nostr_manager(self)?;
    288             mgr.publish_post_reply_event_blocking(
    289                 parent_event_id_hex,
    290                 parent_author_hex,
    291                 content,
    292                 root_event_id_hex,
    293             )
    294             .map_err(|e| RadrootsAppError::relay(e.to_string()))
    295         }
    296         #[cfg(not(feature = "nostr-client"))]
    297         {
    298             let _ = (
    299                 parent_event_id_hex,
    300                 parent_author_hex,
    301                 content,
    302                 root_event_id_hex,
    303             );
    304             Err(RadrootsAppError::unsupported("nostr disabled"))
    305         }
    306     }
    307 
    308     pub fn nostr_start_post_event_stream(
    309         &self,
    310         since_unix: Option<u64>,
    311     ) -> Result<(), RadrootsAppError> {
    312         #[cfg(feature = "nostr-client")]
    313         {
    314             let mgr = nostr_manager(self)?;
    315             mgr.start_post_event_stream(since_unix);
    316             if let Ok(mut rx_guard) = self.post_events_rx.lock() {
    317                 if rx_guard.is_none() {
    318                     *rx_guard = Some(mgr.subscribe_post_events());
    319                 }
    320             }
    321             Ok(())
    322         }
    323         #[cfg(not(feature = "nostr-client"))]
    324         {
    325             let _ = since_unix;
    326             Err(RadrootsAppError::unsupported("nostr disabled"))
    327         }
    328     }
    329 
    330     pub fn nostr_next_post_event(
    331         &self,
    332     ) -> Result<Option<NostrPostEventMetadata>, RadrootsAppError> {
    333         #[cfg(feature = "nostr-client")]
    334         {
    335             let mut rx_guard = match self.post_events_rx.lock() {
    336                 Ok(guard) => guard,
    337                 Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
    338             };
    339             let Some(rx) = rx_guard.as_mut() else {
    340                 return Ok(None);
    341             };
    342             match rx.try_recv() {
    343                 Ok(event) => Ok(Some(map_post_event_metadata(event))),
    344                 Err(TryRecvError::Empty) => Ok(None),
    345                 Err(TryRecvError::Lagged(count)) => Err(RadrootsAppError::relay(format!(
    346                     "post event stream lagged by {count} events"
    347                 ))),
    348                 Err(TryRecvError::Closed) => {
    349                     *rx_guard = None;
    350                     Err(RadrootsAppError::relay("post event stream closed"))
    351                 }
    352             }
    353         }
    354         #[cfg(not(feature = "nostr-client"))]
    355         {
    356             Err(RadrootsAppError::unsupported("nostr disabled"))
    357         }
    358     }
    359 
    360     pub fn nostr_stop_post_event_stream(&self) -> Result<(), RadrootsAppError> {
    361         #[cfg(feature = "nostr-client")]
    362         {
    363             let mgr = nostr_manager(self)?;
    364             mgr.stop_post_event_stream();
    365             if let Ok(mut rx_guard) = self.post_events_rx.lock() {
    366                 *rx_guard = None;
    367             }
    368             Ok(())
    369         }
    370         #[cfg(not(feature = "nostr-client"))]
    371         {
    372             Err(RadrootsAppError::unsupported("nostr disabled"))
    373         }
    374     }
    375 }