lib

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

application_handler.rs (5299B)


      1 #![forbid(unsafe_code)]
      2 
      3 extern crate alloc;
      4 
      5 use alloc::{string::String, vec::Vec};
      6 
      7 use crate::error::RadrootsNostrError;
      8 use crate::events::radroots_nostr_build_event;
      9 #[cfg(feature = "client")]
     10 use crate::filter::radroots_nostr_filter_tag;
     11 #[cfg(feature = "client")]
     12 use crate::tags::radroots_nostr_tag_first_value;
     13 #[cfg(feature = "client")]
     14 use crate::types::{RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKind};
     15 use crate::types::{RadrootsNostrEventBuilder, RadrootsNostrMetadata};
     16 #[cfg(feature = "client")]
     17 use core::time::Duration;
     18 use radroots_events::kinds::KIND_APPLICATION_HANDLER;
     19 
     20 #[derive(Debug, Clone)]
     21 pub struct RadrootsNostrApplicationHandlerSpec {
     22     pub kinds: Vec<u32>,
     23     pub identifier: Option<String>,
     24     pub metadata: Option<RadrootsNostrMetadata>,
     25     pub extra_tags: Vec<Vec<String>>,
     26     pub relays: Vec<String>,
     27     pub nostrconnect_url: Option<String>,
     28 }
     29 
     30 impl RadrootsNostrApplicationHandlerSpec {
     31     pub fn new(kinds: Vec<u32>) -> Self {
     32         Self {
     33             kinds,
     34             identifier: None,
     35             metadata: None,
     36             extra_tags: Vec::new(),
     37             relays: Vec::new(),
     38             nostrconnect_url: None,
     39         }
     40     }
     41 }
     42 
     43 pub fn radroots_nostr_build_application_handler_event(
     44     spec: &RadrootsNostrApplicationHandlerSpec,
     45 ) -> Result<RadrootsNostrEventBuilder, RadrootsNostrError> {
     46     if spec.kinds.is_empty() {
     47         return Err(RadrootsNostrError::FilterTagError(
     48             "application handler kinds are empty".to_string(),
     49         ));
     50     }
     51 
     52     let identifier = spec
     53         .identifier
     54         .clone()
     55         .unwrap_or_else(|| spec.kinds[0].to_string());
     56 
     57     let mut content = String::new();
     58     if let Some(md) = spec.metadata.as_ref()
     59         && radroots_nostr_metadata_has_fields(md)
     60     {
     61         content = serde_json::to_string(md).unwrap_or_default();
     62     }
     63 
     64     let mut tags = Vec::new();
     65     tags.push(vec!["d".to_string(), identifier]);
     66     for kind in &spec.kinds {
     67         tags.push(vec!["k".to_string(), kind.to_string()]);
     68     }
     69     for relay in &spec.relays {
     70         let relay = relay.trim();
     71         if relay.is_empty() {
     72             continue;
     73         }
     74         tags.push(vec!["relay".to_string(), relay.to_string()]);
     75     }
     76     if let Some(url) = spec.nostrconnect_url.as_ref() {
     77         let url = url.trim();
     78         if !url.is_empty() {
     79             tags.push(vec!["nostrconnect_url".to_string(), url.to_string()]);
     80         }
     81     }
     82     for tag in &spec.extra_tags {
     83         if tag.is_empty() {
     84             continue;
     85         }
     86         tags.push(tag.clone());
     87     }
     88 
     89     radroots_nostr_build_event(KIND_APPLICATION_HANDLER, content, tags)
     90 }
     91 
     92 pub fn radroots_nostr_metadata_has_fields(md: &RadrootsNostrMetadata) -> bool {
     93     md.name.is_some()
     94         || md.display_name.is_some()
     95         || md.about.is_some()
     96         || md.website.is_some()
     97         || md.picture.is_some()
     98         || md.banner.is_some()
     99         || md.nip05.is_some()
    100         || md.lud06.is_some()
    101         || md.lud16.is_some()
    102         || !md.custom.is_empty()
    103 }
    104 
    105 #[cfg(feature = "client")]
    106 pub async fn radroots_nostr_publish_application_handler(
    107     client: &crate::client::RadrootsNostrClient,
    108     spec: &RadrootsNostrApplicationHandlerSpec,
    109 ) -> Result<crate::types::RadrootsNostrOutput<crate::types::RadrootsNostrEventId>, RadrootsNostrError>
    110 {
    111     let mut spec = spec.clone();
    112     if spec.identifier.is_none()
    113         && let Some(existing) = fetch_existing_identifier(client, &spec).await?
    114     {
    115         spec.identifier = Some(existing);
    116     }
    117     let builder = radroots_nostr_build_application_handler_event(&spec)?;
    118     crate::client::radroots_nostr_send_event(client, builder).await
    119 }
    120 
    121 #[cfg(feature = "client")]
    122 async fn fetch_existing_identifier(
    123     client: &crate::client::RadrootsNostrClient,
    124     spec: &RadrootsNostrApplicationHandlerSpec,
    125 ) -> Result<Option<String>, RadrootsNostrError> {
    126     let first_kind = spec
    127         .kinds
    128         .first()
    129         .ok_or_else(|| RadrootsNostrError::FilterTagError("kinds are empty".to_string()))?;
    130     let author = client.public_key().await?;
    131     let filter = RadrootsNostrFilter::new()
    132         .author(author)
    133         .kind(RadrootsNostrKind::Custom(KIND_APPLICATION_HANDLER as u16));
    134     let filter = radroots_nostr_filter_tag(filter, "k", vec![first_kind.to_string()])?;
    135     let mut events = client.fetch_events(filter, Duration::from_secs(5)).await?;
    136     events.sort_by_key(|event| event.created_at.as_secs());
    137     let event = events.pop();
    138     Ok(event.and_then(|event| tag_value(&event, "d")))
    139 }
    140 
    141 #[cfg(feature = "client")]
    142 fn tag_value(event: &RadrootsNostrEvent, key: &str) -> Option<String> {
    143     event
    144         .tags
    145         .iter()
    146         .find_map(|tag| radroots_nostr_tag_first_value(tag, key))
    147 }
    148 
    149 #[cfg(test)]
    150 mod tests {
    151     use super::radroots_nostr_metadata_has_fields;
    152     use crate::types::RadrootsNostrMetadata;
    153 
    154     #[test]
    155     fn metadata_has_fields_false_when_empty() {
    156         assert!(!radroots_nostr_metadata_has_fields(
    157             &RadrootsNostrMetadata::default()
    158         ));
    159     }
    160 
    161     #[test]
    162     fn metadata_has_fields_true_when_about_is_set() {
    163         let mut metadata = RadrootsNostrMetadata::default();
    164         metadata.about = Some("ready".to_string());
    165         assert!(radroots_nostr_metadata_has_fields(&metadata));
    166     }
    167 }