1use secp256k1::{schnorr, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey};
47use serde_json::{json, Map, Value};
48use sha2::{Digest, Sha256};
49use std::sync::LazyLock;
50
51pub const MAX_MESSAGE_BYTES: usize = 128 * 1024;
55
56pub const MAX_SUBSCRIPTIONS: usize = 20;
58
59pub const MAX_FILTERS: usize = 10;
61
62pub const MAX_SUBSCRIPTION_ID_CHARS: usize = 64;
64
65pub const MAX_TAGS: usize = 2000;
67
68pub const MAX_EVENTS_PER_ANSWER: usize = 500;
70
71pub const MAX_KIND: u64 = 65_535;
73
74pub const REASON_PREFIXES: &[&str] = &[
76 "duplicate",
77 "pow",
78 "blocked",
79 "rate-limited",
80 "invalid",
81 "restricted",
82 "error",
83];
84
85static SECP: LazyLock<Secp256k1<secp256k1::All>> = LazyLock::new(Secp256k1::new);
86
87#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Event {
94 pub id: String,
95 pub pubkey: String,
96 pub created_at: u64,
97 pub kind: u64,
98 pub tags: Vec<Vec<String>>,
99 pub content: String,
100 pub sig: String,
101}
102
103impl Event {
104 pub fn to_json(&self) -> Value {
105 json!({
106 "id": self.id,
107 "pubkey": self.pubkey,
108 "created_at": self.created_at,
109 "kind": self.kind,
110 "tags": self.tags,
111 "content": self.content,
112 "sig": self.sig,
113 })
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum Escaping {
120 Json,
122 Nip01Literal,
124 GoNostr,
126}
127
128fn push_escaped(out: &mut String, s: &str, escaping: Escaping) {
129 if escaping == Escaping::Json {
130 out.push_str(&serde_json::to_string(s).expect("a string always serialises"));
131 return;
132 }
133 out.push('"');
134 for ch in s.chars() {
135 match ch {
136 '"' => out.push_str("\\\""),
137 '\\' => out.push_str("\\\\"),
138 '\n' => out.push_str("\\n"),
139 '\r' => out.push_str("\\r"),
140 '\t' => out.push_str("\\t"),
141 '\u{8}' if escaping == Escaping::Nip01Literal => out.push_str("\\b"),
142 '\u{c}' if escaping == Escaping::Nip01Literal => out.push_str("\\f"),
143 other => out.push(other),
144 }
145 }
146 out.push('"');
147}
148
149pub fn serialize_for_id(
151 escaping: Escaping,
152 pubkey: &str,
153 created_at: u64,
154 kind: u64,
155 tags: &[Vec<String>],
156 content: &str,
157) -> String {
158 let mut out = String::with_capacity(content.len() + 128);
159 out.push_str("[0,");
160 push_escaped(&mut out, pubkey, escaping);
161 out.push_str(&format!(",{created_at},{kind},["));
162 for (i, tag) in tags.iter().enumerate() {
163 if i > 0 {
164 out.push(',');
165 }
166 out.push('[');
167 for (j, part) in tag.iter().enumerate() {
168 if j > 0 {
169 out.push(',');
170 }
171 push_escaped(&mut out, part, escaping);
172 }
173 out.push(']');
174 }
175 out.push_str("],");
176 push_escaped(&mut out, content, escaping);
177 out.push(']');
178 out
179}
180
181pub fn canonical_serialization(
183 pubkey: &str,
184 created_at: u64,
185 kind: u64,
186 tags: &[Vec<String>],
187 content: &str,
188) -> String {
189 serialize_for_id(Escaping::Json, pubkey, created_at, kind, tags, content)
190}
191
192pub fn compute_id(
194 pubkey: &str,
195 created_at: u64,
196 kind: u64,
197 tags: &[Vec<String>],
198 content: &str,
199) -> String {
200 id_under(Escaping::Json, pubkey, created_at, kind, tags, content)
201}
202
203fn id_under(
204 escaping: Escaping,
205 pubkey: &str,
206 created_at: u64,
207 kind: u64,
208 tags: &[Vec<String>],
209 content: &str,
210) -> String {
211 let serialized = serialize_for_id(escaping, pubkey, created_at, kind, tags, content);
212 hex::encode(Sha256::digest(serialized.as_bytes()))
213}
214
215fn is_contested(ch: char) -> bool {
217 (ch as u32) < 0x20 && !matches!(ch, '\n' | '\r' | '\t')
218}
219
220fn has_contested(tags: &[Vec<String>], content: &str) -> bool {
222 content.chars().any(is_contested) || tags.iter().flatten().any(|p| p.chars().any(is_contested))
223}
224
225fn is_lower_hex(s: &str, len: usize) -> bool {
226 s.len() == len
227 && s.bytes()
228 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct EventRefusal {
234 pub id: Option<String>,
236 pub message: String,
238 pub decision: &'static str,
240}
241
242pub fn verify_event(value: &Value) -> Result<Event, EventRefusal> {
244 let object = value.as_object();
245 let id = object
246 .and_then(|o| o.get("id"))
247 .and_then(Value::as_str)
248 .filter(|s| is_lower_hex(s, 64))
249 .map(str::to_string);
250 let refuse = |message: String, decision: &'static str| EventRefusal {
251 id: id.clone(),
252 message,
253 decision,
254 };
255 let Some(object) = object else {
256 return Err(refuse(
257 "invalid: an event must be a JSON object".to_string(),
258 "fail_closed_invalid_event",
259 ));
260 };
261 let Some(event_id) = id.clone() else {
262 return Err(refuse(
263 "invalid: id must be 64 lowercase hex characters".to_string(),
264 "fail_closed_invalid_event",
265 ));
266 };
267 let pubkey = match object.get("pubkey").and_then(Value::as_str) {
268 Some(p) if is_lower_hex(p, 64) => p.to_string(),
269 _ => {
270 return Err(refuse(
271 "invalid: pubkey must be 64 lowercase hex characters".to_string(),
272 "fail_closed_invalid_event",
273 ))
274 }
275 };
276 let sig = match object.get("sig").and_then(Value::as_str) {
277 Some(s) if is_lower_hex(s, 128) => s.to_string(),
278 _ => {
279 return Err(refuse(
280 "invalid: sig must be 128 lowercase hex characters".to_string(),
281 "fail_closed_invalid_event",
282 ))
283 }
284 };
285 let Some(created_at) = object.get("created_at").and_then(Value::as_u64) else {
286 return Err(refuse(
287 "invalid: created_at must be a non-negative integer".to_string(),
288 "fail_closed_invalid_event",
289 ));
290 };
291 let kind = match object.get("kind").and_then(Value::as_u64) {
292 Some(k) if k <= MAX_KIND => k,
293 _ => {
294 return Err(refuse(
295 format!("invalid: kind must be an integer from 0 to {MAX_KIND}"),
296 "fail_closed_invalid_event",
297 ))
298 }
299 };
300 let tags = match object.get("tags") {
301 Some(Value::Array(items)) => {
302 if items.len() > MAX_TAGS {
303 return Err(refuse(
304 format!("invalid: more than {MAX_TAGS} tags"),
305 "fail_closed_invalid_event",
306 ));
307 }
308 let mut tags = Vec::with_capacity(items.len());
309 for item in items {
310 let Some(parts) = item.as_array() else {
311 return Err(refuse(
312 "invalid: every tag must be an array of strings".to_string(),
313 "fail_closed_invalid_event",
314 ));
315 };
316 let mut tag = Vec::with_capacity(parts.len());
317 for part in parts {
318 match part.as_str() {
319 Some(s) => tag.push(s.to_string()),
320 None => {
321 return Err(refuse(
322 "invalid: every tag must be an array of strings".to_string(),
323 "fail_closed_invalid_event",
324 ))
325 }
326 }
327 }
328 tags.push(tag);
329 }
330 tags
331 }
332 _ => {
333 return Err(refuse(
334 "invalid: tags must be an array".to_string(),
335 "fail_closed_invalid_event",
336 ))
337 }
338 };
339 let Some(content) = object.get("content").and_then(Value::as_str) else {
340 return Err(refuse(
341 "invalid: content must be a string".to_string(),
342 "fail_closed_invalid_event",
343 ));
344 };
345
346 let escapings: &[Escaping] = if has_contested(&tags, content) {
347 &[Escaping::Json, Escaping::Nip01Literal, Escaping::GoNostr]
348 } else {
349 &[Escaping::Json]
350 };
351 let matches_an_escaping = escapings
352 .iter()
353 .any(|e| id_under(*e, &pubkey, created_at, kind, &tags, content) == event_id);
354 if !matches_an_escaping {
355 return Err(refuse(
356 "invalid: event id does not match the hash of its content".to_string(),
357 "fail_closed_invalid_id",
358 ));
359 }
360
361 let bad_signature = || {
362 refuse(
363 "invalid: signature does not verify".to_string(),
364 "fail_closed_invalid_signature",
365 )
366 };
367 let key_bytes = hex::decode(&pubkey).map_err(|_| bad_signature())?;
368 let Ok(key) = XOnlyPublicKey::from_slice(&key_bytes) else {
369 return Err(refuse(
370 "invalid: pubkey is not a point on secp256k1".to_string(),
371 "fail_closed_invalid_signature",
372 ));
373 };
374 let sig_bytes = hex::decode(&sig).map_err(|_| bad_signature())?;
375 let signature = schnorr::Signature::from_slice(&sig_bytes).map_err(|_| bad_signature())?;
376 let digest: [u8; 32] = hex::decode(&event_id)
377 .ok()
378 .and_then(|b| b.try_into().ok())
379 .ok_or_else(bad_signature)?;
380 SECP.verify_schnorr(&signature, &Message::from_digest(digest), &key)
381 .map_err(|_| bad_signature())?;
382
383 Ok(Event {
384 id: event_id,
385 pubkey,
386 created_at,
387 kind,
388 tags,
389 content: content.to_string(),
390 sig,
391 })
392}
393
394pub struct RelayKey {
404 keypair: Keypair,
405 pubkey_hex: String,
406}
407
408impl std::fmt::Debug for RelayKey {
409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410 f.debug_struct("RelayKey")
411 .field("pubkey", &self.pubkey_hex)
412 .finish_non_exhaustive()
413 }
414}
415
416impl RelayKey {
417 pub fn generate() -> Self {
419 loop {
420 let bytes: [u8; 32] = rand::random();
421 if let Ok(secret) = SecretKey::from_slice(&bytes) {
422 return Self::from_secret(secret);
423 }
424 }
425 }
426
427 pub fn from_hex(hex_key: &str) -> Result<Self, String> {
429 let bytes = hex::decode(hex_key.trim())
430 .map_err(|_| "relay_secret_key must be 64 hex characters".to_string())?;
431 if bytes.len() != 32 {
432 return Err("relay_secret_key must be 64 hex characters".to_string());
433 }
434 let secret = SecretKey::from_slice(&bytes)
435 .map_err(|_| "relay_secret_key is not a valid secp256k1 secret key".to_string())?;
436 Ok(Self::from_secret(secret))
437 }
438
439 fn from_secret(secret: SecretKey) -> Self {
440 let keypair = Keypair::from_secret_key(&SECP, &secret);
441 let (xonly, _) = keypair.x_only_public_key();
442 Self {
443 keypair,
444 pubkey_hex: hex::encode(xonly.serialize()),
445 }
446 }
447
448 pub fn pubkey_hex(&self) -> &str {
449 &self.pubkey_hex
450 }
451
452 pub fn sign(
454 &self,
455 created_at: u64,
456 kind: u64,
457 tags: Vec<Vec<String>>,
458 content: String,
459 ) -> Event {
460 let id = compute_id(&self.pubkey_hex, created_at, kind, &tags, &content);
461 let digest: [u8; 32] = hex::decode(&id)
462 .expect("compute_id returns hex")
463 .try_into()
464 .expect("sha256 is 32 bytes");
465 let aux: [u8; 32] = rand::random();
466 let signature =
467 SECP.sign_schnorr_with_aux_rand(&Message::from_digest(digest), &self.keypair, &aux);
468 Event {
469 id,
470 pubkey: self.pubkey_hex.clone(),
471 created_at,
472 kind,
473 tags,
474 content,
475 sig: hex::encode(signature.serialize()),
476 }
477 }
478}
479
480#[derive(Debug, Clone, PartialEq)]
487pub struct Filter {
488 pub ids: Option<Vec<String>>,
489 pub authors: Option<Vec<String>>,
490 pub kinds: Option<Vec<u64>>,
491 pub since: Option<u64>,
492 pub until: Option<u64>,
493 pub limit: Option<u64>,
494 pub tags: Vec<(String, Vec<String>)>,
496 pub raw: Value,
497}
498
499fn hex_list(key: &str, value: &Value) -> Result<Vec<String>, String> {
500 let items = value
501 .as_array()
502 .ok_or_else(|| format!("invalid: filter {key} must be an array"))?;
503 items
504 .iter()
505 .map(|v| match v.as_str() {
506 Some(s) if is_lower_hex(s, 64) => Ok(s.to_string()),
507 _ => Err(format!(
508 "invalid: filter {key} must hold 64-character lowercase hex strings"
509 )),
510 })
511 .collect()
512}
513
514fn timestamp(key: &str, value: &Value) -> Result<u64, String> {
515 value
516 .as_u64()
517 .ok_or_else(|| format!("invalid: filter {key} must be a non-negative integer"))
518}
519
520pub fn parse_filter(value: &Value) -> Result<Filter, String> {
522 let object: &Map<String, Value> = value
523 .as_object()
524 .ok_or_else(|| "invalid: a filter must be a JSON object".to_string())?;
525 let mut filter = Filter {
526 ids: None,
527 authors: None,
528 kinds: None,
529 since: None,
530 until: None,
531 limit: None,
532 tags: Vec::new(),
533 raw: value.clone(),
534 };
535 for (key, v) in object {
536 match key.as_str() {
537 "ids" => filter.ids = Some(hex_list(key, v)?),
538 "authors" => filter.authors = Some(hex_list(key, v)?),
539 "kinds" => {
540 let items = v
541 .as_array()
542 .ok_or_else(|| "invalid: filter kinds must be an array".to_string())?;
543 let kinds = items
544 .iter()
545 .map(|k| match k.as_u64() {
546 Some(k) if k <= MAX_KIND => Ok(k),
547 _ => Err(format!(
548 "invalid: filter kinds must be integers from 0 to {MAX_KIND}"
549 )),
550 })
551 .collect::<Result<Vec<_>, _>>()?;
552 filter.kinds = Some(kinds);
553 }
554 "since" => filter.since = Some(timestamp(key, v)?),
555 "until" => filter.until = Some(timestamp(key, v)?),
556 "limit" => filter.limit = Some(timestamp(key, v)?),
557 tag if tag.len() == 2
558 && tag.starts_with('#')
559 && tag.as_bytes()[1].is_ascii_alphabetic() =>
560 {
561 let items = v
562 .as_array()
563 .ok_or_else(|| format!("invalid: filter {tag} must be an array"))?;
564 let values = items
565 .iter()
566 .map(|s| {
567 s.as_str()
568 .map(str::to_string)
569 .ok_or_else(|| format!("invalid: filter {tag} must hold strings"))
570 })
571 .collect::<Result<Vec<_>, _>>()?;
572 filter.tags.push((tag[1..].to_string(), values));
573 }
574 _ => {
577 let flat = match v {
578 Value::Array(items) => items.iter().all(|i| !i.is_array() && !i.is_object()),
579 Value::Object(_) => false,
580 _ => true,
581 };
582 if !flat {
583 return Err(format!(
584 "invalid: filter field {key} must be a scalar or an array of scalars"
585 ));
586 }
587 }
588 }
589 }
590 Ok(filter)
591}
592
593impl Filter {
594 pub fn matches(&self, event: &Event) -> bool {
597 if let Some(ids) = &self.ids {
598 if !ids.contains(&event.id) {
599 return false;
600 }
601 }
602 if let Some(authors) = &self.authors {
603 if !authors.contains(&event.pubkey) {
604 return false;
605 }
606 }
607 if let Some(kinds) = &self.kinds {
608 if !kinds.contains(&event.kind) {
609 return false;
610 }
611 }
612 if let Some(since) = self.since {
613 if event.created_at < since {
614 return false;
615 }
616 }
617 if let Some(until) = self.until {
618 if event.created_at > until {
619 return false;
620 }
621 }
622 for (letter, values) in &self.tags {
623 let hit = event.tags.iter().any(|tag| {
624 tag.first().is_some_and(|name| name == letter)
625 && tag.get(1).is_some_and(|v| values.contains(v))
626 });
627 if !hit {
628 return false;
629 }
630 }
631 true
632 }
633}
634
635pub fn select_events(filters: &[Filter], events: &[Event], apply_limit: bool) -> Vec<usize> {
643 let mut chosen = vec![false; events.len()];
644 for filter in filters {
645 let mut hits: Vec<usize> = (0..events.len())
646 .filter(|&i| filter.matches(&events[i]))
647 .collect();
648 if apply_limit {
649 if let Some(limit) = filter.limit {
650 hits.sort_by(|&a, &b| {
651 events[b]
652 .created_at
653 .cmp(&events[a].created_at)
654 .then_with(|| events[a].id.cmp(&events[b].id))
655 });
656 hits.truncate(usize::try_from(limit).unwrap_or(usize::MAX));
657 }
658 }
659 for i in hits {
660 chosen[i] = true;
661 }
662 }
663 (0..events.len()).filter(|&i| chosen[i]).collect()
664}
665
666#[derive(Debug, Clone)]
672pub enum ClientMessage {
673 Event(Event),
674 Req {
675 subscription_id: String,
676 filters: Vec<Filter>,
677 },
678 Close {
679 subscription_id: String,
680 },
681}
682
683#[derive(Debug, Clone, PartialEq, Eq)]
685pub struct Refusal {
686 pub reply: String,
688 pub decision: &'static str,
690}
691
692impl Refusal {
693 fn notice(message: impl Into<String>, decision: &'static str) -> Self {
694 Self {
695 reply: notice_message(&message.into()),
696 decision,
697 }
698 }
699 fn closed(subscription_id: &str, message: impl Into<String>, decision: &'static str) -> Self {
700 Self {
701 reply: closed_message(subscription_id, &message.into()),
702 decision,
703 }
704 }
705}
706
707fn subscription_id(value: Option<&Value>, verb: &str) -> Result<String, Refusal> {
708 match value.and_then(Value::as_str) {
709 Some(id) if !id.is_empty() && id.chars().count() <= MAX_SUBSCRIPTION_ID_CHARS => {
710 Ok(id.to_string())
711 }
712 Some(id) if !id.is_empty() => Err(Refusal::closed(
713 id,
714 format!("invalid: subscription id longer than {MAX_SUBSCRIPTION_ID_CHARS} characters"),
715 "fail_closed_bad_subscription_id",
716 )),
717 _ => Err(Refusal::notice(
718 format!("invalid: {verb} needs a non-empty subscription id string"),
719 "fail_closed_bad_subscription_id",
720 )),
721 }
722}
723
724pub fn parse_client_message(text: &str) -> Result<ClientMessage, Refusal> {
726 let value: Value = serde_json::from_str(text).map_err(|e| {
727 let why = if e.to_string().contains("recursion limit") {
728 "nested too deeply"
729 } else {
730 "not valid JSON"
731 };
732 Refusal::notice(
733 format!("invalid: message is {why}"),
734 "fail_closed_malformed",
735 )
736 })?;
737 let Some(items) = value.as_array() else {
738 return Err(Refusal::notice(
739 "invalid: a message must be a JSON array",
740 "fail_closed_malformed",
741 ));
742 };
743 let Some(verb) = items.first().and_then(Value::as_str) else {
744 return Err(Refusal::notice(
745 "invalid: a message must start with its type, a string",
746 "fail_closed_malformed",
747 ));
748 };
749 match verb {
750 "EVENT" => {
751 let Some(event) = items.get(1) else {
752 return Err(Refusal::notice(
753 "invalid: EVENT needs an event",
754 "fail_closed_malformed",
755 ));
756 };
757 verify_event(event)
758 .map(ClientMessage::Event)
759 .map_err(|r| match r.id {
760 Some(id) => Refusal {
761 reply: ok_message(&id, false, &r.message),
762 decision: r.decision,
763 },
764 None => Refusal::notice(r.message, r.decision),
765 })
766 }
767 "REQ" => {
768 let subscription_id = subscription_id(items.get(1), "REQ")?;
769 let raw_filters = &items[2..];
770 if raw_filters.is_empty() {
771 return Err(Refusal::closed(
772 &subscription_id,
773 "invalid: REQ needs at least one filter",
774 "fail_closed_bad_filter",
775 ));
776 }
777 if raw_filters.len() > MAX_FILTERS {
778 return Err(Refusal::closed(
779 &subscription_id,
780 format!("invalid: more than {MAX_FILTERS} filters in one REQ"),
781 "fail_closed_too_many_filters",
782 ));
783 }
784 let filters = raw_filters
785 .iter()
786 .map(parse_filter)
787 .collect::<Result<Vec<_>, _>>()
788 .map_err(|why| Refusal::closed(&subscription_id, why, "fail_closed_bad_filter"))?;
789 Ok(ClientMessage::Req {
790 subscription_id,
791 filters,
792 })
793 }
794 "CLOSE" => Ok(ClientMessage::Close {
795 subscription_id: subscription_id(items.get(1), "CLOSE")?,
796 }),
797 other => Err(Refusal::notice(
798 format!(
799 "error: unsupported message type {}; this relay speaks EVENT, REQ and CLOSE",
800 crate::utils::truncate_for_log(other, 32)
801 ),
802 "fail_closed_unsupported",
803 )),
804 }
805}
806
807fn render(value: Value) -> String {
812 serde_json::to_string(&value).expect("relay messages are plain JSON")
813}
814
815pub fn event_message(subscription_id: &str, event: &Event) -> String {
817 render(json!(["EVENT", subscription_id, event.to_json()]))
818}
819
820pub fn ok_message(event_id: &str, accepted: bool, message: &str) -> String {
822 render(json!(["OK", event_id, accepted, message]))
823}
824
825pub fn eose_message(subscription_id: &str) -> String {
827 render(json!(["EOSE", subscription_id]))
828}
829
830pub fn closed_message(subscription_id: &str, message: &str) -> String {
832 render(json!(["CLOSED", subscription_id, message]))
833}
834
835pub fn notice_message(message: &str) -> String {
837 render(json!(["NOTICE", message]))
838}
839
840pub fn with_prefix(reason: &str, default_prefix: &str) -> String {
843 let reason = reason.trim();
844 let has_prefix = REASON_PREFIXES.iter().any(|p| {
845 reason
846 .strip_prefix(p)
847 .is_some_and(|rest| rest.starts_with(':'))
848 });
849 if has_prefix {
850 reason.to_string()
851 } else if reason.is_empty() {
852 format!("{default_prefix}:")
853 } else {
854 format!("{default_prefix}: {reason}")
855 }
856}
857
858#[derive(Debug, Clone, PartialEq, Eq)]
865pub struct SuppliedEvent {
866 pub kind: u64,
867 pub content: String,
868 pub tags: Vec<Vec<String>>,
869 pub created_at: Option<u64>,
871}
872
873fn uncontested(s: &str) -> String {
875 s.chars().filter(|c| !is_contested(*c)).collect()
876}
877
878fn scalar_text(value: &Value) -> Option<String> {
879 match value {
880 Value::String(s) => Some(uncontested(s)),
881 Value::Number(n) => Some(n.to_string()),
882 Value::Bool(b) => Some(b.to_string()),
883 _ => None,
884 }
885}
886
887pub fn parse_supplied_event(value: &Value) -> Result<SuppliedEvent, String> {
890 let object = value.as_object().ok_or_else(|| {
891 "each event must be an object {kind, content, tags?, created_at?}".to_string()
892 })?;
893 let kind = match object.get("kind") {
894 Some(Value::Number(n)) => n.as_u64(),
895 Some(Value::String(s)) => s.trim().parse().ok(),
896 _ => None,
897 }
898 .filter(|k| *k <= MAX_KIND)
899 .ok_or_else(|| format!("each event needs a kind, an integer from 0 to {MAX_KIND}"))?;
900 let content = match object.get("content") {
901 None | Some(Value::Null) => String::new(),
902 Some(Value::String(s)) => uncontested(s),
903 Some(other) => serde_json::to_string(other).map_err(|e| e.to_string())?,
906 };
907 let tags = match object.get("tags") {
908 None | Some(Value::Null) => Vec::new(),
909 Some(Value::Array(items)) => {
910 if items.len() > MAX_TAGS {
911 return Err(format!("an event may carry at most {MAX_TAGS} tags"));
912 }
913 items
914 .iter()
915 .map(|tag| {
916 tag.as_array()
917 .ok_or_else(|| {
918 "tags must be an array of arrays of strings, e.g. [[\"t\", \"news\"]]"
919 .to_string()
920 })?
921 .iter()
922 .map(|part| {
923 scalar_text(part).ok_or_else(|| {
924 "tags must be an array of arrays of strings".to_string()
925 })
926 })
927 .collect::<Result<Vec<_>, _>>()
928 })
929 .collect::<Result<Vec<_>, _>>()?
930 }
931 Some(_) => return Err("tags must be an array of arrays of strings".to_string()),
932 };
933 let created_at = match object.get("created_at") {
934 None | Some(Value::Null) => None,
935 Some(Value::Number(n)) => Some(
936 n.as_u64()
937 .ok_or_else(|| "created_at must be a unix timestamp in seconds".to_string())?,
938 ),
939 Some(Value::String(s)) => Some(
940 s.trim()
941 .parse()
942 .map_err(|_| "created_at must be a unix timestamp in seconds".to_string())?,
943 ),
944 Some(_) => return Err("created_at must be a unix timestamp in seconds".to_string()),
945 };
946 Ok(SuppliedEvent {
947 kind,
948 content,
949 tags,
950 created_at,
951 })
952}
953
954pub fn parse_supplied_events(value: Option<&Value>) -> Result<Vec<SuppliedEvent>, String> {
956 let items =
957 match value {
958 None | Some(Value::Null) => return Ok(Vec::new()),
959 Some(Value::Array(items)) => items,
960 Some(_) => return Err(
961 "send_nostr_events needs 'events', an array of {kind, content, tags?, created_at?}"
962 .to_string(),
963 ),
964 };
965 if items.len() > MAX_EVENTS_PER_ANSWER {
966 return Err(format!(
967 "at most {MAX_EVENTS_PER_ANSWER} events per send_nostr_events"
968 ));
969 }
970 items.iter().map(parse_supplied_event).collect()
971}
972
973pub fn now_secs() -> u64 {
975 crate::utils::clock::SystemTime::now()
976 .duration_since(crate::utils::clock::UNIX_EPOCH)
977 .map(|d| d.as_secs())
978 .unwrap_or(0)
979}