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 }