1pub 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
81pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
84
85pub const IDLE_TIMEOUT: Duration = Duration::from_secs(600);
94
95const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
97
98pub const MAX_QUEUED_MESSAGES: usize = 64;
101
102const CLOSE_GRACE: Duration = Duration::from_secs(5);
105
106static 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
118const 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#[derive(Debug, Clone)]
128pub struct RelayConfig {
129 pub info: http::RelayInfo,
130 pub handshake_timeout: Duration,
131 pub idle_timeout: Duration,
132}
133
134struct 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 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
228async 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
273enum 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
425struct 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
466enum Job {
468 Event(wire::Event),
469 Req {
470 subscription_id: String,
471 generation: u64,
472 filters: Vec<wire::Filter>,
473 },
474}
475
476enum ReaderExit {
478 PeerClosed,
479 Idle,
480 TooBig,
481 Error,
482}
483
484struct 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 accept_unmasked_frames: false,
505 ..Default::default()
506 };
507 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 _ = &mut worker => None,
570 };
571 match exit {
572 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 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 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
1069fn 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}