application_handler.rs (4869B)
1 //! Typed authoring helpers for NIP-89 application-handler announcements. 2 3 #![forbid(unsafe_code)] 4 5 extern crate alloc; 6 7 use alloc::{string::String, vec::Vec}; 8 9 use crate::error::Error; 10 use crate::events::build_event_unchecked; 11 use crate::types::{GenericBuilder, RadrootsNostrMetadata}; 12 use radroots_event::envelope::kind::KIND_APPLICATION_HANDLER; 13 14 #[derive(Debug, Clone)] 15 pub struct ApplicationHandlerSpec { 16 kinds: Vec<u32>, 17 identifier: Option<String>, 18 metadata: Option<RadrootsNostrMetadata>, 19 extra_tags: Vec<Vec<String>>, 20 relays: Vec<String>, 21 nostrconnect_url: Option<String>, 22 } 23 24 impl ApplicationHandlerSpec { 25 pub fn new(kinds: Vec<u32>) -> Self { 26 Self { 27 kinds, 28 identifier: None, 29 metadata: None, 30 extra_tags: Vec::new(), 31 relays: Vec::new(), 32 nostrconnect_url: None, 33 } 34 } 35 36 #[must_use] 37 pub fn with_identifier(mut self, identifier: impl Into<String>) -> Self { 38 self.identifier = Some(identifier.into()); 39 self 40 } 41 42 #[must_use] 43 pub fn with_metadata(mut self, metadata: RadrootsNostrMetadata) -> Self { 44 self.metadata = Some(metadata); 45 self 46 } 47 48 #[must_use] 49 pub fn with_extra_tags(mut self, extra_tags: Vec<Vec<String>>) -> Self { 50 self.extra_tags = extra_tags; 51 self 52 } 53 54 #[must_use] 55 pub fn with_relays(mut self, relays: Vec<String>) -> Self { 56 self.relays = relays; 57 self 58 } 59 60 #[must_use] 61 pub fn with_nostr_connect_url(mut self, url: impl Into<String>) -> Self { 62 self.nostrconnect_url = Some(url.into()); 63 self 64 } 65 66 pub fn kinds(&self) -> &[u32] { 67 &self.kinds 68 } 69 70 pub fn identifier(&self) -> Option<&str> { 71 self.identifier.as_deref() 72 } 73 74 pub fn metadata(&self) -> Option<&RadrootsNostrMetadata> { 75 self.metadata.as_ref() 76 } 77 78 pub fn extra_tags(&self) -> &[Vec<String>] { 79 &self.extra_tags 80 } 81 82 pub fn relays(&self) -> &[String] { 83 &self.relays 84 } 85 86 pub fn nostr_connect_url(&self) -> Option<&str> { 87 self.nostrconnect_url.as_deref() 88 } 89 } 90 91 pub fn build_application_handler_event( 92 spec: &ApplicationHandlerSpec, 93 ) -> Result<GenericBuilder, Error> { 94 if spec.kinds.is_empty() { 95 return Err(Error::FilterTagError( 96 "application handler kinds are empty".to_string(), 97 )); 98 } 99 100 let identifier = spec 101 .identifier 102 .clone() 103 .unwrap_or_else(|| spec.kinds[0].to_string()); 104 105 let mut content = String::new(); 106 if let Some(md) = spec.metadata.as_ref() 107 && metadata_has_fields(md) 108 { 109 content = serde_json::to_string(md).unwrap_or_default(); 110 } 111 112 let mut tags = Vec::new(); 113 tags.push(vec!["d".to_string(), identifier]); 114 for kind in &spec.kinds { 115 tags.push(vec!["k".to_string(), kind.to_string()]); 116 } 117 for relay in &spec.relays { 118 let relay = relay.trim(); 119 if relay.is_empty() { 120 continue; 121 } 122 tags.push(vec!["relay".to_string(), relay.to_string()]); 123 } 124 if let Some(url) = spec.nostrconnect_url.as_ref() { 125 let url = url.trim(); 126 if !url.is_empty() { 127 tags.push(vec!["nostrconnect_url".to_string(), url.to_string()]); 128 } 129 } 130 for tag in &spec.extra_tags { 131 if tag.is_empty() { 132 continue; 133 } 134 tags.push(tag.clone()); 135 } 136 137 let builder = build_event_unchecked(KIND_APPLICATION_HANDLER, content, tags)?; 138 Ok(GenericBuilder::from_unchecked(builder)) 139 } 140 141 pub fn metadata_has_fields(md: &RadrootsNostrMetadata) -> bool { 142 md.name.is_some() 143 || md.display_name.is_some() 144 || md.about.is_some() 145 || md.website.is_some() 146 || md.picture.is_some() 147 || md.banner.is_some() 148 || md.nip05.is_some() 149 || md.lud06.is_some() 150 || md.lud16.is_some() 151 || !md.custom.is_empty() 152 } 153 154 #[cfg(test)] 155 mod tests { 156 use super::{ApplicationHandlerSpec, build_application_handler_event, metadata_has_fields}; 157 use crate::error::Error; 158 use crate::types::RadrootsNostrMetadata; 159 160 #[test] 161 fn metadata_has_fields_false_when_empty() { 162 assert!(!metadata_has_fields(&RadrootsNostrMetadata::default())); 163 } 164 165 #[test] 166 fn metadata_has_fields_true_when_about_is_set() { 167 let metadata = RadrootsNostrMetadata { 168 about: Some("ready".to_string()), 169 ..Default::default() 170 }; 171 assert!(metadata_has_fields(&metadata)); 172 } 173 174 #[test] 175 fn application_handler_requires_at_least_one_kind() { 176 assert!(matches!( 177 build_application_handler_event(&ApplicationHandlerSpec::new(Vec::new())), 178 Err(Error::FilterTagError(message)) if message == "application handler kinds are empty" 179 )); 180 } 181 }