use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::event::Kind;
use crate::key::{PublicKey, PublicKeyError};
use crate::types::{Url, UrlError};
pub const CONTENT_TYPE: &str = "application/nostr+json+rpc";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Request {
pub method: String,
#[serde(default)]
pub params: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Response {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl Response {
#[must_use]
pub const fn is_error(&self) -> bool {
self.error.is_some()
}
#[must_use]
pub const fn ok(result: serde_json::Value) -> Self {
Self {
result: Some(result),
error: None,
}
}
#[must_use]
pub fn err(message: impl Into<String>) -> Self {
Self {
result: None,
error: Some(message.into()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Method {
SupportedMethods,
BanPubkey,
UnbanPubkey,
ListBannedPubkeys,
AllowPubkey,
UnallowPubkey,
ListAllowedPubkeys,
ListEventsNeedingModeration,
AllowEvent,
BanEvent,
ListBannedEvents,
ChangeRelayName,
ChangeRelayDescription,
ChangeRelayIcon,
AllowKind,
DisallowKind,
ListAllowedKinds,
BlockIp,
UnblockIp,
ListBlockedIps,
Custom(String),
}
impl Method {
#[must_use]
#[expect(
clippy::missing_const_for_fn,
reason = "`Self::Custom` borrows from a heap `String`"
)]
pub fn as_str(&self) -> &str {
match self {
Self::SupportedMethods => "supportedmethods",
Self::BanPubkey => "banpubkey",
Self::UnbanPubkey => "unbanpubkey",
Self::ListBannedPubkeys => "listbannedpubkeys",
Self::AllowPubkey => "allowpubkey",
Self::UnallowPubkey => "unallowpubkey",
Self::ListAllowedPubkeys => "listallowedpubkeys",
Self::ListEventsNeedingModeration => "listeventsneedingmoderation",
Self::AllowEvent => "allowevent",
Self::BanEvent => "banevent",
Self::ListBannedEvents => "listbannedevents",
Self::ChangeRelayName => "changerelayname",
Self::ChangeRelayDescription => "changerelaydescription",
Self::ChangeRelayIcon => "changerelayicon",
Self::AllowKind => "allowkind",
Self::DisallowKind => "disallowkind",
Self::ListAllowedKinds => "listallowedkinds",
Self::BlockIp => "blockip",
Self::UnblockIp => "unblockip",
Self::ListBlockedIps => "listblockedips",
Self::Custom(s) => s.as_str(),
}
}
#[must_use]
pub fn parse(token: &str) -> Self {
match token {
"supportedmethods" => Self::SupportedMethods,
"banpubkey" => Self::BanPubkey,
"unbanpubkey" => Self::UnbanPubkey,
"listbannedpubkeys" => Self::ListBannedPubkeys,
"allowpubkey" => Self::AllowPubkey,
"unallowpubkey" => Self::UnallowPubkey,
"listallowedpubkeys" => Self::ListAllowedPubkeys,
"listeventsneedingmoderation" => Self::ListEventsNeedingModeration,
"allowevent" => Self::AllowEvent,
"banevent" => Self::BanEvent,
"listbannedevents" => Self::ListBannedEvents,
"changerelayname" => Self::ChangeRelayName,
"changerelaydescription" => Self::ChangeRelayDescription,
"changerelayicon" => Self::ChangeRelayIcon,
"allowkind" => Self::AllowKind,
"disallowkind" => Self::DisallowKind,
"listallowedkinds" => Self::ListAllowedKinds,
"blockip" => Self::BlockIp,
"unblockip" => Self::UnblockIp,
"listblockedips" => Self::ListBlockedIps,
_ => Self::Custom(token.to_owned()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PubkeyEntry {
pub pubkey: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EventEntry {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IpEntry {
pub ip: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ManagementError {
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
InvalidPublicKey(#[from] PublicKeyError),
#[error(transparent)]
InvalidUrl(#[from] UrlError),
}
#[must_use]
pub fn pubkey_request(method: &Method, pubkey: &PublicKey, reason: Option<&str>) -> Request {
let mut params = vec![serde_json::Value::String(pubkey.to_hex())];
if let Some(r) = reason {
params.push(serde_json::Value::String(r.to_owned()));
}
Request {
method: method.as_str().to_owned(),
params,
}
}
#[must_use]
pub fn event_request(method: &Method, event_id_hex: &str, reason: Option<&str>) -> Request {
let mut params = vec![serde_json::Value::String(event_id_hex.to_owned())];
if let Some(r) = reason {
params.push(serde_json::Value::String(r.to_owned()));
}
Request {
method: method.as_str().to_owned(),
params,
}
}
#[must_use]
pub fn kind_request(method: &Method, kind: Kind) -> Request {
Request {
method: method.as_str().to_owned(),
params: vec![serde_json::Value::Number(kind.as_u16().into())],
}
}
#[must_use]
pub fn ip_request(method: &Method, ip: &str, reason: Option<&str>) -> Request {
let mut params = vec![serde_json::Value::String(ip.to_owned())];
if let Some(r) = reason {
params.push(serde_json::Value::String(r.to_owned()));
}
Request {
method: method.as_str().to_owned(),
params,
}
}
#[must_use]
pub fn string_request(method: &Method, value: impl Into<String>) -> Request {
Request {
method: method.as_str().to_owned(),
params: vec![serde_json::Value::String(value.into())],
}
}
#[must_use]
pub fn url_request(method: &Method, url: &Url) -> Request {
string_request(method, url.as_str())
}
#[must_use]
pub fn empty_request(method: &Method) -> Request {
Request {
method: method.as_str().to_owned(),
params: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Keys;
fn keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
#[test]
fn ban_pubkey_request_roundtrip() {
let req = pubkey_request(&Method::BanPubkey, keys().public_key(), Some("spam"));
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("banpubkey"));
assert!(json.contains("spam"));
let parsed: Request = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.method, "banpubkey");
assert_eq!(parsed.params.len(), 2);
}
#[test]
fn response_serialisation() {
let resp = Response::ok(serde_json::json!(true));
let json = serde_json::to_string(&resp).unwrap();
assert_eq!(json, r#"{"result":true}"#);
let err = Response::err("forbidden");
let json2 = serde_json::to_string(&err).unwrap();
assert!(json2.contains("forbidden"));
}
#[test]
fn method_round_trip_custom() {
assert_eq!(
Method::parse("nostr.relay.custom"),
Method::Custom("nostr.relay.custom".to_owned())
);
}
#[test]
fn pubkey_entry_roundtrip() {
let entry = PubkeyEntry {
pubkey: keys().public_key().to_hex(),
reason: Some("abuse".into()),
};
let json = serde_json::to_string(&entry).unwrap();
let parsed: PubkeyEntry = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, entry);
}
#[test]
fn empty_request_has_no_params() {
let req = empty_request(&Method::SupportedMethods);
assert!(req.params.is_empty());
}
}