nostrdb-rs

nostrdb in rust!
git clone git://jb55.com/nostrdb-rs
Log | Files | Refs | Submodules | README | LICENSE

nip10.rs (19481B)


      1 use crate::{Error, Tag, Tags};
      2 
      3 #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
      4 pub enum Marker {
      5     Reply,
      6     Root,
      7     Mention,
      8 }
      9 
     10 /// Parsed `e` tags
     11 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     12 pub struct NoteIdRef<'a> {
     13     pub index: u16,
     14     pub id: &'a [u8; 32],
     15     pub relay: Option<&'a str>,
     16     pub marker: Option<Marker>,
     17 }
     18 
     19 impl NoteIdRef<'_> {
     20     pub fn to_owned(&self) -> NoteIdRefBuf {
     21         NoteIdRefBuf {
     22             index: self.index,
     23             marker: self.marker,
     24         }
     25     }
     26 }
     27 
     28 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     29 pub struct NoteIdRefBuf {
     30     pub index: u16,
     31     pub marker: Option<Marker>,
     32 }
     33 
     34 fn tag_to_note_id_ref(tag: Tag<'_>, marker: Option<Marker>, index: i32) -> NoteIdRef<'_> {
     35     let id = tag
     36         .get_unchecked(1)
     37         .variant()
     38         .id()
     39         .expect("expected id at index, do you have the correct note?");
     40     let relay = tag.get(2).and_then(|t| t.variant().str());
     41     NoteIdRef {
     42         index: index as u16,
     43         id,
     44         relay,
     45         marker,
     46     }
     47 }
     48 
     49 impl NoteReplyBuf {
     50     // TODO(jb55): optimize this function. It is not the nicest code.
     51     // We could simplify the index lookup by offsets into the Note's
     52     // string table
     53     pub fn borrow<'a>(&self, tags: Tags<'a>) -> NoteReply<'a> {
     54         let mut root: Option<NoteIdRef<'a>> = None;
     55         let mut reply: Option<NoteIdRef<'a>> = None;
     56         let mut mention: Option<NoteIdRef<'a>> = None;
     57 
     58         let mut index: i32 = -1;
     59         for tag in tags {
     60             index += 1;
     61             if tag.count() < 2 && tag.get_unchecked(0).variant().str() != Some("e") {
     62                 continue;
     63             }
     64 
     65             if self.root.as_ref().is_some_and(|x| x.index == index as u16) {
     66                 root = Some(tag_to_note_id_ref(
     67                     tag,
     68                     self.root.as_ref().unwrap().marker,
     69                     index,
     70                 ))
     71             } else if self.reply.as_ref().is_some_and(|x| x.index == index as u16) {
     72                 reply = Some(tag_to_note_id_ref(
     73                     tag,
     74                     self.reply.as_ref().unwrap().marker,
     75                     index,
     76                 ))
     77             } else if self
     78                 .mention
     79                 .as_ref()
     80                 .is_some_and(|x| x.index == index as u16)
     81             {
     82                 mention = Some(tag_to_note_id_ref(
     83                     tag,
     84                     self.mention.as_ref().unwrap().marker,
     85                     index,
     86                 ))
     87             }
     88         }
     89 
     90         NoteReply {
     91             root,
     92             reply,
     93             mention,
     94         }
     95     }
     96 }
     97 
     98 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     99 pub struct NoteReply<'a> {
    100     root: Option<NoteIdRef<'a>>,
    101     reply: Option<NoteIdRef<'a>>,
    102     mention: Option<NoteIdRef<'a>>,
    103 }
    104 
    105 /// Owned version of NoteReply, stores tag indices
    106 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    107 pub struct NoteReplyBuf {
    108     pub root: Option<NoteIdRefBuf>,
    109     pub reply: Option<NoteIdRefBuf>,
    110     pub mention: Option<NoteIdRefBuf>,
    111 }
    112 
    113 impl<'a> NoteReply<'a> {
    114     pub fn reply_to_root(self) -> Option<NoteIdRef<'a>> {
    115         if self.is_reply_to_root() {
    116             self.root
    117         } else {
    118             None
    119         }
    120     }
    121 
    122     pub fn to_owned(&self) -> NoteReplyBuf {
    123         NoteReplyBuf {
    124             root: self.root.map(|x| x.to_owned()),
    125             reply: self.reply.map(|x| x.to_owned()),
    126             mention: self.mention.map(|x| x.to_owned()),
    127         }
    128     }
    129 
    130     pub fn new(tags: Tags<'a>) -> NoteReply<'a> {
    131         tags_to_note_reply(tags)
    132     }
    133 
    134     pub fn is_reply_to_root(&self) -> bool {
    135         self.root.is_some() && self.reply.is_none()
    136     }
    137 
    138     pub fn root(self) -> Option<NoteIdRef<'a>> {
    139         self.root
    140     }
    141 
    142     pub fn is_reply(&self) -> bool {
    143         self.reply().is_some()
    144     }
    145 
    146     pub fn reply(self) -> Option<NoteIdRef<'a>> {
    147         if self.reply.is_some() {
    148             self.reply
    149         } else if self.root.is_some() {
    150             self.root
    151         } else {
    152             None
    153         }
    154     }
    155 
    156     pub fn mention(self) -> Option<NoteIdRef<'a>> {
    157         self.mention
    158     }
    159 }
    160 
    161 impl Marker {
    162     pub fn new(s: &str) -> Option<Self> {
    163         if s == "reply" {
    164             Some(Marker::Reply)
    165         } else if s == "root" {
    166             Some(Marker::Root)
    167         } else if s == "mention" {
    168             Some(Marker::Mention)
    169         } else {
    170             None
    171         }
    172     }
    173 }
    174 
    175 fn tags_to_note_reply<'a>(tags: Tags<'a>) -> NoteReply<'a> {
    176     let mut root: Option<NoteIdRef<'a>> = None;
    177     let mut reply: Option<NoteIdRef<'a>> = None;
    178     let mut mention: Option<NoteIdRef<'a>> = None;
    179     let mut first: bool = true;
    180     let mut index: i32 = -1;
    181     let mut any_marker: bool = false;
    182 
    183     for tag in tags {
    184         index += 1;
    185 
    186         if root.is_some() && reply.is_some() && mention.is_some() {
    187             break;
    188         }
    189 
    190         let note_ref = if let Ok(note_ref) = tag_to_noteid_ref(tag, index as u16) {
    191             note_ref
    192         } else {
    193             continue;
    194         };
    195 
    196         if let Some(marker) = note_ref.marker {
    197             any_marker = true;
    198             match marker {
    199                 Marker::Root => root = Some(note_ref),
    200                 Marker::Reply => reply = Some(note_ref),
    201                 Marker::Mention => mention = Some(note_ref),
    202             }
    203         } else if !any_marker && first {
    204             root = Some(note_ref);
    205             first = false;
    206         } else if !any_marker && reply.is_none() {
    207             reply = Some(note_ref)
    208         }
    209     }
    210 
    211     NoteReply {
    212         root,
    213         reply,
    214         mention,
    215     }
    216 }
    217 
    218 pub fn tag_to_noteid_ref(tag: Tag<'_>, index: u16) -> Result<NoteIdRef<'_>, Error> {
    219     if tag.count() < 2 {
    220         return Err(Error::DecodeError);
    221     }
    222 
    223     if tag.get_unchecked(0).variant().str() != Some("e") {
    224         return Err(Error::DecodeError);
    225     }
    226 
    227     let id = tag
    228         .get_unchecked(1)
    229         .variant()
    230         .id()
    231         .ok_or(Error::DecodeError)?;
    232 
    233     let relay = tag
    234         .get(2)
    235         .and_then(|t| t.variant().str())
    236         .filter(|x| !x.is_empty());
    237 
    238     let marker = tag
    239         .get(3)
    240         .and_then(|t| t.variant().str())
    241         .and_then(Marker::new);
    242 
    243     Ok(NoteIdRef {
    244         index,
    245         id,
    246         relay,
    247         marker,
    248     })
    249 }
    250 
    251 #[cfg(test)]
    252 mod test {
    253     use crate::*;
    254 
    255     #[tokio::test]
    256     async fn nip10_marker() {
    257         let db = "target/testdbs/nip10_marker";
    258         test_util::cleanup_db(&db);
    259 
    260         {
    261             let ndb = Ndb::new(db, &Config::new()).expect("ndb");
    262             let filter = Filter::new().kinds(vec![1]).build();
    263             let root_id: [u8; 32] =
    264                 hex::decode("7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d4")
    265                     .unwrap()
    266                     .try_into()
    267                     .unwrap();
    268             let reply_id: [u8; 32] =
    269                 hex::decode("7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d3")
    270                     .unwrap()
    271                     .try_into()
    272                     .unwrap();
    273             let sub = ndb.subscribe(&[filter.clone()]).expect("sub_id");
    274             let waiter = ndb.wait_for_notes(sub, 1);
    275 
    276             ndb.process_event(r#"
    277             [
    278               "EVENT",
    279               "huh",
    280               {
    281                 "id": "19377cb4b9b807561830ab6d4c1fae7b9c9f1b623c15d10590cacc859cf19d76",
    282                 "pubkey": "4871687b7b0aee3f1649c866e61724d79d51e673936a5378f5ed90bf7580791f",
    283                 "created_at": 1714170678,
    284                 "kind": 1,
    285                 "tags": [
    286                   ["e", "7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d3", "", "reply" ],
    287                   ["e", "7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d4", "wss://relay.damus.io", "root" ]
    288                 ],
    289                 "content": "hi",
    290                 "sig": "53921b1572c2e4373180a9f71513a0dee286cba6193d983052f96285c08f0e0158773d82ac97991ba8d390f6f54f84d5272c2e945f2e854a750f9cf038c0f759"
    291               }
    292             ]"#).expect("process ok");
    293 
    294             let res = waiter.await.expect("await ok");
    295             assert_eq!(res, vec![NoteKey::new(1)]);
    296             let txn = Transaction::new(&ndb).unwrap();
    297             let res = ndb.query(&txn, &[filter], 1).expect("note");
    298             let note_reply = NoteReply::new(res[0].note.tags());
    299 
    300             assert_eq!(*note_reply.root.unwrap().id, root_id);
    301             assert_eq!(*note_reply.reply.unwrap().id, reply_id);
    302             assert_eq!(
    303                 note_reply.root.unwrap().relay.unwrap(),
    304                 "wss://relay.damus.io"
    305             );
    306         }
    307     }
    308 
    309     #[tokio::test]
    310     async fn nip10_deprecated() {
    311         let db = "target/testdbs/nip10_deprecated_reply";
    312         test_util::cleanup_db(&db);
    313 
    314         {
    315             let ndb = Ndb::new(db, &Config::new()).expect("ndb");
    316             let filter = Filter::new().kinds(vec![1]).build();
    317             let root_id: [u8; 32] =
    318                 hex::decode("7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d4")
    319                     .unwrap()
    320                     .try_into()
    321                     .unwrap();
    322             let reply_id: [u8; 32] =
    323                 hex::decode("7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d3")
    324                     .unwrap()
    325                     .try_into()
    326                     .unwrap();
    327             let sub = ndb.subscribe(&[filter.clone()]).expect("sub_id");
    328             let waiter = ndb.wait_for_notes(sub, 1);
    329 
    330             ndb.process_event(r#"
    331             [
    332               "EVENT",
    333               "huh",
    334               {
    335                 "id": "ebac7df823ab975b6d2696505cf22a959067b74b1761c5581156f2a884036997",
    336                 "pubkey": "118758f9a951c923b8502cfb8b2f329bee2a46356b6fc4f65c1b9b4730e0e9e5",
    337                 "created_at": 1714175831,
    338                 "kind": 1,
    339                 "tags": [
    340                   [
    341                     "e",
    342                     "7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d4"
    343                   ],
    344                   [
    345                     "e",
    346                     "7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d3"
    347                   ]
    348                 ],
    349                 "content": "hi",
    350                 "sig": "05913c7b19a70188d4dec5ac53d5da39fea4d5030c28176e52abb211e1bde60c5947aca8af359a00c8df8d96127b2f945af31f21fe01392b661bae12e7d14b1d"
    351               }
    352             ]"#).expect("process ok");
    353 
    354             let res = waiter.await.expect("await ok");
    355             assert_eq!(res, vec![NoteKey::new(1)]);
    356             let txn = Transaction::new(&ndb).unwrap();
    357             let res = ndb.query(&txn, &[filter], 1).expect("note");
    358             let note_reply = NoteReply::new(res[0].note.tags());
    359 
    360             assert_eq!(*note_reply.root.unwrap().id, root_id);
    361             assert_eq!(*note_reply.reply.unwrap().id, reply_id);
    362             assert_eq!(note_reply.reply_to_root().is_none(), true);
    363             assert_eq!(*note_reply.reply().unwrap().id, reply_id);
    364         }
    365     }
    366 
    367     #[tokio::test]
    368     async fn nip10_mention() {
    369         let db = "target/testdbs/nip10_mention";
    370         test_util::cleanup_db(&db);
    371 
    372         {
    373             let ndb = Ndb::new(db, &Config::new()).expect("ndb");
    374             let filter = Filter::new().kinds([1]).build();
    375             let root_id: [u8; 32] =
    376                 hex::decode("7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d4")
    377                     .unwrap()
    378                     .try_into()
    379                     .unwrap();
    380             let mention_id: [u8; 32] =
    381                 hex::decode("7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d3")
    382                     .unwrap()
    383                     .try_into()
    384                     .unwrap();
    385             let sub = ndb.subscribe(&[filter.clone()]).expect("sub_id");
    386             let waiter = ndb.wait_for_notes(sub, 1);
    387 
    388             ndb.process_event(r#"
    389             [
    390               "EVENT",
    391               "huh",
    392               {
    393                 "id": "9521de81704269f9f61c042355eaa97a845a90c0ce6637b290800fa5a3c0b48d",
    394                 "pubkey": "b3aceb5b36a235377c80dc2a1b3594a1d49e394b4d74fa11bc7cb4cf0bf677b2",
    395                 "created_at": 1714177990,
    396                 "kind": 1,
    397                 "tags": [
    398                   [
    399                     "e",
    400                     "7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d3",
    401                     "",
    402                     "mention"
    403                   ],
    404                   [
    405                     "e",
    406                     "7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d4",
    407                     "wss://relay.damus.io",
    408                     "root"
    409                   ]
    410                 ],
    411                 "content": "hi",
    412                 "sig": "e908ec395f6ea907a4b562b3ebf1bf61653566a5648574a1f8c752285797e5870e57416a0be933ce580fc3d65c874909c9dacbd1575c15bd97b8a68ea2b5160b"
    413               }
    414             ]"#).expect("process ok");
    415 
    416             let res = waiter.await.expect("await ok");
    417             assert_eq!(res, vec![NoteKey::new(1)]);
    418             let txn = Transaction::new(&ndb).unwrap();
    419             let res = ndb.query(&txn, &[filter], 1).expect("note");
    420             let note_reply = NoteReply::new(res[0].note.tags());
    421 
    422             assert_eq!(*note_reply.reply_to_root().unwrap().id, root_id);
    423             assert_eq!(*note_reply.reply().unwrap().id, root_id);
    424             assert_eq!(*note_reply.mention().unwrap().id, mention_id);
    425             assert_eq!(note_reply.is_reply_to_root(), true);
    426             assert_eq!(note_reply.is_reply(), true);
    427         }
    428     }
    429 
    430     #[tokio::test]
    431     async fn nip10_marker_mixed() {
    432         let db = "target/testdbs/nip10_marker_mixed";
    433         test_util::cleanup_db(&db);
    434 
    435         {
    436             let ndb = Ndb::new(db, &Config::new()).expect("ndb");
    437             let filter = Filter::new().kinds([1]).build();
    438             let root_id: [u8; 32] =
    439                 hex::decode("27e71cf53299dafb5dc7bcc0a078357418a4375cb1097bf5184662493f79a627")
    440                     .unwrap()
    441                     .try_into()
    442                     .unwrap();
    443             let reply_id: [u8; 32] =
    444                 hex::decode("1a616998552cf76e9786f76ac68f6104cdae46377330735c68bfe0b9426d2fa8")
    445                     .unwrap()
    446                     .try_into()
    447                     .unwrap();
    448             let sub = ndb.subscribe(&[filter.clone()]).expect("sub_id");
    449             let waiter = ndb.wait_for_notes(sub, 1);
    450 
    451             ndb.process_event(r#"
    452             [
    453               "EVENT",
    454               "nostril-query",
    455               {
    456                 "content": "Go to pleblab plz",
    457                 "created_at": 1714157088,
    458                 "id": "19ae8cd276185f6f48fd7e25736c260ea0ac25d9b591ec3194631e3196e19622",
    459                 "kind": 1,
    460                 "pubkey": "ae1008d23930b776c18092f6eab41e4b09fcf3f03f3641b1b4e6ee3aa166d760",
    461                 "sig": "fdafc7192a0f3b5fef5ae794ef61eb2b3c7cc70bace53f3aa6d4263347581d36add7e9468a4e329d9c986e3a5c46e4689a6b79f60c5cf7778a403316ac5b2629",
    462                 "tags": [
    463                   [
    464                     "e",
    465                     "27e71cf53299dafb5dc7bcc0a078357418a4375cb1097bf5184662493f79a627",
    466                     "",
    467                     "root"
    468                   ],
    469                   [
    470                     "e",
    471                     "f99046bd87be7508d55e139de48517c06ef90830d77a5d3213df858d77bb2f8f"
    472                   ],
    473                   [
    474                     "e",
    475                     "1a616998552cf76e9786f76ac68f6104cdae46377330735c68bfe0b9426d2fa8",
    476                     "",
    477                     "reply"
    478                   ],
    479                   [
    480                     "p",
    481                     "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
    482                   ],
    483                   [
    484                     "p",
    485                     "8ea485266b2285463b13bf835907161c22bb3da1e652b443db14f9cee6720a43"
    486                   ],
    487                   [
    488                     "p",
    489                     "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
    490                   ]
    491                 ]
    492               }
    493             ]
    494             "#).expect("process ok");
    495 
    496             let res = waiter.await.expect("await ok");
    497             assert_eq!(res, vec![NoteKey::new(1)]);
    498             let txn = Transaction::new(&ndb).unwrap();
    499             let res = ndb.query(&txn, &[filter], 1).expect("note");
    500             let note = &res[0].note;
    501             let note_reply = NoteReply::new(note.tags());
    502 
    503             assert_eq!(note_reply.reply_to_root().is_none(), true);
    504             assert_eq!(*note_reply.reply().unwrap().id, reply_id);
    505             assert_eq!(*note_reply.root().unwrap().id, root_id);
    506             assert_eq!(note_reply.mention().is_none(), true);
    507 
    508             // test the to_owned version
    509             let back_again = note_reply.to_owned().borrow(note.tags());
    510             assert_eq!(back_again.reply_to_root().is_none(), true);
    511             assert_eq!(*back_again.reply().unwrap().id, reply_id);
    512             assert_eq!(*back_again.root().unwrap().id, root_id);
    513             assert_eq!(back_again.mention().is_none(), true);
    514         }
    515     }
    516 
    517     #[tokio::test]
    518     async fn nip10_deprecated_reply_to_root() {
    519         let db = "target/testdbs/nip10_deprecated_reply_to_root";
    520         test_util::cleanup_db(&db);
    521 
    522         {
    523             let ndb = Ndb::new(db, &Config::new()).expect("ndb");
    524             let filter = Filter::new().kinds(vec![1]).build();
    525             let root_id: [u8; 32] =
    526                 hex::decode("7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d3")
    527                     .unwrap()
    528                     .try_into()
    529                     .unwrap();
    530             let sub = ndb.subscribe(&[filter.clone()]).expect("sub_id");
    531             let waiter = ndb.wait_for_notes(sub, 1);
    532 
    533             ndb.process_event(r#"
    534             [
    535               "EVENT",
    536               "huh",
    537               {
    538                 "id": "140280b7886c48bddd99684b951c6bb61bebc8270a4989f316282c72aa35e5ba",
    539                 "pubkey": "5ee7067e7155a9abf494e3e47e3249254cf95389a0c6e4f75cbbf35c8c675c23",
    540                 "created_at": 1714178274,
    541                 "kind": 1,
    542                 "tags": [
    543                   [
    544                     "e",
    545                     "7d33c272a74e75c7328b891ab69420dd820cc7544fc65cd29a058c3495fd27d3"
    546                   ]
    547                 ],
    548                 "content": "hi",
    549                 "sig": "e433d468d49fbc0f466b1a8ccefda71b0e17af471e579b56b8ce36477c116109c44d1065103ed6c01f838af92a13e51969d3b458f69c09b6f12785bd07053eb5"
    550               }
    551             ]"#).expect("process ok");
    552 
    553             let res = waiter.await.expect("await ok");
    554             assert_eq!(res, vec![NoteKey::new(1)]);
    555             let txn = Transaction::new(&ndb).unwrap();
    556             let res = ndb.query(&txn, &[filter], 1).expect("note");
    557             let note = &res[0].note;
    558             let note_reply = NoteReply::new(note.tags());
    559 
    560             assert_eq!(*note_reply.reply_to_root().unwrap().id, root_id);
    561             assert_eq!(*note_reply.reply().unwrap().id, root_id);
    562             assert_eq!(note_reply.mention().is_none(), true);
    563 
    564             // test the to_owned version
    565             let back_again = note_reply.to_owned().borrow(note.tags());
    566             assert_eq!(*back_again.reply_to_root().unwrap().id, root_id);
    567             assert_eq!(*back_again.reply().unwrap().id, root_id);
    568             assert_eq!(back_again.mention().is_none(), true);
    569         }
    570     }
    571 }