decode.rs (4990B)
1 #[cfg(not(feature = "std"))] 2 use alloc::{ 3 string::{String, ToString}, 4 vec::Vec, 5 }; 6 7 use radroots_event::{ 8 envelope::kind::KIND_PUBLIC_FILE_METADATA, 9 media::file_metadata::FileMetadata, 10 social::SocialMediaThumbnail, 11 tag::name::{ 12 TAG_ALT, TAG_BLURHASH, TAG_DIMENSIONS, TAG_FALLBACK, TAG_MAGNET, TAG_MIME, 13 TAG_ORIGINAL_SHA256, TAG_SERVICE, TAG_SHA256, TAG_SIZE, TAG_SUMMARY, TAG_THUMB, TAG_URL, 14 }, 15 }; 16 17 use crate::error::EventParseError; 18 use crate::field_helpers::{ 19 optional_tag_value, required_tag_value, tag_values, validate_lowercase_hex_64_tag, 20 }; 21 use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent}; 22 use crate::social_helpers::{first_tag_value, parse_dimensions_tag}; 23 24 const EXPECTED_KIND: &str = "1063"; 25 const TAG_RADROOTS_OWNER_DOCUMENT: &str = "radroots:owner_document"; 26 27 pub fn file_metadata_from_event( 28 kind: u32, 29 tags: &[Vec<String>], 30 content: &str, 31 ) -> Result<FileMetadata, EventParseError> { 32 if kind != KIND_PUBLIC_FILE_METADATA { 33 return Err(EventParseError::InvalidKind { 34 expected: EXPECTED_KIND, 35 got: kind, 36 }); 37 } 38 reject_private_farm_file_tags(tags)?; 39 let url = required_tag_value(tags, TAG_URL)?; 40 let mime_type = required_tag_value(tags, TAG_MIME)?; 41 let sha256 = required_tag_value(tags, TAG_SHA256)?; 42 validate_lowercase_hex_64_tag(&sha256, TAG_SHA256)?; 43 let original_sha256 = optional_hash_tag(tags, TAG_ORIGINAL_SHA256)?; 44 let size = optional_tag_value(tags, TAG_SIZE)? 45 .map(|value| { 46 value 47 .parse::<u64>() 48 .map_err(|err| EventParseError::InvalidNumber(TAG_SIZE, err)) 49 }) 50 .transpose()?; 51 let dimensions = optional_tag_value(tags, TAG_DIMENSIONS)? 52 .map(|value| parse_dimensions_tag(&value, TAG_DIMENSIONS)) 53 .transpose()?; 54 55 Ok(FileMetadata { 56 url, 57 mime_type, 58 sha256, 59 original_sha256, 60 size, 61 dimensions, 62 blurhash: first_tag_value(tags, TAG_BLURHASH), 63 thumbnails: parse_thumbnails(tags)?, 64 summary: first_tag_value(tags, TAG_SUMMARY), 65 alt: first_tag_value(tags, TAG_ALT), 66 fallback: first_tag_value(tags, TAG_FALLBACK), 67 magnet: first_tag_value(tags, TAG_MAGNET), 68 content_hashes: non_empty_vec(tag_values(tags, "i")?), 69 services: non_empty_vec(tag_values(tags, TAG_SERVICE)?), 70 content: if content.is_empty() { 71 None 72 } else { 73 Some(content.to_string()) 74 }, 75 }) 76 } 77 78 fn reject_private_farm_file_tags(tags: &[Vec<String>]) -> Result<(), EventParseError> { 79 if tags 80 .iter() 81 .any(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_RADROOTS_OWNER_DOCUMENT)) 82 { 83 Err(EventParseError::InvalidTag(TAG_RADROOTS_OWNER_DOCUMENT)) 84 } else { 85 Ok(()) 86 } 87 } 88 89 pub fn data_from_event( 90 id: String, 91 author: String, 92 published_at: u64, 93 kind: u32, 94 content: String, 95 tags: Vec<Vec<String>>, 96 ) -> Result<RadrootsParsedData<FileMetadata>, EventParseError> { 97 let metadata = file_metadata_from_event(kind, &tags, &content)?; 98 Ok(RadrootsParsedData::new( 99 id, 100 author, 101 published_at, 102 kind, 103 metadata, 104 )) 105 } 106 107 pub fn parsed_from_event( 108 id: String, 109 author: String, 110 published_at: u64, 111 kind: u32, 112 content: String, 113 tags: Vec<Vec<String>>, 114 sig: String, 115 ) -> Result<RadrootsParsedEvent<FileMetadata>, EventParseError> { 116 let data = data_from_event( 117 id.clone(), 118 author.clone(), 119 published_at, 120 kind, 121 content.clone(), 122 tags.clone(), 123 )?; 124 RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) 125 } 126 127 fn optional_hash_tag( 128 tags: &[Vec<String>], 129 key: &'static str, 130 ) -> Result<Option<String>, EventParseError> { 131 let Some(value) = optional_tag_value(tags, key)? else { 132 return Ok(None); 133 }; 134 validate_lowercase_hex_64_tag(&value, key)?; 135 Ok(Some(value)) 136 } 137 138 fn parse_thumbnails( 139 tags: &[Vec<String>], 140 ) -> Result<Option<Vec<SocialMediaThumbnail>>, EventParseError> { 141 let thumbnails = tags 142 .iter() 143 .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_THUMB)) 144 .map(|tag| { 145 let url = tag 146 .get(1) 147 .cloned() 148 .ok_or(EventParseError::InvalidTag(TAG_THUMB))?; 149 let dimensions = tag 150 .get(2) 151 .filter(|value| !value.trim().is_empty()) 152 .map(|value| parse_dimensions_tag(value, TAG_THUMB)) 153 .transpose()?; 154 Ok(SocialMediaThumbnail { url, dimensions }) 155 }) 156 .collect::<Result<Vec<_>, EventParseError>>()?; 157 Ok(non_empty_vec(thumbnails)) 158 } 159 160 fn non_empty_vec<T>(values: Vec<T>) -> Option<Vec<T>> { 161 if values.is_empty() { 162 None 163 } else { 164 Some(values) 165 } 166 }