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:
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
+}