repost.rs (14928B)
1 #![cfg(feature = "json")] 2 3 use radroots_event::{ 4 envelope::kind::{KIND_ARTICLE, KIND_GENERIC_REPOST, KIND_POST, KIND_REACTION, KIND_REPOST}, 5 post::repost::{GenericRepost, Repost}, 6 social::SocialTarget, 7 tag::name::{TAG_A, TAG_E, TAG_K, TAG_P}, 8 }; 9 use radroots_event_codec::decode::{ 10 EventParseError, 11 repost::{ 12 generic_repost_data_from_event, generic_repost_from_event, 13 generic_repost_parsed_from_event, repost_data_from_event, repost_from_event, 14 repost_parsed_from_event, 15 }, 16 }; 17 use radroots_event_codec::encode::{ 18 EventEncodeError, 19 repost::{ 20 generic_repost_build_tags, generic_repost_to_wire_parts, 21 generic_repost_to_wire_parts_with_kind, repost_build_tags, repost_to_wire_parts, 22 repost_to_wire_parts_with_kind, 23 }, 24 }; 25 26 const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 27 const AUTHOR: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; 28 const EVENT_SIG: &str = concat!( 29 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", 30 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" 31 ); 32 const ARTICLE_D_TAG: &str = "DDDDDDDDDDDDDDDDDDDDDA"; 33 34 fn note_repost() -> Repost { 35 Repost { 36 target: SocialTarget::Event { 37 id: EVENT_ID.to_string(), 38 author: Some(AUTHOR.to_string()), 39 event_kind: Some(KIND_POST), 40 relays: Some(vec!["wss://relay.example.test".to_string()]), 41 }, 42 content: None, 43 } 44 } 45 46 fn generic_article_repost() -> GenericRepost { 47 GenericRepost { 48 target: SocialTarget::Address { 49 address: format!("{KIND_ARTICLE}:{AUTHOR}:{ARTICLE_D_TAG}"), 50 author: Some(AUTHOR.to_string()), 51 event_kind: Some(KIND_ARTICLE), 52 relays: Some(vec!["wss://relay.example.test".to_string()]), 53 }, 54 target_kind: KIND_ARTICLE, 55 content: Some("{\"kind\":30023}".to_string()), 56 } 57 } 58 59 fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool { 60 tags.iter().any(|tag| { 61 tag.first().map(|entry| entry.as_str()) == Some(key) 62 && tag.get(1).map(|entry| entry.as_str()) == Some(value) 63 }) 64 } 65 66 fn replace_tag_value(tags: &mut [Vec<String>], key: &str, value: &str) { 67 let tag = tags 68 .iter_mut() 69 .find(|tag| tag.first().map(|entry| entry.as_str()) == Some(key)) 70 .expect("tag"); 71 tag[1] = value.to_string(); 72 } 73 74 #[test] 75 fn repost_to_wire_parts_roundtrips_kind_one_target() { 76 let repost = note_repost(); 77 let parts = repost_to_wire_parts(&repost).unwrap(); 78 79 assert_eq!(parts.kind, KIND_REPOST); 80 assert!(parts.content.is_empty()); 81 assert!(has_tag(&parts.tags, TAG_E, EVENT_ID)); 82 assert!(has_tag(&parts.tags, TAG_P, AUTHOR)); 83 84 let decoded = repost_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); 85 assert!(matches!( 86 decoded.target, 87 SocialTarget::Event { 88 event_kind: Some(KIND_POST), 89 .. 90 } 91 )); 92 assert!(decoded.content.is_none()); 93 } 94 95 #[test] 96 fn generic_repost_to_wire_parts_roundtrips_address_target() { 97 let repost = generic_article_repost(); 98 let parts = generic_repost_to_wire_parts(&repost).unwrap(); 99 100 assert_eq!(parts.kind, KIND_GENERIC_REPOST); 101 assert_eq!(parts.content, "{\"kind\":30023}"); 102 assert!(has_tag( 103 &parts.tags, 104 TAG_A, 105 format!("{KIND_ARTICLE}:{AUTHOR}:{ARTICLE_D_TAG}").as_str() 106 )); 107 assert!(has_tag( 108 &parts.tags, 109 TAG_K, 110 KIND_ARTICLE.to_string().as_str() 111 )); 112 113 let decoded = generic_repost_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); 114 assert_eq!(decoded.target_kind, KIND_ARTICLE); 115 assert!(matches!( 116 decoded.target, 117 SocialTarget::Address { 118 event_kind: Some(KIND_ARTICLE), 119 .. 120 } 121 )); 122 assert_eq!(decoded.content.as_deref(), Some("{\"kind\":30023}")); 123 } 124 125 #[test] 126 fn repost_codecs_reject_wrong_kind_and_wrong_target_kind() { 127 assert!(matches!( 128 repost_to_wire_parts_with_kind(¬e_repost(), KIND_GENERIC_REPOST), 129 Err(EventEncodeError::InvalidKind(KIND_GENERIC_REPOST)) 130 )); 131 assert!(matches!( 132 generic_repost_to_wire_parts_with_kind(&generic_article_repost(), KIND_REPOST), 133 Err(EventEncodeError::InvalidKind(KIND_REPOST)) 134 )); 135 136 let mut repost = note_repost(); 137 if let SocialTarget::Event { event_kind, .. } = &mut repost.target { 138 *event_kind = Some(KIND_ARTICLE); 139 } 140 assert!(matches!( 141 repost_build_tags(&repost), 142 Err(EventEncodeError::InvalidField("target_kind")) 143 )); 144 145 let mut generic = generic_article_repost(); 146 generic.target_kind = KIND_POST; 147 assert!(matches!( 148 generic_repost_build_tags(&generic), 149 Err(EventEncodeError::InvalidField("target_kind")) 150 )); 151 152 let err = repost_from_event(KIND_GENERIC_REPOST, &[], "").unwrap_err(); 153 assert!(matches!( 154 err, 155 EventParseError::InvalidKind { 156 expected: "6", 157 got: KIND_GENERIC_REPOST 158 } 159 )); 160 161 let err = generic_repost_from_event(KIND_GENERIC_REPOST, &[], "").unwrap_err(); 162 assert!(matches!(err, EventParseError::MissingTag(TAG_K))); 163 } 164 165 #[test] 166 fn repost_event_target_codecs_cover_optional_and_error_edges() { 167 let mut no_relay = note_repost(); 168 no_relay.content = Some("fresh note".to_string()); 169 if let SocialTarget::Event { author, relays, .. } = &mut no_relay.target { 170 *author = None; 171 *relays = None; 172 } 173 let parts = repost_to_wire_parts(&no_relay).unwrap(); 174 assert_eq!(parts.content, "fresh note"); 175 assert!(!parts.tags.iter().any(|tag| { 176 tag.first().map(|entry| entry.as_str()) == Some(TAG_P) 177 || tag 178 .get(2) 179 .map(|entry| !entry.trim().is_empty()) 180 .unwrap_or(false) 181 })); 182 let decoded = repost_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); 183 assert_eq!(decoded.content.as_deref(), Some("fresh note")); 184 assert!(matches!( 185 decoded.target, 186 SocialTarget::Event { relays: None, .. } 187 )); 188 189 let mut invalid_target = note_repost(); 190 invalid_target.target = SocialTarget::Address { 191 address: format!("{KIND_ARTICLE}:{AUTHOR}:{ARTICLE_D_TAG}"), 192 author: Some(AUTHOR.to_string()), 193 event_kind: Some(KIND_ARTICLE), 194 relays: None, 195 }; 196 assert!(matches!( 197 repost_build_tags(&invalid_target), 198 Err(EventEncodeError::InvalidField("target")) 199 )); 200 201 let mut invalid_id = note_repost(); 202 if let SocialTarget::Event { id, .. } = &mut invalid_id.target { 203 *id = "not-a-lowercase-hex-id".to_string(); 204 } 205 assert!(matches!( 206 repost_build_tags(&invalid_id), 207 Err(EventEncodeError::InvalidField("target.id")) 208 )); 209 210 let mut invalid_author = note_repost(); 211 if let SocialTarget::Event { author, .. } = &mut invalid_author.target { 212 *author = Some(" ".to_string()); 213 } 214 assert!(matches!( 215 repost_build_tags(&invalid_author), 216 Err(EventEncodeError::EmptyRequiredField("target.author")) 217 )); 218 219 let mut tags = repost_build_tags(¬e_repost()).unwrap(); 220 let event_tag = tags 221 .iter_mut() 222 .find(|tag| tag.first().map(String::as_str) == Some(TAG_E)) 223 .expect("event tag"); 224 event_tag.truncate(1); 225 assert!(matches!( 226 repost_from_event(KIND_REPOST, &tags, ""), 227 Err(EventParseError::InvalidTag(TAG_E)) 228 )); 229 230 let mut tags = repost_build_tags(¬e_repost()).unwrap(); 231 replace_tag_value(&mut tags, TAG_E, "not-a-lowercase-hex-id"); 232 assert!(matches!( 233 repost_from_event(KIND_REPOST, &tags, ""), 234 Err(EventParseError::InvalidTag(TAG_E)) 235 )); 236 } 237 238 #[test] 239 fn generic_repost_codecs_cover_event_targets_and_error_edges() { 240 let generic = GenericRepost { 241 target: SocialTarget::Event { 242 id: EVENT_ID.to_string(), 243 author: Some(AUTHOR.to_string()), 244 event_kind: Some(KIND_REACTION), 245 relays: Some(vec![ 246 " ".to_string(), 247 "wss://relay.example.test".to_string(), 248 ]), 249 }, 250 target_kind: KIND_REACTION, 251 content: None, 252 }; 253 let parts = generic_repost_to_wire_parts(&generic).unwrap(); 254 assert!(has_tag(&parts.tags, TAG_E, EVENT_ID)); 255 assert!(has_tag( 256 &parts.tags, 257 TAG_K, 258 KIND_REACTION.to_string().as_str() 259 )); 260 let decoded = generic_repost_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); 261 assert!(decoded.content.is_none()); 262 assert!(matches!( 263 decoded.target, 264 SocialTarget::Event { 265 event_kind: Some(KIND_REACTION), 266 .. 267 } 268 )); 269 270 let no_author_event = GenericRepost { 271 target: SocialTarget::Event { 272 id: EVENT_ID.to_string(), 273 author: None, 274 event_kind: Some(KIND_REACTION), 275 relays: None, 276 }, 277 target_kind: KIND_REACTION, 278 content: None, 279 }; 280 let parts = generic_repost_to_wire_parts(&no_author_event).unwrap(); 281 assert!(has_tag(&parts.tags, TAG_E, EVENT_ID)); 282 assert!(!parts.tags.iter().any(|tag| { 283 tag.first().map(String::as_str) == Some(TAG_P) 284 || tag.get(2).map(|value| !value.is_empty()).unwrap_or(false) 285 })); 286 287 let mut generic = generic_article_repost(); 288 if let SocialTarget::Address { author, relays, .. } = &mut generic.target { 289 *author = None; 290 *relays = None; 291 } 292 let parts = generic_repost_to_wire_parts(&generic).unwrap(); 293 let address = parts 294 .tags 295 .iter() 296 .find(|tag| tag.first().map(String::as_str) == Some(TAG_A)) 297 .expect("address tag"); 298 assert_eq!(address.len(), 2); 299 300 let wrong_kind = generic_repost_from_event(KIND_REPOST, &parts.tags, "").unwrap_err(); 301 assert!(matches!( 302 wrong_kind, 303 EventParseError::InvalidKind { 304 expected: "16", 305 got: KIND_REPOST 306 } 307 )); 308 309 let mut tags = parts.tags.clone(); 310 replace_tag_value(&mut tags, TAG_K, KIND_POST.to_string().as_str()); 311 assert!(matches!( 312 generic_repost_from_event(KIND_GENERIC_REPOST, &tags, ""), 313 Err(EventParseError::InvalidTag(TAG_K)) 314 )); 315 316 let mut tags = parts.tags.clone(); 317 replace_tag_value(&mut tags, TAG_K, "not-a-number"); 318 assert!(matches!( 319 generic_repost_from_event(KIND_GENERIC_REPOST, &tags, ""), 320 Err(EventParseError::InvalidNumber(TAG_K, _)) 321 )); 322 323 let tags = vec![vec![TAG_K.to_string(), KIND_REACTION.to_string()]]; 324 assert!(matches!( 325 generic_repost_from_event(KIND_GENERIC_REPOST, &tags, ""), 326 Err(EventParseError::MissingTag(TAG_E)) 327 )); 328 329 let mut tags = generic_repost_build_tags(&generic_article_repost()).unwrap(); 330 replace_tag_value( 331 &mut tags, 332 TAG_A, 333 format!("{KIND_REACTION}:{AUTHOR}:{ARTICLE_D_TAG}").as_str(), 334 ); 335 assert!(matches!( 336 generic_repost_from_event(KIND_GENERIC_REPOST, &tags, ""), 337 Err(EventParseError::InvalidTag(TAG_A)) 338 )); 339 340 let mut tags = generic_repost_build_tags(&no_author_event).unwrap(); 341 let event_tag = tags 342 .iter_mut() 343 .find(|tag| tag.first().map(String::as_str) == Some(TAG_E)) 344 .expect("event tag"); 345 event_tag.truncate(1); 346 assert!(matches!( 347 generic_repost_from_event(KIND_GENERIC_REPOST, &tags, ""), 348 Err(EventParseError::InvalidTag(TAG_E)) 349 )); 350 351 let mut tags = generic_repost_build_tags(&no_author_event).unwrap(); 352 replace_tag_value(&mut tags, TAG_E, "not-a-lowercase-hex-id"); 353 assert!(matches!( 354 generic_repost_from_event(KIND_GENERIC_REPOST, &tags, ""), 355 Err(EventParseError::InvalidTag(TAG_E)) 356 )); 357 358 let mut generic = generic_article_repost(); 359 generic.target_kind = KIND_REACTION; 360 assert!(matches!( 361 generic_repost_build_tags(&generic), 362 Err(EventEncodeError::InvalidField("target_kind")) 363 )); 364 365 let mut generic = generic_article_repost(); 366 if let SocialTarget::Address { author, relays, .. } = &mut generic.target { 367 *author = Some(" ".to_string()); 368 *relays = None; 369 } 370 assert!(matches!( 371 generic_repost_build_tags(&generic), 372 Err(EventEncodeError::EmptyRequiredField("target.author")) 373 )); 374 375 let mut generic = generic_article_repost(); 376 generic.target = SocialTarget::External { 377 id: "https://example.test/repost-target".to_string(), 378 external_kind: "web".to_string(), 379 hint: None, 380 }; 381 assert!(matches!( 382 generic_repost_build_tags(&generic), 383 Err(EventEncodeError::InvalidField("target")) 384 )); 385 386 let mut generic = GenericRepost { 387 target: SocialTarget::Event { 388 id: EVENT_ID.to_string(), 389 author: None, 390 event_kind: None, 391 relays: None, 392 }, 393 target_kind: KIND_REACTION, 394 content: None, 395 }; 396 assert!(matches!( 397 generic_repost_build_tags(&generic), 398 Err(EventEncodeError::InvalidField("target_kind")) 399 )); 400 401 if let SocialTarget::Event { event_kind, .. } = &mut generic.target { 402 *event_kind = Some(KIND_REACTION); 403 } 404 generic.target_kind = KIND_POST; 405 assert!(matches!( 406 generic_repost_build_tags(&generic), 407 Err(EventEncodeError::InvalidField("target_kind")) 408 )); 409 } 410 411 #[test] 412 fn repost_wrappers_preserve_event_metadata() { 413 let parts = repost_to_wire_parts(¬e_repost()).unwrap(); 414 let data = repost_data_from_event( 415 "repost_id".to_string(), 416 "author".to_string(), 417 10, 418 parts.kind, 419 parts.content.clone(), 420 parts.tags.clone(), 421 ) 422 .unwrap(); 423 assert_eq!(data.kind, KIND_REPOST); 424 assert_eq!(data.published_at, 10); 425 426 let parsed = repost_parsed_from_event( 427 EVENT_ID.to_string(), 428 AUTHOR.to_string(), 429 10, 430 parts.kind, 431 parts.content, 432 parts.tags, 433 EVENT_SIG.to_string(), 434 ) 435 .unwrap(); 436 assert_eq!(parsed.event.signature_hex(), EVENT_SIG); 437 438 let generic_parts = generic_repost_to_wire_parts(&generic_article_repost()).unwrap(); 439 let generic_data = generic_repost_data_from_event( 440 "generic_id".to_string(), 441 "author".to_string(), 442 11, 443 generic_parts.kind, 444 generic_parts.content.clone(), 445 generic_parts.tags.clone(), 446 ) 447 .unwrap(); 448 assert_eq!(generic_data.data.target_kind, KIND_ARTICLE); 449 450 let generic_parsed = generic_repost_parsed_from_event( 451 EVENT_ID.to_string(), 452 AUTHOR.to_string(), 453 11, 454 generic_parts.kind, 455 generic_parts.content, 456 generic_parts.tags, 457 EVENT_SIG.to_string(), 458 ) 459 .unwrap(); 460 assert_eq!(generic_parsed.event.created_at_u64(), 11); 461 }