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:
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",