decode.rs (9148B)
1 #[cfg(not(feature = "std"))] 2 use alloc::{ 3 string::{String, ToString}, 4 vec::Vec, 5 }; 6 7 use radroots_events::{ 8 RadrootsNostrEvent, 9 farm::RadrootsFarmRef, 10 kinds::{KIND_FARM, KIND_POST}, 11 post::RadrootsPost, 12 social::{RadrootsSocialFarmAnchor, RadrootsSocialMediaMetadata, RadrootsSocialTarget}, 13 tags::{TAG_A, TAG_IMETA, TAG_Q, TAG_T}, 14 }; 15 16 use crate::error::EventParseError; 17 use crate::field_helpers::{parse_address_tag, tag_values, validate_lowercase_hex_64_tag}; 18 use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent}; 19 use crate::social_helpers::{location_from_tags, parse_dimensions_tag}; 20 21 const DEFAULT_KIND: u32 = KIND_POST; 22 23 pub fn post_from_content(kind: u32, content: &str) -> Result<RadrootsPost, EventParseError> { 24 if kind != DEFAULT_KIND { 25 return Err(EventParseError::InvalidKind { 26 expected: "1", 27 got: kind, 28 }); 29 } 30 if content.trim().is_empty() { 31 return Err(EventParseError::InvalidTag("content")); 32 } 33 Ok(RadrootsPost { 34 content: content.to_string(), 35 farm: None, 36 address_refs: None, 37 location: None, 38 topics: None, 39 quote_refs: None, 40 media: None, 41 }) 42 } 43 44 pub fn post_from_event( 45 kind: u32, 46 tags: &[Vec<String>], 47 content: &str, 48 ) -> Result<RadrootsPost, EventParseError> { 49 let mut post = post_from_content(kind, content)?; 50 post.farm = farm_anchor_from_tags(tags)?; 51 post.address_refs = address_refs_from_tags(tags)?; 52 post.location = location_from_tags(tags); 53 post.topics = non_empty_vec(tag_values(tags, TAG_T)?); 54 post.quote_refs = quote_refs_from_tags(tags)?; 55 post.media = media_from_tags(tags)?; 56 Ok(post) 57 } 58 59 pub fn data_from_event( 60 id: String, 61 author: String, 62 published_at: u32, 63 kind: u32, 64 content: String, 65 tags: Vec<Vec<String>>, 66 ) -> Result<RadrootsParsedData<RadrootsPost>, EventParseError> { 67 let post = post_from_event(kind, &tags, &content)?; 68 Ok(RadrootsParsedData::new( 69 id, 70 author, 71 published_at, 72 kind, 73 post, 74 )) 75 } 76 77 pub fn parsed_from_event( 78 id: String, 79 author: String, 80 published_at: u32, 81 kind: u32, 82 content: String, 83 tags: Vec<Vec<String>>, 84 sig: String, 85 ) -> Result<RadrootsParsedEvent<RadrootsPost>, EventParseError> { 86 let data = data_from_event( 87 id.clone(), 88 author.clone(), 89 published_at, 90 kind, 91 content.clone(), 92 tags.clone(), 93 )?; 94 Ok(RadrootsParsedEvent { 95 event: RadrootsNostrEvent { 96 id, 97 author, 98 created_at: published_at, 99 kind, 100 content, 101 tags, 102 sig, 103 }, 104 data, 105 }) 106 } 107 108 fn farm_anchor_from_tags( 109 tags: &[Vec<String>], 110 ) -> Result<Option<RadrootsSocialFarmAnchor>, EventParseError> { 111 for tag in tags 112 .iter() 113 .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_A)) 114 { 115 let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_A))?; 116 let address = parse_address_tag(value, TAG_A)?; 117 if address.kind == KIND_FARM { 118 let relays = if tag.len() > 2 { 119 Some(tag[2..].to_vec()) 120 } else { 121 None 122 }; 123 return Ok(Some(RadrootsSocialFarmAnchor { 124 farm: RadrootsFarmRef { 125 pubkey: address.pubkey, 126 d_tag: address.d_tag, 127 }, 128 relays, 129 })); 130 } 131 } 132 Ok(None) 133 } 134 135 fn address_refs_from_tags( 136 tags: &[Vec<String>], 137 ) -> Result<Option<Vec<RadrootsSocialTarget>>, EventParseError> { 138 let mut refs = Vec::new(); 139 for tag in tags 140 .iter() 141 .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_A)) 142 { 143 let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_A))?; 144 let address = parse_address_tag(value, TAG_A)?; 145 if address.kind == KIND_FARM { 146 continue; 147 } 148 let relays = if tag.len() > 2 { 149 Some(tag[2..].to_vec()) 150 } else { 151 None 152 }; 153 refs.push(RadrootsSocialTarget::Address { 154 address: value.clone(), 155 author: Some(address.pubkey), 156 event_kind: Some(address.kind), 157 relays, 158 }); 159 } 160 Ok(non_empty_vec(refs)) 161 } 162 163 fn quote_refs_from_tags( 164 tags: &[Vec<String>], 165 ) -> Result<Option<Vec<RadrootsSocialTarget>>, EventParseError> { 166 let mut refs = Vec::new(); 167 for tag in tags 168 .iter() 169 .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_Q)) 170 { 171 let value = tag.get(1).ok_or(EventParseError::InvalidTag(TAG_Q))?; 172 let relays = if tag.len() > 2 { 173 Some(tag[2..].to_vec()) 174 } else { 175 None 176 }; 177 match parse_address_tag(value, TAG_Q) { 178 Ok(address) => refs.push(RadrootsSocialTarget::Address { 179 address: value.clone(), 180 author: Some(address.pubkey), 181 event_kind: Some(address.kind), 182 relays, 183 }), 184 Err(_) => { 185 validate_lowercase_hex_64_tag(value, TAG_Q)?; 186 refs.push(RadrootsSocialTarget::Event { 187 id: value.clone(), 188 author: None, 189 event_kind: None, 190 relays, 191 }); 192 } 193 } 194 } 195 Ok(non_empty_vec(refs)) 196 } 197 198 fn media_from_tags( 199 tags: &[Vec<String>], 200 ) -> Result<Option<Vec<RadrootsSocialMediaMetadata>>, EventParseError> { 201 let mut media = Vec::new(); 202 for tag in tags 203 .iter() 204 .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_IMETA)) 205 { 206 if tag.len() < 2 { 207 return Err(EventParseError::InvalidTag(TAG_IMETA)); 208 } 209 let raw = tag[1..].to_vec(); 210 if raw.iter().any(|value| value.trim().is_empty()) { 211 return Err(EventParseError::InvalidTag(TAG_IMETA)); 212 } 213 let mut item = RadrootsSocialMediaMetadata { 214 imeta: Some(vec![raw.clone()]), 215 ..RadrootsSocialMediaMetadata::default() 216 }; 217 for entry in raw { 218 parse_imeta_entry(&mut item, &entry)?; 219 } 220 media.push(item); 221 } 222 Ok(non_empty_vec(media)) 223 } 224 225 fn parse_imeta_entry( 226 item: &mut RadrootsSocialMediaMetadata, 227 entry: &str, 228 ) -> Result<(), EventParseError> { 229 let Some((key, value)) = entry.split_once(' ') else { 230 return Err(EventParseError::InvalidTag(TAG_IMETA)); 231 }; 232 if value.trim().is_empty() { 233 return Err(EventParseError::InvalidTag(TAG_IMETA)); 234 } 235 match key { 236 "url" => item.url = Some(value.to_string()), 237 "m" => item.mime_type = Some(value.to_string()), 238 "x" => item.sha256 = Some(value.to_string()), 239 "ox" => item.original_sha256 = Some(value.to_string()), 240 "size" => { 241 item.size = Some( 242 value 243 .parse::<u64>() 244 .map_err(|err| EventParseError::InvalidNumber(TAG_IMETA, err))?, 245 ); 246 } 247 "dim" => item.dimensions = Some(parse_dimensions_tag(value, TAG_IMETA)?), 248 "blurhash" => item.blurhash = Some(value.to_string()), 249 "image" => item.image = Some(value.to_string()), 250 "summary" => item.summary = Some(value.to_string()), 251 "alt" => item.alt = Some(value.to_string()), 252 "fallback" => item.fallback = Some(value.to_string()), 253 "magnet" => item.magnet = Some(value.to_string()), 254 "i" => push_repeated_value(&mut item.content_hashes, value), 255 "service" => push_repeated_value(&mut item.services, value), 256 "thumb" => {} 257 _ => {} 258 } 259 Ok(()) 260 } 261 262 fn push_repeated_value(values: &mut Option<Vec<String>>, value: &str) { 263 values.get_or_insert_with(Vec::new).push(value.to_string()); 264 } 265 266 fn non_empty_vec<T>(values: Vec<T>) -> Option<Vec<T>> { 267 if values.is_empty() { 268 None 269 } else { 270 Some(values) 271 } 272 } 273 274 #[cfg(test)] 275 mod tests { 276 use super::*; 277 278 #[test] 279 fn post_decode_accepts_address_ref_without_relays_and_unknown_imeta_keys() { 280 let author = "a".repeat(64); 281 let post = post_from_event( 282 DEFAULT_KIND, 283 &[ 284 vec![ 285 TAG_A.to_string(), 286 format!("30023:{author}:AAAAAAAAAAAAAAAAAAAAAA"), 287 ], 288 vec![ 289 TAG_IMETA.to_string(), 290 "url https://media.example.invalid/a.jpg".to_string(), 291 "custom value".to_string(), 292 ], 293 ], 294 "fresh carrots", 295 ) 296 .expect("post"); 297 298 let refs = post.address_refs.expect("address refs"); 299 assert!(matches!( 300 &refs[0], 301 RadrootsSocialTarget::Address { relays: None, .. } 302 )); 303 let media = post.media.expect("media"); 304 assert_eq!( 305 media[0].url.as_deref(), 306 Some("https://media.example.invalid/a.jpg") 307 ); 308 } 309 }