Skip to main content

netget/server/nostr/
wire.rs

1//! Nostr NIP-01 on the wire: client messages in, relay messages out, and the two things NetGet
2//! owns outright — an event's id and its signature.
3//!
4//! Everything here is pure: no socket, no model, no state. The server loop in `mod.rs` calls
5//! [`parse_client_message`] on every text frame and gets back either a message worth deciding
6//! about or a [`Refusal`] it answers itself.
7//!
8//! # The id is recomputed, the signature is checked, before anything else sees the event
9//!
10//! An `EVENT` whose `id` is not the SHA-256 of its own canonical serialisation, or whose `sig`
11//! is not a valid BIP-340 signature of that id by its `pubkey`, is not an event at all — it is
12//! answered `["OK", <id>, false, "invalid: …"]` here and never reaches the model. There is no
13//! decision in it, and a model asked to judge a forged event could only get it wrong.
14//!
15//! # The canonical serialisation, and where the implementations disagree
16//!
17//! NIP-01 defines the id as `sha256` of `[0,<pubkey>,<created_at>,<kind>,<tags>,<content>]` with
18//! no whitespace, `\n \" \\ \r \t \b \f` escaped as shown and "all other characters included
19//! verbatim". For text without C0 control characters other than `\n \r \t` every
20//! implementation agrees and there is one serialisation. For text with them, there are three
21//! in use, measured rather than assumed (`tests/server/nostr/wire_test.rs`):
22//!
23//! - **JSON** — `\b` and `\f` named, every other C0 control `\u00xx`. What `serde_json`,
24//!   nostr-tools (`JSON.stringify`) and rust-nostr write.
25//! - **NIP-01 as written** — `\b` and `\f` named, every other C0 control raw.
26//! - **go-nostr** — only `\n \" \\ \r \t` escaped; `\b`, `\f` and the rest raw. nak 0.20.7
27//!   signs this way: an event it signed with a U+0008 in its content verifies under this form
28//!   and under neither of the others.
29//!
30//! A relay that knew one of them would refuse events from the others' users as forged. So
31//! [`verify_event`] accepts an id computed under any of the three — the signature is over the
32//! id, so accepting a second serialisation admits no event its author did not sign — and
33//! computes only the first when the text has nothing the three disagree about. Events NetGet
34//! signs itself are serialised the JSON way and never contain such a character:
35//! [`parse_supplied_event`] drops C0 controls other than `\n \r \t` from what the model
36//! supplies, so every client computes the same id for them.
37//!
38//! # Depth
39//!
40//! Frames are parsed with `serde_json`, whose recursion limit (128) is on in this tree — no
41//! crate enables `unbounded_depth` at any feature set — so a depth bomb is a parse error, not a
42//! stack overflow. That bound is enough for stack safety; what reaches the model is bounded far
43//! tighter by the schema checks below, which accept tags only as arrays of strings and filter
44//! values only as scalars or arrays of scalars.
45
46use secp256k1::{schnorr, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey};
47use serde_json::{json, Map, Value};
48use sha2::{Digest, Sha256};
49use std::sync::LazyLock;
50
51/// Largest WebSocket message (and frame) accepted from a client, in bytes. strfry's default
52/// `maxWebsocketPayloadSize` is the same 131072; a text note is a few hundred bytes, and a
53/// larger message is refused by the framing layer before it is buffered in full.
54pub const MAX_MESSAGE_BYTES: usize = 128 * 1024;
55
56/// Open subscriptions one connection may hold. A REQ past it is answered `CLOSED`.
57pub const MAX_SUBSCRIPTIONS: usize = 20;
58
59/// Filters one REQ may carry. Each is a separate query the model is shown.
60pub const MAX_FILTERS: usize = 10;
61
62/// NIP-01: a subscription id is "an arbitrary, non-empty string of max length 64 chars".
63pub const MAX_SUBSCRIPTION_ID_CHARS: usize = 64;
64
65/// Tags one event may carry. strfry's `maxNumTags` default is 2000.
66pub const MAX_TAGS: usize = 2000;
67
68/// Events one `send_nostr_events` answer may ask NetGet to sign.
69pub const MAX_EVENTS_PER_ANSWER: usize = 500;
70
71/// NIP-01 kinds are integers between 0 and 65535.
72pub const MAX_KIND: u64 = 65_535;
73
74/// The machine-readable prefixes NIP-01 gives an `OK` or `CLOSED` message.
75pub 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// ---------------------------------------------------------------------------
88// Events
89// ---------------------------------------------------------------------------
90
91/// A NIP-01 event whose id and signature have been checked, or that NetGet signed itself.
92#[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/// How strings are escaped in a serialisation. See the module doc.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum Escaping {
120    /// `serde_json`, nostr-tools, rust-nostr.
121    Json,
122    /// NIP-01's text taken literally.
123    Nip01Literal,
124    /// go-nostr (nak).
125    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
149/// The serialisation the id is the hash of, under one escaping. See the module doc.
150pub 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
181/// The canonical serialisation NetGet signs with: the JSON escaping.
182pub 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
192/// Lowercase hex of the SHA-256 of the canonical serialisation.
193pub 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
215/// A C0 control the escapings disagree about: anything below U+0020 but `\n \r \t`.
216fn is_contested(ch: char) -> bool {
217    (ch as u32) < 0x20 && !matches!(ch, '\n' | '\r' | '\t')
218}
219
220/// Whether an event's id depends on which escaping computed it.
221fn 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/// Why an `EVENT` was refused before it could reach the model.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct EventRefusal {
234    /// The event's `id` when it had a well-formed one, so the refusal can be an `OK`.
235    pub id: Option<String>,
236    /// The message, NIP-01 prefix included.
237    pub message: String,
238    /// The `decision=` token for the log.
239    pub decision: &'static str,
240}
241
242/// Validate an event's shape, recompute its id and verify its signature.
243pub 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
394// ---------------------------------------------------------------------------
395// The relay's own key
396// ---------------------------------------------------------------------------
397
398/// The key NetGet signs the model's events with.
399///
400/// A model cannot produce a BIP-340 signature, and an event without one is refused by every
401/// client. So events the model supplies are signed by the relay, under the relay's pubkey —
402/// not by their nominal author, whose secret key NetGet does not have.
403pub 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    /// A fresh key from the OS random source.
418    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    /// A key from 64 hex characters.
428    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    /// Build, hash and sign an event under this key.
453    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// ---------------------------------------------------------------------------
481// Filters
482// ---------------------------------------------------------------------------
483
484/// One NIP-01 filter. Keys NIP-01 does not define (`search`, …) are kept in `raw` for the model
485/// and ignored when matching.
486#[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    /// `#e`, `#p`, `#t`…: the single letter and the values the tag's first value must be one of.
495    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
520/// Parse one filter object.
521pub 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            // Extensions (NIP-50 `search`, …) are not matched on, but must still be flat: a
575            // filter is shown to the model, and nothing in NIP-01 nests deeper than this.
576            _ => {
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    /// Whether `event` satisfies every condition of this filter (NIP-01: conditions are ANDed,
595    /// values within one condition ORed). `limit` is not a condition; see [`select_events`].
596    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
635/// Which of `events` a subscription with these `filters` receives, as indices in their
636/// original order.
637///
638/// An event is sent when it matches at least one filter. With `apply_limit` (the answer to the
639/// REQ itself, NIP-01's "initial query"), each filter contributes at most `limit` of its
640/// matches, the newest first and, on equal `created_at`, the lowest id — NIP-01's own order.
641/// Live events pushed after EOSE are not limited.
642pub 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// ---------------------------------------------------------------------------
667// Client messages
668// ---------------------------------------------------------------------------
669
670/// A client message worth acting on. An `EVENT` here has already passed [`verify_event`].
671#[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/// A message NetGet answers itself, without the model.
684#[derive(Debug, Clone, PartialEq, Eq)]
685pub struct Refusal {
686    /// The relay message to send, already rendered.
687    pub reply: String,
688    /// The `decision=` token for the log.
689    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
724/// Parse one text frame from a client.
725pub 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
807// ---------------------------------------------------------------------------
808// Relay messages
809// ---------------------------------------------------------------------------
810
811fn render(value: Value) -> String {
812    serde_json::to_string(&value).expect("relay messages are plain JSON")
813}
814
815/// `["EVENT", <subscription_id>, <event>]`
816pub fn event_message(subscription_id: &str, event: &Event) -> String {
817    render(json!(["EVENT", subscription_id, event.to_json()]))
818}
819
820/// `["OK", <event_id>, <accepted>, <message>]`
821pub fn ok_message(event_id: &str, accepted: bool, message: &str) -> String {
822    render(json!(["OK", event_id, accepted, message]))
823}
824
825/// `["EOSE", <subscription_id>]`
826pub fn eose_message(subscription_id: &str) -> String {
827    render(json!(["EOSE", subscription_id]))
828}
829
830/// `["CLOSED", <subscription_id>, <message>]`
831pub fn closed_message(subscription_id: &str, message: &str) -> String {
832    render(json!(["CLOSED", subscription_id, message]))
833}
834
835/// `["NOTICE", <message>]`
836pub fn notice_message(message: &str) -> String {
837    render(json!(["NOTICE", message]))
838}
839
840/// `reason` as an OK/CLOSED message: kept as written when it already starts with one of
841/// NIP-01's prefixes (`blocked: …`), otherwise given `default_prefix`.
842pub 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// ---------------------------------------------------------------------------
859// Events the model supplies
860// ---------------------------------------------------------------------------
861
862/// What the model gives for one event: everything but `pubkey`, `id` and `sig`, which NetGet
863/// fills in by signing.
864#[derive(Debug, Clone, PartialEq, Eq)]
865pub struct SuppliedEvent {
866    pub kind: u64,
867    pub content: String,
868    pub tags: Vec<Vec<String>>,
869    /// `None` means "now", resolved when NetGet signs.
870    pub created_at: Option<u64>,
871}
872
873/// Text with the C0 controls the id escapings disagree about removed. See the module doc.
874fn 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
887/// Read one event the model supplied. C0 control characters other than `\n \r \t` are
888/// dropped from its content and tags, so no client computes a different id for it.
889pub 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        // A kind-0 profile's content is a JSON object serialised as a string; a model that
904        // writes the object itself means exactly that.
905        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
954/// Read the `events` array of `send_nostr_events`.
955pub 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
973/// Seconds since the Unix epoch, for an event the model gave no `created_at`.
974pub 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}