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