decode.rs (8230B)
1 #[cfg(not(feature = "std"))] 2 use alloc::{ 3 string::{String, ToString}, 4 vec::Vec, 5 }; 6 7 use radroots_events::{ 8 RadrootsNostrEvent, 9 comment::RadrootsComment, 10 kinds::{KIND_COMMENT, KIND_POST}, 11 social::RadrootsSocialTarget, 12 tags::{TAG_E_PREV, TAG_E_ROOT}, 13 }; 14 15 use crate::error::EventParseError; 16 use crate::field_helpers::{parse_address_tag, validate_lowercase_hex_64_tag}; 17 use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent}; 18 19 const DEFAULT_KIND: u32 = KIND_COMMENT; 20 21 pub fn comment_from_tags( 22 kind: u32, 23 tags: &[Vec<String>], 24 content: &str, 25 ) -> Result<RadrootsComment, EventParseError> { 26 if kind != DEFAULT_KIND { 27 return Err(EventParseError::InvalidKind { 28 expected: "1111", 29 got: kind, 30 }); 31 } 32 if content.trim().is_empty() { 33 return Err(EventParseError::InvalidTag("content")); 34 } 35 if tags 36 .iter() 37 .any(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_ROOT)) 38 { 39 return Err(EventParseError::InvalidTag(TAG_E_ROOT)); 40 } 41 if tags 42 .iter() 43 .any(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_E_PREV)) 44 { 45 return Err(EventParseError::InvalidTag(TAG_E_PREV)); 46 } 47 48 let root = parse_comment_target(tags, CommentTargetTags::root())?; 49 let parent = parse_comment_target(tags, CommentTargetTags::parent())?; 50 51 Ok(RadrootsComment { 52 root, 53 parent, 54 content: content.to_string(), 55 }) 56 } 57 58 struct CommentTargetTags { 59 event: &'static str, 60 address: &'static str, 61 external: &'static str, 62 author: &'static str, 63 kind: &'static str, 64 } 65 66 impl CommentTargetTags { 67 fn root() -> Self { 68 Self { 69 event: "E", 70 address: "A", 71 external: "I", 72 author: "P", 73 kind: "K", 74 } 75 } 76 77 fn parent() -> Self { 78 Self { 79 event: "e", 80 address: "a", 81 external: "i", 82 author: "p", 83 kind: "k", 84 } 85 } 86 } 87 88 fn parse_comment_target( 89 tags: &[Vec<String>], 90 keys: CommentTargetTags, 91 ) -> Result<RadrootsSocialTarget, EventParseError> { 92 let event_tag = find_tag(tags, keys.event); 93 let address_tag = find_tag(tags, keys.address); 94 let external_tag = find_tag(tags, keys.external); 95 let target_count = usize::from(event_tag.is_some()) 96 + usize::from(address_tag.is_some()) 97 + usize::from(external_tag.is_some()); 98 if target_count > 1 { 99 return Err(EventParseError::InvalidTag(keys.event)); 100 } 101 102 match (event_tag, address_tag, external_tag) { 103 (Some(tag), None, None) => { 104 let id = tag 105 .get(1) 106 .cloned() 107 .ok_or(EventParseError::InvalidTag(keys.event))?; 108 validate_lowercase_hex_64_tag(&id, keys.event)?; 109 let kind = required_numeric_kind(tags, keys.kind)?; 110 validate_comment_target_kind(kind, keys.kind)?; 111 let author = required_author(tags, keys.author)?; 112 let relays = if tag.len() > 2 { 113 Some(tag[2..].to_vec()) 114 } else { 115 None 116 }; 117 Ok(RadrootsSocialTarget::Event { 118 id, 119 author: Some(author), 120 event_kind: Some(kind), 121 relays, 122 }) 123 } 124 (None, Some(tag), None) => { 125 let value = tag 126 .get(1) 127 .cloned() 128 .ok_or(EventParseError::InvalidTag(keys.address))?; 129 let address = parse_address_tag(&value, keys.address)?; 130 let kind = required_numeric_kind(tags, keys.kind)?; 131 validate_comment_target_kind(kind, keys.kind)?; 132 if kind != address.kind { 133 return Err(EventParseError::InvalidTag(keys.kind)); 134 } 135 let author = required_author(tags, keys.author)?; 136 if author != address.pubkey { 137 return Err(EventParseError::InvalidTag(keys.author)); 138 } 139 let relays = if tag.len() > 2 { 140 Some(tag[2..].to_vec()) 141 } else { 142 None 143 }; 144 Ok(RadrootsSocialTarget::Address { 145 address: value, 146 author: Some(author), 147 event_kind: Some(kind), 148 relays, 149 }) 150 } 151 (None, None, Some(tag)) => { 152 let id = tag 153 .get(1) 154 .cloned() 155 .ok_or(EventParseError::InvalidTag(keys.external))?; 156 if id.trim().is_empty() { 157 return Err(EventParseError::InvalidTag(keys.external)); 158 } 159 let external_kind = required_kind_value(tags, keys.kind)?; 160 if external_kind == "1" { 161 return Err(EventParseError::InvalidTag(keys.kind)); 162 } 163 let hint = tag.get(2).filter(|value| !value.trim().is_empty()).cloned(); 164 Ok(RadrootsSocialTarget::External { 165 id, 166 external_kind, 167 hint, 168 }) 169 } 170 _ => Err(EventParseError::MissingTag(keys.event)), 171 } 172 } 173 174 fn validate_comment_target_kind(kind: u32, key: &'static str) -> Result<(), EventParseError> { 175 if kind == KIND_POST { 176 Err(EventParseError::InvalidTag(key)) 177 } else { 178 Ok(()) 179 } 180 } 181 182 fn find_tag<'a>(tags: &'a [Vec<String>], key: &'static str) -> Option<&'a Vec<String>> { 183 tags.iter() 184 .find(|tag| tag.first().map(|value| value.as_str()) == Some(key)) 185 } 186 187 fn required_author(tags: &[Vec<String>], key: &'static str) -> Result<String, EventParseError> { 188 let value = find_tag(tags, key) 189 .and_then(|tag| tag.get(1)) 190 .cloned() 191 .ok_or(EventParseError::MissingTag(key))?; 192 if value.trim().is_empty() { 193 return Err(EventParseError::InvalidTag(key)); 194 } 195 Ok(value) 196 } 197 198 fn required_kind_value(tags: &[Vec<String>], key: &'static str) -> Result<String, EventParseError> { 199 let value = find_tag(tags, key) 200 .and_then(|tag| tag.get(1)) 201 .cloned() 202 .ok_or(EventParseError::MissingTag(key))?; 203 if value.trim().is_empty() { 204 return Err(EventParseError::InvalidTag(key)); 205 } 206 Ok(value) 207 } 208 209 fn required_numeric_kind(tags: &[Vec<String>], key: &'static str) -> Result<u32, EventParseError> { 210 required_kind_value(tags, key)? 211 .parse::<u32>() 212 .map_err(|err| EventParseError::InvalidNumber(key, err)) 213 } 214 215 pub fn data_from_event( 216 id: String, 217 author: String, 218 published_at: u32, 219 kind: u32, 220 content: String, 221 tags: Vec<Vec<String>>, 222 ) -> Result<RadrootsParsedData<RadrootsComment>, EventParseError> { 223 let comment = comment_from_tags(kind, &tags, &content)?; 224 Ok(RadrootsParsedData::new( 225 id, 226 author, 227 published_at, 228 kind, 229 comment, 230 )) 231 } 232 233 pub fn parsed_from_event( 234 id: String, 235 author: String, 236 published_at: u32, 237 kind: u32, 238 content: String, 239 tags: Vec<Vec<String>>, 240 sig: String, 241 ) -> Result<RadrootsParsedEvent<RadrootsComment>, EventParseError> { 242 let data = data_from_event( 243 id.clone(), 244 author.clone(), 245 published_at, 246 kind, 247 content.clone(), 248 tags.clone(), 249 )?; 250 Ok(RadrootsParsedEvent { 251 event: RadrootsNostrEvent { 252 id, 253 author, 254 created_at: published_at, 255 kind, 256 content, 257 tags, 258 sig, 259 }, 260 data, 261 }) 262 } 263 264 #[cfg(test)] 265 mod tests { 266 use super::*; 267 268 #[test] 269 fn comment_decode_rejects_non_numeric_target_kind() { 270 let err = comment_from_tags( 271 DEFAULT_KIND, 272 &[ 273 vec!["E".to_string(), "a".repeat(64)], 274 vec!["P".to_string(), "b".repeat(64)], 275 vec!["K".to_string(), "kind".to_string()], 276 vec!["i".to_string(), "external-id".to_string()], 277 vec!["k".to_string(), "web".to_string()], 278 ], 279 "content", 280 ) 281 .expect_err("non-numeric kind"); 282 283 assert!(matches!(err, EventParseError::InvalidNumber("K", _))); 284 } 285 }