nip10.rs (19662B)
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<'a> NoteIdRef<'a> { 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(vec![filter.clone()]).expect("sub_id"); 282 let waiter = ndb.wait_for_notes(sub.id, 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, vec![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(vec![filter.clone()]).expect("sub_id"); 336 let waiter = ndb.wait_for_notes(sub.id, 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, vec![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(vec![filter.clone()]).expect("sub_id"); 394 let waiter = ndb.wait_for_notes(sub.id, 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, vec![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(vec![filter.clone()]).expect("sub_id"); 457 let waiter = ndb.wait_for_notes(sub.id, 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, vec![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(vec![filter.clone()]).expect("sub_id"); 539 let waiter = ndb.wait_for_notes(sub.id, 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, vec![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 }