radrootsd

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

commit 1ba826b3a1e9793bdb4a552dea39ce50a9c7d0f8
parent c04ef78acc61daffa4d416a4c79dcb928993ee0a
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Fri, 22 Aug 2025 18:38:00 -0700

Edit `events/profile` rpc, add `radroots-nostr` crate.

Diffstat:
MCargo.lock | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MCargo.toml | 1+
Mcrates/radrootsd/Cargo.toml | 1+
Dcrates/radrootsd/src/infra/nostr.rs | 43-------------------------------------------
Mcrates/radrootsd/src/lib.rs | 4----
Mcrates/radrootsd/src/rpc/events/profile/mod.rs | 65++++++++++++++---------------------------------------------------
Mcrates/radrootsd/src/rpc/relays.rs | 11+++++------
7 files changed, 133 insertions(+), 106 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -355,6 +355,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] name = "chacha20" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -774,9 +780,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -954,6 +962,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 1.0.2", ] [[package]] @@ -990,7 +999,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", "system-configuration", "tokio", "tower-service", @@ -1344,6 +1353,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" [[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1783,6 +1798,61 @@ dependencies = [ ] [[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.16", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.16", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1826,6 +1896,20 @@ dependencies = [ ] [[package]] +name = "radroots-nostr" +version = "0.1.0" +dependencies = [ + "nostr", + "nostr-sdk", + "radroots-events", + "radroots-events-codec", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] name = "radroots-runtime" version = "0.1.0" dependencies = [ @@ -1855,6 +1939,7 @@ dependencies = [ "radroots-core", "radroots-events", "radroots-events-codec", + "radroots-nostr", "radroots-runtime", "reqwest", "serde", @@ -2001,6 +2086,8 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -2008,6 +2095,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -2015,6 +2103,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 1.0.2", ] [[package]] @@ -2125,6 +2214,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ + "web-time", "zeroize", ] @@ -2343,6 +2433,16 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" @@ -2579,7 +2679,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2", + "socket2 0.6.0", "tokio-macros", "windows-sys 0.59.0", ] @@ -3089,6 +3189,16 @@ dependencies = [ ] [[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "webpki-roots" version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -14,6 +14,7 @@ license = "AGPL-3.0" radroots-core = { path = "../../crates/crates/core" } radroots-events = { path = "../../crates/crates/events" } radroots-events-codec = { path = "../../crates/crates/events-codec" } +radroots-nostr = { path = "../../crates/crates/nostr" } radroots-runtime = { path = "../../crates/crates/runtime" } anyhow = { version = "1" } diff --git a/crates/radrootsd/Cargo.toml b/crates/radrootsd/Cargo.toml @@ -11,6 +11,7 @@ description = "The radroots daemon binary" radroots-core = { workspace = true, features = ["std", "serde", "typeshare"] } radroots-events = { workspace = true, features = ["serde"] } radroots-events-codec = { workspace = true, features = ["nostr"] } +radroots-nostr = { workspace = true, features = ["sdk", "codec", "http"] } radroots-runtime = { workspace = true, features = ["cli"] } anyhow = { workspace = true } diff --git a/crates/radrootsd/src/infra/nostr.rs b/crates/radrootsd/src/infra/nostr.rs @@ -1,43 +0,0 @@ -use nostr::{key::PublicKey, nips::nip19::FromBech32}; -use radroots_events::relay_document::models::RadrootsRelayDocument; - -use crate::{rpc::RpcError, utils::ws_to_http}; - -#[derive(Debug, thiserror::Error)] -pub enum NostrError { - #[error("invalid pubkey format: {0}")] - InvalidPubkey(String), -} - -impl From<NostrError> for RpcError { - fn from(err: NostrError) -> Self { - RpcError::InvalidParams(err.to_string()) - } -} - -pub fn parse_pubkey(s: &str) -> Result<PublicKey, NostrError> { - PublicKey::from_bech32(s) - .or_else(|_| PublicKey::from_hex(s)) - .map_err(|_| NostrError::InvalidPubkey(s.to_string())) -} - -pub fn parse_pubkeys(input: &[String]) -> Result<Vec<PublicKey>, RpcError> { - input - .iter() - .map(|s| parse_pubkey(s).map_err(Into::into)) - .collect() -} - -pub async fn fetch_nip11(ws_url: &str) -> Option<RadrootsRelayDocument> { - let http_url = ws_to_http(ws_url)?; - let client = reqwest::Client::new(); - client - .get(&http_url) - .header("Accept", "application/nostr+json") - .send() - .await - .ok()? - .json::<RadrootsRelayDocument>() - .await - .ok() -} diff --git a/crates/radrootsd/src/lib.rs b/crates/radrootsd/src/lib.rs @@ -1,12 +1,8 @@ pub mod cli; pub mod config; -pub mod infra { - pub mod nostr; -} pub mod identity; pub mod radrootsd; pub mod rpc; -pub mod utils; use anyhow::Result; diff --git a/crates/radrootsd/src/rpc/events/profile/mod.rs b/crates/radrootsd/src/rpc/events/profile/mod.rs @@ -2,7 +2,6 @@ use std::time::Duration; use anyhow::Result; use jsonrpsee::RpcModule; -use nostr::nips::nip19::ToBech32; use serde::Deserialize; use serde_json::{Value as JsonValue, json}; @@ -12,7 +11,9 @@ use crate::rpc::RpcError; use radroots_events::profile::models::RadrootsProfile; use radroots_events_codec::profile::encode::to_metadata; -use nostr_sdk::prelude::EventBuilder; +use radroots_nostr::prelude::{ + build_metadata_event, fetch_latest_metadata_for_author, nostr_send_event, npub_string, +}; #[derive(Debug, Deserialize)] struct PublishProfileParams { @@ -27,49 +28,14 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { return Err(RpcError::NoRelays); } - let ctx_pk = ctx.pubkey; + let me_pk = ctx.pubkey; - let filter = nostr::Filter::new() - .authors(vec![ctx_pk]) - .kind(nostr::Kind::Metadata); - - let stored = ctx - .client - .database() - .query(filter.clone()) - .await - .map_err(|e| RpcError::Other(format!("database query failed: {e}")))?; - let fetched = ctx - .client - .fetch_events(filter, Duration::from_secs(10)) + let latest = fetch_latest_metadata_for_author(&ctx.client, me_pk, Duration::from_secs(10)) .await - .map_err(|e| RpcError::Other(format!("network fetch failed: {e}")))?; - - let mut latest: Option<nostr::Event> = None; - - let mut consider = |ev: nostr::Event| { - if ev.kind != nostr::Kind::Metadata { - return; - } - if let Some(cur) = &latest { - if ev.created_at > cur.created_at { - latest = Some(ev); - } - } else { - latest = Some(ev); - } - }; + .map_err(|e| RpcError::Other(format!("metadata fetch failed: {e}")))?; - for ev in stored.into_iter() { - consider(ev); - } - for ev in fetched.into_iter() { - consider(ev); - } - - let ctx_npub = ctx_pk - .to_bech32() - .map_err(|e| RpcError::Other(format!("bech32 encode failed: {e}")))?; + let npub = + npub_string(&me_pk).ok_or_else(|| RpcError::Other("bech32 encode failed".into()))?; let row = if let Some(ev) = latest { let parsed: Option<serde_json::Value> = serde_json::from_str(&ev.content).ok(); @@ -77,8 +43,8 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { serde_json::from_str(&ev.content).ok(); json!({ - "author_hex": ctx_pk.to_string(), - "author_npub": ctx_npub, + "author_hex": me_pk.to_string(), + "author_npub": npub, "event_id": ev.id.to_string(), "created_at": ev.created_at.as_u64(), "content": ev.content, @@ -87,8 +53,8 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { }) } else { json!({ - "author_hex": ctx_pk.to_string(), - "author_npub": ctx_npub, + "author_hex": me_pk.to_string(), + "author_npub": npub, "event_id": null, "created_at": null, "content": null, @@ -111,12 +77,9 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { .map_err(|e| RpcError::InvalidParams(e.to_string()))?; let metadata = to_metadata(&profile).map_err(|e| RpcError::InvalidParams(e.to_string()))?; + let builder = build_metadata_event(&metadata); - let builder = EventBuilder::metadata(&metadata); - - let output = ctx - .client - .send_event_builder(builder) + let output = nostr_send_event(&ctx.client, builder) .await .map_err(|e| RpcError::Other(format!("failed to publish metadata: {e}")))?; diff --git a/crates/radrootsd/src/rpc/relays.rs b/crates/radrootsd/src/rpc/relays.rs @@ -3,10 +3,11 @@ use jsonrpsee::RpcModule; use serde::Deserialize; use serde_json::{Value as JsonValue, json}; -use crate::infra::nostr::fetch_nip11; 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, @@ -30,8 +31,7 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { let AddParams { url } = params .parse() .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - ctx.client - .add_relay(&url) + add_relay(&ctx.client, &url) .await .map_err(|e| RpcError::AddRelay(url.clone(), e.to_string()))?; @@ -43,8 +43,7 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { .parse() .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - ctx.client - .force_remove_relay(&url) + remove_relay(&ctx.client, &url) .await .map_err(|e| RpcError::Other(format!("failed to remove relay {url}: {e}")))?; @@ -109,7 +108,7 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { return Err(RpcError::NoRelays); } let client = ctx.client.clone(); - tokio::spawn(async move { client.connect().await }); + tokio::spawn(async move { connect(&client).await }); Ok::<JsonValue, RpcError>(json!({ "connecting": relays.len() })) })?;