radrootsd

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

commit ef41bf822c833592e422d5d75b8746ecc2b24798
parent 1d6d2726f613a9e7301b11cf897be22e4f3f2f6c
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Sun, 24 Aug 2025 04:41:39 +0000

Add `events/listing` rpc with publish and list methods.

Diffstat:
MCargo.toml | 1+
Acrates/radrootsd/src/rpc/events/listing/list.rs | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/radrootsd/src/rpc/events/listing/mod.rs | 14++++++++++++++
Acrates/radrootsd/src/rpc/events/listing/publish.rs | 42++++++++++++++++++++++++++++++++++++++++++
Mcrates/radrootsd/src/rpc/events/mod.rs | 1+
Mcrates/radrootsd/src/rpc/mod.rs | 1+
Mcrates/radrootsd/src/rpc/relays/connect.rs | 1-
Mcrates/radrootsd/src/rpc/relays/status.rs | 1-
Mcrates/radrootsd/src/rpc/system.rs | 2++
9 files changed, 138 insertions(+), 2 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -16,6 +16,7 @@ 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" } +radroots-trade = { path = "../../crates/crates/trade" } anyhow = { version = "1" } clap = { version = "4" } diff --git a/crates/radrootsd/src/rpc/events/listing/list.rs b/crates/radrootsd/src/rpc/events/listing/list.rs @@ -0,0 +1,77 @@ +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::Deserialize; +use serde_json::{Value as JsonValue, json}; +use std::time::Duration; + +use crate::{radrootsd::Radrootsd, rpc::RpcError}; +use nostr::{Kind, filter::Filter}; +use radroots_nostr::prelude::parse_pubkeys; + +#[derive(Debug, Default, Deserialize)] +struct ListListingParams { + #[serde(default)] + authors: Option<Vec<String>>, + #[serde(default)] + limit: Option<u64>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("events.listing.list", |params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let ListListingParams { authors, limit } = params.parse().unwrap_or_default(); + let limit = limit.unwrap_or(50).min(1000); + + let mut filter = Filter::new().limit((limit as u16).into()); + + let kinds: Vec<u32> = vec![30402]; + let kinds_conv = kinds + .into_iter() + .map(|k| Kind::Custom(k as u16)) + .collect::<Vec<_>>(); + filter = filter.kinds(kinds_conv); + + if let Some(auths) = authors { + let pks = parse_pubkeys(&auths) + .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?; + filter = filter.authors(pks); + } else { + filter = filter.author(ctx.pubkey); + } + + let events = ctx + .client + .fetch_events(filter, Duration::from_secs(10)) + .await + .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; + + let items = events + .into_iter() + .map(|ev| { + let tags: Vec<Vec<String>> = + ev.tags.iter().map(|t| t.as_slice().to_vec()).collect(); + let listing = serde_json::from_str::< + radroots_events::listing::models::RadrootsListing, + >(&ev.content) + .ok(); + + json!({ + "id": ev.id.to_string(), + "author": ev.pubkey.to_string(), + "created_at": ev.created_at.as_u64(), + "kind": ev.kind.as_u16() as u32, + "tags": tags, + "content": ev.content, + "sig": ev.sig.to_string(), + "listing": listing, + }) + }) + .collect::<Vec<_>>(); + + Ok::<JsonValue, RpcError>(json!({ "listings": items })) + })?; + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/events/listing/mod.rs b/crates/radrootsd/src/rpc/events/listing/mod.rs @@ -0,0 +1,14 @@ +use anyhow::Result; +use jsonrpsee::server::RpcModule; + +use crate::radrootsd::Radrootsd; + +pub mod list; +pub mod publish; + +pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { + let mut m = RpcModule::new(radrootsd); + list::register(&mut m)?; + publish::register(&mut m)?; + Ok(m) +} diff --git a/crates/radrootsd/src/rpc/events/listing/publish.rs b/crates/radrootsd/src/rpc/events/listing/publish.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::Deserialize; +use serde_json::{Value as JsonValue, json}; + +use crate::{radrootsd::Radrootsd, rpc::RpcError}; +use radroots_events::listing::models::RadrootsListing; +use radroots_nostr::prelude::{build_nostr_event, nostr_send_event}; + +#[derive(Debug, Deserialize)] +struct PublishListingParams { + listing: RadrootsListing, + #[serde(default)] + tags: Option<Vec<Vec<String>>>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("events.listing.publish", |params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let PublishListingParams { listing, tags } = + params.parse().map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + let content = serde_json::to_string(&listing) + .map_err(|e| RpcError::InvalidParams(format!("invalid listing json: {e}")))?; + let builder = build_nostr_event(30402, content, tags.unwrap_or_default()) + .map_err(|e| RpcError::Other(format!("failed to build listing event: {e}")))?; + + let out = nostr_send_event(&ctx.client, builder) + .await + .map_err(|e| RpcError::Other(format!("failed to publish listing: {e}")))?; + + Ok::<JsonValue, RpcError>(json!({ + "id": out.id().to_string(), + "sent": out.success.into_iter().map(|u| u.to_string()).collect::<Vec<_>>(), + "failed": out.failed.into_iter().map(|(u,e)| (u.to_string(), e.to_string())).collect::<Vec<_>>(), + })) + })?; + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/events/mod.rs b/crates/radrootsd/src/rpc/events/mod.rs @@ -1,2 +1,3 @@ +pub mod listing; pub mod note; pub mod profile; diff --git a/crates/radrootsd/src/rpc/mod.rs b/crates/radrootsd/src/rpc/mod.rs @@ -20,6 +20,7 @@ pub async fn start_rpc(radrootsd: Radrootsd, addr: SocketAddr) -> Result<ServerH root.merge(relays::module(radrootsd.clone())?)?; root.merge(events::profile::module(radrootsd.clone())?)?; root.merge(events::note::module(radrootsd.clone())?)?; + root.merge(events::listing::module(radrootsd.clone())?)?; let handle = server.start(root); Ok(handle) diff --git a/crates/radrootsd/src/rpc/relays/connect.rs b/crates/radrootsd/src/rpc/relays/connect.rs @@ -27,7 +27,6 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { } } - // Idempotent: only spawn if we have anything not connected/connecting let need_connect = disconnected > 0; if need_connect { let client = ctx.client.clone(); diff --git a/crates/radrootsd/src/rpc/relays/status.rs b/crates/radrootsd/src/rpc/relays/status.rs @@ -27,7 +27,6 @@ pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { 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)); diff --git a/crates/radrootsd/src/rpc/system.rs b/crates/radrootsd/src/rpc/system.rs @@ -24,6 +24,8 @@ pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { "system.get_info", "system.help", "system.ping", + "events.listing.list", + "events.listing.publish", "events.note.list", "events.note.publish", "events.profile.list",