notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 97a1482be3ba20a202270874e14ad9b7b9ca32dd
parent 2dd8e1879b57901096844ff831f107475e18a628
Author: kernelkind <kernelkind@gmail.com>
Date:   Mon,  2 Feb 2026 12:25:44 -0500

feat(relay): support CLOSED message

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/enostr/src/relay/message.rs | 29+++++++++++++++++++++++++++++
Mcrates/enostr/src/relay/subs_debug.rs | 6++++++
Mcrates/notedeck/src/app.rs | 6++++++
3 files changed, 41 insertions(+), 0 deletions(-)

diff --git a/crates/enostr/src/relay/message.rs b/crates/enostr/src/relay/message.rs @@ -18,6 +18,7 @@ pub enum RelayMessage<'a> { Eose(&'a str), Event(&'a str, &'a str), Notice(&'a str), + Closed(&'a str, &'a str), } #[derive(Debug)] @@ -79,6 +80,11 @@ impl<'a> RelayMessage<'a> { RelayMessage::Event(sub_id, ev) } + /// Construct a relay `CLOSED` message with its subscription id and reason. + pub fn closed(sub_id: &'a str, message: &'a str) -> Self { + RelayMessage::Closed(sub_id, message) + } + pub fn from_json(msg: &'a str) -> Result<RelayMessage<'a>> { if msg.is_empty() { return Err(Error::Empty); @@ -141,6 +147,18 @@ impl<'a> RelayMessage<'a> { )); } + // CLOSED (NIP-01) + // Relay response format: ["CLOSED", <subscription_id>, <message>] + if msg.starts_with("[\"CLOSED\"") { + let parts: Vec<&'a str> = + serde_json::from_str(msg).map_err(|err| Error::DecodeFailed(err.to_string()))?; + if parts.len() != 3 || parts[0] != "CLOSED" { + return Err(Error::DecodeFailed("Invalid CLOSED format".into())); + } + + return Ok(Self::closed(parts[1], parts[2])); + } + // OK (NIP-20) // Relay response format: ["OK",<event_id>, <true|false>, <message>] if &msg[0..=5] == "[\"OK\"," && msg.len() >= 78 { @@ -206,6 +224,13 @@ mod tests { Ok(RelayMessage::eose("random-subscription-id")), ), ( + r#"["CLOSED","sub1","error: shutting down idle subscription"]"#, + Ok(RelayMessage::closed( + "sub1", + "error: shutting down idle subscription", + )), + ), + ( r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",true,"pow: difficulty 25>=24"]"#, Ok(RelayMessage::ok( "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", @@ -246,6 +271,10 @@ mod tests { r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,404]"#, Err(Error::DecodeFailed("bad boolean value".into())), ), + ( + r#"["CLOSED","sub1"]"#, + Err(Error::DecodeFailed("Invalid CLOSED format".into())), + ), ]; for (input, expected) in tests { diff --git a/crates/enostr/src/relay/subs_debug.rs b/crates/enostr/src/relay/subs_debug.rs @@ -60,6 +60,9 @@ impl From<RelayEvent<'_>> for OwnedRelayEvent { RelayMessage::Eose(s) => format!("EOSE:{s}"), RelayMessage::Event(_, s) => format!("EVENT:{s}"), RelayMessage::Notice(s) => format!("NOTICE:{s}"), + RelayMessage::Closed(sub_id, message) => { + format!("CLOSED:{sub_id}:{message}") + } }; OwnedRelayEvent::Message(relay_msg) } @@ -249,6 +252,9 @@ fn calculate_relay_message_size(message: &RelayMessage) -> usize { RelayMessage::Eose(str_ref) | RelayMessage::Event(str_ref, _) | RelayMessage::Notice(str_ref) => mem::size_of_val(message) + str_ref.len(), + RelayMessage::Closed(sub_id, reason) => { + mem::size_of_val(message) + sub_id.len() + reason.len() + } } } diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs @@ -546,6 +546,12 @@ fn process_message_core(ctx: &mut AppContext<'_>, relay: &str, msg: &RelayMessag RelayMessage::Eose(id) => { tracing::trace!("Relay {} received eose: {id}", relay) } + RelayMessage::Closed(sid, reason) => { + tracing::trace!( + "Relay {} with sub {sid} received close because: {reason}", + relay + ); + } } }