use serde_json::{Value, json};
use crate::nostr_event::NostrEvent;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Filter {
pub ids: Vec<String>,
pub authors: Vec<String>,
pub kinds: Vec<u32>,
pub p_tags: Vec<String>,
pub since: Option<i64>,
pub until: Option<i64>,
pub limit: Option<usize>,
}
impl Filter {
pub fn to_json(&self) -> Value {
let mut m = serde_json::Map::new();
if !self.ids.is_empty() {
m.insert("ids".into(), json!(self.ids));
}
if !self.authors.is_empty() {
m.insert("authors".into(), json!(self.authors));
}
if !self.kinds.is_empty() {
m.insert("kinds".into(), json!(self.kinds));
}
if !self.p_tags.is_empty() {
m.insert("#p".into(), json!(self.p_tags));
}
if let Some(s) = self.since {
m.insert("since".into(), json!(s));
}
if let Some(u) = self.until {
m.insert("until".into(), json!(u));
}
if let Some(l) = self.limit {
m.insert("limit".into(), json!(l));
}
Value::Object(m)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ClientMessage {
Event(NostrEvent),
Req {
sub_id: String,
filters: Vec<Filter>,
},
Close(String),
}
impl ClientMessage {
pub fn to_json_string(&self) -> String {
let v = match self {
ClientMessage::Event(e) => json!(["EVENT", e]),
ClientMessage::Req { sub_id, filters } => {
let mut arr = vec![json!("REQ"), json!(sub_id)];
arr.extend(filters.iter().map(Filter::to_json));
Value::Array(arr)
}
ClientMessage::Close(sub) => json!(["CLOSE", sub]),
};
serde_json::to_string(&v).expect("client message always serializes")
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RelayMessage {
Event { sub_id: String, event: NostrEvent },
Ok {
event_id: String,
accepted: bool,
message: String,
},
Eose(String),
Closed { sub_id: String, message: String },
Notice(String),
Unknown(String),
}
#[derive(Debug, PartialEq, Eq)]
pub enum RelayParseError {
NotJson,
NotArray,
NoType,
BadShape,
BadEvent,
}
impl std::fmt::Display for RelayParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
RelayParseError::NotJson => "relay message is not JSON",
RelayParseError::NotArray => "relay message is not a JSON array",
RelayParseError::NoType => "relay message has no string type tag",
RelayParseError::BadShape => "relay message has the wrong shape for its type",
RelayParseError::BadEvent => "relay EVENT carried a malformed event",
};
write!(f, "{s}")
}
}
impl RelayMessage {
pub fn parse(s: &str) -> Result<RelayMessage, RelayParseError> {
let v: Value = serde_json::from_str(s).map_err(|_| RelayParseError::NotJson)?;
let arr = v.as_array().filter(|a| !a.is_empty());
let arr = arr.ok_or(RelayParseError::NotArray)?;
let tag = arr
.first()
.and_then(Value::as_str)
.ok_or(RelayParseError::NoType)?;
match tag {
"EVENT" => {
let sub_id = str_at(arr, 1)?;
let event_val = arr.get(2).ok_or(RelayParseError::BadShape)?.clone();
let event: NostrEvent =
serde_json::from_value(event_val).map_err(|_| RelayParseError::BadEvent)?;
Ok(RelayMessage::Event { sub_id, event })
}
"OK" => {
let event_id = str_at(arr, 1)?;
let accepted = arr
.get(2)
.and_then(Value::as_bool)
.ok_or(RelayParseError::BadShape)?;
let message = arr.get(3).and_then(Value::as_str).unwrap_or("").to_string();
Ok(RelayMessage::Ok {
event_id,
accepted,
message,
})
}
"EOSE" => Ok(RelayMessage::Eose(str_at(arr, 1)?)),
"CLOSED" => Ok(RelayMessage::Closed {
sub_id: str_at(arr, 1)?,
message: arr.get(2).and_then(Value::as_str).unwrap_or("").to_string(),
}),
"NOTICE" => Ok(RelayMessage::Notice(
arr.get(1).and_then(Value::as_str).unwrap_or("").to_string(),
)),
other => Ok(RelayMessage::Unknown(other.to_string())),
}
}
}
fn str_at(arr: &[Value], i: usize) -> Result<String, RelayParseError> {
arr.get(i)
.and_then(Value::as_str)
.map(str::to_string)
.ok_or(RelayParseError::BadShape)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nostr_event::wire_to_nostr;
use crate::nostr_key::generate_transport_key;
use crate::signing::{generate_keypair, sign_message_v31};
fn an_event() -> NostrEvent {
let (sk, pk) = generate_keypair();
let msg = json!({
"timestamp": "2026-06-14T12:00:00Z",
"from": "did:wire:slate-lotus-1",
"kind": 1,
"body": {"content": "hi"},
});
let wire = sign_message_v31(&msg, &sk, &pk, "slate-lotus").unwrap();
let (nsk, _x) = generate_transport_key();
wire_to_nostr(&wire, &nsk).unwrap()
}
#[test]
fn filter_omits_empty_fields() {
let f = Filter {
p_tags: vec!["abcd".into()],
kinds: vec![1, 4],
since: Some(1700000000),
..Default::default()
};
let v = f.to_json();
assert_eq!(v["#p"], json!(["abcd"]));
assert_eq!(v["kinds"], json!([1, 4]));
assert_eq!(v["since"], json!(1700000000));
assert!(v.get("ids").is_none());
assert!(v.get("authors").is_none());
assert!(v.get("until").is_none());
assert!(v.get("limit").is_none());
}
#[test]
fn client_event_serializes_to_nip01() {
let ev = an_event();
let s = ClientMessage::Event(ev.clone()).to_json_string();
let v: Value = serde_json::from_str(&s).unwrap();
assert_eq!(v[0], "EVENT");
assert_eq!(v[1]["id"], ev.id);
assert_eq!(v[1]["sig"], ev.sig);
}
#[test]
fn client_req_serializes_tag_subid_and_filters() {
let req = ClientMessage::Req {
sub_id: "wire-sub-1".into(),
filters: vec![Filter {
p_tags: vec!["mypub".into()],
kinds: vec![1],
..Default::default()
}],
};
let v: Value = serde_json::from_str(&req.to_json_string()).unwrap();
assert_eq!(v[0], "REQ");
assert_eq!(v[1], "wire-sub-1");
assert_eq!(v[2]["#p"], json!(["mypub"]));
}
#[test]
fn client_close_serializes() {
let v: Value =
serde_json::from_str(&ClientMessage::Close("s1".into()).to_json_string()).unwrap();
assert_eq!(v, json!(["CLOSE", "s1"]));
}
#[test]
fn parse_relay_event() {
let ev = an_event();
let s = serde_json::to_string(&json!(["EVENT", "sub-1", ev])).unwrap();
match RelayMessage::parse(&s).unwrap() {
RelayMessage::Event { sub_id, event } => {
assert_eq!(sub_id, "sub-1");
assert_eq!(event, ev);
}
other => panic!("expected Event, got {other:?}"),
}
}
#[test]
fn parse_ok_eose_closed_notice() {
assert_eq!(
RelayMessage::parse(r#"["OK","abc123",true,"saved"]"#).unwrap(),
RelayMessage::Ok {
event_id: "abc123".into(),
accepted: true,
message: "saved".into()
}
);
assert_eq!(
RelayMessage::parse(r#"["OK","abc123",false]"#).unwrap(),
RelayMessage::Ok {
event_id: "abc123".into(),
accepted: false,
message: String::new()
}
);
assert_eq!(
RelayMessage::parse(r#"["EOSE","sub-1"]"#).unwrap(),
RelayMessage::Eose("sub-1".into())
);
assert_eq!(
RelayMessage::parse(r#"["CLOSED","sub-1","rate-limited"]"#).unwrap(),
RelayMessage::Closed {
sub_id: "sub-1".into(),
message: "rate-limited".into()
}
);
assert_eq!(
RelayMessage::parse(r#"["NOTICE","hello"]"#).unwrap(),
RelayMessage::Notice("hello".into())
);
}
#[test]
fn parse_unknown_type_is_not_an_error() {
assert_eq!(
RelayMessage::parse(r#"["AUTH","challenge"]"#).unwrap(),
RelayMessage::Unknown("AUTH".into())
);
}
#[test]
fn parse_rejects_malformed() {
assert_eq!(
RelayMessage::parse("not json"),
Err(RelayParseError::NotJson)
);
assert_eq!(RelayMessage::parse("{}"), Err(RelayParseError::NotArray));
assert_eq!(RelayMessage::parse("[]"), Err(RelayParseError::NotArray));
assert_eq!(RelayMessage::parse("[123]"), Err(RelayParseError::NoType));
assert_eq!(
RelayMessage::parse(r#"["EVENT","sub",{"not":"an event"}]"#),
Err(RelayParseError::BadEvent)
);
assert_eq!(
RelayMessage::parse(r#"["EOSE"]"#),
Err(RelayParseError::BadShape)
);
}
#[test]
fn client_event_then_relay_event_roundtrip() {
let ev = an_event();
let published = ClientMessage::Event(ev.clone()).to_json_string();
let pv: Value = serde_json::from_str(&published).unwrap();
let echoed = serde_json::to_string(&json!(["EVENT", "s", pv[1]])).unwrap();
match RelayMessage::parse(&echoed).unwrap() {
RelayMessage::Event { event, .. } => assert_eq!(event, ev),
other => panic!("expected Event, got {other:?}"),
}
}
}