Skip to main content

netget/server/nostr/
mod.rs

1//! Nostr relay (NIP-01 over WebSocket, NIP-11 on plain HTTP) — the model is the relay's policy
2//! and its archive.
3//!
4//! One TCP connection carries one HTTP request head. What it asks for decides the rest:
5//!
6//! - **a WebSocket upgrade** — the handshake is answered here (`http.rs`) and the socket is
7//!   handed to tungstenite for framing; every text frame is a NIP-01 message;
8//! - **a GET with `Accept: application/nostr+json`** — the NIP-11 document, built from startup
9//!   parameters, with no model call;
10//! - **anything else** — a one-line page for a browser, a CORS preflight, or an HTTP error.
11//!
12//! # Who decides what
13//!
14//! NetGet owns everything NIP-01 makes mechanical, and none of it costs a model call:
15//!
16//! 1. **An event's id and signature.** `wire::verify_event` recomputes the id and checks the
17//!    BIP-340 signature; a forged or corrupted event is answered `OK false "invalid: …"` and
18//!    never reaches the model.
19//! 2. **Malformed messages, subscription limits, `CLOSE`.** Answered `NOTICE` or `CLOSED`, or,
20//!    for `CLOSE`, simply done.
21//! 3. **Framing of every answer.** The model says accept or reject; NetGet writes the `OK`. The
22//!    model supplies kinds, contents and tags; NetGet signs them with the relay's key
23//!    (`wire::RelayKey`), drops any the subscription's own filters exclude, sends them, then
24//!    `EOSE`.
25//! 4. **Delivery.** An event the model accepts is sent at once to every open subscription it
26//!    matches, on every connection. It is not kept.
27//!
28//! The model decides: whether each published event is taken (`nostr_event`), and which events
29//! answer each subscription (`nostr_req`).
30//!
31//! # Failure is an answer, never silence
32//!
33//! A published event whose decision cannot be made — backend down, a model that answered
34//! nothing — is refused: `OK false "error: …"`, or `"rate-limited: …"` when the backend is
35//! overloaded. A subscription that cannot be answered is `CLOSED` the same way. Silence on an
36//! `EVENT` would leave the publisher waiting; accepting would be fail-open. The texts are fixed
37//! literals; the error itself goes to the log with `decision=fail_closed_llm_error`.
38//!
39//! # One connection, three futures, one task
40//!
41//! The reader (frames in, mechanical answers, idle watchdog), the worker (one message at a time
42//! to the model, in arrival order) and the writer (the only thing that writes to the socket)
43//! run inside the connection's own tracked task, so `stop_server` aborting that task ends all
44//! three. Messages wait for the worker in a queue of [`MAX_QUEUED_MESSAGES`]; past it a
45//! published event is refused `rate-limited:` rather than buffered without bound.
46
47pub mod actions;
48pub mod http;
49pub mod subscriptions;
50pub mod wire;
51
52use crate::llm::action_helper::call_llm;
53use crate::llm::actions::protocol_trait::ActionResult;
54use crate::llm::ollama_client::OllamaClient;
55use crate::logging::emit::Log;
56use crate::protocol::Event;
57use crate::server::accept_bounded::{ConnectionActivity, ConnectionPermit};
58use crate::server::connection::ConnectionId;
59use crate::state::app_state::AppState;
60use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
61use crate::state::ServerId;
62use actions::NostrProtocol;
63use anyhow::Result;
64use futures::stream::{SplitSink, SplitStream, StreamExt};
65use futures::SinkExt;
66use std::net::SocketAddr;
67use std::sync::Arc;
68use std::time::Duration;
69use subscriptions::{ConnShared, Outbound, Relay};
70use tokio::io::{AsyncReadExt, AsyncWriteExt};
71use tokio::net::TcpStream;
72use tokio::sync::{mpsc, Mutex};
73use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
74use tokio_tungstenite::tungstenite::protocol::{CloseFrame, Role, WebSocketConfig};
75use tokio_tungstenite::tungstenite::Message;
76use tokio_tungstenite::WebSocketStream;
77use wire::{ClientMessage, RelayKey};
78
79pub use wire::MAX_MESSAGE_BYTES;
80
81/// How long a connected peer has to send its whole HTTP request head. Every Nostr client sends
82/// the upgrade at once; nothing in this phase involves the model or a human.
83pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
84
85/// How long an open WebSocket may go without a single frame from the client.
86///
87/// A subscription is entitled to sit open with nothing arriving, so the bound is on the peer's
88/// liveness: at half of it the relay sends a Ping, every RFC 6455 endpoint answers by itself,
89/// and a Pong is a frame. What reaches the bound is a peer that stopped reading or vanished
90/// without a FIN. Never fires while a message is being answered — a model call, or a `manual`
91/// rule parked for a human (300 s by default; this is twice that, as in `websocket`). Declared
92/// as `idle_timeout_secs`.
93pub const IDLE_TIMEOUT: Duration = Duration::from_secs(600);
94
95/// Concurrent connections admitted before new ones are refused — the house default.
96const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
97
98/// Messages waiting for the worker on one connection. Mechanical answers (refusals, `CLOSE`)
99/// do not queue; only what needs the model does.
100pub const MAX_QUEUED_MESSAGES: usize = 64;
101
102/// How long to wait for the other side of a closing handshake, and for queued frames to drain,
103/// once a connection is ending.
104const CLOSE_GRACE: Duration = Duration::from_secs(5);
105
106/// What a peer over [`MAX_CONNECTIONS`] gets. A Nostr client is a WebSocket client and so an
107/// HTTP client first (RFC 6455 §4.1): 503 with `Retry-After` is the refusal every client
108/// reports as a failed handshake.
109static CONNECTION_CAP_REFUSAL: std::sync::LazyLock<Vec<u8>> = std::sync::LazyLock::new(|| {
110    http::response(
111        503,
112        "text/plain; charset=utf-8",
113        "Retry-After: 30\r\n",
114        "Too many connections, try again later\n",
115    )
116});
117
118/// Fixed texts NetGet answers with when the model cannot. Nothing from an error reaches a peer.
119const EVENT_UNAVAILABLE: &str = "error: the relay could not decide on this event";
120const EVENT_OVERLOADED: &str = "rate-limited: the relay is at capacity, retry later";
121const EVENT_UNDECIDED: &str = "error: the relay made no decision on this event";
122const REQ_UNAVAILABLE: &str = "error: the relay could not answer this subscription";
123const REQ_OVERLOADED: &str = "rate-limited: the relay is at capacity, retry later";
124const QUEUE_FULL: &str = "rate-limited: too many messages waiting on this connection";
125
126/// Startup configuration, resolved from parameters.
127#[derive(Debug, Clone)]
128pub struct RelayConfig {
129    pub info: http::RelayInfo,
130    pub handshake_timeout: Duration,
131    pub idle_timeout: Duration,
132}
133
134/// What every connection of one relay shares.
135struct ServerShared {
136    llm_client: OllamaClient,
137    app_state: Arc<AppState>,
138    status_tx: mpsc::UnboundedSender<String>,
139    server_id: ServerId,
140    key: Arc<RelayKey>,
141    relay: Relay,
142    config: RelayConfig,
143    info_document: serde_json::Value,
144    local_addr: SocketAddr,
145}
146
147impl ServerShared {
148    fn log(&self) -> Log<'_> {
149        Log::new(Some(&self.status_tx))
150    }
151}
152
153pub struct NostrServer;
154
155impl NostrServer {
156    pub async fn spawn_with_llm_actions(
157        listen_addr: SocketAddr,
158        llm_client: OllamaClient,
159        app_state: Arc<AppState>,
160        status_tx: mpsc::UnboundedSender<String>,
161        server_id: ServerId,
162        key: Arc<RelayKey>,
163        config: RelayConfig,
164    ) -> Result<SocketAddr> {
165        let listener =
166            crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
167        let local_addr = listener.local_addr()?;
168        Log::new(Some(&status_tx)).info(format!(
169            "Nostr relay listening on {} (ws://{}), relay pubkey {}",
170            local_addr,
171            local_addr,
172            key.pubkey_hex()
173        ));
174
175        let shared = Arc::new(ServerShared {
176            llm_client,
177            app_state: app_state.clone(),
178            status_tx: status_tx.clone(),
179            server_id,
180            key,
181            relay: Relay::new(),
182            info_document: config.info.document(),
183            config,
184            local_addr,
185        });
186
187        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
188        let accept_shared = shared.clone();
189        let accept_handle = tokio::spawn(async move {
190            loop {
191                match crate::server::accept_bounded::accept_bounded(
192                    &listener,
193                    &limiter,
194                    &CONNECTION_CAP_REFUSAL,
195                    "Nostr",
196                    Some(&accept_shared.status_tx),
197                )
198                .await
199                {
200                    Ok((socket, peer_addr, permit)) => {
201                        let shared = accept_shared.clone();
202                        // Tracked, not detached: stop_server must abort this task too.
203                        accept_shared
204                            .app_state
205                            .spawn_server_task(server_id, async move {
206                                handle_connection(shared, socket, peer_addr, permit).await;
207                            })
208                            .await;
209                    }
210                    Err(e) => {
211                        accept_shared
212                            .log()
213                            .error(format!("Nostr accept error: {}", e));
214                        break;
215                    }
216                }
217            }
218        });
219
220        app_state
221            .register_server_task(server_id, accept_handle)
222            .await;
223        let _ = status_tx.send("__UPDATE_UI__".to_string());
224        Ok(local_addr)
225    }
226}
227
228/// One accepted connection, start to finish. The permit is held for all of it, so
229/// [`MAX_CONNECTIONS`] caps live connections.
230async fn handle_connection(
231    server: Arc<ServerShared>,
232    socket: TcpStream,
233    peer_addr: SocketAddr,
234    _permit: ConnectionPermit,
235) {
236    let connection_id = ConnectionId::new(server.app_state.get_next_unified_id().await);
237    let now = crate::utils::clock::Instant::now();
238    server
239        .app_state
240        .add_connection_to_server(
241            server.server_id,
242            ConnectionState {
243                id: connection_id,
244                remote_addr: peer_addr,
245                local_addr: server.local_addr,
246                bytes_sent: 0,
247                bytes_received: 0,
248                packets_sent: 0,
249                packets_received: 0,
250                last_activity: now,
251                status: ConnectionStatus::Active,
252                status_changed_at: now,
253                protocol_info: ProtocolConnectionInfo::new(serde_json::json!({"state": "HTTP"})),
254            },
255        )
256        .await;
257    let _ = server.status_tx.send("__UPDATE_UI__".to_string());
258
259    serve(&server, socket, peer_addr, connection_id).await;
260
261    server
262        .app_state
263        .remove_peer_handle(server.server_id, connection_id.as_u32())
264        .await;
265    server.relay.remove(connection_id.as_u32());
266    server
267        .app_state
268        .close_connection_on_server(server.server_id, connection_id)
269        .await;
270    let _ = server.status_tx.send("__UPDATE_UI__".to_string());
271}
272
273/// Why reading the request head stopped.
274enum HeadRead {
275    Head(Vec<u8>, Vec<u8>),
276    TooLarge,
277    Closed,
278}
279
280async fn read_head(socket: &mut TcpStream) -> HeadRead {
281    let mut buf: Vec<u8> = Vec::with_capacity(1024);
282    let mut chunk = [0u8; 2048];
283    loop {
284        if let Some(pos) = http::find_head_end(&buf) {
285            let leftover = buf.split_off(pos + 4);
286            buf.truncate(pos);
287            return HeadRead::Head(buf, leftover);
288        }
289        if buf.len() > http::MAX_REQUEST_HEAD {
290            return HeadRead::TooLarge;
291        }
292        match socket.read(&mut chunk).await {
293            Ok(0) | Err(_) => return HeadRead::Closed,
294            Ok(n) => buf.extend_from_slice(&chunk[..n]),
295        }
296    }
297}
298
299async fn reply_and_close(
300    server: &ServerShared,
301    socket: &mut TcpStream,
302    connection_id: ConnectionId,
303    bytes: &[u8],
304) {
305    if socket.write_all(bytes).await.is_ok() {
306        let _ = socket.flush().await;
307        server
308            .app_state
309            .update_connection_stats(
310                server.server_id,
311                connection_id,
312                None,
313                Some(bytes.len() as u64),
314                None,
315                Some(1),
316            )
317            .await;
318    }
319    let _ = socket.shutdown().await;
320}
321
322async fn serve(
323    server: &Arc<ServerShared>,
324    mut socket: TcpStream,
325    peer_addr: SocketAddr,
326    connection_id: ConnectionId,
327) {
328    let log = server.log();
329    let (head_bytes, leftover) =
330        match tokio::time::timeout(server.config.handshake_timeout, read_head(&mut socket)).await {
331            Err(_) => {
332                log.info(format!(
333                    "Nostr peer {} sent no complete request within {}s; closing \
334                 decision=fail_closed_handshake_timeout",
335                    peer_addr,
336                    server.config.handshake_timeout.as_secs()
337                ));
338                let reply = http::error_response(408, "Timed out waiting for the HTTP request");
339                reply_and_close(server, &mut socket, connection_id, &reply).await;
340                return;
341            }
342            Ok(HeadRead::TooLarge) => {
343                log.warn(format!(
344                    "Nostr peer {} sent a request head over {} bytes \
345                 decision=fail_closed_head_too_large",
346                    peer_addr,
347                    http::MAX_REQUEST_HEAD
348                ));
349                let reply = http::error_response(431, "Request head too large");
350                reply_and_close(server, &mut socket, connection_id, &reply).await;
351                return;
352            }
353            Ok(HeadRead::Closed) => return,
354            Ok(HeadRead::Head(head, leftover)) => (head, leftover),
355        };
356    server
357        .app_state
358        .update_connection_stats(
359            server.server_id,
360            connection_id,
361            Some(head_bytes.len() as u64 + 4),
362            None,
363            Some(1),
364            None,
365        )
366        .await;
367
368    let head = match http::parse_request_head(&head_bytes) {
369        Ok(head) => head,
370        Err(why) => {
371            log.info(format!(
372                "Nostr peer {} sent a malformed request ({}) decision=fail_closed_malformed",
373                peer_addr, why
374            ));
375            let reply = http::error_response(400, "Malformed HTTP request");
376            reply_and_close(server, &mut socket, connection_id, &reply).await;
377            return;
378        }
379    };
380
381    if head.wants_upgrade() {
382        let key = match http::validate_upgrade(&head) {
383            Ok(key) => key,
384            Err((status, reason)) => {
385                log.info(format!(
386                    "Nostr upgrade from {} refused ({} {}) decision=fail_closed_bad_upgrade",
387                    peer_addr, status, reason
388                ));
389                let reply = http::error_response(status, reason);
390                reply_and_close(server, &mut socket, connection_id, &reply).await;
391                return;
392            }
393        };
394        if socket
395            .write_all(http::accept_response(&key).as_bytes())
396            .await
397            .is_err()
398        {
399            return;
400        }
401        let _ = socket.flush().await;
402        session(server, socket, leftover, peer_addr, connection_id).await;
403        return;
404    }
405
406    let reply = if head.method.eq_ignore_ascii_case("OPTIONS") {
407        http::preflight_response()
408    } else if !head.method.eq_ignore_ascii_case("GET") {
409        http::error_response(
410            405,
411            "A Nostr relay answers GET: a WebSocket upgrade or NIP-11",
412        )
413    } else if head.wants_relay_info() {
414        log.info(format!(
415            "Nostr NIP-11 relay information to {} decision=relay_info",
416            peer_addr
417        ));
418        http::relay_info_response(&server.info_document)
419    } else {
420        http::browser_response()
421    };
422    reply_and_close(server, &mut socket, connection_id, &reply).await;
423}
424
425/// Writes one connection's injected actions (the dashboard's `[ message ]`, MCP
426/// `send_to_peer`) as WebSocket frames: every `write` is one relay message, a shutdown is a
427/// close frame. `peer_support` writes each rendered frame with one `write_all`.
428struct FrameWriter {
429    tx: mpsc::UnboundedSender<Outbound>,
430}
431
432impl tokio::io::AsyncWrite for FrameWriter {
433    fn poll_write(
434        self: std::pin::Pin<&mut Self>,
435        _cx: &mut std::task::Context<'_>,
436        buf: &[u8],
437    ) -> std::task::Poll<std::io::Result<usize>> {
438        let text = String::from_utf8_lossy(buf).into_owned();
439        std::task::Poll::Ready(
440            self.tx
441                .send(Outbound::Text(text))
442                .map(|_| buf.len())
443                .map_err(|_| std::io::Error::new(std::io::ErrorKind::BrokenPipe, "closed")),
444        )
445    }
446
447    fn poll_flush(
448        self: std::pin::Pin<&mut Self>,
449        _cx: &mut std::task::Context<'_>,
450    ) -> std::task::Poll<std::io::Result<()>> {
451        std::task::Poll::Ready(Ok(()))
452    }
453
454    fn poll_shutdown(
455        self: std::pin::Pin<&mut Self>,
456        _cx: &mut std::task::Context<'_>,
457    ) -> std::task::Poll<std::io::Result<()>> {
458        let _ = self.tx.send(Outbound::Close {
459            code: 1000,
460            reason: "closed by the relay operator".to_string(),
461        });
462        std::task::Poll::Ready(Ok(()))
463    }
464}
465
466/// Something that needs the model, waiting its turn.
467enum Job {
468    Event(wire::Event),
469    Req {
470        subscription_id: String,
471        generation: u64,
472        filters: Vec<wire::Filter>,
473    },
474}
475
476/// How the reader stopped.
477enum ReaderExit {
478    PeerClosed,
479    Idle,
480    TooBig,
481    Error,
482}
483
484/// Everything one open WebSocket's three futures share.
485struct Session {
486    server: Arc<ServerShared>,
487    conn: Arc<ConnShared>,
488    protocol: Arc<NostrProtocol>,
489    activity: Arc<ConnectionActivity>,
490    connection_id: ConnectionId,
491}
492
493async fn session(
494    server: &Arc<ServerShared>,
495    socket: TcpStream,
496    leftover: Vec<u8>,
497    peer_addr: SocketAddr,
498    connection_id: ConnectionId,
499) {
500    let ws_config = WebSocketConfig {
501        max_message_size: Some(wire::MAX_MESSAGE_BYTES),
502        max_frame_size: Some(wire::MAX_MESSAGE_BYTES),
503        // RFC 6455 §5.1: a server MUST fail the connection on an unmasked client frame.
504        accept_unmasked_frames: false,
505        ..Default::default()
506    };
507    // `from_partially_read`: a client may pipeline its first message behind the request head.
508    let ws =
509        WebSocketStream::from_partially_read(socket, leftover, Role::Server, Some(ws_config)).await;
510    let (sink, stream) = ws.split();
511
512    let (out_tx, out_rx) = mpsc::unbounded_channel::<Outbound>();
513    let conn = Arc::new(ConnShared::new(connection_id.as_u32(), out_tx.clone()));
514    server.relay.add(conn.clone());
515    let protocol = Arc::new(NostrProtocol::for_connection(
516        server.key.clone(),
517        conn.clone(),
518    ));
519
520    let peer_rx = crate::server::peer_support::register_peer_channel(
521        &server.app_state,
522        server.server_id,
523        connection_id.as_u32(),
524    )
525    .await;
526    crate::server::peer_support::spawn_peer_command_task(
527        peer_rx,
528        protocol.clone(),
529        server.app_state.clone(),
530        server.server_id,
531        connection_id.as_u32(),
532        Arc::new(Mutex::new(FrameWriter { tx: out_tx.clone() })),
533        server.status_tx.clone(),
534    );
535
536    server
537        .app_state
538        .with_server_mut(server.server_id, |instance| {
539            if let Some(c) = instance.connections.get_mut(&connection_id) {
540                c.protocol_info = ProtocolConnectionInfo::new(serde_json::json!({
541                    "state": "WebSocket",
542                }));
543            }
544        })
545        .await;
546    server.log().info(format!(
547        "Nostr client {} connected from {}",
548        connection_id, peer_addr
549    ));
550
551    let session = Session {
552        server: server.clone(),
553        conn,
554        protocol,
555        activity: Arc::new(ConnectionActivity::new()),
556        connection_id,
557    };
558    let (job_tx, job_rx) = mpsc::channel::<Job>(MAX_QUEUED_MESSAGES);
559
560    let writer = session.write_frames(sink, out_rx);
561    let reader = session.read_frames(stream, job_tx);
562    let worker = session.answer_jobs(job_rx);
563    tokio::pin!(writer, reader, worker);
564
565    let exit = tokio::select! {
566        exit = &mut reader => Some(exit),
567        _ = &mut writer => None,
568        // Ends only once the reader has dropped the queue's sender, i.e. never first.
569        _ = &mut worker => None,
570    };
571    match exit {
572        // The writer sent a close frame (the model's close_connection, the operator's
573        // disconnect) or failed: give the peer a moment to answer the close.
574        None => {
575            let _ = tokio::time::timeout(CLOSE_GRACE, &mut reader).await;
576        }
577        Some(exit) => {
578            match exit {
579                ReaderExit::Idle => {
580                    session.server.log().info(format!(
581                        "Nostr {} sent no frame for {}s, not even a Pong; closing \
582                         decision=fail_closed_idle_timeout",
583                        connection_id,
584                        session.server.config.idle_timeout.as_secs()
585                    ));
586                    let _ = out_tx.send(Outbound::Close {
587                        code: 1001,
588                        reason: "idle timeout".to_string(),
589                    });
590                }
591                ReaderExit::TooBig => {
592                    session.server.log().warn(format!(
593                        "Nostr {} sent a message over {} bytes; closing with 1009 \
594                         decision=fail_closed_message_too_large",
595                        connection_id,
596                        wire::MAX_MESSAGE_BYTES
597                    ));
598                    let _ = out_tx.send(Outbound::Close {
599                        code: 1009,
600                        reason: "message too big".to_string(),
601                    });
602                }
603                ReaderExit::PeerClosed | ReaderExit::Error => {}
604            }
605            let _ = out_tx.send(Outbound::Shutdown);
606            let _ = tokio::time::timeout(CLOSE_GRACE, &mut writer).await;
607        }
608    }
609    session.server.log().info(format!(
610        "Nostr client {} from {} disconnected",
611        connection_id, peer_addr
612    ));
613}
614
615impl Session {
616    fn log(&self) -> Log<'_> {
617        self.server.log()
618    }
619
620    async fn write_frames(
621        &self,
622        mut sink: SplitSink<WebSocketStream<TcpStream>, Message>,
623        mut out_rx: mpsc::UnboundedReceiver<Outbound>,
624    ) {
625        while let Some(out) = out_rx.recv().await {
626            let (message, closing) = match out {
627                Outbound::Text(text) => (Message::Text(text), false),
628                Outbound::Ping(payload) => (Message::Ping(payload), false),
629                Outbound::Close { code, reason } => (
630                    Message::Close(Some(CloseFrame {
631                        code: CloseCode::from(code),
632                        reason: reason.into(),
633                    })),
634                    true,
635                ),
636                Outbound::Shutdown => {
637                    let _ = sink.flush().await;
638                    return;
639                }
640            };
641            let len = match &message {
642                Message::Text(t) => t.len(),
643                _ => 0,
644            };
645            if sink.send(message).await.is_err() {
646                return;
647            }
648            if len > 0 {
649                self.server
650                    .app_state
651                    .update_connection_stats(
652                        self.server.server_id,
653                        self.connection_id,
654                        None,
655                        Some(len as u64),
656                        None,
657                        Some(1),
658                    )
659                    .await;
660            }
661            if closing {
662                // RFC 6455 §7.1.2: nothing is sent after a close frame.
663                return;
664            }
665        }
666    }
667
668    async fn read_frames(
669        &self,
670        mut stream: SplitStream<WebSocketStream<TcpStream>>,
671        job_tx: mpsc::Sender<Job>,
672    ) -> ReaderExit {
673        let idle = self.server.config.idle_timeout;
674        loop {
675            let keepalive = self.conn.out_tx().clone();
676            let message = tokio::select! {
677                message = stream.next() => message,
678                _ = crate::server::accept_bounded::watch_idle_with_probe(
679                    Arc::clone(&self.activity),
680                    idle,
681                    move || {
682                        let _ = keepalive.send(Outbound::Ping(
683                            crate::server::accept_bounded::KEEPALIVE_PING_PAYLOAD.to_vec(),
684                        ));
685                    },
686                ) => return ReaderExit::Idle,
687            };
688            let Some(message) = message else {
689                return ReaderExit::PeerClosed;
690            };
691            self.activity.touch();
692            match message {
693                Ok(Message::Text(text)) => {
694                    self.server
695                        .app_state
696                        .update_connection_stats(
697                            self.server.server_id,
698                            self.connection_id,
699                            Some(text.len() as u64),
700                            None,
701                            Some(1),
702                            None,
703                        )
704                        .await;
705                    self.on_text(&text, &job_tx);
706                }
707                Ok(Message::Binary(_)) => {
708                    self.log().info(format!(
709                        "Nostr {} sent a binary frame decision=fail_closed_malformed",
710                        self.connection_id
711                    ));
712                    self.conn.send(wire::notice_message(
713                        "invalid: NIP-01 messages are JSON text frames",
714                    ));
715                }
716                Ok(Message::Close(_)) => return ReaderExit::PeerClosed,
717                Ok(_) => {}
718                Err(tokio_tungstenite::tungstenite::Error::Capacity(_)) => {
719                    return ReaderExit::TooBig
720                }
721                Err(e) => {
722                    self.log()
723                        .debug(format!("Nostr {} read error: {}", self.connection_id, e));
724                    return ReaderExit::Error;
725                }
726            }
727        }
728    }
729
730    /// One text frame: answer what is mechanical, queue what needs the model.
731    fn on_text(&self, text: &str, job_tx: &mpsc::Sender<Job>) {
732        let log = self.log();
733        match wire::parse_client_message(text) {
734            Err(refusal) => {
735                log.info(format!(
736                    "Nostr {} message refused without the model decision={}",
737                    self.connection_id, refusal.decision
738                ));
739                self.conn.send(refusal.reply);
740            }
741            Ok(ClientMessage::Close { subscription_id }) => {
742                let was_open = self.conn.close(&subscription_id);
743                log.info(format!(
744                    "Nostr {} CLOSE {} ({}) decision=subscription_closed",
745                    self.connection_id,
746                    crate::utils::truncate_for_log(&subscription_id, 64),
747                    if was_open { "closed" } else { "was not open" }
748                ));
749            }
750            Ok(ClientMessage::Req {
751                subscription_id,
752                filters,
753            }) => match self.conn.open(&subscription_id, filters.clone()) {
754                Err(message) => {
755                    log.info(format!(
756                        "Nostr {} REQ {} refused decision=fail_closed_too_many_subscriptions",
757                        self.connection_id,
758                        crate::utils::truncate_for_log(&subscription_id, 64)
759                    ));
760                    self.conn
761                        .send(wire::closed_message(&subscription_id, &message));
762                }
763                Ok(generation) => {
764                    let job = Job::Req {
765                        subscription_id: subscription_id.clone(),
766                        generation,
767                        filters,
768                    };
769                    if job_tx.try_send(job).is_err() {
770                        self.conn.close(&subscription_id);
771                        log.warn(format!(
772                            "Nostr {} REQ {} refused decision=fail_closed_queue_full",
773                            self.connection_id,
774                            crate::utils::truncate_for_log(&subscription_id, 64)
775                        ));
776                        self.conn
777                            .send(wire::closed_message(&subscription_id, QUEUE_FULL));
778                    }
779                }
780            },
781            Ok(ClientMessage::Event(event)) => {
782                let id = event.id.clone();
783                if job_tx.try_send(Job::Event(event)).is_err() {
784                    log.warn(format!(
785                        "Nostr {} EVENT {} refused decision=fail_closed_queue_full",
786                        self.connection_id, id
787                    ));
788                    self.conn.send(wire::ok_message(&id, false, QUEUE_FULL));
789                }
790            }
791        }
792    }
793
794    async fn answer_jobs(&self, mut job_rx: mpsc::Receiver<Job>) {
795        while let Some(job) = job_rx.recv().await {
796            let _busy = self.activity.busy();
797            match job {
798                Job::Event(event) => self.answer_event(event).await,
799                Job::Req {
800                    subscription_id,
801                    generation,
802                    filters,
803                } => self.answer_req(subscription_id, generation, filters).await,
804            }
805        }
806    }
807
808    async fn ask(&self, event: &Event) -> Result<crate::llm::actions::executor::ExecutionResult> {
809        call_llm(
810            &self.server.llm_client,
811            &self.server.app_state,
812            self.server.server_id,
813            Some(self.connection_id),
814            event,
815            self.protocol.as_ref(),
816        )
817        .await
818    }
819
820    async fn answer_event(&self, event: wire::Event) {
821        let log = self.log();
822        let content = crate::utils::truncate_str(&event.content, actions::MAX_CONTENT_FOR_MODEL);
823        let tags: Vec<&Vec<String>> = event
824            .tags
825            .iter()
826            .take(actions::MAX_TAGS_FOR_MODEL)
827            .collect();
828        let llm_event = Event::new(
829            &actions::NOSTR_EVENT_EVENT,
830            serde_json::json!({
831                "id": event.id,
832                "pubkey": event.pubkey,
833                "kind": event.kind,
834                "created_at": event.created_at,
835                "tags": tags,
836                "tag_count": event.tags.len(),
837                "content": content,
838                "content_truncated": content.len() < event.content.len(),
839                "answer_with": actions::event_answer_with(event.kind),
840            }),
841        );
842        let result = match self.ask(&llm_event).await {
843            Ok(result) => result,
844            Err(e) => {
845                let (category, message) = match crate::utils::WireFailure::classify(&e) {
846                    crate::utils::WireFailure::Overloaded => ("overloaded", EVENT_OVERLOADED),
847                    crate::utils::WireFailure::Unavailable => ("unavailable", EVENT_UNAVAILABLE),
848                };
849                log.warn(format!(
850                    "Nostr {} EVENT {} decision=fail_closed_llm_error category={}",
851                    self.connection_id, event.id, category
852                ));
853                log.debug(format!("Nostr LLM call failed: {}", e));
854                self.conn.send(wire::ok_message(&event.id, false, message));
855                return;
856            }
857        };
858        for message in &result.messages {
859            log.info(message);
860        }
861
862        let mut verdict: Option<std::result::Result<(), String>> = None;
863        let mut frames: Vec<String> = Vec::new();
864        let mut close = false;
865        for item in flatten(result.protocol_results) {
866            match item {
867                ActionResult::Custom { name, data } if verdict.is_none() => match name.as_str() {
868                    "accept_nostr_event" => verdict = Some(Ok(())),
869                    "reject_nostr_event" => {
870                        verdict = Some(Err(data
871                            .get("reason")
872                            .and_then(|r| r.as_str())
873                            .unwrap_or("blocked:")
874                            .to_string()))
875                    }
876                    _ => {}
877                },
878                ActionResult::Output(bytes) => frames.push(String::from_utf8_lossy(&bytes).into()),
879                ActionResult::CloseConnection => close = true,
880                _ => {}
881            }
882        }
883
884        match verdict {
885            Some(Ok(())) => {
886                self.conn.send(wire::ok_message(&event.id, true, ""));
887                let delivered = self.server.relay.broadcast(&event);
888                log.info(format!(
889                    "Nostr {} EVENT {} kind {} decision=model_answer accepted, delivered to {} \
890                     subscription(s)",
891                    self.connection_id, event.id, event.kind, delivered
892                ));
893            }
894            Some(Err(reason)) => {
895                log.info(format!(
896                    "Nostr {} EVENT {} kind {} decision=model_reject ({})",
897                    self.connection_id, event.id, event.kind, reason
898                ));
899                self.conn.send(wire::ok_message(&event.id, false, &reason));
900            }
901            None => {
902                let decision = if result.raw_actions.is_empty() {
903                    "model_silent"
904                } else {
905                    "fail_closed_bad_action"
906                };
907                log.warn(format!(
908                    "Nostr {} EVENT {} decision={} ({} failed action(s)); refusing",
909                    self.connection_id,
910                    event.id,
911                    decision,
912                    result.failures.len()
913                ));
914                self.conn
915                    .send(wire::ok_message(&event.id, false, EVENT_UNDECIDED));
916            }
917        }
918        for frame in frames {
919            self.conn.send(frame);
920        }
921        if close {
922            let _ = self.conn.out_tx().send(Outbound::Close {
923                code: 1000,
924                reason: String::new(),
925            });
926        }
927    }
928
929    async fn answer_req(
930        &self,
931        subscription_id: String,
932        generation: u64,
933        filters: Vec<wire::Filter>,
934    ) {
935        let log = self.log();
936        if !self.conn.is_open(&subscription_id, generation) {
937            log.info(format!(
938                "Nostr {} REQ {} was closed before it was answered decision=stale_request_dropped",
939                self.connection_id,
940                crate::utils::truncate_for_log(&subscription_id, 64)
941            ));
942            return;
943        }
944        let llm_event = Event::new(
945            &actions::NOSTR_REQ_EVENT,
946            serde_json::json!({
947                "subscription_id": subscription_id,
948                "filters": filters.iter().map(|f| f.raw.clone()).collect::<Vec<_>>(),
949                "relay_pubkey": self.server.key.pubkey_hex(),
950                "answer_with": actions::req_answer_with(
951                    &subscription_id,
952                    &filters,
953                    self.server.key.pubkey_hex(),
954                ),
955            }),
956        );
957        self.conn.begin_answer(&subscription_id, generation);
958        let outcome = self.ask(&llm_event).await;
959        self.conn.end_answer();
960
961        let result = match outcome {
962            Ok(result) => result,
963            Err(e) => {
964                let (category, message) = match crate::utils::WireFailure::classify(&e) {
965                    crate::utils::WireFailure::Overloaded => ("overloaded", REQ_OVERLOADED),
966                    crate::utils::WireFailure::Unavailable => ("unavailable", REQ_UNAVAILABLE),
967                };
968                log.warn(format!(
969                    "Nostr {} REQ {} decision=fail_closed_llm_error category={}",
970                    self.connection_id,
971                    crate::utils::truncate_for_log(&subscription_id, 64),
972                    category
973                ));
974                log.debug(format!("Nostr LLM call failed: {}", e));
975                if self.conn.is_open(&subscription_id, generation) {
976                    self.conn.close(&subscription_id);
977                    self.conn
978                        .send(wire::closed_message(&subscription_id, message));
979                }
980                return;
981            }
982        };
983        for message in &result.messages {
984            log.info(message);
985        }
986        let answered = result
987            .protocol_result_actions
988            .iter()
989            .any(|a| a == "send_nostr_events");
990        let mut frames: Vec<String> = Vec::new();
991        let mut close = false;
992        for item in flatten(result.protocol_results) {
993            match item {
994                ActionResult::Output(bytes) => frames.push(String::from_utf8_lossy(&bytes).into()),
995                ActionResult::CloseConnection => close = true,
996                _ => {}
997            }
998        }
999        let events_sent = frames
1000            .iter()
1001            .filter(|f| f.starts_with("[\"EVENT\""))
1002            .count();
1003        let subject = format!(
1004            "Nostr {} REQ {}",
1005            self.connection_id,
1006            crate::utils::truncate_for_log(&subscription_id, 64)
1007        );
1008
1009        if !self.conn.is_open(&subscription_id, generation) {
1010            let closed_by_model = frames.iter().any(|f| {
1011                serde_json::from_str::<serde_json::Value>(f).is_ok_and(|v| {
1012                    v.get(0).and_then(|x| x.as_str()) == Some("CLOSED")
1013                        && v.get(1).and_then(|x| x.as_str()) == Some(subscription_id.as_str())
1014                })
1015            });
1016            if closed_by_model {
1017                log.info(format!(
1018                    "{} decision=model_reject (subscription closed)",
1019                    subject
1020                ));
1021                for frame in frames {
1022                    self.conn.send(frame);
1023                }
1024            } else {
1025                log.info(format!(
1026                    "{} was closed or replaced while the model answered; its events are not \
1027                     sent decision=stale_answer_dropped",
1028                    subject
1029                ));
1030            }
1031        } else if !result.failures.is_empty() && !answered {
1032            log.warn(format!(
1033                "{} decision=fail_closed_bad_action: {}",
1034                subject,
1035                result
1036                    .failures
1037                    .iter()
1038                    .map(|f| format!("{}: {}", f.action, f.error))
1039                    .collect::<Vec<_>>()
1040                    .join("; ")
1041            ));
1042            self.conn.close(&subscription_id);
1043            self.conn
1044                .send(wire::closed_message(&subscription_id, REQ_UNAVAILABLE));
1045        } else {
1046            let decision = if result.raw_actions.is_empty() {
1047                "model_silent"
1048            } else {
1049                "model_answer"
1050            };
1051            log.info(format!(
1052                "{} decision={} events={} then EOSE",
1053                subject, decision, events_sent
1054            ));
1055            for frame in frames {
1056                self.conn.send(frame);
1057            }
1058            self.conn.send(wire::eose_message(&subscription_id));
1059        }
1060        if close {
1061            let _ = self.conn.out_tx().send(Outbound::Close {
1062                code: 1000,
1063                reason: String::new(),
1064            });
1065        }
1066    }
1067}
1068
1069/// Depth-first, in order: `Multiple` is how `send_nostr_events` returns one frame per event.
1070fn flatten(results: Vec<ActionResult>) -> Vec<ActionResult> {
1071    let mut out = Vec::new();
1072    let mut stack: Vec<ActionResult> = results.into_iter().rev().collect();
1073    while let Some(item) = stack.pop() {
1074        match item {
1075            ActionResult::Multiple(items) => stack.extend(items.into_iter().rev()),
1076            other => out.push(other),
1077        }
1078    }
1079    out
1080}