lib

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

nip10_reply_profile.rs (7279B)


      1 mod support;
      2 
      3 use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey};
      4 use radroots_event::post::reply::{AuthoredNip10Reply, Nip10ReplyReference};
      5 use radroots_event_codec::{
      6     admission::{
      7         post::{RadrootsPostAdmissionOutcome, verify_and_admit_post_event},
      8         reply::{admit_thread_excluded_post_candidate, verify_and_admit_nip10_reply_event},
      9     },
     10     decode::reply::RadrootsNip10ReplyStyle,
     11 };
     12 use radroots_nostr::Error;
     13 use radroots_nostr::event::GenericBuilder;
     14 use radroots_nostr::event::Kind as RadrootsNostrKind;
     15 use radroots_nostr::event::Timestamp as RadrootsNostrTimestamp;
     16 use radroots_nostr::event::build_nip10_reply as build_nip10_reply_event;
     17 use radroots_nostr::event::from_nostr;
     18 use radroots_nostr::tag::Tag as RadrootsNostrTag;
     19 use support::{
     20     FIXTURE_ALICE_SECRET_KEY_HEX, FIXTURE_BOB_PUBLIC_KEY_HEX, FIXTURE_CAROL_PUBLIC_KEY_HEX,
     21     RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS,
     22 };
     23 
     24 const CREATED_AT: u64 = 1_784_347_200;
     25 const ROOT_EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
     26 const PARENT_EVENT_ID: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
     27 
     28 #[test]
     29 fn typed_nip10_reply_builders_sign_exact_marked_direct_and_nested_profiles() {
     30     let keys = fixture_keys();
     31     let created_at = RadrootsNostrTimestamp::from_secs(CREATED_AT);
     32     let root = reference(
     33         ROOT_EVENT_ID,
     34         FIXTURE_BOB_PUBLIC_KEY_HEX,
     35         Some(RELAY_PRIMARY_WSS),
     36     );
     37 
     38     let direct =
     39         AuthoredNip10Reply::direct("Direct reply", root.clone()).expect("authored direct reply");
     40     let direct_event = build_nip10_reply_event(&direct)
     41         .expect("typed direct Reply builder")
     42         .custom_created_at(created_at)
     43         .sign_with_keys(&keys)
     44         .expect("signed direct Reply");
     45 
     46     assert_eq!(direct_event.kind, RadrootsNostrKind::TextNote);
     47     assert_eq!(direct_event.created_at, created_at);
     48     assert_eq!(
     49         event_tags(&direct_event),
     50         vec![
     51             tag(&["e", ROOT_EVENT_ID, RELAY_PRIMARY_WSS, "root"]),
     52             tag(&["p", FIXTURE_BOB_PUBLIC_KEY_HEX]),
     53         ]
     54     );
     55     direct_event.verify().expect("valid direct Reply signature");
     56 
     57     let parent = reference(
     58         PARENT_EVENT_ID,
     59         FIXTURE_CAROL_PUBLIC_KEY_HEX,
     60         Some(RELAY_SECONDARY_WSS),
     61     );
     62     let nested =
     63         AuthoredNip10Reply::nested("Nested reply", root, parent).expect("authored nested reply");
     64     let nested_event = build_nip10_reply_event(&nested)
     65         .expect("typed nested Reply builder")
     66         .custom_created_at(created_at)
     67         .sign_with_keys(&keys)
     68         .expect("signed nested Reply");
     69 
     70     assert_eq!(
     71         event_tags(&nested_event),
     72         vec![
     73             tag(&["e", ROOT_EVENT_ID, RELAY_PRIMARY_WSS, "root"]),
     74             tag(&["e", PARENT_EVENT_ID, RELAY_SECONDARY_WSS, "reply"]),
     75             tag(&["p", FIXTURE_BOB_PUBLIC_KEY_HEX]),
     76             tag(&["p", FIXTURE_CAROL_PUBLIC_KEY_HEX]),
     77         ]
     78     );
     79     nested_event.verify().expect("valid nested Reply signature");
     80     let admitted = verify_and_admit_nip10_reply_event(
     81         from_nostr(&nested_event).expect("nested Reply adapter"),
     82     )
     83     .expect("nested Reply admission");
     84     assert!(!admitted.projection().is_direct());
     85     assert_eq!(
     86         admitted
     87             .projection()
     88             .reply_reference()
     89             .expect("nested parent")
     90             .event_id()
     91             .to_hex(),
     92         PARENT_EVENT_ID
     93     );
     94 }
     95 
     96 #[test]
     97 fn signed_nip10_reply_is_thread_excluded_before_semantic_reply_admission() {
     98     let reply = AuthoredNip10Reply::direct(
     99         "Thread reply",
    100         reference(
    101             ROOT_EVENT_ID,
    102             FIXTURE_BOB_PUBLIC_KEY_HEX,
    103             Some(RELAY_PRIMARY_WSS),
    104         ),
    105     )
    106     .expect("authored direct reply");
    107     let event = build_nip10_reply_event(&reply)
    108         .expect("typed Reply builder")
    109         .custom_created_at(RadrootsNostrTimestamp::from_secs(CREATED_AT))
    110         .sign_with_keys(&fixture_keys())
    111         .expect("signed Reply");
    112     let envelope = from_nostr(&event).expect("Radroots event adapter");
    113 
    114     let candidate = match verify_and_admit_post_event(envelope).expect("post admission") {
    115         RadrootsPostAdmissionOutcome::ThreadExcluded(candidate) => candidate,
    116         RadrootsPostAdmissionOutcome::Root(_) => panic!("Reply must never become a root card"),
    117         _ => panic!("unexpected post admission outcome"),
    118     };
    119     let admitted =
    120         admit_thread_excluded_post_candidate(candidate).expect("semantic Reply admission");
    121 
    122     assert_eq!(admitted.contract().id, "radroots.social.reply.v1");
    123     assert_eq!(
    124         admitted.projection().style(),
    125         RadrootsNip10ReplyStyle::Marked
    126     );
    127     assert!(admitted.projection().is_direct());
    128     assert_eq!(
    129         admitted.projection().root().event_id().to_hex(),
    130         ROOT_EVENT_ID
    131     );
    132 }
    133 
    134 #[test]
    135 fn verified_legacy_positional_nip10_reply_remains_tolerated_inbound() {
    136     let keys = fixture_keys();
    137     let event = nostr::EventBuilder::text_note("Legacy positional reply")
    138         .tags([
    139             RadrootsNostrTag::parse(["e", ROOT_EVENT_ID, RELAY_PRIMARY_WSS])
    140                 .expect("positional root reference"),
    141             RadrootsNostrTag::parse(["p", FIXTURE_BOB_PUBLIC_KEY_HEX])
    142                 .expect("root author reference"),
    143         ])
    144         .custom_created_at(RadrootsNostrTimestamp::from_secs(CREATED_AT))
    145         .sign_with_keys(&keys)
    146         .expect("signed inbound fixture");
    147     let envelope = from_nostr(&event).expect("Radroots event adapter");
    148     let admitted =
    149         verify_and_admit_nip10_reply_event(envelope).expect("legacy positional Reply admission");
    150 
    151     assert_eq!(
    152         admitted.projection().style(),
    153         RadrootsNip10ReplyStyle::LegacyPositional
    154     );
    155     assert!(admitted.projection().is_direct());
    156     assert_eq!(
    157         admitted.projection().root().event_id().to_hex(),
    158         ROOT_EVENT_ID
    159     );
    160 }
    161 
    162 #[test]
    163 fn generic_kind_one_builder_cannot_bypass_typed_nip10_reply_authoring() {
    164     let keys = fixture_keys();
    165     let marked_reply = GenericBuilder::text_note("Raw marked reply").tags([
    166         RadrootsNostrTag::parse(["e", ROOT_EVENT_ID, RELAY_PRIMARY_WSS, "root"])
    167             .expect("root reference"),
    168         RadrootsNostrTag::parse(["p", FIXTURE_BOB_PUBLIC_KEY_HEX]).expect("root author reference"),
    169     ]);
    170 
    171     for builder in [GenericBuilder::text_note("Raw root"), marked_reply] {
    172         assert!(matches!(
    173             builder.sign_with_keys(&keys),
    174             Err(Error::TypedAuthoringRequired { kind })
    175                 if kind == RadrootsNostrKind::TextNote.as_u16()
    176         ));
    177     }
    178 }
    179 
    180 fn fixture_keys() -> RadrootsNostrKeys {
    181     RadrootsNostrKeys::new(
    182         RadrootsNostrSecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("fixture secret key"),
    183     )
    184 }
    185 
    186 fn reference(event_id: &str, author: &str, relay: Option<&str>) -> Nip10ReplyReference {
    187     Nip10ReplyReference::parse(event_id, author, relay).expect("valid Reply reference")
    188 }
    189 
    190 fn event_tags(event: &nostr::Event) -> Vec<Vec<String>> {
    191     event
    192         .tags
    193         .iter()
    194         .map(|tag| tag.as_slice().to_vec())
    195         .collect()
    196 }
    197 
    198 fn tag(values: &[&str]) -> Vec<String> {
    199     values.iter().map(|value| (*value).to_owned()).collect()
    200 }