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:
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)