1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5
6use serde::de::{MapAccess, Visitor};
7use serde::{Deserialize, Deserializer, Serialize};
8use serde_json::Value;
9use url::Url;
10
11use crate::event::SignedEvent;
12use crate::{
13 LocalIdentity, NostrEventDraft, NostrEventId, NostrEventKind, NostrPublicKey, NostrTag,
14 SignedNostrEvent, SoftchatError,
15};
16
17pub const MAX_RELAY_FRAME_BYTES: usize = 512 * 1024;
19pub const MAX_RELAY_BATCH_EVENTS: usize = 256;
21pub const MAX_SUBSCRIPTION_ID_CHARS: usize = 64;
23pub const MAX_RELAY_FILTERS: usize = 64;
25pub const MAX_RELAY_FILTER_VALUES: usize = 1_024;
27pub const MAX_RELAY_MESSAGE_CHARS: usize = 4_096;
29pub const NIP42_AUTH_WINDOW_SECONDS: u64 = 600;
31
32#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
34pub struct RelayFilter {
35 #[serde(skip_serializing_if = "Vec::is_empty")]
37 pub ids: Vec<String>,
38 #[serde(skip_serializing_if = "Vec::is_empty")]
40 pub authors: Vec<String>,
41 #[serde(skip_serializing_if = "Vec::is_empty")]
43 pub kinds: Vec<u16>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub since: Option<u64>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub until: Option<u64>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub limit: Option<u32>,
53 #[serde(flatten)]
55 pub generic_tags: BTreeMap<String, Vec<String>>,
56}
57
58impl RelayFilter {
59 pub fn from_json(json: &str) -> Result<Self, SoftchatError> {
65 if json.len() > MAX_RELAY_FRAME_BYTES {
66 return Err(SoftchatError::InvalidRelayFilter);
67 }
68 let mut deserializer = serde_json::Deserializer::from_str(json);
69 let filter =
70 Self::deserialize(&mut deserializer).map_err(|_| SoftchatError::InvalidRelayFilter)?;
71 deserializer
72 .end()
73 .map_err(|_| SoftchatError::InvalidRelayFilter)?;
74 filter.validate()
75 }
76
77 pub fn to_json(&self) -> Result<String, SoftchatError> {
83 self.clone().validate()?;
84 serde_json::to_string(self).map_err(|_| SoftchatError::InvalidRelayFilter)
85 }
86
87 fn validate(self) -> Result<Self, SoftchatError> {
88 validate_prefixes(&self.ids)?;
89 validate_prefixes(&self.authors)?;
90 validate_value_count(self.kinds.len())?;
91 if self
92 .since
93 .zip(self.until)
94 .is_some_and(|(since, until)| since > until)
95 {
96 return Err(SoftchatError::InvalidRelayFilter);
97 }
98 if self.limit == Some(0) {
99 return Err(SoftchatError::InvalidRelayFilter);
100 }
101 for (key, values) in &self.generic_tags {
102 if !is_generic_tag_key(key) || values.is_empty() {
103 return Err(SoftchatError::InvalidRelayFilter);
104 }
105 validate_value_count(values.len())?;
106 if values
107 .iter()
108 .any(|value| value.len() > MAX_RELAY_FRAME_BYTES)
109 {
110 return Err(SoftchatError::InvalidRelayFilter);
111 }
112 }
113 Ok(self)
114 }
115}
116
117impl<'de> Deserialize<'de> for RelayFilter {
118 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
119 where
120 D: Deserializer<'de>,
121 {
122 struct FilterVisitor;
123
124 impl<'de> Visitor<'de> for FilterVisitor {
125 type Value = RelayFilter;
126
127 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128 formatter.write_str("a bounded NIP-01 filter object")
129 }
130
131 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
132 where
133 A: MapAccess<'de>,
134 {
135 let mut filter = RelayFilter::default();
136 let mut seen = BTreeSet::new();
137 while let Some(key) = map.next_key::<String>()? {
138 if !seen.insert(key.clone()) {
139 return Err(serde::de::Error::custom("duplicate filter field"));
140 }
141 match key.as_str() {
142 "ids" => filter.ids = map.next_value()?,
143 "authors" => filter.authors = map.next_value()?,
144 "kinds" => filter.kinds = map.next_value()?,
145 "since" => filter.since = Some(map.next_value()?),
146 "until" => filter.until = Some(map.next_value()?),
147 "limit" => filter.limit = Some(map.next_value()?),
148 _ if is_generic_tag_key(&key) => {
149 filter.generic_tags.insert(key, map.next_value()?);
150 }
151 _ => return Err(serde::de::Error::custom("unknown filter field")),
152 }
153 }
154 filter.validate().map_err(serde::de::Error::custom)
155 }
156 }
157
158 deserializer.deserialize_map(FilterVisitor)
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq)]
164pub enum ClientRelayFrame {
165 Event(SignedNostrEvent),
167 Events(Vec<SignedNostrEvent>),
169 Req {
171 subscription_id: String,
173 filters: Vec<RelayFilter>,
175 },
176 Close {
178 subscription_id: String,
180 },
181 Count {
183 subscription_id: String,
185 filters: Vec<RelayFilter>,
187 },
188 Auth(SignedNostrEvent),
190}
191
192impl ClientRelayFrame {
193 pub fn to_json(&self) -> Result<String, SoftchatError> {
199 let mut values = Vec::new();
200 match self {
201 Self::Event(event) => {
202 values.push(Value::String("EVENT".to_owned()));
203 values.push(event_value(event)?);
204 }
205 Self::Events(events) => {
206 validate_batch_events(events)?;
207 values.push(Value::String("EVENTS".to_owned()));
208 for event in events {
209 values.push(event_value(event)?);
210 }
211 }
212 Self::Req {
213 subscription_id,
214 filters,
215 } => {
216 validate_subscription_id(subscription_id)?;
217 validate_filters(filters)?;
218 values.push(Value::String("REQ".to_owned()));
219 values.push(Value::String(subscription_id.clone()));
220 append_filter_values(&mut values, filters)?;
221 }
222 Self::Close { subscription_id } => {
223 validate_subscription_id(subscription_id)?;
224 values.push(Value::String("CLOSE".to_owned()));
225 values.push(Value::String(subscription_id.clone()));
226 }
227 Self::Count {
228 subscription_id,
229 filters,
230 } => {
231 validate_subscription_id(subscription_id)?;
232 validate_filters(filters)?;
233 values.push(Value::String("COUNT".to_owned()));
234 values.push(Value::String(subscription_id.clone()));
235 append_filter_values(&mut values, filters)?;
236 }
237 Self::Auth(event) => {
238 if event.kind() != NostrEventKind::CLIENT_AUTHENTICATION {
239 return Err(SoftchatError::InvalidRelayAuthentication);
240 }
241 values.push(Value::String("AUTH".to_owned()));
242 values.push(event_value(event)?);
243 }
244 }
245 encode_frame(values)
246 }
247}
248
249#[derive(Clone, Debug, Eq, PartialEq)]
251pub enum RelayResponseFrame {
252 Event {
254 subscription_id: String,
256 event: SignedNostrEvent,
258 },
259 Ok(BatchAcknowledgement),
261 Eose {
263 subscription_id: String,
265 },
266 Closed {
268 subscription_id: String,
270 message: String,
272 },
273 Notice(String),
275 Auth(String),
277 Count {
279 subscription_id: String,
281 count: u64,
283 },
284}
285
286impl RelayResponseFrame {
287 pub fn to_json(&self) -> Result<String, SoftchatError> {
293 let values = match self {
294 Self::Event {
295 subscription_id,
296 event,
297 } => {
298 validate_subscription_id(subscription_id)?;
299 vec![
300 Value::String("EVENT".to_owned()),
301 Value::String(subscription_id.clone()),
302 event_value(event)?,
303 ]
304 }
305 Self::Ok(acknowledgement) => {
306 acknowledgement.validate()?;
307 vec![
308 Value::String("OK".to_owned()),
309 Value::String(acknowledgement.event_id.clone()),
310 Value::Bool(acknowledgement.accepted),
311 Value::String(acknowledgement.message.clone()),
312 ]
313 }
314 Self::Eose { subscription_id } => {
315 validate_subscription_id(subscription_id)?;
316 vec![
317 Value::String("EOSE".to_owned()),
318 Value::String(subscription_id.clone()),
319 ]
320 }
321 Self::Closed {
322 subscription_id,
323 message,
324 } => {
325 validate_subscription_id(subscription_id)?;
326 validate_relay_text(message)?;
327 vec![
328 Value::String("CLOSED".to_owned()),
329 Value::String(subscription_id.clone()),
330 Value::String(message.clone()),
331 ]
332 }
333 Self::Notice(message) => {
334 validate_relay_text(message)?;
335 vec![
336 Value::String("NOTICE".to_owned()),
337 Value::String(message.clone()),
338 ]
339 }
340 Self::Auth(challenge) => {
341 validate_relay_text(challenge)?;
342 if challenge.is_empty() {
343 return Err(SoftchatError::InvalidRelayAuthentication);
344 }
345 vec![
346 Value::String("AUTH".to_owned()),
347 Value::String(challenge.clone()),
348 ]
349 }
350 Self::Count {
351 subscription_id,
352 count,
353 } => {
354 validate_subscription_id(subscription_id)?;
355 vec![
356 Value::String("COUNT".to_owned()),
357 Value::String(subscription_id.clone()),
358 serde_json::json!({ "count": count }),
359 ]
360 }
361 };
362 encode_frame(values)
363 }
364}
365
366#[derive(Clone, Debug, Eq, PartialEq)]
368#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
369pub struct BatchAcknowledgement {
370 pub event_id: String,
372 pub accepted: bool,
374 pub message: String,
376}
377
378impl BatchAcknowledgement {
379 fn validate(&self) -> Result<(), SoftchatError> {
380 NostrEventId::from_hex(&self.event_id).map_err(|_| SoftchatError::InvalidRelayBatch)?;
381 validate_relay_text(&self.message).map_err(|_| SoftchatError::InvalidRelayBatch)
382 }
383}
384
385#[derive(Clone, Copy, Debug, Eq, PartialEq)]
387#[cfg_attr(feature = "native-bindings", derive(uniffi::Enum))]
388pub enum BatchEventState {
389 Pending,
391 Accepted,
393 Rejected,
395}
396
397#[derive(Clone, Debug, Eq, PartialEq)]
399#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
400pub struct BatchProgress {
401 pub event_ids: Vec<String>,
403 pub states: Vec<BatchEventState>,
405 pub messages: Vec<String>,
407 pub complete: bool,
409}
410
411pub fn parse_client_relay_frame(json: &str) -> Result<ClientRelayFrame, SoftchatError> {
418 let values = parse_frame_array(json)?;
419 let command = command(&values)?;
420 match command {
421 "EVENT" if values.len() == 2 => Ok(ClientRelayFrame::Event(parse_event(&values[1])?)),
422 "EVENTS" if values.len() >= 2 => {
423 let events = values[1..]
424 .iter()
425 .map(parse_event)
426 .collect::<Result<Vec<_>, _>>()?;
427 validate_batch_events(&events)?;
428 Ok(ClientRelayFrame::Events(events))
429 }
430 "REQ" | "COUNT" if values.len() >= 3 => {
431 let subscription_id = parse_subscription_id(&values[1])?;
432 let filters = values[2..]
433 .iter()
434 .map(parse_filter_value)
435 .collect::<Result<Vec<_>, _>>()?;
436 validate_filters(&filters)?;
437 if command == "REQ" {
438 Ok(ClientRelayFrame::Req {
439 subscription_id,
440 filters,
441 })
442 } else {
443 Ok(ClientRelayFrame::Count {
444 subscription_id,
445 filters,
446 })
447 }
448 }
449 "CLOSE" if values.len() == 2 => Ok(ClientRelayFrame::Close {
450 subscription_id: parse_subscription_id(&values[1])?,
451 }),
452 "AUTH" if values.len() == 2 => {
453 let event = parse_event(&values[1])?;
454 if event.kind() != NostrEventKind::CLIENT_AUTHENTICATION {
455 return Err(SoftchatError::InvalidRelayAuthentication);
456 }
457 Ok(ClientRelayFrame::Auth(event))
458 }
459 _ => Err(SoftchatError::InvalidRelayFrame),
460 }
461}
462
463pub fn parse_relay_response_frame(json: &str) -> Result<RelayResponseFrame, SoftchatError> {
470 let values = parse_frame_array(json)?;
471 match command(&values)? {
472 "EVENT" if values.len() == 3 => Ok(RelayResponseFrame::Event {
473 subscription_id: parse_subscription_id(&values[1])?,
474 event: parse_event(&values[2])?,
475 }),
476 "OK" if values.len() == 4 => {
477 let acknowledgement = BatchAcknowledgement {
478 event_id: value_string(&values[1])?.to_owned(),
479 accepted: values[2]
480 .as_bool()
481 .ok_or(SoftchatError::InvalidRelayFrame)?,
482 message: value_string(&values[3])?.to_owned(),
483 };
484 acknowledgement
485 .validate()
486 .map_err(|_| SoftchatError::InvalidRelayFrame)?;
487 Ok(RelayResponseFrame::Ok(acknowledgement))
488 }
489 "EOSE" if values.len() == 2 => Ok(RelayResponseFrame::Eose {
490 subscription_id: parse_subscription_id(&values[1])?,
491 }),
492 "CLOSED" if values.len() == 3 => {
493 let message = value_string(&values[2])?.to_owned();
494 validate_relay_text(&message)?;
495 Ok(RelayResponseFrame::Closed {
496 subscription_id: parse_subscription_id(&values[1])?,
497 message,
498 })
499 }
500 "NOTICE" if values.len() == 2 => {
501 let message = value_string(&values[1])?.to_owned();
502 validate_relay_text(&message)?;
503 Ok(RelayResponseFrame::Notice(message))
504 }
505 "AUTH" if values.len() == 2 => {
506 let challenge = value_string(&values[1])?.to_owned();
507 validate_relay_text(&challenge)?;
508 if challenge.is_empty() {
509 return Err(SoftchatError::InvalidRelayAuthentication);
510 }
511 Ok(RelayResponseFrame::Auth(challenge))
512 }
513 "COUNT" if values.len() == 3 => {
514 let count = values[2]
515 .as_object()
516 .filter(|object| object.len() == 1)
517 .and_then(|object| object.get("count"))
518 .and_then(Value::as_u64)
519 .ok_or(SoftchatError::InvalidRelayFrame)?;
520 Ok(RelayResponseFrame::Count {
521 subscription_id: parse_subscription_id(&values[1])?,
522 count,
523 })
524 }
525 _ => Err(SoftchatError::InvalidRelayFrame),
526 }
527}
528
529#[cfg_attr(feature = "native-bindings", uniffi::export)]
536pub fn encode_events_frame(events: Vec<SignedEvent>) -> Result<String, SoftchatError> {
537 let events = events
538 .into_iter()
539 .map(SignedNostrEvent::try_from)
540 .collect::<Result<Vec<_>, _>>()
541 .map_err(|_| SoftchatError::InvalidRelayBatch)?;
542 ClientRelayFrame::Events(events)
543 .to_json()
544 .map_err(|_| SoftchatError::InvalidRelayBatch)
545}
546
547#[cfg_attr(feature = "native-bindings", uniffi::export)]
557pub fn track_batch_acknowledgements(
558 event_ids: Vec<String>,
559 acknowledgements: Vec<BatchAcknowledgement>,
560) -> Result<BatchProgress, SoftchatError> {
561 if event_ids.is_empty() || event_ids.len() > MAX_RELAY_BATCH_EVENTS {
562 return Err(SoftchatError::InvalidRelayBatch);
563 }
564
565 let mut indexes = BTreeMap::new();
566 for (index, event_id) in event_ids.iter().enumerate() {
567 NostrEventId::from_hex(event_id).map_err(|_| SoftchatError::InvalidRelayBatch)?;
568 if indexes.insert(event_id.clone(), index).is_some() {
569 return Err(SoftchatError::InvalidRelayBatch);
570 }
571 }
572
573 let mut states = vec![BatchEventState::Pending; event_ids.len()];
574 let mut messages = vec![String::new(); event_ids.len()];
575 for acknowledgement in acknowledgements {
576 acknowledgement.validate()?;
577 let index = *indexes
578 .get(&acknowledgement.event_id)
579 .ok_or(SoftchatError::InvalidRelayBatch)?;
580 let state = if acknowledgement.accepted {
581 BatchEventState::Accepted
582 } else {
583 BatchEventState::Rejected
584 };
585 match states[index] {
586 BatchEventState::Pending => {
587 states[index] = state;
588 messages[index] = acknowledgement.message;
589 }
590 current if current == state && messages[index] == acknowledgement.message => {}
591 _ => return Err(SoftchatError::InvalidRelayBatch),
592 }
593 }
594
595 let complete = states
596 .iter()
597 .all(|state| *state != BatchEventState::Pending);
598 Ok(BatchProgress {
599 event_ids,
600 states,
601 messages,
602 complete,
603 })
604}
605
606pub fn plan_nip42_authentication(
613 identity: &LocalIdentity,
614 challenge: &str,
615 relay_url: &str,
616 created_at: u64,
617) -> Result<SignedNostrEvent, SoftchatError> {
618 validate_challenge(challenge)?;
619 validate_relay_url(relay_url)?;
620 let tags = vec![
621 NostrTag::new(vec!["relay", relay_url])
622 .map_err(|_| SoftchatError::InvalidRelayAuthentication)?,
623 NostrTag::new(vec!["challenge", challenge])
624 .map_err(|_| SoftchatError::InvalidRelayAuthentication)?,
625 ];
626 let draft = NostrEventDraft::new(created_at, NostrEventKind::CLIENT_AUTHENTICATION, tags, "")
627 .map_err(|_| SoftchatError::InvalidRelayAuthentication)?;
628 identity
629 .sign_event(draft)
630 .map_err(|_| SoftchatError::InvalidRelayAuthentication)
631}
632
633pub fn validate_nip42_authentication(
640 event: &SignedNostrEvent,
641 expected_author: &NostrPublicKey,
642 challenge: &str,
643 relay_url: &str,
644 now: u64,
645) -> Result<(), SoftchatError> {
646 validate_challenge(challenge)?;
647 validate_relay_url(relay_url)?;
648 if event.kind() != NostrEventKind::CLIENT_AUTHENTICATION
649 || event.public_key() != *expected_author
650 || !event.content().is_empty()
651 || event.created_at().abs_diff(now) > NIP42_AUTH_WINDOW_SECONDS
652 {
653 return Err(SoftchatError::InvalidRelayAuthentication);
654 }
655
656 let mut found_relay = false;
657 let mut found_challenge = false;
658 for tag in event.tags() {
659 match tag.first().map(String::as_str) {
660 Some("relay") if !found_relay && tag.len() == 2 && tag[1] == relay_url => {
661 found_relay = true;
662 }
663 Some("challenge") if !found_challenge && tag.len() == 2 && tag[1] == challenge => {
664 found_challenge = true;
665 }
666 Some("relay" | "challenge") => {
667 return Err(SoftchatError::InvalidRelayAuthentication);
668 }
669 _ => {}
670 }
671 }
672 if found_relay && found_challenge {
673 Ok(())
674 } else {
675 Err(SoftchatError::InvalidRelayAuthentication)
676 }
677}
678
679fn parse_frame_array(json: &str) -> Result<Vec<Value>, SoftchatError> {
680 if json.is_empty() || json.len() > MAX_RELAY_FRAME_BYTES {
681 return Err(SoftchatError::InvalidRelayFrame);
682 }
683 let value = crate::json::parse_value(json).map_err(|_| SoftchatError::InvalidRelayFrame)?;
684 value
685 .as_array()
686 .filter(|values| !values.is_empty())
687 .cloned()
688 .ok_or(SoftchatError::InvalidRelayFrame)
689}
690
691fn command(values: &[Value]) -> Result<&str, SoftchatError> {
692 values
693 .first()
694 .and_then(Value::as_str)
695 .ok_or(SoftchatError::InvalidRelayFrame)
696}
697
698fn value_string(value: &Value) -> Result<&str, SoftchatError> {
699 value.as_str().ok_or(SoftchatError::InvalidRelayFrame)
700}
701
702fn parse_event(value: &Value) -> Result<SignedNostrEvent, SoftchatError> {
703 let json = serde_json::to_string(value).map_err(|_| SoftchatError::InvalidRelayFrame)?;
704 SignedNostrEvent::from_json(&json).map_err(|_| SoftchatError::InvalidRelayFrame)
705}
706
707fn event_value(event: &SignedNostrEvent) -> Result<Value, SoftchatError> {
708 serde_json::from_str(&event.to_json()?).map_err(|_| SoftchatError::InvalidRelayFrame)
709}
710
711fn parse_filter_value(value: &Value) -> Result<RelayFilter, SoftchatError> {
712 RelayFilter::from_json(
713 &serde_json::to_string(value).map_err(|_| SoftchatError::InvalidRelayFilter)?,
714 )
715}
716
717fn append_filter_values(
718 values: &mut Vec<Value>,
719 filters: &[RelayFilter],
720) -> Result<(), SoftchatError> {
721 for filter in filters {
722 values.push(
723 serde_json::to_value(filter.clone().validate()?)
724 .map_err(|_| SoftchatError::InvalidRelayFilter)?,
725 );
726 }
727 Ok(())
728}
729
730fn encode_frame(values: Vec<Value>) -> Result<String, SoftchatError> {
731 let json = serde_json::to_string(&values).map_err(|_| SoftchatError::InvalidRelayFrame)?;
732 if json.len() > MAX_RELAY_FRAME_BYTES {
733 return Err(SoftchatError::InvalidRelayFrame);
734 }
735 Ok(json)
736}
737
738fn parse_subscription_id(value: &Value) -> Result<String, SoftchatError> {
739 let subscription_id = value_string(value)?.to_owned();
740 validate_subscription_id(&subscription_id)?;
741 Ok(subscription_id)
742}
743
744fn validate_subscription_id(subscription_id: &str) -> Result<(), SoftchatError> {
745 let length = subscription_id.chars().count();
746 if length == 0 || length > MAX_SUBSCRIPTION_ID_CHARS {
747 return Err(SoftchatError::InvalidRelayFrame);
748 }
749 Ok(())
750}
751
752fn validate_relay_text(value: &str) -> Result<(), SoftchatError> {
753 if value.chars().count() > MAX_RELAY_MESSAGE_CHARS {
754 return Err(SoftchatError::InvalidRelayFrame);
755 }
756 Ok(())
757}
758
759fn validate_challenge(challenge: &str) -> Result<(), SoftchatError> {
760 validate_relay_text(challenge).map_err(|_| SoftchatError::InvalidRelayAuthentication)?;
761 if challenge.is_empty() {
762 return Err(SoftchatError::InvalidRelayAuthentication);
763 }
764 Ok(())
765}
766
767fn validate_relay_url(relay_url: &str) -> Result<(), SoftchatError> {
768 let url = Url::parse(relay_url).map_err(|_| SoftchatError::InvalidRelayAuthentication)?;
769 if !matches!(url.scheme(), "ws" | "wss") || url.host_str().is_none() || url.fragment().is_some()
770 {
771 return Err(SoftchatError::InvalidRelayAuthentication);
772 }
773 Ok(())
774}
775
776fn validate_prefixes(values: &[String]) -> Result<(), SoftchatError> {
777 validate_value_count(values.len())?;
778 for value in values {
779 if value.is_empty()
780 || value.len() > 64
781 || !value
782 .bytes()
783 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
784 {
785 return Err(SoftchatError::InvalidRelayFilter);
786 }
787 }
788 Ok(())
789}
790
791fn validate_value_count(count: usize) -> Result<(), SoftchatError> {
792 if count > MAX_RELAY_FILTER_VALUES {
793 return Err(SoftchatError::InvalidRelayFilter);
794 }
795 Ok(())
796}
797
798fn is_generic_tag_key(key: &str) -> bool {
799 let bytes = key.as_bytes();
800 bytes.len() == 2 && bytes[0] == b'#' && bytes[1].is_ascii_alphabetic()
801}
802
803fn validate_filters(filters: &[RelayFilter]) -> Result<(), SoftchatError> {
804 if filters.is_empty() || filters.len() > MAX_RELAY_FILTERS {
805 return Err(SoftchatError::InvalidRelayFrame);
806 }
807 for filter in filters {
808 filter
809 .clone()
810 .validate()
811 .map_err(|_| SoftchatError::InvalidRelayFrame)?;
812 }
813 Ok(())
814}
815
816fn validate_batch_events(events: &[SignedNostrEvent]) -> Result<(), SoftchatError> {
817 if events.is_empty() || events.len() > MAX_RELAY_BATCH_EVENTS {
818 return Err(SoftchatError::InvalidRelayBatch);
819 }
820 let mut ids = BTreeSet::new();
821 if events.iter().any(|event| !ids.insert(event.id())) {
822 return Err(SoftchatError::InvalidRelayBatch);
823 }
824 Ok(())
825}
826
827#[cfg(test)]
828mod tests {
829 use super::*;
830
831 const ALICE_SECRET: &str = "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a";
832
833 fn event(kind: NostrEventKind, created_at: u64) -> Result<SignedNostrEvent, SoftchatError> {
834 let identity = LocalIdentity::from_secret_hex(ALICE_SECRET)?;
835 identity.sign_event(NostrEventDraft::new(created_at, kind, Vec::new(), "test")?)
836 }
837
838 #[test]
839 fn filters_round_trip_generic_tags_and_reject_duplicate_fields() -> Result<(), SoftchatError> {
840 let filter = RelayFilter::from_json(
841 r##"{"ids":["ab"],"authors":["cd"],"kinds":[14],"since":1,"until":2,"limit":3,"#p":["ef"]}"##,
842 )?;
843 assert_eq!(RelayFilter::from_json(&filter.to_json()?)?, filter);
844 assert!(RelayFilter::from_json(r#"{"limit":1,"limit":2}"#).is_err());
845 assert!(RelayFilter::from_json(r#"{"unknown":[]}"#).is_err());
846 assert!(RelayFilter::from_json(r#"{"since":2,"until":1}"#).is_err());
847 assert!(parse_client_relay_frame(r#"["REQ","sub",{"limit":1,"limit":2}]"#).is_err());
848 assert!(
849 parse_relay_response_frame(r#"["EVENT","sub",{"id":"00","nested":{"a":1,"a":2}}]"#,)
850 .is_err()
851 );
852 Ok(())
853 }
854
855 #[test]
856 fn client_and_relay_frames_round_trip_and_reject_trailing_items() -> Result<(), SoftchatError> {
857 let authored = event(NostrEventKind::SHORT_TEXT_NOTE, 1)?;
858 for frame in [
859 ClientRelayFrame::Event(authored.clone()),
860 ClientRelayFrame::Req {
861 subscription_id: "sub".to_owned(),
862 filters: vec![RelayFilter::default()],
863 },
864 ClientRelayFrame::Close {
865 subscription_id: "sub".to_owned(),
866 },
867 ClientRelayFrame::Count {
868 subscription_id: "sub".to_owned(),
869 filters: vec![RelayFilter::default()],
870 },
871 ] {
872 let encoded = frame.to_json()?;
873 assert_eq!(parse_client_relay_frame(&encoded)?, frame);
874 }
875
876 let response = RelayResponseFrame::Event {
877 subscription_id: "sub".to_owned(),
878 event: authored,
879 };
880 assert_eq!(parse_relay_response_frame(&response.to_json()?)?, response);
881 assert!(parse_relay_response_frame(r#"["EOSE","sub","trailing"]"#).is_err());
882 assert!(parse_client_relay_frame(r#"["UNKNOWN"]"#).is_err());
883 Ok(())
884 }
885
886 #[test]
887 fn events_batch_is_flat_non_empty_unique_and_bounded() -> Result<(), SoftchatError> {
888 let first = event(NostrEventKind::SHORT_TEXT_NOTE, 1)?;
889 let second = event(NostrEventKind::SHORT_TEXT_NOTE, 2)?;
890 let frame = ClientRelayFrame::Events(vec![first.clone(), second.clone()]);
891 let json = frame.to_json()?;
892 assert_eq!(parse_client_relay_frame(&json)?, frame);
893 assert!(json.starts_with("[\"EVENTS\",{"));
894 assert!(ClientRelayFrame::Events(Vec::new()).to_json().is_err());
895 assert!(
896 ClientRelayFrame::Events(vec![first.clone(), first])
897 .to_json()
898 .is_err()
899 );
900 Ok(())
901 }
902
903 #[test]
904 fn batch_tracker_is_per_event_idempotent_and_conflict_strict() -> Result<(), SoftchatError> {
905 let first = event(NostrEventKind::SHORT_TEXT_NOTE, 1)?.id().to_hex();
906 let second = event(NostrEventKind::SHORT_TEXT_NOTE, 2)?.id().to_hex();
907 let accepted = BatchAcknowledgement {
908 event_id: first.clone(),
909 accepted: true,
910 message: String::new(),
911 };
912 let rejected = BatchAcknowledgement {
913 event_id: second.clone(),
914 accepted: false,
915 message: "blocked".to_owned(),
916 };
917 let progress = track_batch_acknowledgements(
918 vec![first.clone(), second.clone()],
919 vec![rejected.clone(), accepted.clone(), accepted.clone()],
920 )?;
921 assert_eq!(
922 progress.states,
923 vec![BatchEventState::Accepted, BatchEventState::Rejected]
924 );
925 assert!(progress.complete);
926
927 let conflict = BatchAcknowledgement {
928 accepted: false,
929 ..accepted
930 };
931 assert!(
932 track_batch_acknowledgements(vec![first, second], vec![conflict, rejected]).is_ok()
933 );
934 let event_id = progress.event_ids[0].clone();
935 assert!(
936 track_batch_acknowledgements(
937 progress.event_ids,
938 vec![
939 BatchAcknowledgement {
940 event_id: event_id.clone(),
941 accepted: true,
942 message: String::new(),
943 },
944 BatchAcknowledgement {
945 event_id,
946 accepted: false,
947 message: String::new(),
948 },
949 ],
950 )
951 .is_err()
952 );
953 Ok(())
954 }
955
956 #[test]
957 fn nip42_plan_binds_challenge_relay_author_and_time() -> Result<(), SoftchatError> {
958 let identity = LocalIdentity::from_secret_hex(ALICE_SECRET)?;
959 let event =
960 plan_nip42_authentication(&identity, "challenge", "wss://relay.example", 1_000)?;
961 validate_nip42_authentication(
962 &event,
963 &identity.public_key(),
964 "challenge",
965 "wss://relay.example",
966 1_500,
967 )?;
968 assert!(
969 validate_nip42_authentication(
970 &event,
971 &identity.public_key(),
972 "wrong",
973 "wss://relay.example",
974 1_500,
975 )
976 .is_err()
977 );
978 assert!(
979 validate_nip42_authentication(
980 &event,
981 &identity.public_key(),
982 "challenge",
983 "wss://relay.example",
984 1_601,
985 )
986 .is_err()
987 );
988 Ok(())
989 }
990}