radrootsd

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

commit 8b95d98721d1312d7f4969cfb8f653e4781465e7
parent 71aecb031a1caf03e1c73d91d7538074c5ff4882
Author: triesap <triesap@radroots.dev>
Date:   Sat,  3 Jan 2026 17:03:27 +0000

jsonrpc: unify event list params and views

- Centralize list params parsing and defaults in jsonrpc::params
- Factor Nostr event view helpers into jsonrpc::nostr module
- Enforce MAX_LIMIT and shared timeout handling across listing methods
- Add since/until bounds and response sorting in event list endpoints

Diffstat:
Msrc/api/jsonrpc/methods/domains/trade/listing/dvm.rs | 44++++++++++++++------------------------------
Msrc/api/jsonrpc/methods/domains/trade/listing/helpers.rs | 24+++++-------------------
Msrc/api/jsonrpc/methods/domains/trade/listing/list.rs | 48+++++++++++++++++++-----------------------------
Msrc/api/jsonrpc/methods/domains/trade/listing/orders.rs | 44++++++++++++++------------------------------
Msrc/api/jsonrpc/methods/domains/trade/listing/series.rs | 5+++--
Msrc/api/jsonrpc/methods/domains/trade/listing/types.rs | 11+----------
Msrc/api/jsonrpc/methods/events/listing/list.rs | 85++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/api/jsonrpc/methods/events/post/list.rs | 66++++++++++++++++++++++++++++++++----------------------------------
Msrc/api/jsonrpc/methods/events/profile/list.rs | 141++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/api/jsonrpc/mod.rs | 2++
Asrc/api/jsonrpc/nostr.rs | 39+++++++++++++++++++++++++++++++++++++++
Asrc/api/jsonrpc/params.rs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 353 insertions(+), 225 deletions(-)

diff --git a/src/api/jsonrpc/methods/domains/trade/listing/dvm.rs b/src/api/jsonrpc/methods/domains/trade/listing/dvm.rs @@ -4,8 +4,8 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; use serde::{Deserialize, Serialize}; +use crate::api::jsonrpc::params::{parse_pubkeys_opt, timeout_or, EventListParams}; use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_nostr::prelude::radroots_nostr_parse_pubkeys; use radroots_trade::listing::dvm_kinds::TRADE_LISTING_DVM_KINDS; use super::helpers::{fetch_dvm_events, parse_listing_addr}; @@ -17,19 +17,11 @@ struct TradeListingDvmListParams { #[serde(default)] order_id: Option<String>, #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] recipients: Option<Vec<String>>, #[serde(default)] kinds: Option<Vec<u16>>, - #[serde(default)] - limit: Option<u64>, - #[serde(default)] - since: Option<u64>, - #[serde(default)] - until: Option<u64>, - #[serde(default)] - timeout_secs: Option<u64>, + #[serde(default, flatten)] + query: EventListParams, } #[derive(Clone, Debug, Serialize)] @@ -47,33 +39,25 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res let TradeListingDvmListParams { listing_addr, order_id, - authors, recipients, kinds, + query, + } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + let EventListParams { + authors, limit, since, until, timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + } = query; let addr = parse_listing_addr(&listing_addr)?; let kinds = kinds.unwrap_or_else(|| TRADE_LISTING_DVM_KINDS.to_vec()); - let authors = match authors { - Some(authors) => Some( - radroots_nostr_parse_pubkeys(&authors) - .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?, - ), - None => None, - }; - let recipients = match recipients { - Some(recipients) => Some( - radroots_nostr_parse_pubkeys(&recipients) - .map_err(|e| RpcError::InvalidParams(format!("invalid recipient: {e}")))?, - ), - None => None, - }; + let authors = parse_pubkeys_opt("author", authors)?; + let recipients = parse_pubkeys_opt("recipient", recipients)?; let events = fetch_dvm_events( &ctx.state.client, @@ -85,7 +69,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res since, until, limit, - timeout_secs.unwrap_or(10), + timeout_or(timeout_secs), ) .await?; diff --git a/src/api/jsonrpc/methods/domains/trade/listing/helpers.rs b/src/api/jsonrpc/methods/domains/trade/listing/helpers.rs @@ -18,32 +18,18 @@ use radroots_trade::listing::{ dvm::{TradeListingAddress, TradeListingEnvelope}, }; -use super::types::{DvmEventView, ListingEventView, NostrEventView, TradeListingOrderSummary}; +use super::types::{DvmEventView, ListingEventView, TradeListingOrderSummary}; +use crate::api::jsonrpc::nostr::{event_tags, event_view, event_view_with_tags}; +use crate::api::jsonrpc::params::MAX_LIMIT; use crate::api::jsonrpc::RpcError; pub(crate) const LISTING_KIND: u16 = 30402; -pub(crate) fn event_tags(event: &RadrootsNostrEvent) -> Vec<Vec<String>> { - event.tags.iter().map(|t| t.as_slice().to_vec()).collect() -} - -pub(crate) fn event_view(event: &RadrootsNostrEvent) -> NostrEventView { - NostrEventView { - id: event.id.to_string(), - author: event.pubkey.to_string(), - created_at: event.created_at.as_secs(), - kind: event.kind.as_u16() as u32, - tags: event_tags(event), - content: event.content.clone(), - sig: event.sig.to_string(), - } -} - pub(crate) fn listing_view(event: &RadrootsNostrEvent) -> ListingEventView { let tags = event_tags(event); let listing = listing_from_event_parts(&tags, &event.content).ok(); ListingEventView { - event: event_view(event), + event: event_view_with_tags(event, tags), listing, } } @@ -158,7 +144,7 @@ pub(crate) async fn fetch_dvm_events( filter = filter.until(RadrootsNostrTimestamp::from_secs(until)); } if let Some(limit) = limit { - filter = filter.limit(limit.min(1000) as usize); + filter = filter.limit(limit.min(MAX_LIMIT) as usize); } let events = client diff --git a/src/api/jsonrpc/methods/domains/trade/listing/list.rs b/src/api/jsonrpc/methods/domains/trade/listing/list.rs @@ -2,32 +2,25 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::time::Duration; +use crate::api::jsonrpc::params::{ + apply_time_bounds, + limit_or, + parse_pubkeys_opt, + timeout_or, + EventListParams, +}; use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; use radroots_nostr::prelude::{ - radroots_nostr_parse_pubkeys, RadrootsNostrFilter, RadrootsNostrKind, - RadrootsNostrTimestamp, }; use super::helpers::{listing_view, LISTING_KIND}; use super::types::ListingEventView; -#[derive(Debug, Default, Deserialize)] -struct TradeListingListParams { - #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] - limit: Option<u64>, - #[serde(default)] - since: Option<u64>, - #[serde(default)] - until: Option<u64>, -} - #[derive(Clone, Debug, Serialize)] struct TradeListingListResponse { listings: Vec<ListingEventView>, @@ -40,36 +33,33 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res return Err(RpcError::NoRelays); } - let TradeListingListParams { + let EventListParams { authors, limit, since, until, - } = params.parse().unwrap_or_default(); + timeout_secs, + } = params + .parse::<Option<EventListParams>>() + .map_err(|e| RpcError::InvalidParams(e.to_string()))? + .unwrap_or_default(); - let limit = limit.unwrap_or(50).min(1000) as usize; + let limit = limit_or(limit); let mut filter = RadrootsNostrFilter::new() .kind(RadrootsNostrKind::Custom(LISTING_KIND)) .limit(limit); - if let Some(authors) = authors { - let pks = radroots_nostr_parse_pubkeys(&authors) - .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?; - filter = filter.authors(pks); + if let Some(authors) = parse_pubkeys_opt("author", authors)? { + filter = filter.authors(authors); } else { filter = filter.author(ctx.state.pubkey); } - if let Some(since) = since { - filter = filter.since(RadrootsNostrTimestamp::from_secs(since)); - } - if let Some(until) = until { - filter = filter.until(RadrootsNostrTimestamp::from_secs(until)); - } + filter = apply_time_bounds(filter, since, until); let events = ctx .state .client - .fetch_events(filter, Duration::from_secs(10)) + .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs))) .await .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; diff --git a/src/api/jsonrpc/methods/domains/trade/listing/orders.rs b/src/api/jsonrpc/methods/domains/trade/listing/orders.rs @@ -4,8 +4,8 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; use serde::{Deserialize, Serialize}; +use crate::api::jsonrpc::params::{parse_pubkeys_opt, timeout_or, EventListParams}; use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_nostr::prelude::radroots_nostr_parse_pubkeys; use radroots_trade::listing::dvm_kinds::TRADE_LISTING_DVM_KINDS; use super::helpers::{fetch_dvm_events, order_summaries, parse_listing_addr}; @@ -15,19 +15,11 @@ use super::types::TradeListingOrderSummary; struct TradeListingOrdersParams { listing_addr: String, #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] recipients: Option<Vec<String>>, #[serde(default)] kinds: Option<Vec<u16>>, - #[serde(default)] - limit: Option<u64>, - #[serde(default)] - since: Option<u64>, - #[serde(default)] - until: Option<u64>, - #[serde(default)] - timeout_secs: Option<u64>, + #[serde(default, flatten)] + query: EventListParams, } #[derive(Clone, Debug, Serialize)] @@ -44,33 +36,25 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res let TradeListingOrdersParams { listing_addr, - authors, recipients, kinds, + query, + } = params + .parse() + .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + + let EventListParams { + authors, limit, since, until, timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; + } = query; let addr = parse_listing_addr(&listing_addr)?; let kinds = kinds.unwrap_or_else(|| TRADE_LISTING_DVM_KINDS.to_vec()); - let authors = match authors { - Some(authors) => Some( - radroots_nostr_parse_pubkeys(&authors) - .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?, - ), - None => None, - }; - let recipients = match recipients { - Some(recipients) => Some( - radroots_nostr_parse_pubkeys(&recipients) - .map_err(|e| RpcError::InvalidParams(format!("invalid recipient: {e}")))?, - ), - None => None, - }; + let authors = parse_pubkeys_opt("author", authors)?; + let recipients = parse_pubkeys_opt("recipient", recipients)?; let events = fetch_dvm_events( &ctx.state.client, @@ -82,7 +66,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res since, until, limit, - timeout_secs.unwrap_or(10), + timeout_or(timeout_secs), ) .await?; diff --git a/src/api/jsonrpc/methods/domains/trade/listing/series.rs b/src/api/jsonrpc/methods/domains/trade/listing/series.rs @@ -3,6 +3,7 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; use serde::{Deserialize, Serialize}; +use crate::api::jsonrpc::params::timeout_or; use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; use radroots_trade::listing::dvm_kinds::TRADE_LISTING_DVM_KINDS; @@ -60,7 +61,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res let include_dvm = include_dvm.unwrap_or(true); let listing = if include_listing { - fetch_latest_listing_event(&ctx.state.client, &addr, timeout_secs.unwrap_or(10)) + fetch_latest_listing_event(&ctx.state.client, &addr, timeout_or(timeout_secs)) .await? .as_ref() .map(listing_view) @@ -79,7 +80,7 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res since, until, limit, - timeout_secs.unwrap_or(10), + timeout_or(timeout_secs), ) .await? } else { diff --git a/src/api/jsonrpc/methods/domains/trade/listing/types.rs b/src/api/jsonrpc/methods/domains/trade/listing/types.rs @@ -4,16 +4,7 @@ use radroots_events::listing::RadrootsListing; use radroots_trade::listing::dvm::TradeListingEnvelope; use serde::Serialize; -#[derive(Clone, Debug, Serialize)] -pub struct NostrEventView { - pub id: String, - pub author: String, - pub created_at: u64, - pub kind: u32, - pub tags: Vec<Vec<String>>, - pub content: String, - pub sig: String, -} +use crate::api::jsonrpc::nostr::NostrEventView; #[derive(Clone, Debug, Serialize)] pub struct ListingEventView { diff --git a/src/api/jsonrpc/methods/events/listing/list.rs b/src/api/jsonrpc/methods/events/listing/list.rs @@ -1,23 +1,35 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; -use serde::Deserialize; -use serde_json::{Value as JsonValue, json}; +use serde::Serialize; use std::time::Duration; +use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags, NostrEventView}; +use crate::api::jsonrpc::params::{ + apply_time_bounds, + limit_or, + parse_pubkeys_opt, + timeout_or, + EventListParams, +}; use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; +use radroots_events::kinds::KIND_LISTING; +use radroots_events::listing::RadrootsListing; use radroots_nostr::prelude::{ - radroots_nostr_parse_pubkeys, RadrootsNostrFilter, RadrootsNostrKind, }; use radroots_trade::listing::codec::listing_from_event_parts; -#[derive(Debug, Default, Deserialize)] -struct ListListingParams { - #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] - limit: Option<u64>, +#[derive(Clone, Debug, Serialize)] +struct ListingEventFlat { + #[serde(flatten)] + event: NostrEventView, + listing: Option<RadrootsListing>, +} + +#[derive(Clone, Debug, Serialize)] +struct ListingListResponse { + listings: Vec<ListingEventFlat>, } pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { @@ -27,54 +39,51 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res return Err(RpcError::NoRelays); } - let ListListingParams { authors, limit } = params.parse().unwrap_or_default(); - let limit = limit.unwrap_or(50).min(1000); + let EventListParams { + authors, + limit, + since, + until, + timeout_secs, + } = params + .parse::<Option<EventListParams>>() + .map_err(|e| RpcError::InvalidParams(e.to_string()))? + .unwrap_or_default(); - let mut filter = RadrootsNostrFilter::new().limit((limit as u16).into()); + let limit = limit_or(limit); - let kinds: Vec<u32> = vec![30402]; - let kinds_conv = kinds - .into_iter() - .map(|k| RadrootsNostrKind::Custom(k as u16)) - .collect::<Vec<_>>(); - filter = filter.kinds(kinds_conv); + let mut filter = RadrootsNostrFilter::new() + .limit(limit) + .kind(RadrootsNostrKind::Custom(KIND_LISTING as u16)); - if let Some(auths) = authors { - let pks = radroots_nostr_parse_pubkeys(&auths) - .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?; - filter = filter.authors(pks); + if let Some(authors) = parse_pubkeys_opt("author", authors)? { + filter = filter.authors(authors); } else { filter = filter.author(ctx.state.pubkey); } + filter = apply_time_bounds(filter, since, until); let events = ctx .state .client - .fetch_events(filter, Duration::from_secs(10)) + .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs))) .await .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - let items = events + let mut items = events .into_iter() .map(|ev| { - let tags: Vec<Vec<String>> = - ev.tags.iter().map(|t| t.as_slice().to_vec()).collect(); + let tags = event_tags(&ev); let listing = listing_from_event_parts(&tags, &ev.content).ok(); - - json!({ - "id": ev.id.to_string(), - "author": ev.pubkey.to_string(), - "created_at": ev.created_at.as_secs(), - "kind": ev.kind.as_u16() as u32, - "tags": tags, - "content": ev.content, - "sig": ev.sig.to_string(), - "listing": listing, - }) + ListingEventFlat { + event: event_view_with_tags(&ev, tags), + listing, + } }) .collect::<Vec<_>>(); + items.sort_by(|a, b| b.event.created_at.cmp(&a.event.created_at)); - Ok::<JsonValue, RpcError>(json!({ "listings": items })) + Ok::<ListingListResponse, RpcError>(ListingListResponse { listings: items }) })?; Ok(()) } diff --git a/src/api/jsonrpc/methods/events/post/list.rs b/src/api/jsonrpc/methods/events/post/list.rs @@ -1,22 +1,25 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; -use serde::Deserialize; -use serde_json::{Value as JsonValue, json}; +use serde::Serialize; use std::time::Duration; +use crate::api::jsonrpc::nostr::{event_view, NostrEventView}; +use crate::api::jsonrpc::params::{ + apply_time_bounds, + limit_or, + parse_pubkeys_opt, + timeout_or, + EventListParams, +}; use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; use radroots_nostr::prelude::{ - radroots_nostr_parse_pubkeys, RadrootsNostrFilter, RadrootsNostrKind, }; -#[derive(Debug, Default, Deserialize)] -struct ListProfilesParams { - #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] - limit: Option<u64>, +#[derive(Clone, Debug, Serialize)] +struct PostListResponse { + posts: Vec<NostrEventView>, } pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { @@ -26,45 +29,40 @@ pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Res return Err(RpcError::NoRelays); } - let ListProfilesParams { authors, limit } = params.parse().unwrap_or_default(); - let limit = limit.unwrap_or(50); + let EventListParams { + authors, + limit, + since, + until, + timeout_secs, + } = params + .parse::<Option<EventListParams>>() + .map_err(|e| RpcError::InvalidParams(e.to_string()))? + .unwrap_or_default(); + + let limit = limit_or(limit); let mut filter = RadrootsNostrFilter::new() .kind(RadrootsNostrKind::TextNote) - .limit(limit.try_into().unwrap()); - if let Some(auths) = authors { - let pks = radroots_nostr_parse_pubkeys(&auths) - .map_err(|e| RpcError::InvalidParams(format!("invalid author: {e}")))?; - filter = filter.authors(pks); + .limit(limit); + if let Some(authors) = parse_pubkeys_opt("author", authors)? { + filter = filter.authors(authors); } else { filter = filter.author(ctx.state.pubkey); } + filter = apply_time_bounds(filter, since, until); let events = ctx .state .client - .fetch_events(filter, Duration::from_secs(10)) + .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs))) .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_secs(), - "kind": ev.kind.as_u16() as u32, - "tags": tags, - "content": ev.content, - "sig": ev.sig.to_string(), - }) - }) - .collect(); + let mut items = events.into_iter().map(|ev| event_view(&ev)).collect::<Vec<_>>(); + items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - Ok::<JsonValue, RpcError>(json!({ "Profiles": items })) + Ok::<PostListResponse, RpcError>(PostListResponse { posts: items }) })?; Ok(()) diff --git a/src/api/jsonrpc/methods/events/profile/list.rs b/src/api/jsonrpc/methods/events/profile/list.rs @@ -1,57 +1,132 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; -use serde_json::{Value as JsonValue, json}; +use serde::Serialize; +use serde_json::Value as JsonValue; +use std::collections::HashMap; use std::time::Duration; +use crate::api::jsonrpc::params::{ + apply_time_bounds, + limit_or, + parse_pubkeys_opt, + timeout_or, + EventListParams, +}; use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; +use radroots_events::profile::RadrootsProfile; use radroots_nostr::prelude::{ - radroots_nostr_fetch_metadata_for_author, radroots_nostr_npub_string, + RadrootsNostrFilter, + RadrootsNostrKind, + RadrootsNostrEvent, + RadrootsNostrPublicKey, }; +#[derive(Clone, Debug, Serialize)] +struct ProfileListRow { + author_hex: String, + author_npub: String, + event_id: Option<String>, + created_at: Option<u64>, + content: Option<String>, + metadata_json: Option<JsonValue>, + radroots_profile: Option<RadrootsProfile>, +} + +#[derive(Clone, Debug, Serialize)] +struct ProfileListResponse { + profiles: Vec<ProfileListRow>, +} + pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { registry.track("events.profile.list"); - m.register_async_method("events.profile.list", |_params, ctx, _| async move { + m.register_async_method("events.profile.list", |params, ctx, _| async move { if ctx.state.client.relays().await.is_empty() { return Err(RpcError::NoRelays); } - let me_pk = ctx.state.pubkey; + let EventListParams { + authors, + limit, + since, + until, + timeout_secs, + } = params + .parse::<Option<EventListParams>>() + .map_err(|e| RpcError::InvalidParams(e.to_string()))? + .unwrap_or_default(); + + let authors = match parse_pubkeys_opt("author", authors)? { + Some(authors) => authors, + None => vec![ctx.state.pubkey], + }; - let latest = radroots_nostr_fetch_metadata_for_author(&ctx.state.client, me_pk, Duration::from_secs(10)) + let mut filter = RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::Metadata) + .authors(authors.clone()) + .limit(limit_or(limit)); + filter = apply_time_bounds(filter, since, until); + + let mut latest_by_author: HashMap<RadrootsNostrPublicKey, RadrootsNostrEvent> = + HashMap::new(); + let stored = ctx + .state + .client + .database() + .query(filter.clone()) + .await + .map_err(|e| RpcError::Other(format!("metadata query failed: {e}")))?; + let fetched = ctx + .state + .client + .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs))) .await .map_err(|e| RpcError::Other(format!("metadata fetch failed: {e}")))?; - let npub = radroots_nostr_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::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_secs(), - "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 + for event in stored.into_iter().chain(fetched.into_iter()) { + match latest_by_author.get(&event.pubkey) { + Some(cur) if event.created_at <= cur.created_at => {} + _ => { + latest_by_author.insert(event.pubkey, event); + } + } + } + + let profiles = authors + .into_iter() + .map(|author| { + let npub = radroots_nostr_npub_string(&author) + .ok_or_else(|| RpcError::Other("bech32 encode failed".into()))?; + let row = match latest_by_author.get(&author) { + Some(event) => { + let parsed: Option<JsonValue> = serde_json::from_str(&event.content).ok(); + let profile: Option<RadrootsProfile> = + serde_json::from_str(&event.content).ok(); + ProfileListRow { + author_hex: author.to_string(), + author_npub: npub, + event_id: Some(event.id.to_string()), + created_at: Some(event.created_at.as_secs()), + content: Some(event.content.clone()), + metadata_json: parsed, + radroots_profile: profile, + } + } + None => ProfileListRow { + author_hex: author.to_string(), + author_npub: npub, + event_id: None, + created_at: None, + content: None, + metadata_json: None, + radroots_profile: None, + }, + }; + Ok(row) }) - }; + .collect::<Result<Vec<_>, RpcError>>()?; - Ok::<JsonValue, RpcError>(json!({ "profiles": [row] })) + Ok::<ProfileListResponse, RpcError>(ProfileListResponse { profiles }) })?; Ok(()) diff --git a/src/api/jsonrpc/mod.rs b/src/api/jsonrpc/mod.rs @@ -10,6 +10,8 @@ use crate::radrootsd::Radrootsd; mod context; mod error; +mod nostr; +mod params; mod registry; mod server; diff --git a/src/api/jsonrpc/nostr.rs b/src/api/jsonrpc/nostr.rs @@ -0,0 +1,39 @@ +#![forbid(unsafe_code)] + +use serde::Serialize; + +use radroots_nostr::prelude::RadrootsNostrEvent; + +#[derive(Clone, Debug, Serialize)] +pub struct NostrEventView { + pub id: String, + pub author: String, + pub created_at: u64, + pub kind: u32, + pub tags: Vec<Vec<String>>, + pub content: String, + pub sig: String, +} + +pub(crate) fn event_tags(event: &RadrootsNostrEvent) -> Vec<Vec<String>> { + event.tags.iter().map(|t| t.as_slice().to_vec()).collect() +} + +pub(crate) fn event_view(event: &RadrootsNostrEvent) -> NostrEventView { + event_view_with_tags(event, event_tags(event)) +} + +pub(crate) fn event_view_with_tags( + event: &RadrootsNostrEvent, + tags: Vec<Vec<String>>, +) -> NostrEventView { + NostrEventView { + id: event.id.to_string(), + author: event.pubkey.to_string(), + created_at: event.created_at.as_secs(), + kind: event.kind.as_u16() as u32, + tags, + content: event.content.clone(), + sig: event.sig.to_string(), + } +} diff --git a/src/api/jsonrpc/params.rs b/src/api/jsonrpc/params.rs @@ -0,0 +1,69 @@ +#![forbid(unsafe_code)] + +use serde::Deserialize; + +use crate::api::jsonrpc::RpcError; +use radroots_nostr::prelude::{ + radroots_nostr_parse_pubkeys, + RadrootsNostrFilter, + RadrootsNostrPublicKey, + RadrootsNostrTimestamp, +}; + +pub const DEFAULT_LIMIT: u64 = 50; +pub const MAX_LIMIT: u64 = 1000; +pub const DEFAULT_TIMEOUT_SECS: u64 = 10; + +#[derive(Debug, Default, Deserialize)] +pub struct EventListParams { + #[serde(default)] + pub authors: Option<Vec<String>>, + #[serde(default)] + pub limit: Option<u64>, + #[serde(default)] + pub since: Option<u64>, + #[serde(default)] + pub until: Option<u64>, + #[serde(default)] + pub timeout_secs: Option<u64>, +} + +pub(crate) fn limit_or(limit: Option<u64>) -> usize { + limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize +} + +pub(crate) fn timeout_or(timeout_secs: Option<u64>) -> u64 { + timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS) +} + +pub(crate) fn parse_pubkeys( + label: &str, + values: &[String], +) -> Result<Vec<RadrootsNostrPublicKey>, RpcError> { + radroots_nostr_parse_pubkeys(values) + .map_err(|e| RpcError::InvalidParams(format!("invalid {label}: {e}"))) +} + +pub(crate) fn parse_pubkeys_opt( + label: &str, + values: Option<Vec<String>>, +) -> Result<Option<Vec<RadrootsNostrPublicKey>>, RpcError> { + match values { + Some(values) => Ok(Some(parse_pubkeys(label, &values)?)), + None => Ok(None), + } +} + +pub(crate) fn apply_time_bounds( + mut filter: RadrootsNostrFilter, + since: Option<u64>, + until: Option<u64>, +) -> RadrootsNostrFilter { + if let Some(since) = since { + filter = filter.since(RadrootsNostrTimestamp::from_secs(since)); + } + if let Some(until) = until { + filter = filter.until(RadrootsNostrTimestamp::from_secs(until)); + } + filter +}