1use std::fmt;
2use std::str::FromStr;
3
4use nostr::event::Error as NostrEventError;
5use nostr::secp256k1::schnorr::Signature;
6use nostr::{Event, EventId, Keys, Kind, Tag, Timestamp, UnsignedEvent};
7use serde::{Deserialize, Serialize};
8
9use crate::diagnostics::{self, Operation};
10use crate::{NostrPublicKey, SoftchatError};
11
12pub const MAX_NOSTR_EVENT_JSON_BYTES: usize = 512 * 1024;
17
18pub const MAX_PORTABLE_TIMESTAMP_SECONDS: u64 = 9_007_199_254_740_991;
24
25#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
27pub struct NostrEventId {
28 inner: EventId,
29}
30
31impl NostrEventId {
32 pub fn from_hex(value: &str) -> Result<Self, SoftchatError> {
38 if !is_hex_of_length(value, 64) {
39 return Err(SoftchatError::InvalidEventId);
40 }
41 let inner = EventId::from_hex(value).map_err(|_| SoftchatError::InvalidEventId)?;
42 Ok(Self { inner })
43 }
44
45 #[must_use]
47 pub fn to_hex(self) -> String {
48 self.inner.to_hex()
49 }
50
51 pub(crate) const fn from_inner(inner: EventId) -> Self {
52 Self { inner }
53 }
54
55 pub(crate) const fn as_inner(&self) -> &EventId {
56 &self.inner
57 }
58}
59
60impl fmt::Debug for NostrEventId {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 formatter
63 .debug_tuple("NostrEventId")
64 .field(&self.to_hex())
65 .finish()
66 }
67}
68
69impl fmt::Display for NostrEventId {
70 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71 formatter.write_str(&self.to_hex())
72 }
73}
74
75impl FromStr for NostrEventId {
76 type Err = SoftchatError;
77
78 fn from_str(value: &str) -> Result<Self, Self::Err> {
79 Self::from_hex(value)
80 }
81}
82
83#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
85pub struct NostrEventSignature {
86 inner: Signature,
87}
88
89impl NostrEventSignature {
90 pub fn from_hex(value: &str) -> Result<Self, SoftchatError> {
96 if !is_hex_of_length(value, 128) {
97 return Err(SoftchatError::InvalidEventSignature);
98 }
99 let inner = Signature::from_str(value).map_err(|_| SoftchatError::InvalidEventSignature)?;
100 Ok(Self { inner })
101 }
102
103 #[must_use]
105 pub fn to_hex(self) -> String {
106 self.inner.to_string()
107 }
108
109 const fn from_inner(inner: Signature) -> Self {
110 Self { inner }
111 }
112
113 const fn as_inner(&self) -> &Signature {
114 &self.inner
115 }
116}
117
118impl fmt::Debug for NostrEventSignature {
119 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120 formatter
121 .debug_tuple("NostrEventSignature")
122 .field(&self.to_hex())
123 .finish()
124 }
125}
126
127impl fmt::Display for NostrEventSignature {
128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129 formatter.write_str(&self.to_hex())
130 }
131}
132
133impl FromStr for NostrEventSignature {
134 type Err = SoftchatError;
135
136 fn from_str(value: &str) -> Result<Self, Self::Err> {
137 Self::from_hex(value)
138 }
139}
140
141#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
143pub struct NostrEventKind(u16);
144
145impl NostrEventKind {
146 pub const USER_METADATA: Self = Self(0);
148 pub const SHORT_TEXT_NOTE: Self = Self(1);
150 pub const FOLLOW_LIST: Self = Self(3);
152 pub const EVENT_DELETION_REQUEST: Self = Self(5);
154 pub const REACTION: Self = Self(7);
156 pub const SEAL: Self = Self(13);
158 pub const PRIVATE_DIRECT_MESSAGE: Self = Self(14);
160 pub const GENERIC_REPOST: Self = Self(16);
162 pub const UPDATED_CONTENT: Self = Self(1010);
164 pub const GIFT_WRAP: Self = Self(1059);
166 pub const FILE_METADATA: Self = Self(1063);
168 pub const EPHEMERAL_GIFT_WRAP: Self = Self(21059);
170 pub const TYPING: Self = Self(21234);
172 pub const CLIENT_AUTHENTICATION: Self = Self(22242);
174 pub const HTTP_AUTHENTICATION: Self = Self(27235);
176 pub const APPLICATION_DATA: Self = Self(30078);
178 pub const APPLICATION_DATA_SYNC: Self = Self(30079);
180
181 #[must_use]
183 pub const fn from_u16(value: u16) -> Self {
184 Self(value)
185 }
186
187 pub fn try_from_u32(value: u32) -> Result<Self, SoftchatError> {
194 let value = u16::try_from(value).map_err(|_| SoftchatError::InvalidEventKind)?;
195 Ok(Self(value))
196 }
197
198 #[must_use]
200 pub const fn as_u16(self) -> u16 {
201 self.0
202 }
203
204 fn as_inner(self) -> Kind {
205 Kind::from_u16(self.0)
206 }
207}
208
209#[derive(Clone, Eq, Hash, PartialEq)]
211pub struct NostrTag {
212 inner: Tag,
213}
214
215impl NostrTag {
216 pub fn new<I, S>(values: I) -> Result<Self, SoftchatError>
225 where
226 I: IntoIterator<Item = S>,
227 S: Into<String>,
228 {
229 let inner = Tag::parse(values).map_err(|_| SoftchatError::InvalidEventTag)?;
230 Ok(Self { inner })
231 }
232
233 #[must_use]
235 pub fn values(&self) -> &[String] {
236 self.inner.as_slice()
237 }
238
239 fn into_inner(self) -> Tag {
240 self.inner
241 }
242}
243
244impl fmt::Debug for NostrTag {
245 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246 formatter
247 .debug_tuple("NostrTag")
248 .field(&self.values())
249 .finish()
250 }
251}
252
253#[derive(Clone, Debug, Eq, PartialEq)]
259pub struct NostrEventDraft {
260 created_at: u64,
261 kind: NostrEventKind,
262 tags: Vec<NostrTag>,
263 content: String,
264}
265
266impl NostrEventDraft {
267 pub fn new(
273 created_at: u64,
274 kind: NostrEventKind,
275 tags: Vec<NostrTag>,
276 content: impl Into<String>,
277 ) -> Result<Self, SoftchatError> {
278 validate_timestamp(created_at)?;
279 let draft = Self {
280 created_at,
281 kind,
282 tags,
283 content: content.into(),
284 };
285 check_draft_size(&draft)?;
286 Ok(draft)
287 }
288
289 #[must_use]
291 pub const fn created_at(&self) -> u64 {
292 self.created_at
293 }
294
295 #[must_use]
297 pub const fn kind(&self) -> NostrEventKind {
298 self.kind
299 }
300
301 pub fn tags(&self) -> impl ExactSizeIterator<Item = &[String]> {
303 self.tags.iter().map(NostrTag::values)
304 }
305
306 #[must_use]
308 pub fn content(&self) -> &str {
309 &self.content
310 }
311
312 pub(crate) fn sign_with_keys(self, keys: &Keys) -> Result<SignedNostrEvent, SoftchatError> {
313 let unsigned = UnsignedEvent::new(
314 keys.public_key(),
315 Timestamp::from(self.created_at),
316 self.kind.as_inner(),
317 self.tags.into_iter().map(NostrTag::into_inner),
318 self.content,
319 );
320 let event = unsigned
321 .sign_with_keys(keys)
322 .map_err(|_| SoftchatError::EventSigningFailed)?;
323 SignedNostrEvent::from_verified_inner(event)
324 }
325}
326
327#[derive(Clone, Eq, PartialEq)]
333pub struct NostrRumor {
334 id: NostrEventId,
335 inner: UnsignedEvent,
336}
337
338impl NostrRumor {
339 pub fn new(public_key: NostrPublicKey, draft: NostrEventDraft) -> Result<Self, SoftchatError> {
346 let mut inner = UnsignedEvent::new(
347 *public_key.as_inner(),
348 Timestamp::from(draft.created_at),
349 draft.kind.as_inner(),
350 draft.tags.into_iter().map(NostrTag::into_inner),
351 draft.content,
352 );
353 let id = NostrEventId::from_inner(inner.id());
354 let rumor = Self { id, inner };
355 check_json_size(&rumor.to_json()?)?;
356 Ok(rumor)
357 }
358
359 pub fn from_json(json: &str) -> Result<Self, SoftchatError> {
368 check_json_size(json)?;
369 let wire: WireRumor =
370 serde_json::from_str(json).map_err(|_| SoftchatError::InvalidEventJson)?;
371 if !is_lower_hex_of_length(&wire.id, 64) {
372 return Err(SoftchatError::InvalidEventId);
373 }
374 if !is_lower_hex_of_length(&wire.pubkey, 64) {
375 return Err(SoftchatError::InvalidPublicKey);
376 }
377
378 let declared_id = NostrEventId::from_hex(&wire.id)?;
379 let tags = wire
380 .tags
381 .into_iter()
382 .map(NostrTag::new)
383 .collect::<Result<Vec<_>, _>>()?;
384 let draft = NostrEventDraft::new(
385 wire.created_at,
386 NostrEventKind::try_from_u32(wire.kind)?,
387 tags,
388 wire.content,
389 )?;
390 let rumor = Self::new(NostrPublicKey::from_hex(&wire.pubkey)?, draft)?;
391 if rumor.id != declared_id {
392 return Err(SoftchatError::InvalidEventId);
393 }
394 Ok(rumor)
395 }
396
397 pub fn to_json(&self) -> Result<String, SoftchatError> {
403 serde_json::to_string(&self.inner).map_err(|_| SoftchatError::InvalidEventJson)
404 }
405
406 #[must_use]
408 pub const fn id(&self) -> NostrEventId {
409 self.id
410 }
411
412 #[must_use]
414 pub fn public_key(&self) -> NostrPublicKey {
415 NostrPublicKey::from_inner(self.inner.pubkey)
416 }
417
418 #[must_use]
420 pub const fn created_at(&self) -> u64 {
421 self.inner.created_at.as_secs()
422 }
423
424 #[must_use]
426 pub fn kind(&self) -> NostrEventKind {
427 NostrEventKind::from_u16(self.inner.kind.as_u16())
428 }
429
430 pub fn tags(&self) -> impl ExactSizeIterator<Item = &[String]> {
432 self.inner.tags.iter().map(Tag::as_slice)
433 }
434
435 #[must_use]
437 pub fn content(&self) -> &str {
438 &self.inner.content
439 }
440}
441
442impl fmt::Debug for NostrRumor {
443 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
444 formatter
445 .debug_struct("NostrRumor")
446 .field("id", &self.id())
447 .field("public_key", &self.public_key())
448 .field("created_at", &self.created_at())
449 .field("kind", &self.kind())
450 .field("tags", &self.tags().collect::<Vec<_>>())
451 .field("content", &self.content())
452 .finish()
453 }
454}
455
456#[derive(Clone, Eq, PartialEq)]
458pub struct SignedNostrEvent {
459 inner: Event,
460}
461
462impl SignedNostrEvent {
463 pub fn from_json(json: &str) -> Result<Self, SoftchatError> {
473 let result = Self::from_json_inner(json);
474 if result.is_ok() {
475 diagnostics::record_success(Operation::VerifyEvent, json.len(), json.len());
476 } else {
477 diagnostics::record_rejection(json.len());
478 }
479 result
480 }
481
482 fn from_json_inner(json: &str) -> Result<Self, SoftchatError> {
483 check_json_size(json)?;
484 let wire: WireSignedEvent =
485 serde_json::from_str(json).map_err(|_| SoftchatError::InvalidEventJson)?;
486
487 if !is_lower_hex_of_length(&wire.id, 64) {
488 return Err(SoftchatError::InvalidEventId);
489 }
490 if !is_lower_hex_of_length(&wire.pubkey, 64) {
491 return Err(SoftchatError::InvalidPublicKey);
492 }
493 if !is_lower_hex_of_length(&wire.sig, 128) {
494 return Err(SoftchatError::InvalidEventSignature);
495 }
496
497 let id = NostrEventId::from_hex(&wire.id)?;
498 let public_key = NostrPublicKey::from_hex(&wire.pubkey)?;
499 let kind = NostrEventKind::try_from_u32(wire.kind)?;
500 let signature = NostrEventSignature::from_hex(&wire.sig)?;
501 let tags = wire
502 .tags
503 .into_iter()
504 .map(NostrTag::new)
505 .collect::<Result<Vec<_>, _>>()?;
506
507 Self::new(
508 id,
509 public_key,
510 wire.created_at,
511 kind,
512 tags,
513 wire.content,
514 signature,
515 )
516 }
517
518 #[allow(clippy::too_many_arguments)]
524 pub fn new(
525 id: NostrEventId,
526 public_key: NostrPublicKey,
527 created_at: u64,
528 kind: NostrEventKind,
529 tags: Vec<NostrTag>,
530 content: String,
531 signature: NostrEventSignature,
532 ) -> Result<Self, SoftchatError> {
533 validate_timestamp(created_at)?;
534
535 let event = Event::new(
536 *id.as_inner(),
537 *public_key.as_inner(),
538 Timestamp::from(created_at),
539 kind.as_inner(),
540 tags.into_iter().map(NostrTag::into_inner),
541 content,
542 *signature.as_inner(),
543 );
544
545 Self::from_verified_inner(event)
546 }
547
548 pub fn to_json(&self) -> Result<String, SoftchatError> {
554 serde_json::to_string(&self.inner).map_err(|_| SoftchatError::InvalidEventJson)
555 }
556
557 #[must_use]
559 pub const fn id(&self) -> NostrEventId {
560 NostrEventId::from_inner(self.inner.id)
561 }
562
563 #[must_use]
565 pub fn public_key(&self) -> NostrPublicKey {
566 NostrPublicKey::from_inner(self.inner.pubkey)
567 }
568
569 #[must_use]
571 pub const fn created_at(&self) -> u64 {
572 self.inner.created_at.as_secs()
573 }
574
575 #[must_use]
577 pub fn kind(&self) -> NostrEventKind {
578 NostrEventKind::from_u16(self.inner.kind.as_u16())
579 }
580
581 pub fn tags(&self) -> impl ExactSizeIterator<Item = &[String]> {
583 self.inner.tags.iter().map(Tag::as_slice)
584 }
585
586 #[must_use]
588 pub fn content(&self) -> &str {
589 &self.inner.content
590 }
591
592 #[must_use]
594 pub const fn signature(&self) -> NostrEventSignature {
595 NostrEventSignature::from_inner(self.inner.sig)
596 }
597
598 fn from_verified_inner(event: Event) -> Result<Self, SoftchatError> {
599 match event.verify() {
600 Ok(()) => {}
601 Err(NostrEventError::InvalidId) => return Err(SoftchatError::InvalidEventId),
602 Err(NostrEventError::InvalidSignature) => {
603 return Err(SoftchatError::InvalidEventSignature);
604 }
605 Err(_) => return Err(SoftchatError::InvalidEventJson),
606 }
607
608 let event = Self { inner: event };
609 let canonical = event.to_json()?;
610 check_json_size(&canonical)?;
611 Ok(event)
612 }
613}
614
615impl fmt::Debug for SignedNostrEvent {
616 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
617 formatter
618 .debug_struct("SignedNostrEvent")
619 .field("id", &self.id())
620 .field("public_key", &self.public_key())
621 .field("created_at", &self.created_at())
622 .field("kind", &self.kind())
623 .field("tags", &self.tags().collect::<Vec<_>>())
624 .field("content", &self.content())
625 .field("signature", &self.signature())
626 .finish()
627 }
628}
629
630#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
632#[serde(rename_all = "camelCase", deny_unknown_fields)]
633#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
634pub struct SignedEvent {
635 pub id: String,
637 pub public_key: String,
639 pub created_at: i64,
641 pub kind: i32,
643 pub tags: Vec<Vec<String>>,
645 pub content: String,
647 pub signature: String,
649}
650
651#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
653#[serde(rename_all = "camelCase", deny_unknown_fields)]
654#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
655pub struct EventDraft {
656 pub created_at: i64,
658 pub kind: i32,
660 pub tags: Vec<Vec<String>>,
662 pub content: String,
664}
665
666#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
668#[serde(rename_all = "camelCase", deny_unknown_fields)]
669#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
670pub struct RumorEvent {
671 pub id: String,
673 pub public_key: String,
675 pub created_at: i64,
677 pub kind: i32,
679 pub tags: Vec<Vec<String>>,
681 pub content: String,
683}
684
685impl TryFrom<EventDraft> for NostrEventDraft {
686 type Error = SoftchatError;
687
688 fn try_from(draft: EventDraft) -> Result<Self, Self::Error> {
689 let created_at =
690 u64::try_from(draft.created_at).map_err(|_| SoftchatError::InvalidEventTimestamp)?;
691 let raw_kind = u32::try_from(draft.kind).map_err(|_| SoftchatError::InvalidEventKind)?;
692 let tags = draft
693 .tags
694 .into_iter()
695 .map(NostrTag::new)
696 .collect::<Result<Vec<_>, _>>()?;
697 Self::new(
698 created_at,
699 NostrEventKind::try_from_u32(raw_kind)?,
700 tags,
701 draft.content,
702 )
703 }
704}
705
706impl From<&NostrEventDraft> for EventDraft {
707 fn from(draft: &NostrEventDraft) -> Self {
708 Self {
709 created_at: draft.created_at() as i64,
710 kind: i32::from(draft.kind().as_u16()),
711 tags: draft.tags().map(<[String]>::to_vec).collect(),
712 content: draft.content().to_owned(),
713 }
714 }
715}
716
717impl TryFrom<RumorEvent> for NostrRumor {
718 type Error = SoftchatError;
719
720 fn try_from(rumor: RumorEvent) -> Result<Self, Self::Error> {
721 let created_at =
722 u64::try_from(rumor.created_at).map_err(|_| SoftchatError::InvalidEventTimestamp)?;
723 let raw_kind = u32::try_from(rumor.kind).map_err(|_| SoftchatError::InvalidEventKind)?;
724 let tags = rumor
725 .tags
726 .into_iter()
727 .map(NostrTag::new)
728 .collect::<Result<Vec<_>, _>>()?;
729 let draft = NostrEventDraft::new(
730 created_at,
731 NostrEventKind::try_from_u32(raw_kind)?,
732 tags,
733 rumor.content,
734 )?;
735 let validated = Self::new(NostrPublicKey::from_hex(&rumor.public_key)?, draft)?;
736 if validated.id().to_hex() != rumor.id {
737 return Err(SoftchatError::InvalidEventId);
738 }
739 Ok(validated)
740 }
741}
742
743impl From<&NostrRumor> for RumorEvent {
744 fn from(rumor: &NostrRumor) -> Self {
745 Self {
746 id: rumor.id().to_hex(),
747 public_key: rumor.public_key().to_hex(),
748 created_at: rumor.created_at() as i64,
749 kind: i32::from(rumor.kind().as_u16()),
750 tags: rumor.tags().map(<[String]>::to_vec).collect(),
751 content: rumor.content().to_owned(),
752 }
753 }
754}
755
756impl TryFrom<SignedEvent> for SignedNostrEvent {
757 type Error = SoftchatError;
758
759 fn try_from(event: SignedEvent) -> Result<Self, Self::Error> {
760 if !is_lower_hex_of_length(&event.id, 64) {
761 return Err(SoftchatError::InvalidEventId);
762 }
763 if !is_lower_hex_of_length(&event.public_key, 64) {
764 return Err(SoftchatError::InvalidPublicKey);
765 }
766 if !is_lower_hex_of_length(&event.signature, 128) {
767 return Err(SoftchatError::InvalidEventSignature);
768 }
769
770 let created_at =
771 u64::try_from(event.created_at).map_err(|_| SoftchatError::InvalidEventTimestamp)?;
772 let raw_kind = u32::try_from(event.kind).map_err(|_| SoftchatError::InvalidEventKind)?;
773 let tags = event
774 .tags
775 .into_iter()
776 .map(NostrTag::new)
777 .collect::<Result<Vec<_>, _>>()?;
778
779 Self::new(
780 NostrEventId::from_hex(&event.id)?,
781 NostrPublicKey::from_hex(&event.public_key)?,
782 created_at,
783 NostrEventKind::try_from_u32(raw_kind)?,
784 tags,
785 event.content,
786 NostrEventSignature::from_hex(&event.signature)?,
787 )
788 }
789}
790
791impl From<&SignedNostrEvent> for SignedEvent {
792 fn from(event: &SignedNostrEvent) -> Self {
793 Self {
794 id: event.id().to_hex(),
795 public_key: event.public_key().to_hex(),
796 created_at: event.created_at() as i64,
797 kind: i32::from(event.kind().as_u16()),
798 tags: event.tags().map(<[String]>::to_vec).collect(),
799 content: event.content().to_owned(),
800 signature: event.signature().to_hex(),
801 }
802 }
803}
804
805#[derive(Debug, Deserialize)]
806struct WireSignedEvent {
807 id: String,
808 pubkey: String,
809 created_at: u64,
810 kind: u32,
811 tags: Vec<Vec<String>>,
812 content: String,
813 sig: String,
814}
815
816#[derive(Debug, Deserialize)]
817#[serde(deny_unknown_fields)]
818struct WireRumor {
819 id: String,
820 pubkey: String,
821 created_at: u64,
822 kind: u32,
823 tags: Vec<Vec<String>>,
824 content: String,
825}
826
827#[cfg_attr(feature = "native-bindings", uniffi::export)]
829pub fn validate_nostr_event_id(event_id_hex: String) -> Result<String, SoftchatError> {
830 NostrEventId::from_hex(&event_id_hex).map(NostrEventId::to_hex)
831}
832
833#[cfg_attr(feature = "native-bindings", uniffi::export)]
835pub fn validate_nostr_event_signature(signature_hex: String) -> Result<String, SoftchatError> {
836 NostrEventSignature::from_hex(&signature_hex).map(NostrEventSignature::to_hex)
837}
838
839#[cfg_attr(feature = "native-bindings", uniffi::export)]
841pub fn validate_nostr_event_kind(kind: i32) -> Result<i32, SoftchatError> {
842 let raw = u32::try_from(kind).map_err(|_| SoftchatError::InvalidEventKind)?;
843 Ok(i32::from(NostrEventKind::try_from_u32(raw)?.as_u16()))
844}
845
846#[cfg_attr(feature = "native-bindings", uniffi::export)]
848pub fn validate_nostr_tag(values: Vec<String>) -> Result<Vec<String>, SoftchatError> {
849 NostrTag::new(values).map(|tag| tag.values().to_vec())
850}
851
852#[cfg_attr(feature = "native-bindings", uniffi::export)]
854pub fn validate_nostr_event_draft(draft: EventDraft) -> Result<EventDraft, SoftchatError> {
855 NostrEventDraft::try_from(draft).map(|draft| EventDraft::from(&draft))
856}
857
858#[cfg_attr(feature = "native-bindings", uniffi::export)]
860pub fn parse_nostr_rumor_json(json: String) -> Result<RumorEvent, SoftchatError> {
861 NostrRumor::from_json(&json).map(|rumor| RumorEvent::from(&rumor))
862}
863
864#[cfg_attr(feature = "native-bindings", uniffi::export)]
866pub fn validate_nostr_rumor(rumor: RumorEvent) -> Result<RumorEvent, SoftchatError> {
867 NostrRumor::try_from(rumor).map(|rumor| RumorEvent::from(&rumor))
868}
869
870#[cfg_attr(feature = "native-bindings", uniffi::export)]
872pub fn nostr_rumor_json(rumor: RumorEvent) -> Result<String, SoftchatError> {
873 NostrRumor::try_from(rumor)?.to_json()
874}
875
876#[cfg_attr(feature = "native-bindings", uniffi::export)]
878pub fn parse_signed_nostr_event_json(json: String) -> Result<SignedEvent, SoftchatError> {
879 SignedNostrEvent::from_json(&json).map(|event| SignedEvent::from(&event))
880}
881
882#[cfg_attr(feature = "native-bindings", uniffi::export)]
884pub fn validate_signed_nostr_event(event: SignedEvent) -> Result<SignedEvent, SoftchatError> {
885 SignedNostrEvent::try_from(event).map(|event| SignedEvent::from(&event))
886}
887
888#[cfg_attr(feature = "native-bindings", uniffi::export)]
890pub fn signed_nostr_event_json(event: SignedEvent) -> Result<String, SoftchatError> {
891 SignedNostrEvent::try_from(event)?.to_json()
892}
893
894fn check_json_size(json: &str) -> Result<(), SoftchatError> {
895 if json.len() > MAX_NOSTR_EVENT_JSON_BYTES {
896 return Err(SoftchatError::EventTooLarge);
897 }
898 Ok(())
899}
900
901fn check_draft_size(draft: &NostrEventDraft) -> Result<(), SoftchatError> {
902 #[derive(Serialize)]
903 struct SizeProbe<'a> {
904 id: &'static str,
905 pubkey: &'static str,
906 created_at: u64,
907 kind: u16,
908 tags: Vec<&'a [String]>,
909 content: &'a str,
910 sig: &'static str,
911 }
912
913 const EVENT_ID_PLACEHOLDER: &str =
914 "0000000000000000000000000000000000000000000000000000000000000000";
915 const SIGNATURE_PLACEHOLDER: &str = concat!(
916 "0000000000000000000000000000000000000000000000000000000000000000",
917 "0000000000000000000000000000000000000000000000000000000000000000"
918 );
919
920 let probe = SizeProbe {
921 id: EVENT_ID_PLACEHOLDER,
922 pubkey: EVENT_ID_PLACEHOLDER,
923 created_at: draft.created_at,
924 kind: draft.kind.as_u16(),
925 tags: draft.tags().collect(),
926 content: draft.content(),
927 sig: SIGNATURE_PLACEHOLDER,
928 };
929 let json = serde_json::to_string(&probe).map_err(|_| SoftchatError::InvalidEventJson)?;
930 check_json_size(&json)
931}
932
933fn validate_timestamp(created_at: u64) -> Result<(), SoftchatError> {
934 if created_at > MAX_PORTABLE_TIMESTAMP_SECONDS {
935 return Err(SoftchatError::InvalidEventTimestamp);
936 }
937 Ok(())
938}
939
940fn is_hex_of_length(value: &str, expected: usize) -> bool {
941 value.len() == expected && value.bytes().all(|byte| byte.is_ascii_hexdigit())
942}
943
944fn is_lower_hex_of_length(value: &str, expected: usize) -> bool {
945 value.len() == expected
946 && value
947 .bytes()
948 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
949}
950
951#[cfg(test)]
952mod tests {
953 use super::*;
954 use crate::LocalIdentity;
955
956 const VALID_EVENT: &str =
957 include_str!("../../../fixtures/nostr/nip01/signed-event-with-empty-tag-fields.json");
958 const ALICE_SECRET: &str = "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a";
959 const BOB_PUBLIC_KEY: &str = "4ddeb9109a8cd29ba279a637f5ec344f2479ee07df1f4043f3fe26d8948cfef9";
960
961 #[test]
962 fn parses_verifies_and_preserves_exact_raw_tags() -> Result<(), SoftchatError> {
963 let event = SignedNostrEvent::from_json(VALID_EVENT.trim())?;
964
965 assert_eq!(
966 event.id().to_hex(),
967 "f55c30722f056e330d8a7a6a9ba1522f7522c0f1ced1c93d78ea833c78a3d6ec"
968 );
969 assert_eq!(event.kind().as_u16(), 3);
970 assert_eq!(
971 event.tags().next(),
972 Some(
973 [
974 "p",
975 "4ddeb9109a8cd29ba279a637f5ec344f2479ee07df1f4043f3fe26d8948cfef9",
976 "",
977 "",
978 ]
979 .map(String::from)
980 .as_slice()
981 )
982 );
983
984 let canonical = event.to_json()?;
985 let reparsed = SignedNostrEvent::from_json(&canonical)?;
986 assert_eq!(reparsed, event);
987 assert_eq!(canonical, VALID_EVENT.trim());
988 Ok(())
989 }
990
991 #[test]
992 fn reports_id_before_signature_tampering() {
993 let tampered = VALID_EVENT.replace("\"content\":\"\"", "\"content\":\"changed\"");
994 assert!(matches!(
995 SignedNostrEvent::from_json(&tampered),
996 Err(SoftchatError::InvalidEventId)
997 ));
998 }
999
1000 #[test]
1001 fn rejects_invalid_signature_after_valid_id() {
1002 let tampered = VALID_EVENT.replacen("\"sig\":\"5", "\"sig\":\"4", 1);
1003 assert!(matches!(
1004 SignedNostrEvent::from_json(&tampered),
1005 Err(SoftchatError::InvalidEventSignature)
1006 ));
1007 }
1008
1009 #[test]
1010 fn rejects_noncanonical_wire_hex_and_invalid_shapes() {
1011 let uppercase_id = VALID_EVENT.replacen("\"id\":\"f", "\"id\":\"F", 1);
1012 assert!(matches!(
1013 SignedNostrEvent::from_json(&uppercase_id),
1014 Err(SoftchatError::InvalidEventId)
1015 ));
1016
1017 let empty_tag = VALID_EVENT.replacen(r#"[["p","4ddeb"#, r#"[[] ,["p","4ddeb"#, 1);
1018 assert!(matches!(
1019 SignedNostrEvent::from_json(&empty_tag),
1020 Err(SoftchatError::InvalidEventTag)
1021 ));
1022
1023 let invalid_kind = VALID_EVENT.replacen("\"kind\":3", "\"kind\":65536", 1);
1024 assert!(matches!(
1025 SignedNostrEvent::from_json(&invalid_kind),
1026 Err(SoftchatError::InvalidEventKind)
1027 ));
1028 }
1029
1030 #[test]
1031 fn rejects_unportable_timestamps_and_oversized_json() {
1032 let invalid_timestamp = VALID_EVENT.replacen(
1033 "\"created_at\":1698412975",
1034 "\"created_at\":9007199254740992",
1035 1,
1036 );
1037 assert!(matches!(
1038 SignedNostrEvent::from_json(&invalid_timestamp),
1039 Err(SoftchatError::InvalidEventTimestamp)
1040 ));
1041
1042 let oversized = " ".repeat(MAX_NOSTR_EVENT_JSON_BYTES + 1);
1043 assert!(matches!(
1044 SignedNostrEvent::from_json(&oversized),
1045 Err(SoftchatError::EventTooLarge)
1046 ));
1047 }
1048
1049 #[test]
1050 fn scalar_binding_validators_canonicalize_and_reject() -> Result<(), SoftchatError> {
1051 let id = validate_nostr_event_id("AB".repeat(32))?;
1052 assert_eq!(id, "ab".repeat(32));
1053 assert!(matches!(
1054 validate_nostr_event_kind(-1),
1055 Err(SoftchatError::InvalidEventKind)
1056 ));
1057 assert!(matches!(
1058 validate_nostr_tag(Vec::new()),
1059 Err(SoftchatError::InvalidEventTag)
1060 ));
1061
1062 let mut decoded_wire = parse_signed_nostr_event_json(VALID_EVENT.trim().to_owned())?;
1063 decoded_wire.id.make_ascii_uppercase();
1064 assert!(matches!(
1065 validate_signed_nostr_event(decoded_wire),
1066 Err(SoftchatError::InvalidEventId)
1067 ));
1068 Ok(())
1069 }
1070
1071 #[test]
1072 fn signs_explicit_drafts_without_normalizing_tags() -> Result<(), SoftchatError> {
1073 let identity = LocalIdentity::from_secret_hex(ALICE_SECRET)?;
1074 let draft = NostrEventDraft::new(
1075 1_700_000_000,
1076 NostrEventKind::SHORT_TEXT_NOTE,
1077 vec![NostrTag::new([
1078 "p".to_owned(),
1079 BOB_PUBLIC_KEY.to_owned(),
1080 String::new(),
1081 String::new(),
1082 ])?],
1083 "A signed protocol event",
1084 )?;
1085
1086 let signed = identity.sign_event(draft)?;
1087 assert_eq!(signed.public_key(), identity.public_key());
1088 assert_eq!(signed.created_at(), 1_700_000_000);
1089 assert_eq!(signed.kind(), NostrEventKind::SHORT_TEXT_NOTE);
1090 assert_eq!(
1091 signed.tags().next(),
1092 Some(["p", BOB_PUBLIC_KEY, "", ""].map(String::from).as_slice())
1093 );
1094
1095 let reparsed = SignedNostrEvent::from_json(&signed.to_json()?)?;
1096 assert_eq!(reparsed, signed);
1097 Ok(())
1098 }
1099
1100 #[test]
1101 fn validates_drafts_before_signing() {
1102 assert!(matches!(
1103 NostrEventDraft::new(
1104 MAX_PORTABLE_TIMESTAMP_SECONDS + 1,
1105 NostrEventKind::SHORT_TEXT_NOTE,
1106 Vec::new(),
1107 "invalid",
1108 ),
1109 Err(SoftchatError::InvalidEventTimestamp)
1110 ));
1111 assert!(matches!(
1112 NostrEventDraft::new(
1113 1_700_000_000,
1114 NostrEventKind::SHORT_TEXT_NOTE,
1115 Vec::new(),
1116 "x".repeat(MAX_NOSTR_EVENT_JSON_BYTES),
1117 ),
1118 Err(SoftchatError::EventTooLarge)
1119 ));
1120 }
1121}