radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit 8339260d6b70db00d407d9473011bb4e80a21b1c
parent 59cf13debba2a7040a8851078480f74225d9f08a
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Sat, 23 Aug 2025 18:59:59 +0000

Edit `relays` rpc methods file tree, update/fix nostr utils usage.

Diffstat:
Mcrates/radrootsd/src/rpc/error.rs | 10+++++++++-
Mcrates/radrootsd/src/rpc/events/profile/list.rs | 4++--
Dcrates/radrootsd/src/rpc/relays.rs | 117-------------------------------------------------------------------------------
Acrates/radrootsd/src/rpc/relays/add.rs | 28++++++++++++++++++++++++++++
Acrates/radrootsd/src/rpc/relays/connect.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Acrates/radrootsd/src/rpc/relays/list.rs | 16++++++++++++++++
Acrates/radrootsd/src/rpc/relays/mod.rs | 22++++++++++++++++++++++
Acrates/radrootsd/src/rpc/relays/remove.rs | 28++++++++++++++++++++++++++++
Acrates/radrootsd/src/rpc/relays/status.rs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/radrootsd/src/rpc/system.rs | 17++++++++++++++++-
Dcrates/radrootsd/src/utils.rs | 13-------------
11 files changed, 224 insertions(+), 134 deletions(-)

diff --git a/crates/radrootsd/src/rpc/error.rs b/crates/radrootsd/src/rpc/error.rs @@ -9,12 +9,20 @@ pub enum RpcError { NoRelays, #[error("invalid params: {0}")] InvalidParams(String), + #[error("method not found: {0}")] + MethodNotFound(String), #[error("{0}")] Other(String), } impl From<RpcError> for ErrorObjectOwned { fn from(err: RpcError) -> Self { - ErrorObject::owned(-32000, err.to_string(), None::<()>) + match err { + RpcError::InvalidParams(msg) => ErrorObject::owned(-32602, msg, None::<()>), + RpcError::MethodNotFound(name) => { + ErrorObject::owned(-32601, format!("method not found: {name}"), None::<()>) + } + other => ErrorObject::owned(-32000, other.to_string(), None::<()>), + } } } diff --git a/crates/radrootsd/src/rpc/events/profile/list.rs b/crates/radrootsd/src/rpc/events/profile/list.rs @@ -4,7 +4,7 @@ use serde_json::{Value as JsonValue, json}; use std::time::Duration; use crate::{radrootsd::Radrootsd, rpc::RpcError}; -use radroots_nostr::prelude::{fetch_latest_metadata_for_author, npub_string}; +use radroots_nostr::prelude::{fetch_metadata_for_author, npub_string}; pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { m.register_async_method("events.profile.list", |_params, ctx, _| async move { @@ -14,7 +14,7 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { let me_pk = ctx.pubkey; - let latest = fetch_latest_metadata_for_author(&ctx.client, me_pk, Duration::from_secs(10)) + let latest = fetch_metadata_for_author(&ctx.client, me_pk, Duration::from_secs(10)) .await .map_err(|e| RpcError::Other(format!("metadata fetch failed: {e}")))?; diff --git a/crates/radrootsd/src/rpc/relays.rs b/crates/radrootsd/src/rpc/relays.rs @@ -1,117 +0,0 @@ -use anyhow::Result; -use jsonrpsee::RpcModule; -use serde::Deserialize; -use serde_json::{Value as JsonValue, json}; - -use crate::radrootsd::Radrootsd; -use crate::rpc::RpcError; - -use radroots_nostr::prelude::{add_relay, connect, fetch_nip11, remove_relay}; - -#[derive(Debug, Deserialize)] -struct AddParams { - url: String, -} - -#[derive(Debug, Deserialize)] -struct RemoveParams { - url: String, -} - -#[derive(Debug, Deserialize)] -struct StatusParams { - #[serde(default)] - include_nip11: bool, -} - -pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { - let mut m = RpcModule::new(radrootsd); - - m.register_async_method("relays.add", |params, ctx, _| async move { - let AddParams { url } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - add_relay(&ctx.client, &url) - .await - .map_err(|e| RpcError::AddRelay(url.clone(), e.to_string()))?; - - Ok::<JsonValue, RpcError>(json!({ "added": url })) - })?; - - m.register_async_method("relays.remove", |params, ctx, _| async move { - let RemoveParams { url } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - remove_relay(&ctx.client, &url) - .await - .map_err(|e| RpcError::Other(format!("failed to remove relay {url}: {e}")))?; - - Ok::<JsonValue, RpcError>(json!({ "removed": url })) - })?; - - m.register_async_method("relays.list", |_p, ctx, _| async move { - let relays = ctx.client.relays().await; - Ok::<JsonValue, RpcError>(json!( - relays.keys().map(|u| u.to_string()).collect::<Vec<_>>() - )) - })?; - - m.register_async_method("relays.status", |params, ctx, _| async move { - let StatusParams { include_nip11 } = params.parse().unwrap_or(StatusParams { - include_nip11: false, - }); - - let relays = ctx.client.relays().await; - - let mut out = Vec::with_capacity(relays.len()); - - for (relay_url, relay) in relays { - let url_str = relay_url.to_string(); - let parsed = reqwest::Url::parse(&url_str).ok(); - - let scheme = parsed.as_ref().map(|u| u.scheme().to_string()); - let host = parsed - .as_ref() - .and_then(|u| u.host_str()) - .map(|s| s.to_string()); - let port = parsed.as_ref().and_then(|u| u.port()); - let onion = host - .as_deref() - .map(|h| h.ends_with(".onion")) - .unwrap_or(false); - - let mut row = json!({ - "url": url_str, - "status": format!("{}", relay.status()), - "scheme": scheme, - "host": host, - "port": port, - "onion": onion - }); - - if include_nip11 { - if let Some(doc) = fetch_nip11(row["url"].as_str().unwrap()).await { - row["nip11"] = json!(doc); - } - } - - out.push(row); - } - - Ok::<JsonValue, RpcError>(json!(out)) - })?; - - m.register_async_method("relays.connect", |_p, ctx, _| async move { - let relays = ctx.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - let client = ctx.client.clone(); - tokio::spawn(async move { connect(&client).await }); - - Ok::<JsonValue, RpcError>(json!({ "connecting": relays.len() })) - })?; - - Ok(m) -} diff --git a/crates/radrootsd/src/rpc/relays/add.rs b/crates/radrootsd/src/rpc/relays/add.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use jsonrpsee::RpcModule; +use radroots_nostr::prelude::add_relay; +use serde::Deserialize; +use serde_json::{Value as JsonValue, json}; + +use crate::radrootsd::Radrootsd; +use crate::rpc::RpcError; + +#[derive(Debug, Deserialize)] +struct AddParams { + url: String, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("relays.add", |params, ctx, _| async move { + let AddParams { url } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + add_relay(&ctx.client, &url) + .await + .map_err(|e| RpcError::AddRelay(url.clone(), e.to_string()))?; + + Ok::<JsonValue, RpcError>(json!({ "added": url })) + })?; + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/relays/connect.rs b/crates/radrootsd/src/rpc/relays/connect.rs @@ -0,0 +1,45 @@ +use anyhow::Result; +use jsonrpsee::RpcModule; +use serde_json::{Value as JsonValue, json}; + +use crate::radrootsd::Radrootsd; +use crate::rpc::RpcError; + +use nostr_sdk::RelayStatus; +use radroots_nostr::prelude::connect; + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("relays.connect", |_p, ctx, _| async move { + let relays = ctx.client.relays().await; + if relays.is_empty() { + return Err(RpcError::NoRelays); + } + + let mut connected = 0usize; + let mut connecting = 0usize; + let mut disconnected = 0usize; + + for (_, r) in &relays { + match r.status() { + RelayStatus::Connected => connected += 1, + RelayStatus::Connecting => connecting += 1, + _ => disconnected += 1, + } + } + + // Idempotent: only spawn if we have anything not connected/connecting + let need_connect = disconnected > 0; + if need_connect { + let client = ctx.client.clone(); + tokio::spawn(async move { connect(&client).await }); + } + + Ok::<JsonValue, RpcError>(json!({ + "connected": connected, + "connecting": connecting, + "disconnected": disconnected, + "spawned_connect": need_connect + })) + })?; + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/relays/list.rs b/crates/radrootsd/src/rpc/relays/list.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use jsonrpsee::RpcModule; +use serde_json::{Value as JsonValue, json}; + +use crate::radrootsd::Radrootsd; +use crate::rpc::RpcError; + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("relays.list", |_p, ctx, _| async move { + let relays = ctx.client.relays().await; + Ok::<JsonValue, RpcError>(json!( + relays.keys().map(|u| u.to_string()).collect::<Vec<_>>() + )) + })?; + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/relays/mod.rs b/crates/radrootsd/src/rpc/relays/mod.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use jsonrpsee::RpcModule; + +use crate::radrootsd::Radrootsd; + +pub mod add; +pub mod connect; +pub mod list; +pub mod remove; +pub mod status; + +pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { + let mut m = RpcModule::new(radrootsd); + + add::register(&mut m)?; + remove::register(&mut m)?; + list::register(&mut m)?; + status::register(&mut m)?; + connect::register(&mut m)?; + + Ok(m) +} diff --git a/crates/radrootsd/src/rpc/relays/remove.rs b/crates/radrootsd/src/rpc/relays/remove.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use jsonrpsee::RpcModule; +use radroots_nostr::prelude::remove_relay; +use serde::Deserialize; +use serde_json::{Value as JsonValue, json}; + +use crate::radrootsd::Radrootsd; +use crate::rpc::RpcError; + +#[derive(Debug, Deserialize)] +struct RemoveParams { + url: String, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("relays.remove", |params, ctx, _| async move { + let RemoveParams { url } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + remove_relay(&ctx.client, &url) + .await + .map_err(|e| RpcError::Other(format!("failed to remove relay {url}: {e}")))?; + + Ok::<JsonValue, RpcError>(json!({ "removed": url })) + })?; + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/relays/status.rs b/crates/radrootsd/src/rpc/relays/status.rs @@ -0,0 +1,58 @@ +use anyhow::Result; +use jsonrpsee::RpcModule; +use serde::Deserialize; +use serde_json::{Map as JsonMap, Value as JsonValue, json}; + +use crate::radrootsd::Radrootsd; +use crate::rpc::RpcError; +use radroots_nostr::prelude::fetch_nip11; + +#[derive(Debug, Deserialize)] +struct StatusParams { + #[serde(default)] + include_nip11: bool, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("relays.status", |params, ctx, _| async move { + let StatusParams { include_nip11 } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + let relays = ctx.client.relays().await; + let mut out = Vec::with_capacity(relays.len()); + + for (relay_url, relay) in relays { + let url_str = relay_url.to_string(); + let status_str = format!("{}", relay.status()); + let parsed = reqwest::Url::parse(&url_str).ok(); + + // Build with locals; only insert present fields. + let mut row = JsonMap::new(); + row.insert("url".into(), json!(url_str)); + row.insert("status".into(), json!(status_str)); + + if let Some(u) = &parsed { + row.insert("scheme".into(), json!(u.scheme())); + if let Some(h) = u.host_str() { + row.insert("host".into(), json!(h)); + row.insert("onion".into(), json!(h.ends_with(".onion"))); + } + if let Some(p) = u.port() { + row.insert("port".into(), json!(p)); + } + } + + if include_nip11 { + if let Some(doc) = fetch_nip11(row["url"].as_str().unwrap()).await { + row.insert("nip11".into(), json!(doc)); + } + } + + out.push(JsonValue::Object(row)); + } + + Ok::<JsonValue, RpcError>(json!(out)) + })?; + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/system.rs b/crates/radrootsd/src/rpc/system.rs @@ -19,7 +19,22 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { })?; m.register_method("system.help", |_p, _ctx, _| { - vec!["system.ping", "system.get_info", "system.help"] + vec![ + /* %% radrootsd-methods %% */ + "system.get_info", + "system.help", + "system.ping", + "events.note.list", + "events.note.publish", + "events.profile.list", + "events.profile.publish", + "relays.add", + "relays.connect", + "relays.list", + "relays.remove", + "relays.status", + /* %% radrootsd-methods %% */ + ] })?; Ok(m) diff --git a/crates/radrootsd/src/utils.rs b/crates/radrootsd/src/utils.rs @@ -1,13 +0,0 @@ -pub fn ws_to_http(ws: &str) -> Option<String> { - let mut u = reqwest::Url::parse(ws).ok()?; - let scheme = u.scheme().to_owned(); - - let new_scheme = match scheme.as_str() { - "wss" => "https", - "ws" => "http", - other => other, - }; - - u.set_scheme(new_scheme).ok()?; - Some(u.into()) -}