1#![allow(
2 unreachable_pub,
3 reason = "UniFFI adapter records and functions stay inside this private core module"
4)]
5
6use std::fmt;
7use std::str::FromStr;
8
9use nostr::event::Error as NostrEventError;
10use nostr::secp256k1::schnorr::Signature;
11use nostr::{Event, EventId, Keys, Kind, Tag, Timestamp, UnsignedEvent};
12use serde::{Deserialize, Serialize};
13
14use crate::diagnostics::{self, Operation};
15use crate::{NostrPublicKey, SoftchatError};
16
17pub const MAX_NOSTR_EVENT_JSON_BYTES: usize = 512 * 1024;
22
23pub const MAX_PORTABLE_TIMESTAMP_SECONDS: u64 = 9_007_199_254_740_991;
29
30#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub struct NostrEventId {
33 inner: EventId,
34}
35
36impl NostrEventId {
37 pub fn from_hex(value: &str) -> Result<Self, SoftchatError> {
43 if !is_hex_of_length(value, 64) {
44 return Err(SoftchatError::InvalidEventId);
45 }
46 let inner = EventId::from_hex(value).map_err(|_| SoftchatError::InvalidEventId)?;
47 Ok(Self { inner })
48 }
49
50 #[must_use]
52 pub fn to_hex(self) -> String {
53 self.inner.to_hex()
54 }
55
56 pub(crate) const fn from_inner(inner: EventId) -> Self {
57 Self { inner }
58 }
59
60 pub(crate) const fn as_inner(&self) -> &EventId {
61 &self.inner
62 }
63}
64
65impl fmt::Debug for NostrEventId {
66 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67 formatter
68 .debug_tuple("NostrEventId")
69 .field(&self.to_hex())
70 .finish()
71 }
72}
73
74impl fmt::Display for NostrEventId {
75 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76 formatter.write_str(&self.to_hex())
77 }
78}
79
80impl FromStr for NostrEventId {
81 type Err = SoftchatError;
82
83 fn from_str(value: &str) -> Result<Self, Self::Err> {
84 Self::from_hex(value)
85 }
86}
87
88#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
90pub struct NostrEventSignature {
91 inner: Signature,
92}
93
94impl NostrEventSignature {
95 pub fn from_hex(value: &str) -> Result<Self, SoftchatError> {
101 if !is_hex_of_length(value, 128) {
102 return Err(SoftchatError::InvalidEventSignature);
103 }
104 let inner = Signature::from_str(value).map_err(|_| SoftchatError::InvalidEventSignature)?;
105 Ok(Self { inner })
106 }
107
108 #[must_use]
110 pub fn to_hex(self) -> String {
111 self.inner.to_string()
112 }
113
114 const fn from_inner(inner: Signature) -> Self {
115 Self { inner }
116 }
117
118 const fn as_inner(&self) -> &Signature {
119 &self.inner
120 }
121}
122
123impl fmt::Debug for NostrEventSignature {
124 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125 formatter
126 .debug_tuple("NostrEventSignature")
127 .field(&self.to_hex())
128 .finish()
129 }
130}
131
132impl fmt::Display for NostrEventSignature {
133 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134 formatter.write_str(&self.to_hex())
135 }
136}
137
138impl FromStr for NostrEventSignature {
139 type Err = SoftchatError;
140
141 fn from_str(value: &str) -> Result<Self, Self::Err> {
142 Self::from_hex(value)
143 }
144}
145
146#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
148pub struct NostrEventKind(u16);
149
150impl NostrEventKind {
151 pub const USER_METADATA: Self = Self(0);
153 pub const SHORT_TEXT_NOTE: Self = Self(1);
155 pub const FOLLOW_LIST: Self = Self(3);
157 pub const EVENT_DELETION_REQUEST: Self = Self(5);
159 pub const REACTION: Self = Self(7);
161 pub const SEAL: Self = Self(13);
163 pub const PRIVATE_DIRECT_MESSAGE: Self = Self(14);
165 pub const GENERIC_REPOST: Self = Self(16);
167 pub const UPDATED_CONTENT: Self = Self(1010);
169 pub const GIFT_WRAP: Self = Self(1059);
171 pub const FILE_METADATA: Self = Self(1063);
173 pub const EPHEMERAL_GIFT_WRAP: Self = Self(21059);
175 pub const TYPING: Self = Self(21234);
177 pub const CLIENT_AUTHENTICATION: Self = Self(22242);
179 pub const HTTP_AUTHENTICATION: Self = Self(27235);
181 pub const APPLICATION_DATA: Self = Self(30078);
183 pub const APPLICATION_DATA_SYNC: Self = Self(30079);
185
186 #[must_use]
188 pub const fn from_u16(value: u16) -> Self {
189 Self(value)
190 }
191
192 pub fn try_from_u32(value: u32) -> Result<Self, SoftchatError> {
199 let value = u16::try_from(value).map_err(|_| SoftchatError::InvalidEventKind)?;
200 Ok(Self(value))
201 }
202
203 #[must_use]
205 pub const fn as_u16(self) -> u16 {
206 self.0
207 }
208
209 fn as_inner(self) -> Kind {
210 Kind::from_u16(self.0)
211 }
212}
213
214#[derive(Clone, Eq, Hash, PartialEq)]
216pub struct NostrTag {
217 inner: Tag,
218}
219
220impl NostrTag {
221 pub fn new<I, S>(values: I) -> Result<Self, SoftchatError>
230 where
231 I: IntoIterator<Item = S>,
232 S: Into<String>,
233 {
234 let inner = Tag::parse(values).map_err(|_| SoftchatError::InvalidEventTag)?;
235 Ok(Self { inner })
236 }
237
238 #[must_use]
240 pub fn values(&self) -> &[String] {
241 self.inner.as_slice()
242 }
243
244 fn into_inner(self) -> Tag {
245 self.inner
246 }
247}
248
249impl fmt::Debug for NostrTag {
250 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
251 formatter
252 .debug_tuple("NostrTag")
253 .field(&self.values())
254 .finish()
255 }
256}
257
258#[derive(Clone, Debug, Eq, PartialEq)]
264pub struct NostrEventDraft {
265 created_at: u64,
266 kind: NostrEventKind,
267 tags: Vec<NostrTag>,
268 content: String,
269}
270
271impl NostrEventDraft {
272 pub fn new(
278 created_at: u64,
279 kind: NostrEventKind,
280 tags: Vec<NostrTag>,
281 content: impl Into<String>,
282 ) -> Result<Self, SoftchatError> {
283 validate_timestamp(created_at)?;
284 let draft = Self {
285 created_at,
286 kind,
287 tags,
288 content: content.into(),
289 };
290 check_draft_size(&draft)?;
291 Ok(draft)
292 }
293
294 #[must_use]
296 pub const fn created_at(&self) -> u64 {
297 self.created_at
298 }
299
300 #[must_use]
302 pub const fn kind(&self) -> NostrEventKind {
303 self.kind
304 }
305
306 pub fn tags(&self) -> impl ExactSizeIterator<Item = &[String]> {
308 self.tags.iter().map(NostrTag::values)
309 }
310
311 #[must_use]
313 pub fn content(&self) -> &str {
314 &self.content
315 }
316
317 #[cfg(any(feature = "sqlite-storage", test))]
318 pub(crate) fn rumor_json_bytes(&self) -> Result<usize, SoftchatError> {
319 #[derive(Serialize)]
320 struct RumorSizeProbe<'a> {
321 id: &'static str,
322 pubkey: &'static str,
323 created_at: u64,
324 kind: u16,
325 tags: Vec<&'a [String]>,
326 content: &'a str,
327 }
328
329 const EVENT_KEY_PLACEHOLDER: &str =
330 "0000000000000000000000000000000000000000000000000000000000000000";
331 let probe = RumorSizeProbe {
332 id: EVENT_KEY_PLACEHOLDER,
333 pubkey: EVENT_KEY_PLACEHOLDER,
334 created_at: self.created_at,
335 kind: self.kind.as_u16(),
336 tags: self.tags().collect(),
337 content: self.content(),
338 };
339 serde_json::to_string(&probe)
340 .map(|json| json.len())
341 .map_err(|_| SoftchatError::InvalidEventJson)
342 }
343
344 pub(crate) fn sign_with_keys(self, keys: &Keys) -> Result<SignedNostrEvent, SoftchatError> {
345 let unsigned = UnsignedEvent::new(
346 keys.public_key(),
347 Timestamp::from(self.created_at),
348 self.kind.as_inner(),
349 self.tags.into_iter().map(NostrTag::into_inner),
350 self.content,
351 );
352 let event = unsigned
353 .sign_with_keys(keys)
354 .map_err(|_| SoftchatError::EventSigningFailed)?;
355 SignedNostrEvent::from_verified_inner(event)
356 }
357}
358
359#[derive(Clone, Eq, PartialEq)]
365pub struct NostrRumor {
366 id: NostrEventId,
367 inner: UnsignedEvent,
368}
369
370impl NostrRumor {
371 pub fn new(public_key: NostrPublicKey, draft: NostrEventDraft) -> Result<Self, SoftchatError> {
378 let mut inner = UnsignedEvent::new(
379 *public_key.as_inner(),
380 Timestamp::from(draft.created_at),
381 draft.kind.as_inner(),
382 draft.tags.into_iter().map(NostrTag::into_inner),
383 draft.content,
384 );
385 let id = NostrEventId::from_inner(inner.id());
386 let rumor = Self { id, inner };
387 check_json_size(&rumor.to_json()?)?;
388 Ok(rumor)
389 }
390
391 pub fn from_json(json: &str) -> Result<Self, SoftchatError> {
400 check_json_size(json)?;
401 let wire: WireRumor =
402 serde_json::from_str(json).map_err(|_| SoftchatError::InvalidEventJson)?;
403 if !is_lower_hex_of_length(&wire.id, 64) {
404 return Err(SoftchatError::InvalidEventId);
405 }
406 if !is_lower_hex_of_length(&wire.pubkey, 64) {
407 return Err(SoftchatError::InvalidPublicKey);
408 }
409
410 let declared_id = NostrEventId::from_hex(&wire.id)?;
411 let tags = wire
412 .tags
413 .into_iter()
414 .map(NostrTag::new)
415 .collect::<Result<Vec<_>, _>>()?;
416 let draft = NostrEventDraft::new(
417 wire.created_at,
418 NostrEventKind::try_from_u32(wire.kind)?,
419 tags,
420 wire.content,
421 )?;
422 let rumor = Self::new(NostrPublicKey::from_hex(&wire.pubkey)?, draft)?;
423 if rumor.id != declared_id {
424 return Err(SoftchatError::InvalidEventId);
425 }
426 Ok(rumor)
427 }
428
429 pub fn to_json(&self) -> Result<String, SoftchatError> {
435 serde_json::to_string(&self.inner).map_err(|_| SoftchatError::InvalidEventJson)
436 }
437
438 #[must_use]
440 pub const fn id(&self) -> NostrEventId {
441 self.id
442 }
443
444 #[must_use]
446 pub fn public_key(&self) -> NostrPublicKey {
447 NostrPublicKey::from_inner(self.inner.pubkey)
448 }
449
450 #[must_use]
452 pub const fn created_at(&self) -> u64 {
453 self.inner.created_at.as_secs()
454 }
455
456 #[must_use]
458 pub fn kind(&self) -> NostrEventKind {
459 NostrEventKind::from_u16(self.inner.kind.as_u16())
460 }
461
462 pub fn tags(&self) -> impl ExactSizeIterator<Item = &[String]> {
464 self.inner.tags.iter().map(Tag::as_slice)
465 }
466
467 #[must_use]
469 pub fn content(&self) -> &str {
470 &self.inner.content
471 }
472}
473
474impl fmt::Debug for NostrRumor {
475 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
476 formatter
477 .debug_struct("NostrRumor")
478 .field("id", &self.id())
479 .field("public_key", &self.public_key())
480 .field("created_at", &self.created_at())
481 .field("kind", &self.kind())
482 .field("tags", &self.tags().collect::<Vec<_>>())
483 .field("content", &self.content())
484 .finish()
485 }
486}
487
488#[derive(Clone, Eq, PartialEq)]
490pub struct SignedNostrEvent {
491 inner: Event,
492}
493
494impl SignedNostrEvent {
495 pub fn from_json(json: &str) -> Result<Self, SoftchatError> {
507 let result = Self::from_json_inner(json);
508 if result.is_ok() {
509 diagnostics::record_success(Operation::VerifyEvent, json.len(), json.len());
510 } else {
511 diagnostics::record_rejection(json.len());
512 }
513 result
514 }
515
516 fn from_json_inner(json: &str) -> Result<Self, SoftchatError> {
517 check_json_size(json)?;
518 let wire: WireSignedEvent =
519 serde_json::from_str(json).map_err(|_| SoftchatError::InvalidEventJson)?;
520
521 if !is_lower_hex_of_length(&wire.id, 64) {
522 return Err(SoftchatError::InvalidEventId);
523 }
524 if !is_lower_hex_of_length(&wire.pubkey, 64) {
525 return Err(SoftchatError::InvalidPublicKey);
526 }
527 if !is_lower_hex_of_length(&wire.sig, 128) {
528 return Err(SoftchatError::InvalidEventSignature);
529 }
530
531 let id = NostrEventId::from_hex(&wire.id)?;
532 let public_key = NostrPublicKey::from_hex(&wire.pubkey)?;
533 let kind = NostrEventKind::try_from_u32(wire.kind)?;
534 let signature = NostrEventSignature::from_hex(&wire.sig)?;
535 let tags = wire
536 .tags
537 .into_iter()
538 .map(NostrTag::new)
539 .collect::<Result<Vec<_>, _>>()?;
540
541 Self::new(
542 id,
543 public_key,
544 wire.created_at,
545 kind,
546 tags,
547 wire.content,
548 signature,
549 )
550 }
551
552 #[allow(clippy::too_many_arguments)]
558 pub fn new(
559 id: NostrEventId,
560 public_key: NostrPublicKey,
561 created_at: u64,
562 kind: NostrEventKind,
563 tags: Vec<NostrTag>,
564 content: String,
565 signature: NostrEventSignature,
566 ) -> Result<Self, SoftchatError> {
567 validate_timestamp(created_at)?;
568
569 let event = Event::new(
570 *id.as_inner(),
571 *public_key.as_inner(),
572 Timestamp::from(created_at),
573 kind.as_inner(),
574 tags.into_iter().map(NostrTag::into_inner),
575 content,
576 *signature.as_inner(),
577 );
578
579 Self::from_verified_inner(event)
580 }
581
582 pub fn to_json(&self) -> Result<String, SoftchatError> {
588 serde_json::to_string(&self.inner).map_err(|_| SoftchatError::InvalidEventJson)
589 }
590
591 #[must_use]
593 pub const fn id(&self) -> NostrEventId {
594 NostrEventId::from_inner(self.inner.id)
595 }
596
597 #[must_use]
599 pub fn public_key(&self) -> NostrPublicKey {
600 NostrPublicKey::from_inner(self.inner.pubkey)
601 }
602
603 #[must_use]
605 pub const fn created_at(&self) -> u64 {
606 self.inner.created_at.as_secs()
607 }
608
609 #[must_use]
611 pub fn kind(&self) -> NostrEventKind {
612 NostrEventKind::from_u16(self.inner.kind.as_u16())
613 }
614
615 pub fn tags(&self) -> impl ExactSizeIterator<Item = &[String]> {
617 self.inner.tags.iter().map(Tag::as_slice)
618 }
619
620 #[must_use]
622 pub fn content(&self) -> &str {
623 &self.inner.content
624 }
625
626 #[must_use]
628 pub const fn signature(&self) -> NostrEventSignature {
629 NostrEventSignature::from_inner(self.inner.sig)
630 }
631
632 fn from_verified_inner(event: Event) -> Result<Self, SoftchatError> {
633 match event.verify() {
634 Ok(()) => {}
635 Err(NostrEventError::InvalidId) => return Err(SoftchatError::InvalidEventId),
636 Err(NostrEventError::InvalidSignature) => {
637 return Err(SoftchatError::InvalidEventSignature);
638 }
639 Err(_) => return Err(SoftchatError::InvalidEventJson),
640 }
641
642 let event = Self { inner: event };
643 let canonical = event.to_json()?;
644 check_json_size(&canonical)?;
645 Ok(event)
646 }
647}
648
649impl fmt::Debug for SignedNostrEvent {
650 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
651 formatter
652 .debug_struct("SignedNostrEvent")
653 .field("id", &self.id())
654 .field("public_key", &self.public_key())
655 .field("created_at", &self.created_at())
656 .field("kind", &self.kind())
657 .field("tags", &self.tags().collect::<Vec<_>>())
658 .field("content", &self.content())
659 .field("signature", &self.signature())
660 .finish()
661 }
662}
663
664#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
666#[serde(rename_all = "camelCase", deny_unknown_fields)]
667#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
668pub struct SignedEvent {
669 pub id: String,
671 pub public_key: String,
673 pub created_at: i64,
675 pub kind: i32,
677 pub tags: Vec<Vec<String>>,
679 pub content: String,
681 pub signature: String,
683}
684
685#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
687#[serde(rename_all = "camelCase", deny_unknown_fields)]
688#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
689pub struct EventDraft {
690 pub created_at: i64,
692 pub kind: i32,
694 pub tags: Vec<Vec<String>>,
696 pub content: String,
698}
699
700#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
702#[serde(rename_all = "camelCase", deny_unknown_fields)]
703#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
704pub struct RumorEvent {
705 pub id: String,
707 pub public_key: String,
709 pub created_at: i64,
711 pub kind: i32,
713 pub tags: Vec<Vec<String>>,
715 pub content: String,
717}
718
719impl TryFrom<EventDraft> for NostrEventDraft {
720 type Error = SoftchatError;
721
722 fn try_from(draft: EventDraft) -> Result<Self, Self::Error> {
723 let created_at =
724 u64::try_from(draft.created_at).map_err(|_| SoftchatError::InvalidEventTimestamp)?;
725 let raw_kind = u32::try_from(draft.kind).map_err(|_| SoftchatError::InvalidEventKind)?;
726 let tags = draft
727 .tags
728 .into_iter()
729 .map(NostrTag::new)
730 .collect::<Result<Vec<_>, _>>()?;
731 Self::new(
732 created_at,
733 NostrEventKind::try_from_u32(raw_kind)?,
734 tags,
735 draft.content,
736 )
737 }
738}
739
740impl From<&NostrEventDraft> for EventDraft {
741 fn from(draft: &NostrEventDraft) -> Self {
742 Self {
743 created_at: draft.created_at() as i64,
744 kind: i32::from(draft.kind().as_u16()),
745 tags: draft.tags().map(<[String]>::to_vec).collect(),
746 content: draft.content().to_owned(),
747 }
748 }
749}
750
751impl TryFrom<RumorEvent> for NostrRumor {
752 type Error = SoftchatError;
753
754 fn try_from(rumor: RumorEvent) -> Result<Self, Self::Error> {
755 let created_at =
756 u64::try_from(rumor.created_at).map_err(|_| SoftchatError::InvalidEventTimestamp)?;
757 let raw_kind = u32::try_from(rumor.kind).map_err(|_| SoftchatError::InvalidEventKind)?;
758 let tags = rumor
759 .tags
760 .into_iter()
761 .map(NostrTag::new)
762 .collect::<Result<Vec<_>, _>>()?;
763 let draft = NostrEventDraft::new(
764 created_at,
765 NostrEventKind::try_from_u32(raw_kind)?,
766 tags,
767 rumor.content,
768 )?;
769 let validated = Self::new(NostrPublicKey::from_hex(&rumor.public_key)?, draft)?;
770 if validated.id().to_hex() != rumor.id {
771 return Err(SoftchatError::InvalidEventId);
772 }
773 Ok(validated)
774 }
775}
776
777impl From<&NostrRumor> for RumorEvent {
778 fn from(rumor: &NostrRumor) -> Self {
779 Self {
780 id: rumor.id().to_hex(),
781 public_key: rumor.public_key().to_hex(),
782 created_at: rumor.created_at() as i64,
783 kind: i32::from(rumor.kind().as_u16()),
784 tags: rumor.tags().map(<[String]>::to_vec).collect(),
785 content: rumor.content().to_owned(),
786 }
787 }
788}
789
790impl TryFrom<SignedEvent> for SignedNostrEvent {
791 type Error = SoftchatError;
792
793 fn try_from(event: SignedEvent) -> Result<Self, Self::Error> {
794 if !is_lower_hex_of_length(&event.id, 64) {
795 return Err(SoftchatError::InvalidEventId);
796 }
797 if !is_lower_hex_of_length(&event.public_key, 64) {
798 return Err(SoftchatError::InvalidPublicKey);
799 }
800 if !is_lower_hex_of_length(&event.signature, 128) {
801 return Err(SoftchatError::InvalidEventSignature);
802 }
803
804 let created_at =
805 u64::try_from(event.created_at).map_err(|_| SoftchatError::InvalidEventTimestamp)?;
806 let raw_kind = u32::try_from(event.kind).map_err(|_| SoftchatError::InvalidEventKind)?;
807 let tags = event
808 .tags
809 .into_iter()
810 .map(NostrTag::new)
811 .collect::<Result<Vec<_>, _>>()?;
812
813 Self::new(
814 NostrEventId::from_hex(&event.id)?,
815 NostrPublicKey::from_hex(&event.public_key)?,
816 created_at,
817 NostrEventKind::try_from_u32(raw_kind)?,
818 tags,
819 event.content,
820 NostrEventSignature::from_hex(&event.signature)?,
821 )
822 }
823}
824
825impl From<&SignedNostrEvent> for SignedEvent {
826 fn from(event: &SignedNostrEvent) -> Self {
827 Self {
828 id: event.id().to_hex(),
829 public_key: event.public_key().to_hex(),
830 created_at: event.created_at() as i64,
831 kind: i32::from(event.kind().as_u16()),
832 tags: event.tags().map(<[String]>::to_vec).collect(),
833 content: event.content().to_owned(),
834 signature: event.signature().to_hex(),
835 }
836 }
837}
838
839#[derive(Debug, Deserialize)]
840struct WireSignedEvent {
841 id: String,
842 pubkey: String,
843 created_at: u64,
844 kind: u32,
845 tags: Vec<Vec<String>>,
846 content: String,
847 sig: String,
848}
849
850#[derive(Debug, Deserialize)]
851#[serde(deny_unknown_fields)]
852struct WireRumor {
853 id: String,
854 pubkey: String,
855 created_at: u64,
856 kind: u32,
857 tags: Vec<Vec<String>>,
858 content: String,
859}
860
861#[cfg(any(feature = "native-bindings", test))]
863#[cfg_attr(feature = "native-bindings", uniffi::export)]
864pub fn validate_nostr_event_id(event_id_hex: String) -> Result<String, SoftchatError> {
865 NostrEventId::from_hex(&event_id_hex).map(NostrEventId::to_hex)
866}
867
868#[cfg(any(feature = "native-bindings", test))]
870#[cfg_attr(feature = "native-bindings", uniffi::export)]
871pub fn validate_nostr_event_signature(signature_hex: String) -> Result<String, SoftchatError> {
872 NostrEventSignature::from_hex(&signature_hex).map(NostrEventSignature::to_hex)
873}
874
875#[cfg(any(feature = "native-bindings", test))]
877#[cfg_attr(feature = "native-bindings", uniffi::export)]
878pub fn validate_nostr_event_kind(kind: i32) -> Result<i32, SoftchatError> {
879 let raw = u32::try_from(kind).map_err(|_| SoftchatError::InvalidEventKind)?;
880 Ok(i32::from(NostrEventKind::try_from_u32(raw)?.as_u16()))
881}
882
883#[cfg(any(feature = "native-bindings", test))]
885#[cfg_attr(feature = "native-bindings", uniffi::export)]
886pub fn validate_nostr_tag(values: Vec<String>) -> Result<Vec<String>, SoftchatError> {
887 NostrTag::new(values).map(|tag| tag.values().to_vec())
888}
889
890#[cfg(any(feature = "native-bindings", test))]
892#[cfg_attr(feature = "native-bindings", uniffi::export)]
893pub fn validate_nostr_event_draft(draft: EventDraft) -> Result<EventDraft, SoftchatError> {
894 NostrEventDraft::try_from(draft).map(|draft| EventDraft::from(&draft))
895}
896
897#[cfg(any(
899 feature = "native-bindings",
900 all(feature = "javascript-bindings", target_arch = "wasm32"),
901 test
902))]
903#[cfg_attr(feature = "native-bindings", uniffi::export)]
904pub fn parse_nostr_rumor_json(json: String) -> Result<RumorEvent, SoftchatError> {
905 NostrRumor::from_json(&json).map(|rumor| RumorEvent::from(&rumor))
906}
907
908#[cfg(any(feature = "native-bindings", test))]
910#[cfg_attr(feature = "native-bindings", uniffi::export)]
911pub fn validate_nostr_rumor(rumor: RumorEvent) -> Result<RumorEvent, SoftchatError> {
912 NostrRumor::try_from(rumor).map(|rumor| RumorEvent::from(&rumor))
913}
914
915#[cfg(any(
917 feature = "native-bindings",
918 all(feature = "javascript-bindings", target_arch = "wasm32"),
919 test
920))]
921#[cfg_attr(feature = "native-bindings", uniffi::export)]
922pub fn nostr_rumor_json(rumor: RumorEvent) -> Result<String, SoftchatError> {
923 NostrRumor::try_from(rumor)?.to_json()
924}
925
926#[cfg(any(feature = "native-bindings", test))]
928#[cfg_attr(feature = "native-bindings", uniffi::export)]
929pub fn parse_signed_nostr_event_json(json: String) -> Result<SignedEvent, SoftchatError> {
930 SignedNostrEvent::from_json(&json).map(|event| SignedEvent::from(&event))
931}
932
933#[cfg(any(feature = "native-bindings", test))]
935#[cfg_attr(feature = "native-bindings", uniffi::export)]
936pub fn validate_signed_nostr_event(event: SignedEvent) -> Result<SignedEvent, SoftchatError> {
937 SignedNostrEvent::try_from(event).map(|event| SignedEvent::from(&event))
938}
939
940#[cfg(any(
942 feature = "native-bindings",
943 all(feature = "javascript-bindings", target_arch = "wasm32"),
944 test
945))]
946#[cfg_attr(feature = "native-bindings", uniffi::export)]
947pub fn signed_nostr_event_json(event: SignedEvent) -> Result<String, SoftchatError> {
948 SignedNostrEvent::try_from(event)?.to_json()
949}
950
951fn check_json_size(json: &str) -> Result<(), SoftchatError> {
952 if json.len() > MAX_NOSTR_EVENT_JSON_BYTES {
953 return Err(SoftchatError::EventTooLarge);
954 }
955 Ok(())
956}
957
958fn check_draft_size(draft: &NostrEventDraft) -> Result<(), SoftchatError> {
959 #[derive(Serialize)]
960 struct SizeProbe<'a> {
961 id: &'static str,
962 pubkey: &'static str,
963 created_at: u64,
964 kind: u16,
965 tags: Vec<&'a [String]>,
966 content: &'a str,
967 sig: &'static str,
968 }
969
970 const EVENT_ID_PLACEHOLDER: &str =
971 "0000000000000000000000000000000000000000000000000000000000000000";
972 const SIGNATURE_PLACEHOLDER: &str = concat!(
973 "0000000000000000000000000000000000000000000000000000000000000000",
974 "0000000000000000000000000000000000000000000000000000000000000000"
975 );
976
977 let probe = SizeProbe {
978 id: EVENT_ID_PLACEHOLDER,
979 pubkey: EVENT_ID_PLACEHOLDER,
980 created_at: draft.created_at,
981 kind: draft.kind.as_u16(),
982 tags: draft.tags().collect(),
983 content: draft.content(),
984 sig: SIGNATURE_PLACEHOLDER,
985 };
986 let json = serde_json::to_string(&probe).map_err(|_| SoftchatError::InvalidEventJson)?;
987 check_json_size(&json)
988}
989
990fn validate_timestamp(created_at: u64) -> Result<(), SoftchatError> {
991 if created_at > MAX_PORTABLE_TIMESTAMP_SECONDS {
992 return Err(SoftchatError::InvalidEventTimestamp);
993 }
994 Ok(())
995}
996
997fn is_hex_of_length(value: &str, expected: usize) -> bool {
998 value.len() == expected && value.bytes().all(|byte| byte.is_ascii_hexdigit())
999}
1000
1001fn is_lower_hex_of_length(value: &str, expected: usize) -> bool {
1002 value.len() == expected
1003 && value
1004 .bytes()
1005 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1006}
1007
1008#[cfg(test)]
1009mod tests {
1010 use super::*;
1011 use crate::LocalIdentity;
1012
1013 const VALID_EVENT: &str =
1014 include_str!("../../../fixtures/nostr/nip01/signed-event-with-empty-tag-fields.json");
1015 const ALICE_SECRET: &str = "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a";
1016 const BOB_PUBLIC_KEY: &str = "4ddeb9109a8cd29ba279a637f5ec344f2479ee07df1f4043f3fe26d8948cfef9";
1017
1018 #[test]
1019 fn parses_verifies_and_preserves_exact_raw_tags() -> Result<(), SoftchatError> {
1020 let event = SignedNostrEvent::from_json(VALID_EVENT.trim())?;
1021
1022 assert_eq!(
1023 event.id().to_hex(),
1024 "f55c30722f056e330d8a7a6a9ba1522f7522c0f1ced1c93d78ea833c78a3d6ec"
1025 );
1026 assert_eq!(event.kind().as_u16(), 3);
1027 assert_eq!(
1028 event.tags().next(),
1029 Some(
1030 [
1031 "p",
1032 "4ddeb9109a8cd29ba279a637f5ec344f2479ee07df1f4043f3fe26d8948cfef9",
1033 "",
1034 "",
1035 ]
1036 .map(String::from)
1037 .as_slice()
1038 )
1039 );
1040
1041 let canonical = event.to_json()?;
1042 let reparsed = SignedNostrEvent::from_json(&canonical)?;
1043 assert_eq!(reparsed, event);
1044 assert_eq!(canonical, VALID_EVENT.trim());
1045 Ok(())
1046 }
1047
1048 #[test]
1049 fn reports_id_before_signature_tampering() {
1050 let tampered = VALID_EVENT.replace("\"content\":\"\"", "\"content\":\"changed\"");
1051 assert!(matches!(
1052 SignedNostrEvent::from_json(&tampered),
1053 Err(SoftchatError::InvalidEventId)
1054 ));
1055 }
1056
1057 #[test]
1058 fn rejects_invalid_signature_after_valid_id() {
1059 let tampered = VALID_EVENT.replacen("\"sig\":\"5", "\"sig\":\"4", 1);
1060 assert!(matches!(
1061 SignedNostrEvent::from_json(&tampered),
1062 Err(SoftchatError::InvalidEventSignature)
1063 ));
1064 }
1065
1066 #[test]
1067 fn rejects_noncanonical_wire_hex_and_invalid_shapes() {
1068 let uppercase_id = VALID_EVENT.replacen("\"id\":\"f", "\"id\":\"F", 1);
1069 assert!(matches!(
1070 SignedNostrEvent::from_json(&uppercase_id),
1071 Err(SoftchatError::InvalidEventId)
1072 ));
1073
1074 let empty_tag = VALID_EVENT.replacen(r#"[["p","4ddeb"#, r#"[[] ,["p","4ddeb"#, 1);
1075 assert!(matches!(
1076 SignedNostrEvent::from_json(&empty_tag),
1077 Err(SoftchatError::InvalidEventTag)
1078 ));
1079
1080 let invalid_kind = VALID_EVENT.replacen("\"kind\":3", "\"kind\":65536", 1);
1081 assert!(matches!(
1082 SignedNostrEvent::from_json(&invalid_kind),
1083 Err(SoftchatError::InvalidEventKind)
1084 ));
1085
1086 let duplicate_content = VALID_EVENT.replacen(
1087 "\"content\":\"\"",
1088 "\"content\":\"\",\"content\":\"ambiguous\"",
1089 1,
1090 );
1091 assert!(matches!(
1092 SignedNostrEvent::from_json(&duplicate_content),
1093 Err(SoftchatError::InvalidEventJson)
1094 ));
1095 }
1096
1097 #[test]
1098 fn rejects_unportable_timestamps_and_oversized_json() {
1099 let invalid_timestamp = VALID_EVENT.replacen(
1100 "\"created_at\":1698412975",
1101 "\"created_at\":9007199254740992",
1102 1,
1103 );
1104 assert!(matches!(
1105 SignedNostrEvent::from_json(&invalid_timestamp),
1106 Err(SoftchatError::InvalidEventTimestamp)
1107 ));
1108
1109 let oversized = " ".repeat(MAX_NOSTR_EVENT_JSON_BYTES + 1);
1110 assert!(matches!(
1111 SignedNostrEvent::from_json(&oversized),
1112 Err(SoftchatError::EventTooLarge)
1113 ));
1114 }
1115
1116 #[test]
1117 fn scalar_binding_validators_canonicalize_and_reject() -> Result<(), SoftchatError> {
1118 let id = validate_nostr_event_id("AB".repeat(32))?;
1119 assert_eq!(id, "ab".repeat(32));
1120 assert!(matches!(
1121 validate_nostr_event_kind(-1),
1122 Err(SoftchatError::InvalidEventKind)
1123 ));
1124 assert!(matches!(
1125 validate_nostr_tag(Vec::new()),
1126 Err(SoftchatError::InvalidEventTag)
1127 ));
1128
1129 let mut decoded_wire = parse_signed_nostr_event_json(VALID_EVENT.trim().to_owned())?;
1130 decoded_wire.id.make_ascii_uppercase();
1131 assert!(matches!(
1132 validate_signed_nostr_event(decoded_wire),
1133 Err(SoftchatError::InvalidEventId)
1134 ));
1135 Ok(())
1136 }
1137
1138 #[test]
1139 fn signs_explicit_drafts_without_normalizing_tags() -> Result<(), SoftchatError> {
1140 let identity = LocalIdentity::from_secret_hex(ALICE_SECRET)?;
1141 let draft = NostrEventDraft::new(
1142 1_700_000_000,
1143 NostrEventKind::SHORT_TEXT_NOTE,
1144 vec![NostrTag::new([
1145 "p".to_owned(),
1146 BOB_PUBLIC_KEY.to_owned(),
1147 String::new(),
1148 String::new(),
1149 ])?],
1150 "A signed protocol event",
1151 )?;
1152
1153 let signed = identity.sign_event(draft)?;
1154 assert_eq!(signed.public_key(), identity.public_key());
1155 assert_eq!(signed.created_at(), 1_700_000_000);
1156 assert_eq!(signed.kind(), NostrEventKind::SHORT_TEXT_NOTE);
1157 assert_eq!(
1158 signed.tags().next(),
1159 Some(["p", BOB_PUBLIC_KEY, "", ""].map(String::from).as_slice())
1160 );
1161
1162 let reparsed = SignedNostrEvent::from_json(&signed.to_json()?)?;
1163 assert_eq!(reparsed, signed);
1164 Ok(())
1165 }
1166
1167 #[test]
1168 fn validates_drafts_before_signing() {
1169 assert!(matches!(
1170 NostrEventDraft::new(
1171 MAX_PORTABLE_TIMESTAMP_SECONDS + 1,
1172 NostrEventKind::SHORT_TEXT_NOTE,
1173 Vec::new(),
1174 "invalid",
1175 ),
1176 Err(SoftchatError::InvalidEventTimestamp)
1177 ));
1178 assert!(matches!(
1179 NostrEventDraft::new(
1180 1_700_000_000,
1181 NostrEventKind::SHORT_TEXT_NOTE,
1182 Vec::new(),
1183 "x".repeat(MAX_NOSTR_EVENT_JSON_BYTES),
1184 ),
1185 Err(SoftchatError::EventTooLarge)
1186 ));
1187 }
1188}