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 }