lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

repost.rs (14961B)


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