nostrdb-rs

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

nip10.rs (19613B)


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