registry_v7.rs (53322B)
1 //! Frozen NIP-22 comment semantics for event-contract registry v7. 2 3 #[cfg(not(feature = "std"))] 4 use alloc::{format, string::String, vec::Vec}; 5 use core::fmt; 6 7 use radroots_event::{ 8 envelope::kind::KIND_COMMENT, 9 id::{AddressableCoordinate, AddressableCoordinateParts, EventId, ParseError}, 10 post::comment::{ 11 Nip22CommentRootKind, RADROOTS_NIP22_COMMENT_CONTENT_MAX_BYTES, 12 RADROOTS_NIP22_COMMENT_EVENT_WIRE_MAX_BYTES, RADROOTS_NIP22_COMMENT_TAG_ELEMENT_MAX_BYTES, 13 RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT, RADROOTS_NIP22_COMMENT_TAG_TOTAL_ELEMENT_MAX_COUNT, 14 RADROOTS_NIP22_COMMENT_TAG_TOTAL_MAX_BYTES, 15 }, 16 tag::relay_hint::NostrRelayHint, 17 }; 18 use radroots_identity::PublicKey; 19 20 use crate::verification::v1::RadrootsSignatureVerifiedEvent; 21 22 const RADROOTS_NIP22_COMMENT_SIGNED_EVENT_FIXED_MAX_BYTES: usize = "{\"id\":\"".len() 23 + 64 24 + "\",\"pubkey\":\"".len() 25 + 64 26 + "\",\"created_at\":".len() 27 + ",\"kind\":1111,\"tags\":".len() 28 + ",\"content\":".len() 29 + ",\"sig\":\"".len() 30 + 128 31 + "\"}".len(); 32 33 #[derive(Clone, Debug, PartialEq, Eq)] 34 #[non_exhaustive] 35 pub enum RadrootsNip22CommentDiagnostic { 36 RootRelayIgnored { 37 tag_index: usize, 38 raw_tag: Vec<String>, 39 }, 40 RootAuthorRelayIgnored { 41 tag_index: usize, 42 raw_tag: Vec<String>, 43 }, 44 RootAuthorHintIgnored { 45 tag_index: usize, 46 raw_tag: Vec<String>, 47 }, 48 ParentRelayIgnored { 49 tag_index: usize, 50 raw_tag: Vec<String>, 51 }, 52 ParentAuthorRelayIgnored { 53 tag_index: usize, 54 raw_tag: Vec<String>, 55 }, 56 ParentAuthorHintIgnored { 57 tag_index: usize, 58 raw_tag: Vec<String>, 59 }, 60 ParentAuthorShapeIgnored { 61 tag_index: usize, 62 raw_tag: Vec<String>, 63 }, 64 ParentAuthorInvalidIgnored { 65 tag_index: usize, 66 raw_tag: Vec<String>, 67 }, 68 ParentAuthorDuplicateIgnored { 69 tag_index: usize, 70 raw_tag: Vec<String>, 71 }, 72 RevisionRelayIgnored { 73 tag_index: usize, 74 raw_tag: Vec<String>, 75 }, 76 } 77 78 impl RadrootsNip22CommentDiagnostic { 79 pub const fn code(&self) -> &'static str { 80 match self { 81 Self::RootRelayIgnored { .. } => "comment_root_relay_ignored", 82 Self::RootAuthorRelayIgnored { .. } => "comment_root_author_relay_ignored", 83 Self::RootAuthorHintIgnored { .. } => "comment_root_author_hint_ignored", 84 Self::ParentRelayIgnored { .. } => "comment_parent_relay_ignored", 85 Self::ParentAuthorRelayIgnored { .. } => "comment_parent_author_relay_ignored", 86 Self::ParentAuthorHintIgnored { .. } => "comment_parent_author_hint_ignored", 87 Self::ParentAuthorShapeIgnored { .. } => "comment_parent_author_shape_ignored", 88 Self::ParentAuthorInvalidIgnored { .. } => "comment_parent_author_invalid_ignored", 89 Self::ParentAuthorDuplicateIgnored { .. } => "comment_parent_author_duplicate_ignored", 90 Self::RevisionRelayIgnored { .. } => "comment_revision_relay_ignored", 91 } 92 } 93 94 pub const fn tag_index(&self) -> usize { 95 match self { 96 Self::RootRelayIgnored { tag_index, .. } 97 | Self::RootAuthorRelayIgnored { tag_index, .. } 98 | Self::RootAuthorHintIgnored { tag_index, .. } 99 | Self::ParentRelayIgnored { tag_index, .. } 100 | Self::ParentAuthorRelayIgnored { tag_index, .. } 101 | Self::ParentAuthorHintIgnored { tag_index, .. } 102 | Self::ParentAuthorShapeIgnored { tag_index, .. } 103 | Self::ParentAuthorInvalidIgnored { tag_index, .. } 104 | Self::ParentAuthorDuplicateIgnored { tag_index, .. } 105 | Self::RevisionRelayIgnored { tag_index, .. } => *tag_index, 106 } 107 } 108 109 pub fn raw_tag(&self) -> &[String] { 110 match self { 111 Self::RootRelayIgnored { raw_tag, .. } 112 | Self::RootAuthorRelayIgnored { raw_tag, .. } 113 | Self::RootAuthorHintIgnored { raw_tag, .. } 114 | Self::ParentRelayIgnored { raw_tag, .. } 115 | Self::ParentAuthorRelayIgnored { raw_tag, .. } 116 | Self::ParentAuthorHintIgnored { raw_tag, .. } 117 | Self::ParentAuthorShapeIgnored { raw_tag, .. } 118 | Self::ParentAuthorInvalidIgnored { raw_tag, .. } 119 | Self::ParentAuthorDuplicateIgnored { raw_tag, .. } 120 | Self::RevisionRelayIgnored { raw_tag, .. } => raw_tag, 121 } 122 } 123 } 124 125 #[derive(Clone, Debug, PartialEq, Eq)] 126 pub struct RadrootsInboundNip22Participant { 127 tag_index: usize, 128 pubkey: PublicKey, 129 relay: Option<NostrRelayHint>, 130 raw_tag: Vec<String>, 131 } 132 133 impl RadrootsInboundNip22Participant { 134 pub const fn tag_index(&self) -> usize { 135 self.tag_index 136 } 137 138 pub const fn pubkey(&self) -> &PublicKey { 139 &self.pubkey 140 } 141 142 pub const fn relay(&self) -> Option<&NostrRelayHint> { 143 self.relay.as_ref() 144 } 145 146 pub fn raw_tag(&self) -> &[String] { 147 &self.raw_tag 148 } 149 } 150 151 #[derive(Clone, Debug, PartialEq, Eq)] 152 pub struct RadrootsInboundNip22EventRoot { 153 tag_index: usize, 154 event_id: EventId, 155 kind_tag_index: usize, 156 kind_raw_tag: Vec<String>, 157 kind: Nip22CommentRootKind, 158 relay: Option<NostrRelayHint>, 159 author_hint: Option<PublicKey>, 160 author: RadrootsInboundNip22Participant, 161 raw_tag: Vec<String>, 162 } 163 164 impl RadrootsInboundNip22EventRoot { 165 pub const fn tag_index(&self) -> usize { 166 self.tag_index 167 } 168 169 pub const fn event_id(&self) -> &EventId { 170 &self.event_id 171 } 172 173 pub const fn kind_tag_index(&self) -> usize { 174 self.kind_tag_index 175 } 176 177 pub fn kind_raw_tag(&self) -> &[String] { 178 &self.kind_raw_tag 179 } 180 181 pub const fn kind(&self) -> Nip22CommentRootKind { 182 self.kind 183 } 184 185 pub const fn relay(&self) -> Option<&NostrRelayHint> { 186 self.relay.as_ref() 187 } 188 189 pub const fn author_hint(&self) -> Option<&PublicKey> { 190 self.author_hint.as_ref() 191 } 192 193 pub const fn author(&self) -> &RadrootsInboundNip22Participant { 194 &self.author 195 } 196 197 pub fn raw_tag(&self) -> &[String] { 198 &self.raw_tag 199 } 200 } 201 202 #[derive(Clone, Debug, PartialEq, Eq)] 203 pub struct RadrootsInboundNip22AddressRoot { 204 tag_index: usize, 205 coordinate: AddressableCoordinate, 206 kind_tag_index: usize, 207 kind_raw_tag: Vec<String>, 208 kind: Nip22CommentRootKind, 209 relay: Option<NostrRelayHint>, 210 author: RadrootsInboundNip22Participant, 211 raw_tag: Vec<String>, 212 } 213 214 impl RadrootsInboundNip22AddressRoot { 215 pub const fn tag_index(&self) -> usize { 216 self.tag_index 217 } 218 219 pub const fn coordinate(&self) -> &AddressableCoordinate { 220 &self.coordinate 221 } 222 223 pub const fn kind_tag_index(&self) -> usize { 224 self.kind_tag_index 225 } 226 227 pub fn kind_raw_tag(&self) -> &[String] { 228 &self.kind_raw_tag 229 } 230 231 pub const fn kind(&self) -> Nip22CommentRootKind { 232 self.kind 233 } 234 235 pub const fn relay(&self) -> Option<&NostrRelayHint> { 236 self.relay.as_ref() 237 } 238 239 pub const fn author(&self) -> &RadrootsInboundNip22Participant { 240 &self.author 241 } 242 243 pub fn raw_tag(&self) -> &[String] { 244 &self.raw_tag 245 } 246 } 247 248 #[derive(Clone, Debug, PartialEq, Eq)] 249 pub enum RadrootsInboundNip22CommentRoot { 250 Event(RadrootsInboundNip22EventRoot), 251 Address(RadrootsInboundNip22AddressRoot), 252 } 253 254 impl RadrootsInboundNip22CommentRoot { 255 pub const fn author(&self) -> &RadrootsInboundNip22Participant { 256 match self { 257 Self::Event(root) => root.author(), 258 Self::Address(root) => root.author(), 259 } 260 } 261 262 pub const fn kind(&self) -> Nip22CommentRootKind { 263 match self { 264 Self::Event(root) => root.kind(), 265 Self::Address(root) => root.kind(), 266 } 267 } 268 } 269 270 #[derive(Clone, Debug, PartialEq, Eq)] 271 pub struct RadrootsInboundNip22CommentParent { 272 tag_index: usize, 273 event_id: EventId, 274 kind_tag_index: usize, 275 kind_raw_tag: Vec<String>, 276 kind: u32, 277 relay: Option<NostrRelayHint>, 278 author_hint: Option<PublicKey>, 279 author: RadrootsInboundNip22Participant, 280 raw_tag: Vec<String>, 281 } 282 283 impl RadrootsInboundNip22CommentParent { 284 pub const fn tag_index(&self) -> usize { 285 self.tag_index 286 } 287 288 pub const fn event_id(&self) -> &EventId { 289 &self.event_id 290 } 291 292 pub const fn kind_tag_index(&self) -> usize { 293 self.kind_tag_index 294 } 295 296 pub fn kind_raw_tag(&self) -> &[String] { 297 &self.kind_raw_tag 298 } 299 300 pub const fn kind(&self) -> u32 { 301 self.kind 302 } 303 304 pub const fn relay(&self) -> Option<&NostrRelayHint> { 305 self.relay.as_ref() 306 } 307 308 pub const fn author_hint(&self) -> Option<&PublicKey> { 309 self.author_hint.as_ref() 310 } 311 312 pub const fn author(&self) -> &RadrootsInboundNip22Participant { 313 &self.author 314 } 315 316 pub fn raw_tag(&self) -> &[String] { 317 &self.raw_tag 318 } 319 } 320 321 #[derive(Clone, Debug, PartialEq, Eq)] 322 pub struct RadrootsInboundNip22CurrentRevision { 323 tag_index: usize, 324 event_id: EventId, 325 relay: Option<NostrRelayHint>, 326 raw_tag: Vec<String>, 327 } 328 329 impl RadrootsInboundNip22CurrentRevision { 330 pub const fn tag_index(&self) -> usize { 331 self.tag_index 332 } 333 334 pub const fn event_id(&self) -> &EventId { 335 &self.event_id 336 } 337 338 pub const fn relay(&self) -> Option<&NostrRelayHint> { 339 self.relay.as_ref() 340 } 341 342 pub fn raw_tag(&self) -> &[String] { 343 &self.raw_tag 344 } 345 } 346 347 #[derive(Clone, Debug, PartialEq, Eq)] 348 pub struct RadrootsInboundNip22TopLevelEventReference { 349 tag_index: usize, 350 event_id: EventId, 351 kind_tag_index: usize, 352 kind_raw_tag: Vec<String>, 353 kind: u32, 354 relay: Option<NostrRelayHint>, 355 author_hint: Option<PublicKey>, 356 author: RadrootsInboundNip22Participant, 357 raw_tag: Vec<String>, 358 } 359 360 #[derive(Clone, Debug, PartialEq, Eq)] 361 pub struct RadrootsInboundNip22TopLevelAddressReference { 362 tag_index: usize, 363 coordinate: AddressableCoordinate, 364 kind_tag_index: usize, 365 kind_raw_tag: Vec<String>, 366 kind: u32, 367 relay: Option<NostrRelayHint>, 368 author: RadrootsInboundNip22Participant, 369 raw_tag: Vec<String>, 370 } 371 372 impl RadrootsInboundNip22TopLevelAddressReference { 373 pub const fn tag_index(&self) -> usize { 374 self.tag_index 375 } 376 377 pub const fn coordinate(&self) -> &AddressableCoordinate { 378 &self.coordinate 379 } 380 381 pub const fn kind_tag_index(&self) -> usize { 382 self.kind_tag_index 383 } 384 385 pub fn kind_raw_tag(&self) -> &[String] { 386 &self.kind_raw_tag 387 } 388 389 pub const fn kind(&self) -> u32 { 390 self.kind 391 } 392 393 pub const fn relay(&self) -> Option<&NostrRelayHint> { 394 self.relay.as_ref() 395 } 396 397 pub const fn author(&self) -> &RadrootsInboundNip22Participant { 398 &self.author 399 } 400 401 pub fn raw_tag(&self) -> &[String] { 402 &self.raw_tag 403 } 404 } 405 406 impl RadrootsInboundNip22TopLevelEventReference { 407 pub const fn tag_index(&self) -> usize { 408 self.tag_index 409 } 410 411 pub const fn event_id(&self) -> &EventId { 412 &self.event_id 413 } 414 415 pub const fn kind_tag_index(&self) -> usize { 416 self.kind_tag_index 417 } 418 419 pub fn kind_raw_tag(&self) -> &[String] { 420 &self.kind_raw_tag 421 } 422 423 pub const fn kind(&self) -> u32 { 424 self.kind 425 } 426 427 pub const fn relay(&self) -> Option<&NostrRelayHint> { 428 self.relay.as_ref() 429 } 430 431 pub const fn author_hint(&self) -> Option<&PublicKey> { 432 self.author_hint.as_ref() 433 } 434 435 pub const fn author(&self) -> &RadrootsInboundNip22Participant { 436 &self.author 437 } 438 439 pub fn raw_tag(&self) -> &[String] { 440 &self.raw_tag 441 } 442 } 443 444 #[derive(Clone, Debug, PartialEq, Eq)] 445 pub enum RadrootsInboundNip22CommentPosition { 446 TopLevelEvent { 447 reference: RadrootsInboundNip22TopLevelEventReference, 448 }, 449 TopLevelAddress { 450 reference: RadrootsInboundNip22TopLevelAddressReference, 451 current_revision: RadrootsInboundNip22CurrentRevision, 452 }, 453 Nested { 454 parent: RadrootsInboundNip22CommentParent, 455 }, 456 } 457 458 #[derive(Clone, Debug, PartialEq, Eq)] 459 pub struct RadrootsInboundNip22CommentProjection { 460 root: RadrootsInboundNip22CommentRoot, 461 position: RadrootsInboundNip22CommentPosition, 462 mentions: Vec<RadrootsInboundNip22Participant>, 463 diagnostics: Vec<RadrootsNip22CommentDiagnostic>, 464 raw_tags: Vec<Vec<String>>, 465 } 466 467 impl RadrootsInboundNip22CommentProjection { 468 pub const fn root(&self) -> &RadrootsInboundNip22CommentRoot { 469 &self.root 470 } 471 472 pub const fn position(&self) -> &RadrootsInboundNip22CommentPosition { 473 &self.position 474 } 475 476 /// Valid distinct lowercase `p` mentions other than the selected parent 477 /// author, in source order. 478 pub fn mentions(&self) -> &[RadrootsInboundNip22Participant] { 479 &self.mentions 480 } 481 482 pub const fn is_direct(&self) -> bool { 483 !matches!( 484 &self.position, 485 RadrootsInboundNip22CommentPosition::Nested { .. } 486 ) 487 } 488 489 pub fn diagnostics(&self) -> &[RadrootsNip22CommentDiagnostic] { 490 &self.diagnostics 491 } 492 493 /// Exact inbound tags, including supplemental tags, in source order. 494 pub fn raw_tags(&self) -> &[Vec<String>] { 495 &self.raw_tags 496 } 497 498 pub const fn contract_id(&self) -> &'static str { 499 "radroots.social.comment.v1" 500 } 501 } 502 503 #[non_exhaustive] 504 #[derive(Clone, Debug, PartialEq, Eq)] 505 pub enum RadrootsNip22CommentProjectionError { 506 UnsupportedKind { 507 actual: u32, 508 }, 509 ContentMissing, 510 ContentTooLarge { 511 max: usize, 512 actual: usize, 513 }, 514 RootFormUnsupported { 515 tag_index: usize, 516 }, 517 ParentFormUnsupported { 518 tag_index: usize, 519 }, 520 RootCardinality { 521 actual: usize, 522 }, 523 RootKindCardinality { 524 actual: usize, 525 }, 526 RootAuthorCardinality { 527 actual: usize, 528 }, 529 ParentKindCardinality { 530 actual: usize, 531 }, 532 RootKindUnsupported { 533 tag_index: usize, 534 }, 535 RootAuthorInvalid { 536 tag_index: usize, 537 }, 538 RootReferenceShape { 539 tag_index: usize, 540 }, 541 RootEventIdInvalid { 542 tag_index: usize, 543 error: ParseError, 544 }, 545 RootCoordinateInvalid { 546 tag_index: usize, 547 }, 548 RootKindMismatch { 549 tag_index: usize, 550 }, 551 RootAuthorMismatch { 552 tag_index: usize, 553 }, 554 ParentKindInvalid { 555 tag_index: usize, 556 }, 557 ParentCardinality { 558 event_count: usize, 559 address_count: usize, 560 }, 561 ParentReferenceShape { 562 tag_index: usize, 563 }, 564 ParentEventIdInvalid { 565 tag_index: usize, 566 error: ParseError, 567 }, 568 ParentCoordinateInvalid { 569 tag_index: usize, 570 }, 571 ParentReferenceMismatch { 572 tag_index: usize, 573 }, 574 RevisionMissing { 575 actual: usize, 576 }, 577 RevisionShape { 578 tag_index: usize, 579 }, 580 RevisionEventIdInvalid { 581 tag_index: usize, 582 error: ParseError, 583 }, 584 ParentAuthorMissing, 585 ParentAuthorAmbiguous, 586 ParentAuthorMismatch, 587 TagCountExceeded { 588 max: usize, 589 actual: usize, 590 }, 591 TagElementCountExceeded { 592 max: usize, 593 actual: usize, 594 }, 595 TagElementTooLarge { 596 max: usize, 597 actual: usize, 598 tag_index: usize, 599 element_index: usize, 600 }, 601 TagBytesExceeded { 602 max: usize, 603 actual: usize, 604 }, 605 EventWireTooLarge { 606 max: usize, 607 actual: usize, 608 }, 609 } 610 611 impl RadrootsNip22CommentProjectionError { 612 pub const fn code(&self) -> &'static str { 613 match self { 614 Self::UnsupportedKind { .. } => "unsupported_kind", 615 Self::ContentMissing => "comment_content_missing", 616 Self::ContentTooLarge { .. } => "comment_content_too_large", 617 Self::RootFormUnsupported { .. } => "comment_root_form_unsupported", 618 Self::ParentFormUnsupported { .. } => "comment_parent_form_unsupported", 619 Self::RootCardinality { .. } => "comment_root_cardinality", 620 Self::RootKindCardinality { .. } => "comment_root_kind_cardinality", 621 Self::RootAuthorCardinality { .. } => "comment_root_author_cardinality", 622 Self::ParentKindCardinality { .. } => "comment_parent_kind_cardinality", 623 Self::RootKindUnsupported { .. } => "comment_root_kind_unsupported", 624 Self::RootAuthorInvalid { .. } => "comment_root_author_invalid", 625 Self::RootReferenceShape { .. } => "comment_root_reference_shape", 626 Self::RootEventIdInvalid { .. } => "comment_root_event_id_invalid", 627 Self::RootCoordinateInvalid { .. } => "comment_root_coordinate_invalid", 628 Self::RootKindMismatch { .. } => "comment_root_kind_mismatch", 629 Self::RootAuthorMismatch { .. } => "comment_root_author_mismatch", 630 Self::ParentKindInvalid { .. } => "comment_parent_kind_invalid", 631 Self::ParentCardinality { .. } => "comment_parent_cardinality", 632 Self::ParentReferenceShape { .. } => "comment_parent_reference_shape", 633 Self::ParentEventIdInvalid { .. } => "comment_parent_event_id_invalid", 634 Self::ParentCoordinateInvalid { .. } => "comment_parent_coordinate_invalid", 635 Self::ParentReferenceMismatch { .. } => "comment_parent_reference_mismatch", 636 Self::RevisionMissing { .. } => "comment_revision_missing", 637 Self::RevisionShape { .. } => "comment_revision_shape", 638 Self::RevisionEventIdInvalid { .. } => "comment_revision_event_id_invalid", 639 Self::ParentAuthorMissing => "comment_parent_author_missing", 640 Self::ParentAuthorAmbiguous => "comment_parent_author_ambiguous", 641 Self::ParentAuthorMismatch => "comment_parent_author_mismatch", 642 Self::TagCountExceeded { .. } => "comment_tag_count_exceeded", 643 Self::TagElementCountExceeded { .. } => "comment_tag_element_count_exceeded", 644 Self::TagElementTooLarge { .. } => "comment_tag_element_too_large", 645 Self::TagBytesExceeded { .. } => "comment_tag_bytes_exceeded", 646 Self::EventWireTooLarge { .. } => "comment_event_wire_too_large", 647 } 648 } 649 } 650 651 impl fmt::Display for RadrootsNip22CommentProjectionError { 652 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 653 match self { 654 Self::UnsupportedKind { actual } => { 655 write!(formatter, "NIP-22 comment kind must be 1111, got {actual}") 656 } 657 Self::ContentMissing => formatter.write_str("NIP-22 comment content is blank"), 658 Self::ContentTooLarge { max, actual } => { 659 write!( 660 formatter, 661 "NIP-22 comment content is {actual} bytes; max is {max}" 662 ) 663 } 664 Self::RootFormUnsupported { tag_index } => write!( 665 formatter, 666 "NIP-22 comment tag {tag_index} uses unsupported external root authority" 667 ), 668 Self::ParentFormUnsupported { tag_index } => write!( 669 formatter, 670 "NIP-22 comment tag {tag_index} uses unsupported external parent authority" 671 ), 672 Self::RootCardinality { actual } => write!( 673 formatter, 674 "NIP-22 comment has {actual} E/A root authority tags; expected exactly one" 675 ), 676 Self::RootKindCardinality { actual } => write!( 677 formatter, 678 "NIP-22 comment has {actual} K tags; expected exactly one" 679 ), 680 Self::RootAuthorCardinality { actual } => write!( 681 formatter, 682 "NIP-22 comment has {actual} P tags; expected exactly one" 683 ), 684 Self::ParentKindCardinality { actual } => write!( 685 formatter, 686 "NIP-22 comment has {actual} k tags; expected exactly one" 687 ), 688 Self::RootKindUnsupported { tag_index } => write!( 689 formatter, 690 "NIP-22 comment root kind at tag {tag_index} is unsupported or noncanonical" 691 ), 692 Self::RootAuthorInvalid { tag_index } => { 693 write!( 694 formatter, 695 "NIP-22 comment root author tag {tag_index} is invalid" 696 ) 697 } 698 Self::RootReferenceShape { tag_index } => { 699 write!( 700 formatter, 701 "NIP-22 comment root tag {tag_index} has an invalid shape" 702 ) 703 } 704 Self::RootEventIdInvalid { tag_index, error } => write!( 705 formatter, 706 "NIP-22 comment root tag {tag_index} has an invalid event id: {error}" 707 ), 708 Self::RootCoordinateInvalid { tag_index } => write!( 709 formatter, 710 "NIP-22 comment root tag {tag_index} has an invalid coordinate" 711 ), 712 Self::RootKindMismatch { tag_index } => write!( 713 formatter, 714 "NIP-22 comment root tag {tag_index} conflicts with K authority" 715 ), 716 Self::RootAuthorMismatch { tag_index } => write!( 717 formatter, 718 "NIP-22 comment root tag {tag_index} conflicts with P authority" 719 ), 720 Self::ParentKindInvalid { tag_index } => write!( 721 formatter, 722 "NIP-22 comment parent kind tag {tag_index} is invalid" 723 ), 724 Self::ParentCardinality { 725 event_count, 726 address_count, 727 } => write!( 728 formatter, 729 "NIP-22 comment has {event_count} e and {address_count} a parent tags" 730 ), 731 Self::ParentReferenceShape { tag_index } => write!( 732 formatter, 733 "NIP-22 comment parent tag {tag_index} has an invalid shape" 734 ), 735 Self::ParentEventIdInvalid { tag_index, error } => write!( 736 formatter, 737 "NIP-22 comment parent tag {tag_index} has an invalid event id: {error}" 738 ), 739 Self::ParentCoordinateInvalid { tag_index } => write!( 740 formatter, 741 "NIP-22 comment parent tag {tag_index} has an invalid coordinate" 742 ), 743 Self::ParentReferenceMismatch { tag_index } => write!( 744 formatter, 745 "NIP-22 comment parent tag {tag_index} conflicts with root authority" 746 ), 747 Self::RevisionMissing { actual } => write!( 748 formatter, 749 "NIP-22 comment has {actual} current-revision e tags; expected exactly one" 750 ), 751 Self::RevisionShape { tag_index } => write!( 752 formatter, 753 "NIP-22 comment revision tag {tag_index} has an invalid shape" 754 ), 755 Self::RevisionEventIdInvalid { tag_index, error } => write!( 756 formatter, 757 "NIP-22 comment revision tag {tag_index} has an invalid event id: {error}" 758 ), 759 Self::ParentAuthorMissing => { 760 formatter.write_str("NIP-22 comment parent author is missing") 761 } 762 Self::ParentAuthorAmbiguous => { 763 formatter.write_str("NIP-22 comment parent author is ambiguous") 764 } 765 Self::ParentAuthorMismatch => { 766 formatter.write_str("NIP-22 comment parent author conflicts with authority") 767 } 768 Self::TagCountExceeded { max, actual } => { 769 write!(formatter, "NIP-22 comment has {actual} tags; max is {max}") 770 } 771 Self::TagElementCountExceeded { max, actual } => write!( 772 formatter, 773 "NIP-22 comment has {actual} total tag elements; max is {max}" 774 ), 775 Self::TagElementTooLarge { 776 max, 777 actual, 778 tag_index, 779 element_index, 780 } => write!( 781 formatter, 782 "NIP-22 comment tag {tag_index} element {element_index} is {actual} bytes; max is {max}" 783 ), 784 Self::TagBytesExceeded { max, actual } => write!( 785 formatter, 786 "NIP-22 comment tag bytes are {actual}; max is {max}" 787 ), 788 Self::EventWireTooLarge { max, actual } => write!( 789 formatter, 790 "NIP-22 comment compact signed event is {actual} bytes; max is {max}" 791 ), 792 } 793 } 794 } 795 796 #[cfg(feature = "std")] 797 impl std::error::Error for RadrootsNip22CommentProjectionError {} 798 799 /// Projects a signature-and-id verified kind-1111 event as a NIP-22 comment. 800 /// 801 /// Authority tags are order-independent. Supplemental tags and valid distinct 802 /// lowercase `p` mentions are retained. Relay hints and event-reference author 803 /// hints are advisory inputs: malformed values are diagnosed, while 804 /// well-formed hints that conflict with required `P`/`p` authority fail 805 /// projection. 806 pub fn project_verified_nip22_comment_event( 807 verified_event: &RadrootsSignatureVerifiedEvent, 808 ) -> Result<RadrootsInboundNip22CommentProjection, RadrootsNip22CommentProjectionError> { 809 project_verified_nip22_comment_event_registry_v7(verified_event) 810 } 811 812 /// Projects a verified NIP-22 comment with contract-registry-v7 semantics. 813 pub fn project_verified_nip22_comment_event_registry_v7( 814 verified_event: &RadrootsSignatureVerifiedEvent, 815 ) -> Result<RadrootsInboundNip22CommentProjection, RadrootsNip22CommentProjectionError> { 816 let event = verified_event.event(); 817 project_nip22_comment_parts( 818 event.kind_u32(), 819 &event.tags_as_vec(), 820 event.content(), 821 decimal_digits(event.created_at_u64()), 822 ) 823 } 824 825 pub(crate) fn project_nip22_comment_parts( 826 kind: u32, 827 tags: &[Vec<String>], 828 content: &str, 829 created_at_digits: usize, 830 ) -> Result<RadrootsInboundNip22CommentProjection, RadrootsNip22CommentProjectionError> { 831 if kind != KIND_COMMENT { 832 return Err(RadrootsNip22CommentProjectionError::UnsupportedKind { actual: kind }); 833 } 834 if content.chars().all(char::is_whitespace) { 835 return Err(RadrootsNip22CommentProjectionError::ContentMissing); 836 } 837 if content.len() > RADROOTS_NIP22_COMMENT_CONTENT_MAX_BYTES { 838 return Err(RadrootsNip22CommentProjectionError::ContentTooLarge { 839 max: RADROOTS_NIP22_COMMENT_CONTENT_MAX_BYTES, 840 actual: content.len(), 841 }); 842 } 843 validate_tag_and_wire_budgets(tags, content, created_at_digits)?; 844 845 if let Some((tag_index, _)) = find_tags(tags, "I").first() { 846 return Err(RadrootsNip22CommentProjectionError::RootFormUnsupported { 847 tag_index: *tag_index, 848 }); 849 } 850 if let Some((tag_index, _)) = find_tags(tags, "i").first() { 851 return Err(RadrootsNip22CommentProjectionError::ParentFormUnsupported { 852 tag_index: *tag_index, 853 }); 854 } 855 let event_roots = find_tags(tags, "E"); 856 let address_roots = find_tags(tags, "A"); 857 let root_count = event_roots.len() + address_roots.len(); 858 if root_count != 1 { 859 return Err(RadrootsNip22CommentProjectionError::RootCardinality { actual: root_count }); 860 } 861 let kind_tag = exactly_one_authority_tag(tags, "K", |actual| { 862 RadrootsNip22CommentProjectionError::RootKindCardinality { actual } 863 })?; 864 let root_author_tag = exactly_one_authority_tag(tags, "P", |actual| { 865 RadrootsNip22CommentProjectionError::RootAuthorCardinality { actual } 866 })?; 867 let parent_kind_tag = exactly_one_authority_tag(tags, "k", |actual| { 868 RadrootsNip22CommentProjectionError::ParentKindCardinality { actual } 869 })?; 870 871 let mut diagnostics = Vec::new(); 872 let root_kind = parse_root_kind(kind_tag)?; 873 let root_author = parse_required_root_author(root_author_tag, &mut diagnostics)?; 874 let participants = project_participants(tags, &mut diagnostics); 875 let parent_event_tags = find_tags(tags, "e"); 876 let parent_address_tags = find_tags(tags, "a"); 877 878 let (root, position, mut mentions) = if let Some(root_tag) = event_roots.first().copied() { 879 let event_reference = 880 parse_event_reference(root_tag, ReferenceRole::Root, &mut diagnostics)?; 881 if event_reference 882 .author_hint 883 .as_ref() 884 .is_some_and(|author_hint| author_hint != root_author.pubkey()) 885 { 886 return Err(RadrootsNip22CommentProjectionError::RootAuthorMismatch { 887 tag_index: root_tag.0, 888 }); 889 } 890 let root = RadrootsInboundNip22EventRoot { 891 tag_index: root_tag.0, 892 event_id: event_reference.event_id, 893 kind_tag_index: kind_tag.0, 894 kind_raw_tag: kind_tag.1.clone(), 895 kind: root_kind, 896 relay: event_reference.relay, 897 author_hint: event_reference.author_hint, 898 author: root_author.clone(), 899 raw_tag: root_tag.1.clone(), 900 }; 901 let parent_kind = parse_parent_kind(parent_kind_tag)?; 902 if parent_kind != root_kind.as_u32() && parent_kind != KIND_COMMENT { 903 return Err(RadrootsNip22CommentProjectionError::ParentKindInvalid { 904 tag_index: parent_kind_tag.0, 905 }); 906 } 907 908 if parent_kind == root_kind.as_u32() { 909 require_parent_cardinality(&parent_event_tags, &parent_address_tags, 1, 0)?; 910 let parent_event_tag = parent_event_tags[0]; 911 let parent = 912 parse_event_reference(parent_event_tag, ReferenceRole::Parent, &mut diagnostics)?; 913 if parent.event_id != root.event_id { 914 return Err( 915 RadrootsNip22CommentProjectionError::ParentReferenceMismatch { 916 tag_index: parent_event_tag.0, 917 }, 918 ); 919 } 920 let (selected_author, mentions) = select_direct_parent_author( 921 participants, 922 root.author.pubkey(), 923 parent.author_hint.as_ref(), 924 )?; 925 let position = RadrootsInboundNip22CommentPosition::TopLevelEvent { 926 reference: RadrootsInboundNip22TopLevelEventReference { 927 tag_index: parent_event_tag.0, 928 event_id: parent.event_id, 929 kind_tag_index: parent_kind_tag.0, 930 kind_raw_tag: parent_kind_tag.1.clone(), 931 kind: parent_kind, 932 relay: parent.relay, 933 author_hint: parent.author_hint, 934 author: selected_author, 935 raw_tag: parent_event_tag.1.clone(), 936 }, 937 }; 938 ( 939 RadrootsInboundNip22CommentRoot::Event(root), 940 position, 941 mentions, 942 ) 943 } else { 944 require_parent_cardinality(&parent_event_tags, &parent_address_tags, 1, 0)?; 945 let parent_event_tag = parent_event_tags[0]; 946 let parent_reference = 947 parse_event_reference(parent_event_tag, ReferenceRole::Parent, &mut diagnostics)?; 948 if parent_reference.event_id == root.event_id { 949 return Err( 950 RadrootsNip22CommentProjectionError::ParentReferenceMismatch { 951 tag_index: parent_event_tag.0, 952 }, 953 ); 954 } 955 let (selected_author, mentions) = 956 select_nested_parent_author(participants, parent_reference.author_hint.as_ref())?; 957 let parent = RadrootsInboundNip22CommentParent { 958 tag_index: parent_event_tag.0, 959 event_id: parent_reference.event_id, 960 kind_tag_index: parent_kind_tag.0, 961 kind_raw_tag: parent_kind_tag.1.clone(), 962 kind: parent_kind, 963 relay: parent_reference.relay, 964 author_hint: parent_reference.author_hint, 965 author: selected_author, 966 raw_tag: parent_event_tag.1.clone(), 967 }; 968 ( 969 RadrootsInboundNip22CommentRoot::Event(root), 970 RadrootsInboundNip22CommentPosition::Nested { parent }, 971 mentions, 972 ) 973 } 974 } else { 975 let root_tag = address_roots[0]; 976 let address_reference = 977 parse_address_reference(root_tag, ReferenceRole::Root, &mut diagnostics)?; 978 if address_reference.kind != root_kind { 979 return Err(RadrootsNip22CommentProjectionError::RootKindMismatch { 980 tag_index: root_tag.0, 981 }); 982 } 983 if address_reference.author != *root_author.pubkey() { 984 return Err(RadrootsNip22CommentProjectionError::RootAuthorMismatch { 985 tag_index: root_tag.0, 986 }); 987 } 988 let root = RadrootsInboundNip22AddressRoot { 989 tag_index: root_tag.0, 990 coordinate: address_reference.coordinate, 991 kind_tag_index: kind_tag.0, 992 kind_raw_tag: kind_tag.1.clone(), 993 kind: root_kind, 994 relay: address_reference.relay, 995 author: root_author.clone(), 996 raw_tag: root_tag.1.clone(), 997 }; 998 let parent_kind = parse_parent_kind(parent_kind_tag)?; 999 if parent_kind != root_kind.as_u32() && parent_kind != KIND_COMMENT { 1000 return Err(RadrootsNip22CommentProjectionError::ParentKindInvalid { 1001 tag_index: parent_kind_tag.0, 1002 }); 1003 } 1004 1005 if parent_kind == root_kind.as_u32() { 1006 if parent_address_tags.len() != 1 { 1007 return Err(RadrootsNip22CommentProjectionError::ParentCardinality { 1008 event_count: parent_event_tags.len(), 1009 address_count: parent_address_tags.len(), 1010 }); 1011 } 1012 if parent_event_tags.len() != 1 { 1013 return Err(RadrootsNip22CommentProjectionError::RevisionMissing { 1014 actual: parent_event_tags.len(), 1015 }); 1016 } 1017 let parent_address = parse_address_reference( 1018 parent_address_tags[0], 1019 ReferenceRole::Parent, 1020 &mut diagnostics, 1021 )?; 1022 if parent_address.coordinate != root.coordinate { 1023 return Err( 1024 RadrootsNip22CommentProjectionError::ParentReferenceMismatch { 1025 tag_index: parent_address_tags[0].0, 1026 }, 1027 ); 1028 } 1029 let current_revision = parse_current_revision(parent_event_tags[0], &mut diagnostics)?; 1030 let (selected_author, mentions) = 1031 select_direct_parent_author(participants, root.author.pubkey(), None)?; 1032 let position = RadrootsInboundNip22CommentPosition::TopLevelAddress { 1033 reference: RadrootsInboundNip22TopLevelAddressReference { 1034 tag_index: parent_address_tags[0].0, 1035 coordinate: parent_address.coordinate, 1036 kind_tag_index: parent_kind_tag.0, 1037 kind_raw_tag: parent_kind_tag.1.clone(), 1038 kind: parent_kind, 1039 relay: parent_address.relay, 1040 author: selected_author, 1041 raw_tag: parent_address_tags[0].1.clone(), 1042 }, 1043 current_revision, 1044 }; 1045 ( 1046 RadrootsInboundNip22CommentRoot::Address(root), 1047 position, 1048 mentions, 1049 ) 1050 } else { 1051 require_parent_cardinality(&parent_event_tags, &parent_address_tags, 1, 0)?; 1052 let parent_event_tag = parent_event_tags[0]; 1053 let parent_reference = 1054 parse_event_reference(parent_event_tag, ReferenceRole::Parent, &mut diagnostics)?; 1055 let (selected_author, mentions) = 1056 select_nested_parent_author(participants, parent_reference.author_hint.as_ref())?; 1057 let parent = RadrootsInboundNip22CommentParent { 1058 tag_index: parent_event_tag.0, 1059 event_id: parent_reference.event_id, 1060 kind_tag_index: parent_kind_tag.0, 1061 kind_raw_tag: parent_kind_tag.1.clone(), 1062 kind: parent_kind, 1063 relay: parent_reference.relay, 1064 author_hint: parent_reference.author_hint, 1065 author: selected_author, 1066 raw_tag: parent_event_tag.1.clone(), 1067 }; 1068 ( 1069 RadrootsInboundNip22CommentRoot::Address(root), 1070 RadrootsInboundNip22CommentPosition::Nested { parent }, 1071 mentions, 1072 ) 1073 } 1074 }; 1075 1076 mentions.sort_by_key(RadrootsInboundNip22Participant::tag_index); 1077 diagnostics.sort_by_key(RadrootsNip22CommentDiagnostic::tag_index); 1078 1079 Ok(RadrootsInboundNip22CommentProjection { 1080 root, 1081 position, 1082 mentions, 1083 diagnostics, 1084 raw_tags: tags.to_vec(), 1085 }) 1086 } 1087 1088 type IndexedTag<'a> = (usize, &'a Vec<String>); 1089 1090 struct ParsedEventReference { 1091 event_id: EventId, 1092 relay: Option<NostrRelayHint>, 1093 author_hint: Option<PublicKey>, 1094 } 1095 1096 struct ParsedAddressReference { 1097 coordinate: AddressableCoordinate, 1098 author: PublicKey, 1099 kind: Nip22CommentRootKind, 1100 relay: Option<NostrRelayHint>, 1101 } 1102 1103 #[derive(Clone, Copy)] 1104 enum ReferenceRole { 1105 Root, 1106 Parent, 1107 } 1108 1109 impl ReferenceRole { 1110 const fn relay_role(self) -> RelayRole { 1111 match self { 1112 Self::Root => RelayRole::Root, 1113 Self::Parent => RelayRole::Parent, 1114 } 1115 } 1116 1117 fn author_hint_diagnostic( 1118 self, 1119 tag_index: usize, 1120 raw_tag: Vec<String>, 1121 ) -> RadrootsNip22CommentDiagnostic { 1122 match self { 1123 Self::Root => { 1124 RadrootsNip22CommentDiagnostic::RootAuthorHintIgnored { tag_index, raw_tag } 1125 } 1126 Self::Parent => { 1127 RadrootsNip22CommentDiagnostic::ParentAuthorHintIgnored { tag_index, raw_tag } 1128 } 1129 } 1130 } 1131 1132 const fn reference_shape_error(self, tag_index: usize) -> RadrootsNip22CommentProjectionError { 1133 match self { 1134 Self::Root => RadrootsNip22CommentProjectionError::RootReferenceShape { tag_index }, 1135 Self::Parent => RadrootsNip22CommentProjectionError::ParentReferenceShape { tag_index }, 1136 } 1137 } 1138 1139 const fn event_id_error( 1140 self, 1141 tag_index: usize, 1142 error: ParseError, 1143 ) -> RadrootsNip22CommentProjectionError { 1144 match self { 1145 Self::Root => { 1146 RadrootsNip22CommentProjectionError::RootEventIdInvalid { tag_index, error } 1147 } 1148 Self::Parent => { 1149 RadrootsNip22CommentProjectionError::ParentEventIdInvalid { tag_index, error } 1150 } 1151 } 1152 } 1153 1154 const fn coordinate_error(self, tag_index: usize) -> RadrootsNip22CommentProjectionError { 1155 match self { 1156 Self::Root => RadrootsNip22CommentProjectionError::RootCoordinateInvalid { tag_index }, 1157 Self::Parent => { 1158 RadrootsNip22CommentProjectionError::ParentCoordinateInvalid { tag_index } 1159 } 1160 } 1161 } 1162 } 1163 1164 #[derive(Clone, Copy)] 1165 enum RelayRole { 1166 Root, 1167 RootAuthor, 1168 Parent, 1169 ParentAuthor, 1170 Revision, 1171 } 1172 1173 impl RelayRole { 1174 fn diagnostic(self, tag_index: usize, raw_tag: Vec<String>) -> RadrootsNip22CommentDiagnostic { 1175 match self { 1176 Self::Root => RadrootsNip22CommentDiagnostic::RootRelayIgnored { tag_index, raw_tag }, 1177 Self::RootAuthor => { 1178 RadrootsNip22CommentDiagnostic::RootAuthorRelayIgnored { tag_index, raw_tag } 1179 } 1180 Self::Parent => { 1181 RadrootsNip22CommentDiagnostic::ParentRelayIgnored { tag_index, raw_tag } 1182 } 1183 Self::ParentAuthor => { 1184 RadrootsNip22CommentDiagnostic::ParentAuthorRelayIgnored { tag_index, raw_tag } 1185 } 1186 Self::Revision => { 1187 RadrootsNip22CommentDiagnostic::RevisionRelayIgnored { tag_index, raw_tag } 1188 } 1189 } 1190 } 1191 } 1192 1193 fn find_tags<'a>(tags: &'a [Vec<String>], name: &str) -> Vec<IndexedTag<'a>> { 1194 tags.iter() 1195 .enumerate() 1196 .filter(|(_, tag)| tag.first().is_some_and(|actual| actual == name)) 1197 .collect() 1198 } 1199 1200 fn exactly_one_authority_tag<'a>( 1201 tags: &'a [Vec<String>], 1202 name: &str, 1203 error: impl FnOnce(usize) -> RadrootsNip22CommentProjectionError, 1204 ) -> Result<IndexedTag<'a>, RadrootsNip22CommentProjectionError> { 1205 let matches = find_tags(tags, name); 1206 match matches.as_slice() { 1207 [tag] => Ok(*tag), 1208 _ => Err(error(matches.len())), 1209 } 1210 } 1211 1212 fn parse_root_kind( 1213 (tag_index, tag): IndexedTag<'_>, 1214 ) -> Result<Nip22CommentRootKind, RadrootsNip22CommentProjectionError> { 1215 let kind = tag 1216 .get(1) 1217 .filter(|_| tag.len() == 2) 1218 .and_then(|value| parse_canonical_kind_token(value)) 1219 .ok_or(RadrootsNip22CommentProjectionError::RootKindUnsupported { tag_index })?; 1220 Nip22CommentRootKind::parse(kind) 1221 .map_err(|_| RadrootsNip22CommentProjectionError::RootKindUnsupported { tag_index }) 1222 } 1223 1224 fn parse_parent_kind( 1225 (tag_index, tag): IndexedTag<'_>, 1226 ) -> Result<u32, RadrootsNip22CommentProjectionError> { 1227 let kind = tag 1228 .get(1) 1229 .filter(|_| tag.len() == 2) 1230 .and_then(|value| parse_canonical_kind_token(value)) 1231 .ok_or(RadrootsNip22CommentProjectionError::ParentKindInvalid { tag_index })?; 1232 if kind == KIND_COMMENT || Nip22CommentRootKind::parse(kind).is_ok() { 1233 Ok(kind) 1234 } else { 1235 Err(RadrootsNip22CommentProjectionError::ParentKindInvalid { tag_index }) 1236 } 1237 } 1238 1239 fn parse_canonical_kind_token(value: &str) -> Option<u32> { 1240 if value.is_empty() 1241 || !value.bytes().all(|byte| byte.is_ascii_digit()) 1242 || value.len() > 1 && value.starts_with('0') 1243 { 1244 return None; 1245 } 1246 value.parse().ok() 1247 } 1248 1249 fn parse_required_root_author( 1250 (tag_index, tag): IndexedTag<'_>, 1251 diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>, 1252 ) -> Result<RadrootsInboundNip22Participant, RadrootsNip22CommentProjectionError> { 1253 if !matches!(tag.len(), 2 | 3) { 1254 return Err(RadrootsNip22CommentProjectionError::RootAuthorInvalid { tag_index }); 1255 } 1256 let pubkey = PublicKey::from_hex(&tag[1]) 1257 .map_err(|_| RadrootsNip22CommentProjectionError::RootAuthorInvalid { tag_index })?; 1258 let relay = project_relay(tag_index, tag, 2, RelayRole::RootAuthor, diagnostics); 1259 Ok(RadrootsInboundNip22Participant { 1260 tag_index, 1261 pubkey, 1262 relay, 1263 raw_tag: tag.clone(), 1264 }) 1265 } 1266 1267 fn project_participants( 1268 tags: &[Vec<String>], 1269 diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>, 1270 ) -> Vec<RadrootsInboundNip22Participant> { 1271 let mut participants = Vec::new(); 1272 for (tag_index, tag) in find_tags(tags, "p") { 1273 if !matches!(tag.len(), 2 | 3) { 1274 diagnostics.push(RadrootsNip22CommentDiagnostic::ParentAuthorShapeIgnored { 1275 tag_index, 1276 raw_tag: tag.clone(), 1277 }); 1278 continue; 1279 } 1280 let Ok(pubkey) = PublicKey::from_hex(&tag[1]) else { 1281 diagnostics.push(RadrootsNip22CommentDiagnostic::ParentAuthorInvalidIgnored { 1282 tag_index, 1283 raw_tag: tag.clone(), 1284 }); 1285 continue; 1286 }; 1287 let relay = project_relay(tag_index, tag, 2, RelayRole::ParentAuthor, diagnostics); 1288 if participants 1289 .iter() 1290 .any(|participant: &RadrootsInboundNip22Participant| participant.pubkey == pubkey) 1291 { 1292 diagnostics.push( 1293 RadrootsNip22CommentDiagnostic::ParentAuthorDuplicateIgnored { 1294 tag_index, 1295 raw_tag: tag.clone(), 1296 }, 1297 ); 1298 continue; 1299 } 1300 participants.push(RadrootsInboundNip22Participant { 1301 tag_index, 1302 pubkey, 1303 relay, 1304 raw_tag: tag.clone(), 1305 }); 1306 } 1307 participants 1308 } 1309 1310 fn parse_event_reference( 1311 (tag_index, tag): IndexedTag<'_>, 1312 role: ReferenceRole, 1313 diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>, 1314 ) -> Result<ParsedEventReference, RadrootsNip22CommentProjectionError> { 1315 if !(2..=4).contains(&tag.len()) { 1316 return Err(role.reference_shape_error(tag_index)); 1317 } 1318 let event_id = 1319 EventId::parse(&tag[1]).map_err(|error| role.event_id_error(tag_index, error))?; 1320 let relay = project_relay(tag_index, tag, 2, role.relay_role(), diagnostics); 1321 let author_hint = tag 1322 .get(3) 1323 .and_then(|value| match PublicKey::from_hex(value) { 1324 Ok(author) => Some(author), 1325 Err(_) => { 1326 diagnostics.push(role.author_hint_diagnostic(tag_index, tag.clone())); 1327 None 1328 } 1329 }); 1330 Ok(ParsedEventReference { 1331 event_id, 1332 relay, 1333 author_hint, 1334 }) 1335 } 1336 1337 fn parse_address_reference( 1338 (tag_index, tag): IndexedTag<'_>, 1339 role: ReferenceRole, 1340 diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>, 1341 ) -> Result<ParsedAddressReference, RadrootsNip22CommentProjectionError> { 1342 if !matches!(tag.len(), 2 | 3) { 1343 return Err(role.reference_shape_error(tag_index)); 1344 } 1345 let canonical_kind = tag[1] 1346 .split_once(':') 1347 .and_then(|(kind, _)| parse_canonical_kind_token(kind)) 1348 .ok_or_else(|| role.coordinate_error(tag_index))?; 1349 let parts = 1350 AddressableCoordinateParts::parse(&tag[1]).map_err(|_| role.coordinate_error(tag_index))?; 1351 if parts.kind != canonical_kind { 1352 return Err(role.coordinate_error(tag_index)); 1353 } 1354 let kind = 1355 Nip22CommentRootKind::parse(parts.kind).map_err(|_| role.coordinate_error(tag_index))?; 1356 let coordinate = AddressableCoordinate::parse(format_coordinate(&parts)) 1357 .map_err(|_| role.coordinate_error(tag_index))?; 1358 let relay = project_relay(tag_index, tag, 2, role.relay_role(), diagnostics); 1359 Ok(ParsedAddressReference { 1360 coordinate, 1361 author: parts.pubkey, 1362 kind, 1363 relay, 1364 }) 1365 } 1366 1367 fn format_coordinate(parts: &AddressableCoordinateParts) -> String { 1368 format!("{}:{}:{}", parts.kind, parts.pubkey, parts.d_tag) 1369 } 1370 1371 fn parse_current_revision( 1372 (tag_index, tag): IndexedTag<'_>, 1373 diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>, 1374 ) -> Result<RadrootsInboundNip22CurrentRevision, RadrootsNip22CommentProjectionError> { 1375 if !matches!(tag.len(), 2 | 3) { 1376 return Err(RadrootsNip22CommentProjectionError::RevisionShape { tag_index }); 1377 } 1378 let event_id = EventId::parse(&tag[1]).map_err(|error| { 1379 RadrootsNip22CommentProjectionError::RevisionEventIdInvalid { tag_index, error } 1380 })?; 1381 let relay = project_relay(tag_index, tag, 2, RelayRole::Revision, diagnostics); 1382 Ok(RadrootsInboundNip22CurrentRevision { 1383 tag_index, 1384 event_id, 1385 relay, 1386 raw_tag: tag.clone(), 1387 }) 1388 } 1389 1390 fn require_parent_cardinality( 1391 event_tags: &[IndexedTag<'_>], 1392 address_tags: &[IndexedTag<'_>], 1393 expected_event_count: usize, 1394 expected_address_count: usize, 1395 ) -> Result<(), RadrootsNip22CommentProjectionError> { 1396 if event_tags.len() == expected_event_count && address_tags.len() == expected_address_count { 1397 Ok(()) 1398 } else { 1399 Err(RadrootsNip22CommentProjectionError::ParentCardinality { 1400 event_count: event_tags.len(), 1401 address_count: address_tags.len(), 1402 }) 1403 } 1404 } 1405 1406 fn select_direct_parent_author( 1407 mut participants: Vec<RadrootsInboundNip22Participant>, 1408 expected_author: &PublicKey, 1409 author_hint: Option<&PublicKey>, 1410 ) -> Result< 1411 ( 1412 RadrootsInboundNip22Participant, 1413 Vec<RadrootsInboundNip22Participant>, 1414 ), 1415 RadrootsNip22CommentProjectionError, 1416 > { 1417 if participants.is_empty() { 1418 return Err(RadrootsNip22CommentProjectionError::ParentAuthorMissing); 1419 } 1420 if author_hint.is_some_and(|hint| hint != expected_author) { 1421 return Err(RadrootsNip22CommentProjectionError::ParentAuthorMismatch); 1422 } 1423 let Some(selected_index) = participants 1424 .iter() 1425 .position(|participant| participant.pubkey() == expected_author) 1426 else { 1427 return Err(RadrootsNip22CommentProjectionError::ParentAuthorMismatch); 1428 }; 1429 let selected = participants.remove(selected_index); 1430 Ok((selected, participants)) 1431 } 1432 1433 fn select_nested_parent_author( 1434 mut participants: Vec<RadrootsInboundNip22Participant>, 1435 author_hint: Option<&PublicKey>, 1436 ) -> Result< 1437 ( 1438 RadrootsInboundNip22Participant, 1439 Vec<RadrootsInboundNip22Participant>, 1440 ), 1441 RadrootsNip22CommentProjectionError, 1442 > { 1443 if participants.is_empty() { 1444 return Err(RadrootsNip22CommentProjectionError::ParentAuthorMissing); 1445 } 1446 let selected_index = if let Some(author_hint) = author_hint { 1447 participants 1448 .iter() 1449 .position(|participant| participant.pubkey() == author_hint) 1450 .ok_or(RadrootsNip22CommentProjectionError::ParentAuthorMismatch)? 1451 } else { 1452 if participants.len() > 1 { 1453 return Err(RadrootsNip22CommentProjectionError::ParentAuthorAmbiguous); 1454 } 1455 0 1456 }; 1457 let selected = participants.remove(selected_index); 1458 Ok((selected, participants)) 1459 } 1460 1461 fn project_relay( 1462 tag_index: usize, 1463 tag: &[String], 1464 element_index: usize, 1465 role: RelayRole, 1466 diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>, 1467 ) -> Option<NostrRelayHint> { 1468 let value = tag.get(element_index)?; 1469 if value.is_empty() { 1470 return None; 1471 } 1472 match NostrRelayHint::parse(value) { 1473 Ok(relay) => Some(relay), 1474 Err(_) => { 1475 diagnostics.push(role.diagnostic(tag_index, tag.to_vec())); 1476 None 1477 } 1478 } 1479 } 1480 1481 fn validate_tag_and_wire_budgets( 1482 tags: &[Vec<String>], 1483 content: &str, 1484 created_at_digits: usize, 1485 ) -> Result<(), RadrootsNip22CommentProjectionError> { 1486 if tags.len() > RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT { 1487 return Err(RadrootsNip22CommentProjectionError::TagCountExceeded { 1488 max: RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT, 1489 actual: tags.len(), 1490 }); 1491 } 1492 let tag_element_count = tags 1493 .iter() 1494 .fold(0usize, |total, tag| total.saturating_add(tag.len())); 1495 if tag_element_count > RADROOTS_NIP22_COMMENT_TAG_TOTAL_ELEMENT_MAX_COUNT { 1496 return Err( 1497 RadrootsNip22CommentProjectionError::TagElementCountExceeded { 1498 max: RADROOTS_NIP22_COMMENT_TAG_TOTAL_ELEMENT_MAX_COUNT, 1499 actual: tag_element_count, 1500 }, 1501 ); 1502 } 1503 let mut tag_bytes = 0usize; 1504 let mut tags_json_bytes = 2usize; 1505 for (tag_index, tag) in tags.iter().enumerate() { 1506 if tag_index > 0 { 1507 tags_json_bytes = tags_json_bytes.saturating_add(1); 1508 } 1509 tags_json_bytes = tags_json_bytes.saturating_add(2); 1510 for (element_index, element) in tag.iter().enumerate() { 1511 if element.len() > RADROOTS_NIP22_COMMENT_TAG_ELEMENT_MAX_BYTES { 1512 return Err(RadrootsNip22CommentProjectionError::TagElementTooLarge { 1513 max: RADROOTS_NIP22_COMMENT_TAG_ELEMENT_MAX_BYTES, 1514 actual: element.len(), 1515 tag_index, 1516 element_index, 1517 }); 1518 } 1519 if element_index > 0 { 1520 tags_json_bytes = tags_json_bytes.saturating_add(1); 1521 } 1522 tags_json_bytes = tags_json_bytes.saturating_add(canonical_json_string_bytes(element)); 1523 tag_bytes = tag_bytes.saturating_add(element.len()); 1524 } 1525 } 1526 if tag_bytes > RADROOTS_NIP22_COMMENT_TAG_TOTAL_MAX_BYTES { 1527 return Err(RadrootsNip22CommentProjectionError::TagBytesExceeded { 1528 max: RADROOTS_NIP22_COMMENT_TAG_TOTAL_MAX_BYTES, 1529 actual: tag_bytes, 1530 }); 1531 } 1532 let actual = RADROOTS_NIP22_COMMENT_SIGNED_EVENT_FIXED_MAX_BYTES 1533 .saturating_add(created_at_digits) 1534 .saturating_add(tags_json_bytes) 1535 .saturating_add(canonical_json_string_bytes(content)); 1536 if actual > RADROOTS_NIP22_COMMENT_EVENT_WIRE_MAX_BYTES { 1537 return Err(RadrootsNip22CommentProjectionError::EventWireTooLarge { 1538 max: RADROOTS_NIP22_COMMENT_EVENT_WIRE_MAX_BYTES, 1539 actual, 1540 }); 1541 } 1542 Ok(()) 1543 } 1544 1545 fn canonical_json_string_bytes(value: &str) -> usize { 1546 value.chars().fold(2usize, |total, character| { 1547 total.saturating_add(match character { 1548 '"' | '\\' | '\u{0008}' | '\t' | '\n' | '\u{000c}' | '\r' => 2, 1549 '\u{0000}'..='\u{001f}' => 6, 1550 _ => character.len_utf8(), 1551 }) 1552 }) 1553 } 1554 1555 const fn decimal_digits(mut value: u64) -> usize { 1556 let mut digits = 1usize; 1557 while value >= 10 { 1558 value /= 10; 1559 digits += 1; 1560 } 1561 digits 1562 } 1563 1564 #[cfg(test)] 1565 mod tests;