radrootsd

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

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

Add `events/note` rpc with publish and list methods, refactor rpc module file trees.

Diffstat:
Mcrates/radrootsd/src/rpc/events/mod.rs | 1+
Acrates/radrootsd/src/rpc/events/note/list.rs | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/radrootsd/src/rpc/events/note/mod.rs | 14++++++++++++++
Acrates/radrootsd/src/rpc/events/note/publish.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/radrootsd/src/rpc/events/profile/list.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/radrootsd/src/rpc/events/profile/mod.rs | 98++++---------------------------------------------------------------------------
Acrates/radrootsd/src/rpc/events/profile/publish.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/radrootsd/src/rpc/mod.rs | 1+
8 files changed, 246 insertions(+), 93 deletions(-)

diff --git a/crates/radrootsd/src/rpc/events/mod.rs b/crates/radrootsd/src/rpc/events/mod.rs @@ -1 +1,2 @@ +pub mod note; pub mod profile; diff --git a/crates/radrootsd/src/rpc/events/note/list.rs b/crates/radrootsd/src/rpc/events/note/list.rs @@ -0,0 +1,66 @@ +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 ListNotesParams { + #[serde(default)] + authors: Option<Vec<String>>, + #[serde(default)] + limit: Option<u64>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("events.note.list", |params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let ListNotesParams { authors, limit } = params.parse().unwrap_or_default(); + let limit = limit.unwrap_or(50); + + let mut filter = Filter::new() + .kind(Kind::TextNote) + .limit(limit.try_into().unwrap()); + 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: Vec<JsonValue> = events + .into_iter() + .map(|ev| { + let tags: Vec<Vec<String>> = + ev.tags.iter().map(|t| t.as_slice().to_vec()).collect(); + 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(), + }) + }) + .collect(); + + Ok::<JsonValue, RpcError>(json!({ "notes": items })) + })?; + + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/events/note/mod.rs b/crates/radrootsd/src/rpc/events/note/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/note/publish.rs b/crates/radrootsd/src/rpc/events/note/publish.rs @@ -0,0 +1,54 @@ +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_nostr::prelude::{build_nostr_event, nostr_send_event}; + +#[derive(Debug, Deserialize)] +struct PublishNoteParams { + content: String, + #[serde(default)] + tags: Option<Vec<Vec<String>>>, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("events.note.publish", |params, ctx, _| async move { + let relays = ctx.client.relays().await; + if relays.is_empty() { + return Err(RpcError::NoRelays); + } + + let PublishNoteParams { content, tags } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + if content.trim().is_empty() { + return Err(RpcError::InvalidParams("content must not be empty".into())); + } + + let builder = build_nostr_event(1, content, tags.unwrap_or_default()) + .map_err(|e| RpcError::Other(format!("failed to build note: {e}")))?; + + let output = nostr_send_event(&ctx.client, builder) + .await + .map_err(|e| RpcError::Other(format!("failed to publish note: {e}")))?; + + let id_hex = output.id().to_string(); + let sent: Vec<String> = output.success.into_iter().map(|u| u.to_string()).collect(); + let failed: Vec<(String, String)> = output + .failed + .into_iter() + .map(|(u, e)| (u.to_string(), e.to_string())) + .collect(); + + Ok::<JsonValue, RpcError>(json!({ + "id": id_hex, + "sent": sent, + "failed": failed + })) + })?; + + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/events/profile/list.rs b/crates/radrootsd/src/rpc/events/profile/list.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use jsonrpsee::server::RpcModule; +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}; + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("events.profile.list", |_params, ctx, _| async move { + if ctx.client.relays().await.is_empty() { + return Err(RpcError::NoRelays); + } + + let me_pk = ctx.pubkey; + + let latest = fetch_latest_metadata_for_author(&ctx.client, me_pk, Duration::from_secs(10)) + .await + .map_err(|e| RpcError::Other(format!("metadata fetch 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(); + let profile: Option<radroots_events::profile::models::RadrootsProfile> = + serde_json::from_str(&ev.content).ok(); + + json!({ + "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, + "metadata_json": parsed, + "radroots_profile": profile, + }) + } else { + json!({ + "author_hex": me_pk.to_string(), + "author_npub": npub, + "event_id": null, + "created_at": null, + "content": null, + "metadata_json": null, + "radroots_profile": null + }) + }; + + Ok::<JsonValue, RpcError>(json!({ "profiles": [row] })) + })?; + + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/events/profile/mod.rs b/crates/radrootsd/src/rpc/events/profile/mod.rs @@ -1,102 +1,14 @@ -use std::time::Duration; - use anyhow::Result; -use jsonrpsee::RpcModule; -use serde::Deserialize; -use serde_json::{Value as JsonValue, json}; +use jsonrpsee::server::RpcModule; use crate::radrootsd::Radrootsd; -use crate::rpc::RpcError; - -use radroots_events::profile::models::RadrootsProfile; -use radroots_events_codec::profile::encode::to_metadata; -use radroots_nostr::prelude::{ - build_metadata_event, fetch_latest_metadata_for_author, nostr_send_event, npub_string, -}; - -#[derive(Debug, Deserialize)] -struct PublishProfileParams { - profile: RadrootsProfile, -} +pub mod list; +pub mod publish; pub fn module(radrootsd: Radrootsd) -> Result<RpcModule<Radrootsd>> { let mut m = RpcModule::new(radrootsd); - - m.register_async_method("events.profile.list", |_params, ctx, _| async move { - if ctx.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let me_pk = ctx.pubkey; - - let latest = fetch_latest_metadata_for_author(&ctx.client, me_pk, Duration::from_secs(10)) - .await - .map_err(|e| RpcError::Other(format!("metadata fetch 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(); - let profile: Option<radroots_events::profile::models::RadrootsProfile> = - serde_json::from_str(&ev.content).ok(); - - json!({ - "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, - "metadata_json": parsed, - "radroots_profile": profile, - }) - } else { - json!({ - "author_hex": me_pk.to_string(), - "author_npub": npub, - "event_id": null, - "created_at": null, - "content": null, - "metadata_json": null, - "radroots_profile": null - }) - }; - - Ok::<JsonValue, RpcError>(json!({ "profiles": [row] })) - })?; - - m.register_async_method("events.profile.publish", |params, ctx, _| async move { - let relays = ctx.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishProfileParams { profile } = params - .parse() - .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 output = nostr_send_event(&ctx.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish metadata: {e}")))?; - - let id_hex = output.id().to_string(); - let sent: Vec<String> = output.success.into_iter().map(|u| u.to_string()).collect(); - let failed: Vec<(String, String)> = output - .failed - .into_iter() - .map(|(u, e)| (u.to_string(), e.to_string())) - .collect(); - - Ok::<JsonValue, RpcError>(json!({ - "id": id_hex, - "sent": sent, - "failed": failed - })) - })?; - + list::register(&mut m)?; + publish::register(&mut m)?; Ok(m) } diff --git a/crates/radrootsd/src/rpc/events/profile/publish.rs b/crates/radrootsd/src/rpc/events/profile/publish.rs @@ -0,0 +1,51 @@ +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::profile::models::RadrootsProfile; +use radroots_events_codec::profile::encode::to_metadata; +use radroots_nostr::prelude::{build_metadata_event, nostr_send_event}; + +#[derive(Debug, Deserialize)] +struct PublishProfileParams { + profile: RadrootsProfile, +} + +pub fn register(m: &mut RpcModule<Radrootsd>) -> Result<()> { + m.register_async_method("events.profile.publish", |params, ctx, _| async move { + let relays = ctx.client.relays().await; + if relays.is_empty() { + return Err(RpcError::NoRelays); + } + + let PublishProfileParams { profile } = params + .parse() + .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 output = nostr_send_event(&ctx.client, builder) + .await + .map_err(|e| RpcError::Other(format!("failed to publish metadata: {e}")))?; + + let id_hex = output.id().to_string(); + let sent: Vec<String> = output.success.into_iter().map(|u| u.to_string()).collect(); + let failed: Vec<(String, String)> = output + .failed + .into_iter() + .map(|(u, e)| (u.to_string(), e.to_string())) + .collect(); + + Ok::<JsonValue, RpcError>(json!({ + "id": id_hex, + "sent": sent, + "failed": failed + })) + })?; + + Ok(()) +} diff --git a/crates/radrootsd/src/rpc/mod.rs b/crates/radrootsd/src/rpc/mod.rs @@ -19,6 +19,7 @@ pub async fn start_rpc(radrootsd: Radrootsd, addr: SocketAddr) -> Result<ServerH root.merge(system::module(radrootsd.clone())?)?; root.merge(relays::module(radrootsd.clone())?)?; root.merge(events::profile::module(radrootsd.clone())?)?; + root.merge(events::note::module(radrootsd.clone())?)?; let handle = server.start(root); Ok(handle)