radrootsd

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

commit 67f46d7d2f1849604b6ae5d1d39656731c16f820
parent 2b5e92190530b66d3c8e0bb832ae1e674187610f
Author: triesap <triesap@radroots.dev>
Date:   Tue,  6 Jan 2026 00:08:35 +0000

core: reset rpc surface and add nip46 status stub

- archive legacy domains/events/system rpc modules
- register only relays and nip46 status endpoints
- add nip46/jsonrpc stub module skeleton
- add empty build/events/validate/nip46 module roots

Diffstat:
Dsrc/api/jsonrpc/methods/domains/mod.rs | 3---
Dsrc/api/jsonrpc/methods/domains/trade/listing/dvm.rs | 79-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/domains/trade/listing/get.rs | 43-------------------------------------------
Dsrc/api/jsonrpc/methods/domains/trade/listing/helpers.rs | 313-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/domains/trade/listing/list.rs | 72------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/domains/trade/listing/mod.rs | 29-----------------------------
Dsrc/api/jsonrpc/methods/domains/trade/listing/order.rs | 181-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/domains/trade/listing/orders.rs | 78------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/domains/trade/listing/series.rs | 105-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/domains/trade/listing/types.rs | 38--------------------------------------
Dsrc/api/jsonrpc/methods/domains/trade/listing/validate.rs | 363-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/domains/trade/mod.rs | 14--------------
Dsrc/api/jsonrpc/methods/events/comment/get.rs | 58----------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/comment/list.rs | 275-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/comment/mod.rs | 18------------------
Dsrc/api/jsonrpc/methods/events/comment/publish.rs | 51---------------------------------------------------
Dsrc/api/jsonrpc/methods/events/dvm_feedback/get.rs | 55-------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/dvm_feedback/list.rs | 237-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/dvm_feedback/mod.rs | 18------------------
Dsrc/api/jsonrpc/methods/events/dvm_feedback/publish.rs | 51---------------------------------------------------
Dsrc/api/jsonrpc/methods/events/dvm_request/get.rs | 55-------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/dvm_request/list.rs | 252-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/dvm_request/mod.rs | 18------------------
Dsrc/api/jsonrpc/methods/events/dvm_request/publish.rs | 65-----------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/dvm_result/get.rs | 55-------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/dvm_result/list.rs | 272-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/dvm_result/mod.rs | 18------------------
Dsrc/api/jsonrpc/methods/events/dvm_result/publish.rs | 59-----------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/farm/get.rs | 61-------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/farm/list.rs | 197-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/farm/mod.rs | 16----------------
Dsrc/api/jsonrpc/methods/events/farm/publish.rs | 50--------------------------------------------------
Dsrc/api/jsonrpc/methods/events/follow/get.rs | 56--------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/follow/list.rs | 214-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/follow/mod.rs | 20--------------------
Dsrc/api/jsonrpc/methods/events/follow/publish.rs | 51---------------------------------------------------
Dsrc/api/jsonrpc/methods/events/follow/update.rs | 143-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/helpers.rs | 105-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/list_set/get.rs | 73-------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/list_set/list.rs | 360-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/list_set/mod.rs | 18------------------
Dsrc/api/jsonrpc/methods/events/list_set/publish.rs | 64----------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/listing/get.rs | 61-------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/listing/list.rs | 238-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/listing/mod.rs | 16----------------
Dsrc/api/jsonrpc/methods/events/listing/publish.rs | 46----------------------------------------------
Dsrc/api/jsonrpc/methods/events/mod.rs | 15---------------
Dsrc/api/jsonrpc/methods/events/plot/get.rs | 61-------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/plot/list.rs | 201-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/plot/mod.rs | 16----------------
Dsrc/api/jsonrpc/methods/events/plot/publish.rs | 50--------------------------------------------------
Dsrc/api/jsonrpc/methods/events/post/get.rs | 57---------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/post/list.rs | 173-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/post/mod.rs | 16----------------
Dsrc/api/jsonrpc/methods/events/post/publish.rs | 44--------------------------------------------
Dsrc/api/jsonrpc/methods/events/profile/get.rs | 60------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/profile/list.rs | 213-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/profile/mod.rs | 16----------------
Dsrc/api/jsonrpc/methods/events/profile/publish.rs | 43-------------------------------------------
Dsrc/api/jsonrpc/methods/events/reaction/get.rs | 58----------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/reaction/list.rs | 199-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/reaction/mod.rs | 18------------------
Dsrc/api/jsonrpc/methods/events/reaction/publish.rs | 51---------------------------------------------------
Dsrc/api/jsonrpc/methods/events/resource_area/get.rs | 62--------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/resource_area/list.rs | 247-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/resource_area/mod.rs | 16----------------
Dsrc/api/jsonrpc/methods/events/resource_area/publish.rs | 51---------------------------------------------------
Dsrc/api/jsonrpc/methods/events/resource_cap/get.rs | 62--------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/resource_cap/list.rs | 218-------------------------------------------------------------------------------
Dsrc/api/jsonrpc/methods/events/resource_cap/mod.rs | 16----------------
Dsrc/api/jsonrpc/methods/events/resource_cap/publish.rs | 51---------------------------------------------------
Msrc/api/jsonrpc/methods/mod.rs | 21++-------------------
Asrc/api/jsonrpc/methods/nip46/mod.rs | 14++++++++++++++
Asrc/api/jsonrpc/methods/nip46/status.rs | 18++++++++++++++++++
Dsrc/api/jsonrpc/methods/system.rs | 34----------------------------------
Asrc/build/mod.rs | 1+
Asrc/events/mod.rs | 1+
Msrc/lib.rs | 4++++
Asrc/nip46/mod.rs | 1+
Asrc/validate/mod.rs | 1+
80 files changed, 42 insertions(+), 6731 deletions(-)

diff --git a/src/api/jsonrpc/methods/domains/mod.rs b/src/api/jsonrpc/methods/domains/mod.rs @@ -1,3 +0,0 @@ -#![forbid(unsafe_code)] - -pub mod trade; diff --git a/src/api/jsonrpc/methods/domains/trade/listing/dvm.rs b/src/api/jsonrpc/methods/domains/trade/listing/dvm.rs @@ -1,79 +0,0 @@ -#![forbid(unsafe_code)] - -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_trade::listing::dvm_kinds::TRADE_LISTING_DVM_KINDS; - -use super::helpers::{fetch_dvm_events, parse_listing_addr}; -use super::types::DvmEventView; - -#[derive(Debug, Deserialize)] -struct TradeListingDvmListParams { - listing_addr: String, - #[serde(default)] - order_id: Option<String>, - #[serde(default)] - recipients: Option<Vec<String>>, - #[serde(default)] - kinds: Option<Vec<u16>>, - #[serde(default, flatten)] - query: EventListParams, -} - -#[derive(Clone, Debug, Serialize)] -struct TradeListingDvmListResponse { - events: Vec<DvmEventView>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("trade.listing.dvm.list"); - m.register_async_method("trade.listing.dvm.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let TradeListingDvmListParams { - listing_addr, - order_id, - recipients, - kinds, - query, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let EventListParams { - authors, - limit, - since, - until, - timeout_secs, - } = query; - - let addr = parse_listing_addr(&listing_addr)?; - let kinds = kinds.unwrap_or_else(|| TRADE_LISTING_DVM_KINDS.to_vec()); - let authors = parse_pubkeys_opt("author", authors)?; - let recipients = parse_pubkeys_opt("recipient", recipients)?; - - let events = fetch_dvm_events( - &ctx.state.client, - &addr, - &kinds, - order_id.as_deref(), - authors.as_deref(), - recipients.as_deref(), - since, - until, - limit, - timeout_or(timeout_secs), - ) - .await?; - - Ok::<TradeListingDvmListResponse, RpcError>(TradeListingDvmListResponse { events }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/domains/trade/listing/get.rs b/src/api/jsonrpc/methods/domains/trade/listing/get.rs @@ -1,43 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; - -use super::helpers::{fetch_latest_listing_event, listing_view, parse_listing_addr}; -use super::types::ListingEventView; - -#[derive(Debug, Deserialize)] -struct TradeListingGetParams { - listing_addr: String, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct TradeListingGetResponse { - listing: Option<ListingEventView>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("trade.listing.get"); - m.register_async_method("trade.listing.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let TradeListingGetParams { - listing_addr, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let addr = parse_listing_addr(&listing_addr)?; - let latest = fetch_latest_listing_event(&ctx.state.client, &addr, timeout_secs.unwrap_or(10)).await?; - let listing = latest.as_ref().map(listing_view); - Ok::<TradeListingGetResponse, RpcError>(TradeListingGetResponse { listing }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/domains/trade/listing/helpers.rs b/src/api/jsonrpc/methods/domains/trade/listing/helpers.rs @@ -1,313 +0,0 @@ -#![forbid(unsafe_code)] - -use std::collections::HashMap; -use std::time::Duration; - -use radroots_nostr::prelude::{ - radroots_nostr_parse_pubkey, - RadrootsNostrClient, - RadrootsNostrCoordinate, - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, - RadrootsNostrPublicKey, - RadrootsNostrTimestamp, -}; -use radroots_trade::listing::{ - codec::listing_from_event_parts, - dvm::{TradeListingAddress, TradeListingEnvelope}, -}; - -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 listing_view(event: &RadrootsNostrEvent) -> ListingEventView { - let tags = event_tags(event); - let listing = listing_from_event_parts(&tags, &event.content).ok(); - ListingEventView { - event: event_view_with_tags(event, tags), - listing, - } -} - -pub(crate) fn parse_listing_addr(listing_addr: &str) -> Result<TradeListingAddress, RpcError> { - let addr = TradeListingAddress::parse(listing_addr) - .map_err(|_| RpcError::InvalidParams("invalid listing_addr".to_string()))?; - if addr.kind != LISTING_KIND { - return Err(RpcError::InvalidParams("unsupported listing kind".to_string())); - } - Ok(addr) -} - -pub(crate) fn listing_filter(addr: &TradeListingAddress) -> Result<RadrootsNostrFilter, RpcError> { - let author = radroots_nostr_parse_pubkey(&addr.seller_pubkey) - .map_err(|e| RpcError::InvalidParams(format!("invalid listing author: {e}")))?; - Ok(RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(addr.kind)) - .author(author) - .identifier(addr.listing_id.clone())) -} - -pub(crate) async fn fetch_latest_listing_event( - client: &RadrootsNostrClient, - listing_addr: &TradeListingAddress, - timeout_secs: u64, -) -> Result<Option<RadrootsNostrEvent>, RpcError> { - let mut filter = listing_filter(listing_addr)?; - filter = filter.limit(25); - let events = client - .fetch_events(filter, Duration::from_secs(timeout_secs)) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - let mut latest: Option<RadrootsNostrEvent> = None; - for event in events { - match &latest { - Some(cur) if event.created_at <= cur.created_at => {} - _ => latest = Some(event), - } - } - Ok(latest) -} - -pub(crate) fn dvm_filter( - listing_addr: &TradeListingAddress, - kinds: &[u16], -) -> Result<RadrootsNostrFilter, RpcError> { - let author = radroots_nostr_parse_pubkey(&listing_addr.seller_pubkey) - .map_err(|e| RpcError::InvalidParams(format!("invalid listing author: {e}")))?; - let coordinate = RadrootsNostrCoordinate::new( - RadrootsNostrKind::Custom(listing_addr.kind), - author, - ) - .identifier(listing_addr.listing_id.clone()); - let kinds = kinds - .iter() - .map(|kind| RadrootsNostrKind::Custom(*kind)) - .collect::<Vec<_>>(); - Ok(RadrootsNostrFilter::new() - .kinds(kinds) - .coordinate(&coordinate)) -} - -pub(crate) fn dvm_event_view(event: &RadrootsNostrEvent) -> DvmEventView { - let envelope = serde_json::from_str::<TradeListingEnvelope<serde_json::Value>>(&event.content) - .ok(); - let envelope_error = envelope - .as_ref() - .and_then(|env| env.validate().err()) - .map(|err| err.to_string()) - .or_else(|| { - if envelope.is_some() { - None - } else { - Some("invalid envelope json".to_string()) - } - }); - DvmEventView { - event: event_view(event), - envelope, - envelope_error, - } -} - -pub(crate) async fn fetch_dvm_events( - client: &RadrootsNostrClient, - listing_addr: &TradeListingAddress, - kinds: &[u16], - order_id: Option<&str>, - authors: Option<&[RadrootsNostrPublicKey]>, - recipients: Option<&[RadrootsNostrPublicKey]>, - since: Option<u64>, - until: Option<u64>, - limit: Option<u64>, - timeout_secs: u64, -) -> Result<Vec<DvmEventView>, RpcError> { - let mut filter = dvm_filter(listing_addr, kinds)?; - - if let Some(order_id) = order_id { - filter = filter.identifier(order_id); - } - if let Some(authors) = authors { - filter = filter.authors(authors.to_vec()); - } - if let Some(recipients) = recipients { - filter = filter.pubkeys(recipients.to_vec()); - } - if let Some(since) = since { - filter = filter.since(RadrootsNostrTimestamp::from_secs(since)); - } - if let Some(until) = until { - filter = filter.until(RadrootsNostrTimestamp::from_secs(until)); - } - if let Some(limit) = limit { - filter = filter.limit(limit.min(MAX_LIMIT) as usize); - } - - let events = client - .fetch_events(filter, Duration::from_secs(timeout_secs)) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let mut out = events - .into_iter() - .map(|event| dvm_event_view(&event)) - .collect::<Vec<_>>(); - out.sort_by(|a, b| a.event.created_at.cmp(&b.event.created_at)); - Ok(out) -} - -pub(crate) fn order_id_from_event(event: &DvmEventView) -> Option<String> { - if let Some(envelope) = &event.envelope { - if let Some(order_id) = &envelope.order_id { - return Some(order_id.clone()); - } - } - event - .event - .tags - .iter() - .find_map(|tag| match tag.get(0).map(String::as_str) { - Some("d") => tag.get(1).cloned(), - _ => None, - }) -} - -pub(crate) fn order_summaries( - events: &[DvmEventView], - listing_addr: &str, -) -> Vec<TradeListingOrderSummary> { - let mut summary_map: HashMap<String, TradeListingOrderSummary> = HashMap::new(); - - for event in events { - let order_id = match order_id_from_event(event) { - Some(id) => id, - None => continue, - }; - let entry = summary_map.entry(order_id.clone()).or_insert_with(|| { - TradeListingOrderSummary { - order_id, - listing_addr: listing_addr.to_string(), - event_count: 0, - first_seen_at: event.event.created_at, - last_seen_at: event.event.created_at, - last_event_id: event.event.id.clone(), - last_event_kind: event.event.kind, - } - }); - entry.event_count += 1; - if event.event.created_at < entry.first_seen_at { - entry.first_seen_at = event.event.created_at; - } - if event.event.created_at >= entry.last_seen_at { - entry.last_seen_at = event.event.created_at; - entry.last_event_id = event.event.id.clone(); - entry.last_event_kind = event.event.kind; - } - } - - let mut summaries: Vec<TradeListingOrderSummary> = summary_map.into_values().collect(); - summaries.sort_by(|a, b| b.last_seen_at.cmp(&a.last_seen_at)); - summaries -} - -#[cfg(test)] -mod tests { - use super::{dvm_event_view, order_id_from_event, order_summaries, LISTING_KIND}; - use radroots_nostr::prelude::RadrootsNostrEvent; - use radroots_trade::listing::dvm::{TradeListingEnvelope, TradeListingMessageType}; - use serde_json::json; - - fn dvm_event( - id: &str, - pubkey: &str, - created_at: u64, - kind: u16, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 6); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": kind, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - #[test] - fn dvm_event_view_parses_envelope_and_prefers_envelope_order_id() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let listing_addr = format!("{LISTING_KIND}:{pubkey}:AAAAAAAAAAAAAAAAAAAAAg"); - let envelope = TradeListingEnvelope::new( - TradeListingMessageType::OrderRequest, - listing_addr, - Some("env-order".to_string()), - json!({}), - ); - let content = serde_json::to_string(&envelope).expect("envelope"); - let id = format!("{:064x}", 1); - let tags = vec![vec!["d".to_string(), "tag-order".to_string()]]; - let event = dvm_event(&id, pubkey, 100, 5321, tags, &content); - - let view = dvm_event_view(&event); - - assert!(view.envelope.is_some()); - assert!(view.envelope_error.is_none()); - assert_eq!(order_id_from_event(&view).as_deref(), Some("env-order")); - } - - #[test] - fn dvm_event_view_invalid_json_sets_error() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let id = format!("{:064x}", 2); - let event = dvm_event(&id, pubkey, 120, 5321, Vec::new(), "not-json"); - - let view = dvm_event_view(&event); - - assert!(view.envelope.is_none()); - assert_eq!(view.envelope_error.as_deref(), Some("invalid envelope json")); - } - - #[test] - fn order_summaries_counts_and_sorts() { - let pubkey = "3bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let listing_addr = format!("{LISTING_KIND}:{pubkey}:AAAAAAAAAAAAAAAAAAAAAg"); - let order_a = vec![vec!["d".to_string(), "order-a".to_string()]]; - let order_b = vec![vec!["d".to_string(), "order-b".to_string()]]; - - let id_a1 = format!("{:064x}", 3); - let id_a2 = format!("{:064x}", 4); - let id_b1 = format!("{:064x}", 5); - - let ev_a1 = dvm_event(&id_a1, pubkey, 10, 5321, order_a.clone(), ""); - let ev_a2 = dvm_event(&id_a2, pubkey, 20, 6321, order_a.clone(), ""); - let ev_b1 = dvm_event(&id_b1, pubkey, 15, 5321, order_b, ""); - - let views = vec![ - dvm_event_view(&ev_a1), - dvm_event_view(&ev_a2), - dvm_event_view(&ev_b1), - ]; - - let summaries = order_summaries(&views, &listing_addr); - - assert_eq!(summaries.len(), 2); - assert_eq!(summaries[0].order_id, "order-a"); - assert_eq!(summaries[0].event_count, 2); - assert_eq!(summaries[0].first_seen_at, 10); - assert_eq!(summaries[0].last_seen_at, 20); - assert_eq!(summaries[0].last_event_id, id_a2); - assert_eq!(summaries[0].last_event_kind, 6321); - assert_eq!(summaries[1].order_id, "order-b"); - assert_eq!(summaries[1].event_count, 1); - assert_eq!(summaries[1].last_seen_at, 15); - } -} diff --git a/src/api/jsonrpc/methods/domains/trade/listing/list.rs b/src/api/jsonrpc/methods/domains/trade/listing/list.rs @@ -1,72 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -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::{ - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -use super::helpers::{listing_view, LISTING_KIND}; -use super::types::ListingEventView; - -#[derive(Clone, Debug, Serialize)] -struct TradeListingListResponse { - listings: Vec<ListingEventView>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("trade.listing.list"); - m.register_async_method("trade.listing.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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::Custom(LISTING_KIND)) - .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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let mut listings = events.into_iter().map(|ev| listing_view(&ev)).collect::<Vec<_>>(); - listings.sort_by(|a, b| b.event.created_at.cmp(&a.event.created_at)); - - Ok::<TradeListingListResponse, RpcError>(TradeListingListResponse { listings }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/domains/trade/listing/mod.rs b/src/api/jsonrpc/methods/domains/trade/listing/mod.rs @@ -1,29 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod dvm; -pub mod get; -pub mod list; -pub mod order; -pub mod orders; -pub mod series; -pub mod validate; - -mod helpers; -mod types; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - get::register(&mut m, &registry)?; - list::register(&mut m, &registry)?; - dvm::register(&mut m, &registry)?; - series::register(&mut m, &registry)?; - order::register(&mut m, &registry)?; - orders::register(&mut m, &registry)?; - validate::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/domains/trade/listing/order.rs b/src/api/jsonrpc/methods/domains/trade/listing/order.rs @@ -1,181 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_parse_pubkey, - radroots_nostr_send_event, - RadrootsNostrPublicKey, -}; -use radroots_trade::listing::dvm::{ - TradeListingEnvelope, - TradeListingMessageType, - TradeOrderResponse, -}; -use radroots_trade::listing::order::TradeOrder; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; - -use super::helpers::parse_listing_addr; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "snake_case", tag = "type")] -enum TradeListingOrderPayload { - OrderRequest { order: TradeOrder }, - OrderResponse { - order_id: String, - accepted: bool, - #[serde(default)] - reason: Option<String>, - }, -} - -#[derive(Debug, Deserialize)] -struct TradeListingOrderParams { - listing_addr: String, - recipient_pubkey: String, - #[serde(flatten)] - payload: TradeListingOrderPayload, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("trade.listing.order"); - m.register_async_method("trade.listing.order", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let TradeListingOrderParams { - listing_addr, - recipient_pubkey, - payload, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let addr = parse_listing_addr(&listing_addr)?; - let listing_addr = addr.as_str(); - - let recipient = radroots_nostr_parse_pubkey(&recipient_pubkey) - .map_err(|e| RpcError::InvalidParams(format!("invalid recipient_pubkey: {e}")))?; - let recipient_pubkey = recipient.to_string(); - - let (message_type, order_id, content) = match payload { - TradeListingOrderPayload::OrderRequest { order } => { - validate_order_request(&order, &addr, &ctx.state.pubkey, &listing_addr)?; - let order_id = order.order_id.trim().to_string(); - let envelope = TradeListingEnvelope::new( - TradeListingMessageType::OrderRequest, - listing_addr.clone(), - Some(order_id.clone()), - order, - ); - envelope - .validate() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - let content = serde_json::to_string(&envelope) - .map_err(|e| RpcError::Other(format!("failed to encode envelope: {e}")))?; - (TradeListingMessageType::OrderRequest, order_id, content) - } - TradeListingOrderPayload::OrderResponse { - order_id, - accepted, - reason, - } => { - validate_order_response(&order_id, &addr, &ctx.state.pubkey)?; - let order_id = order_id.trim().to_string(); - let response = TradeOrderResponse { accepted, reason }; - let envelope = TradeListingEnvelope::new( - TradeListingMessageType::OrderResponse, - listing_addr.clone(), - Some(order_id.clone()), - response, - ); - envelope - .validate() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - let content = serde_json::to_string(&envelope) - .map_err(|e| RpcError::Other(format!("failed to encode envelope: {e}")))?; - (TradeListingMessageType::OrderResponse, order_id, content) - } - }; - - let tags = vec![ - vec!["p".to_string(), recipient_pubkey], - vec!["a".to_string(), listing_addr.clone()], - vec!["d".to_string(), order_id], - ]; - - let builder = radroots_nostr_build_event(message_type.kind() as u32, content, tags) - .map_err(|e| RpcError::Other(format!("failed to build order event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish order event: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - Ok(()) -} - -fn validate_order_request( - order: &TradeOrder, - addr: &radroots_trade::listing::dvm::TradeListingAddress, - runtime_pubkey: &RadrootsNostrPublicKey, - listing_addr: &str, -) -> Result<(), RpcError> { - let order_id = order.order_id.trim(); - if order_id.is_empty() { - return Err(RpcError::InvalidParams("order_id must not be empty".to_string())); - } - - if order.listing_addr.trim() != listing_addr { - return Err(RpcError::InvalidParams( - "order listing_addr must match listing_addr".to_string(), - )); - } - - let buyer_pubkey = radroots_nostr_parse_pubkey(&order.buyer_pubkey) - .map_err(|e| RpcError::InvalidParams(format!("invalid buyer_pubkey: {e}")))?; - if &buyer_pubkey != runtime_pubkey { - return Err(RpcError::InvalidParams( - "buyer_pubkey must match runtime key".to_string(), - )); - } - - let seller_pubkey = radroots_nostr_parse_pubkey(&order.seller_pubkey) - .map_err(|e| RpcError::InvalidParams(format!("invalid seller_pubkey: {e}")))?; - let listing_seller = radroots_nostr_parse_pubkey(&addr.seller_pubkey) - .map_err(|e| RpcError::InvalidParams(format!("invalid listing author: {e}")))?; - if seller_pubkey != listing_seller { - return Err(RpcError::InvalidParams( - "seller_pubkey must match listing_addr seller".to_string(), - )); - } - - Ok(()) -} - -fn validate_order_response( - order_id: &str, - addr: &radroots_trade::listing::dvm::TradeListingAddress, - runtime_pubkey: &RadrootsNostrPublicKey, -) -> Result<(), RpcError> { - if order_id.trim().is_empty() { - return Err(RpcError::InvalidParams("order_id must not be empty".to_string())); - } - - let listing_seller = radroots_nostr_parse_pubkey(&addr.seller_pubkey) - .map_err(|e| RpcError::InvalidParams(format!("invalid listing author: {e}")))?; - if &listing_seller != runtime_pubkey { - return Err(RpcError::InvalidParams( - "order_response must be authored by the listing seller".to_string(), - )); - } - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/domains/trade/listing/orders.rs b/src/api/jsonrpc/methods/domains/trade/listing/orders.rs @@ -1,78 +0,0 @@ -#![forbid(unsafe_code)] - -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_trade::listing::dvm_kinds::TRADE_LISTING_DVM_KINDS; - -use super::helpers::{fetch_dvm_events, order_summaries, parse_listing_addr}; -use super::types::TradeListingOrderSummary; - -#[derive(Debug, Deserialize)] -struct TradeListingOrdersParams { - listing_addr: String, - #[serde(default)] - recipients: Option<Vec<String>>, - #[serde(default)] - kinds: Option<Vec<u16>>, - #[serde(default, flatten)] - query: EventListParams, -} - -#[derive(Clone, Debug, Serialize)] -struct TradeListingOrdersResponse { - orders: Vec<TradeListingOrderSummary>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("trade.listing.orders.list"); - m.register_async_method("trade.listing.orders.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let TradeListingOrdersParams { - listing_addr, - recipients, - kinds, - query, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let EventListParams { - authors, - limit, - since, - until, - timeout_secs, - } = query; - - let addr = parse_listing_addr(&listing_addr)?; - let kinds = kinds.unwrap_or_else(|| TRADE_LISTING_DVM_KINDS.to_vec()); - let authors = parse_pubkeys_opt("author", authors)?; - let recipients = parse_pubkeys_opt("recipient", recipients)?; - - let events = fetch_dvm_events( - &ctx.state.client, - &addr, - &kinds, - None, - authors.as_deref(), - recipients.as_deref(), - since, - until, - limit, - timeout_or(timeout_secs), - ) - .await?; - - let orders = order_summaries(&events, &listing_addr); - - Ok::<TradeListingOrdersResponse, RpcError>(TradeListingOrdersResponse { orders }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/domains/trade/listing/series.rs b/src/api/jsonrpc/methods/domains/trade/listing/series.rs @@ -1,105 +0,0 @@ -#![forbid(unsafe_code)] - -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; - -use super::helpers::{ - fetch_dvm_events, fetch_latest_listing_event, listing_view, order_summaries, parse_listing_addr, -}; -use super::types::{TradeListingOrderSummary, TradeListingSeriesView}; - -#[derive(Debug, Deserialize)] -struct TradeListingSeriesParams { - listing_addr: String, - #[serde(default)] - order_id: Option<String>, - #[serde(default)] - include_listing: Option<bool>, - #[serde(default)] - include_dvm: Option<bool>, - #[serde(default)] - limit: Option<u64>, - #[serde(default)] - since: Option<u64>, - #[serde(default)] - until: Option<u64>, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct TradeListingSeriesResponse { - series: TradeListingSeriesView, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("trade.listing.series.get"); - m.register_async_method("trade.listing.series.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let TradeListingSeriesParams { - listing_addr, - order_id, - include_listing, - include_dvm, - limit, - since, - until, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let addr = parse_listing_addr(&listing_addr)?; - let include_listing = include_listing.unwrap_or(true); - let include_dvm = include_dvm.unwrap_or(true); - - let listing = if include_listing { - fetch_latest_listing_event(&ctx.state.client, &addr, timeout_or(timeout_secs)) - .await? - .as_ref() - .map(listing_view) - } else { - None - }; - - let dvm_events = if include_dvm { - fetch_dvm_events( - &ctx.state.client, - &addr, - &TRADE_LISTING_DVM_KINDS, - order_id.as_deref(), - None, - None, - since, - until, - limit, - timeout_or(timeout_secs), - ) - .await? - } else { - Vec::new() - }; - - let orders = if include_dvm { - order_summaries(&dvm_events, &listing_addr) - } else { - Vec::<TradeListingOrderSummary>::new() - }; - - let series = TradeListingSeriesView { - listing, - dvm_events, - orders, - }; - - Ok::<TradeListingSeriesResponse, RpcError>(TradeListingSeriesResponse { series }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/domains/trade/listing/types.rs b/src/api/jsonrpc/methods/domains/trade/listing/types.rs @@ -1,38 +0,0 @@ -#![forbid(unsafe_code)] - -use radroots_events::listing::RadrootsListing; -use radroots_trade::listing::dvm::TradeListingEnvelope; -use serde::Serialize; - -use crate::api::jsonrpc::nostr::NostrEventView; - -#[derive(Clone, Debug, Serialize)] -pub struct ListingEventView { - pub event: NostrEventView, - pub listing: Option<RadrootsListing>, -} - -#[derive(Clone, Debug, Serialize)] -pub struct DvmEventView { - pub event: NostrEventView, - pub envelope: Option<TradeListingEnvelope<serde_json::Value>>, - pub envelope_error: Option<String>, -} - -#[derive(Clone, Debug, Serialize)] -pub struct TradeListingOrderSummary { - pub order_id: String, - pub listing_addr: String, - pub event_count: usize, - pub first_seen_at: u64, - pub last_seen_at: u64, - pub last_event_id: String, - pub last_event_kind: u32, -} - -#[derive(Clone, Debug, Serialize)] -pub struct TradeListingSeriesView { - pub listing: Option<ListingEventView>, - pub dvm_events: Vec<DvmEventView>, - pub orders: Vec<TradeListingOrderSummary>, -} diff --git a/src/api/jsonrpc/methods/domains/trade/listing/validate.rs b/src/api/jsonrpc/methods/domains/trade/listing/validate.rs @@ -1,363 +0,0 @@ -#![forbid(unsafe_code)] - -use std::time::Duration; - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use radroots_events::kinds::KIND_FARM; -use radroots_events::listing::RadrootsListingFarmRef; -use radroots_events::{RadrootsNostrEvent as RadrootsWireEvent, RadrootsNostrEventPtr}; -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_parse_pubkey, - radroots_nostr_send_event, - RadrootsNostrClient, - RadrootsNostrEvent as RadrootsRawEvent, - RadrootsNostrEventId, - RadrootsNostrFilter, - RadrootsNostrKind, -}; -use radroots_nostr::util::event_created_at_u32_saturating; -use radroots_trade::listing::dvm::{ - TradeListingEnvelope, - TradeListingMessageType, - TradeListingValidateRequest, - TradeListingValidateResult, -}; -use radroots_trade::listing::validation::{validate_listing_event, TradeListingValidationError}; - -use crate::api::jsonrpc::nostr::{event_tags, publish_response, PublishResponse}; -use crate::api::jsonrpc::params::timeout_or; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; - -use super::helpers::{fetch_latest_listing_event, parse_listing_addr}; - -#[derive(Debug, Deserialize)] -struct TradeListingValidateParams { - listing_addr: String, - #[serde(default)] - listing_event: Option<RadrootsNostrEventPtr>, - #[serde(default)] - timeout_secs: Option<u64>, - #[serde(default)] - recipient_pubkey: Option<String>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("trade.listing.validate.request"); - m.register_async_method("trade.listing.validate.request", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let TradeListingValidateRequestParams { - listing_addr, - recipient_pubkey, - listing_event, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let addr = parse_listing_addr(&listing_addr)?; - let listing_addr = addr.as_str(); - - let recipient = radroots_nostr_parse_pubkey(&recipient_pubkey) - .map_err(|e| RpcError::InvalidParams(format!("invalid recipient_pubkey: {e}")))?; - - let payload = TradeListingValidateRequest { listing_event }; - let envelope = TradeListingEnvelope::new( - TradeListingMessageType::ListingValidateRequest, - listing_addr.clone(), - None, - payload, - ); - envelope - .validate() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - let content = serde_json::to_string(&envelope) - .map_err(|e| RpcError::Other(format!("failed to encode envelope: {e}")))?; - let tags = vec![ - vec!["p".to_string(), recipient.to_string()], - vec!["a".to_string(), listing_addr.clone()], - ]; - - let builder = radroots_nostr_build_event( - TradeListingMessageType::ListingValidateRequest.kind() as u32, - content, - tags, - ) - .map_err(|e| RpcError::Other(format!("failed to build validate request event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish validate request: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - registry.track("trade.listing.validate"); - m.register_async_method("trade.listing.validate", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let TradeListingValidateParams { - listing_addr, - listing_event, - timeout_secs, - recipient_pubkey, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let addr = parse_listing_addr(&listing_addr)?; - let listing_addr = addr.as_str(); - let timeout_secs = timeout_or(timeout_secs); - - let listing_event = if let Some(ptr) = listing_event { - let event_id = RadrootsNostrEventId::parse(&ptr.id) - .map_err(|e| RpcError::InvalidParams(format!("invalid listing_event id: {e}")))?; - match fetch_event_by_id(&ctx.state.client, event_id, timeout_secs).await { - Ok(event) => event, - Err(_) => { - let errors = vec![TradeListingValidationError::ListingEventFetchFailed { - listing_addr: listing_addr.clone(), - }]; - let result = TradeListingValidateResult { - valid: false, - errors, - }; - return Ok::<TradeListingValidateResult, RpcError>(result); - } - } - } else { - match fetch_latest_listing_event(&ctx.state.client, &addr, timeout_secs).await { - Ok(event) => event, - Err(err) => { - if matches!(err, RpcError::InvalidParams(_)) { - return Err(err); - } - let errors = vec![TradeListingValidationError::ListingEventFetchFailed { - listing_addr: listing_addr.clone(), - }]; - let result = TradeListingValidateResult { - valid: false, - errors, - }; - return Ok::<TradeListingValidateResult, RpcError>(result); - } - } - }; - - let errors = if let Some(event) = listing_event { - let rr_event = radroots_event_from_nostr(&event); - match validate_listing_event(&rr_event) { - Ok(listing) => validate_farm_dependencies(&ctx.state.client, &listing.listing.farm, timeout_secs).await, - Err(err) => vec![err], - } - } else { - vec![TradeListingValidationError::ListingEventNotFound { - listing_addr: listing_addr.clone(), - }] - }; - - let result = TradeListingValidateResult { - valid: errors.is_empty(), - errors, - }; - - if let Some(recipient_pubkey) = recipient_pubkey { - publish_validate_result( - &ctx.state.client, - &listing_addr, - &recipient_pubkey, - &result, - ) - .await?; - } - Ok::<TradeListingValidateResult, RpcError>(result) - })?; - Ok(()) -} - -#[derive(Debug, Deserialize)] -struct TradeListingValidateRequestParams { - listing_addr: String, - recipient_pubkey: String, - #[serde(default)] - listing_event: Option<RadrootsNostrEventPtr>, -} - -async fn publish_validate_result( - client: &RadrootsNostrClient, - listing_addr: &str, - recipient_pubkey: &str, - result: &TradeListingValidateResult, -) -> Result<(), RpcError> { - let recipient = radroots_nostr_parse_pubkey(recipient_pubkey) - .map_err(|e| RpcError::InvalidParams(format!("invalid recipient_pubkey: {e}")))?; - let envelope = TradeListingEnvelope::new( - TradeListingMessageType::ListingValidateResult, - listing_addr.to_string(), - None, - result.clone(), - ); - envelope - .validate() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - let content = serde_json::to_string(&envelope) - .map_err(|e| RpcError::Other(format!("failed to encode envelope: {e}")))?; - let tags = vec![ - vec!["p".to_string(), recipient.to_string()], - vec!["a".to_string(), listing_addr.to_string()], - ]; - - let builder = radroots_nostr_build_event( - TradeListingMessageType::ListingValidateResult.kind() as u32, - content, - tags, - ) - .map_err(|e| RpcError::Other(format!("failed to build validate result event: {e}")))?; - - let output = radroots_nostr_send_event(client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish validate result: {e}")))?; - if !output.failed.is_empty() { - return Err(RpcError::Other(format!( - "validate result delivery failed: {:?}", - output.failed - ))); - } - Ok(()) -} - -async fn fetch_event_by_id( - client: &RadrootsNostrClient, - event_id: RadrootsNostrEventId, - timeout_secs: u64, -) -> Result<Option<RadrootsRawEvent>, RpcError> { - let filter = RadrootsNostrFilter::new().id(event_id); - let events = client - .fetch_events(filter, Duration::from_secs(timeout_secs)) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - Ok(events.into_iter().next()) -} - -async fn fetch_latest_event_by_kind( - client: &RadrootsNostrClient, - filter: RadrootsNostrFilter, - kind: RadrootsNostrKind, - timeout_secs: u64, -) -> Result<Option<RadrootsRawEvent>, RpcError> { - let events = client - .fetch_events(filter, Duration::from_secs(timeout_secs)) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - let mut latest: Option<RadrootsRawEvent> = None; - for ev in events { - if ev.kind != kind { - continue; - } - match &latest { - Some(cur) if ev.created_at <= cur.created_at => {} - _ => latest = Some(ev), - } - } - Ok(latest) -} - -async fn validate_farm_dependencies( - client: &RadrootsNostrClient, - farm: &RadrootsListingFarmRef, - timeout_secs: u64, -) -> Vec<TradeListingValidationError> { - let mut errors = Vec::new(); - let farm_pubkey = farm.pubkey.trim(); - let farm_d_tag = farm.d_tag.trim(); - let author = match radroots_nostr_parse_pubkey(farm_pubkey) { - Ok(author) => author, - Err(_) => { - errors.push(TradeListingValidationError::MissingFarmProfile); - errors.push(TradeListingValidationError::MissingFarmRecord); - return errors; - } - }; - - let profile_filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Metadata) - .author(author.clone()); - let profile_event = - match fetch_latest_event_by_kind(client, profile_filter, RadrootsNostrKind::Metadata, timeout_secs).await { - Ok(event) => event, - Err(_) => None, - }; - let has_profile = profile_event - .map(|event| tag_has_value(&event_tags(&event), "t", "radroots:type:farm")) - .unwrap_or(false); - if !has_profile { - errors.push(TradeListingValidationError::MissingFarmProfile); - } - - if !farm_d_tag.is_empty() { - let record_filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(KIND_FARM as u16)) - .author(author) - .identifier(farm_d_tag.to_string()); - let record_event = match fetch_latest_event_by_kind( - client, - record_filter, - RadrootsNostrKind::Custom(KIND_FARM as u16), - timeout_secs, - ) - .await - { - Ok(event) => event, - Err(_) => None, - }; - if record_event.is_none() { - errors.push(TradeListingValidationError::MissingFarmRecord); - } - } else { - errors.push(TradeListingValidationError::MissingFarmRecord); - } - - errors -} - -fn radroots_event_from_nostr(event: &RadrootsRawEvent) -> RadrootsWireEvent { - RadrootsWireEvent { - id: event.id.to_string(), - author: event.pubkey.to_string(), - created_at: event_created_at_u32_saturating(event), - kind: event.kind.as_u16() as u32, - tags: event_tags(event), - content: event.content.clone(), - sig: event.sig.to_string(), - } -} - -fn tag_has_value(tags: &[Vec<String>], key: &str, value: &str) -> bool { - tags.iter().any(|tag| { - tag.get(0).map(|k| k.as_str()) == Some(key) - && tag.get(1).map(|v| v.as_str()) == Some(value) - }) -} - -#[cfg(test)] -mod tests { - use super::tag_has_value; - - #[test] - fn tag_has_value_matches_exact() { - let tags = vec![ - vec!["t".to_string(), "radroots:type:farm".to_string()], - vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAg".to_string()], - ]; - assert!(tag_has_value(&tags, "t", "radroots:type:farm")); - assert!(!tag_has_value(&tags, "t", "radroots:type:individual")); - } - -} diff --git a/src/api/jsonrpc/methods/domains/trade/mod.rs b/src/api/jsonrpc/methods/domains/trade/mod.rs @@ -1,14 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod listing; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx.clone()); - m.merge(listing::module(ctx, registry)?)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/comment/get.rs b/src/api/jsonrpc/methods/events/comment/get.rs @@ -1,58 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_COMMENT; -use radroots_nostr::prelude::{ - RadrootsNostrEventId, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -use super::list::{build_comment_rows, CommentRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - require_non_empty, -}; - -#[derive(Debug, Deserialize)] -struct CommentGetParams { - id: String, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct CommentGetResponse { - comment: Option<CommentRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.comment.get"); - m.register_async_method("events.comment.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let CommentGetParams { id, timeout_secs } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let id = require_non_empty("id", id)?; - let event_id = RadrootsNostrEventId::parse(&id) - .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(KIND_COMMENT as u16)) - .id(event_id); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let comment = event.and_then(|event| build_comment_rows(vec![event]).into_iter().next()); - - Ok::<CommentGetResponse, RpcError>(CommentGetResponse { comment }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/comment/list.rs b/src/api/jsonrpc/methods/events/comment/list.rs @@ -1,275 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -use crate::api::jsonrpc::params::{ - apply_time_bounds, - limit_or, - parse_pubkeys_opt, - timeout_or, - EventListParams, -}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use crate::api::jsonrpc::methods::events::helpers::require_non_empty; -use radroots_events::comment::RadrootsComment; -use radroots_events::kinds::KIND_COMMENT; -use radroots_events_codec::comment::decode::comment_from_tags; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct CommentRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - comment: Option<RadrootsComment>, -} - -#[derive(Clone, Debug, Serialize)] -struct CommentListResponse { - comments: Vec<CommentRow>, -} - -#[derive(Debug, Default, Deserialize)] -struct CommentListParams { - #[serde(flatten)] - base: EventListParams, - #[serde(default)] - root_id: Option<String>, - #[serde(default)] - parent_id: Option<String>, -} - -pub(crate) fn build_comment_rows<I>(events: I) -> Vec<CommentRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let comment = parse_comment_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - CommentRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - comment, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_comment_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsComment> { - let kind = event.kind.as_u16() as u32; - comment_from_tags(kind, tags, &event.content).ok() -} - -fn comment_matches_filter( - comment: &RadrootsComment, - root_id: Option<&str>, - parent_id: Option<&str>, -) -> bool { - if let Some(root_id) = root_id { - if comment.root.id != root_id { - return false; - } - } - if let Some(parent_id) = parent_id { - if comment.parent.id != parent_id { - return false; - } - } - true -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.comment.list"); - m.register_async_method("events.comment.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let CommentListParams { - base, - root_id, - parent_id, - } = params - .parse::<Option<CommentListParams>>() - .map_err(|e| RpcError::InvalidParams(e.to_string()))? - .unwrap_or_default(); - - let EventListParams { - authors, - limit, - since, - until, - timeout_secs, - } = base; - - let root_id = match root_id { - Some(value) => Some(require_non_empty("root_id", value)?), - None => None, - }; - let parent_id = match parent_id { - Some(value) => Some(require_non_empty("parent_id", value)?), - None => None, - }; - - let limit = limit_or(limit); - - let mut filter = RadrootsNostrFilter::new() - .limit(limit) - .kind(RadrootsNostrKind::Custom(KIND_COMMENT as u16)); - - 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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let mut items = build_comment_rows(events); - if root_id.is_some() || parent_id.is_some() { - items.retain(|row| { - row.comment - .as_ref() - .map(|comment| comment_matches_filter(comment, root_id.as_deref(), parent_id.as_deref())) - .unwrap_or(false) - }); - } - if items.len() > limit { - items.truncate(limit); - } - - Ok::<CommentListResponse, RpcError>(CommentListResponse { comments: items }) - })?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_comment_rows; - use radroots_events::comment::RadrootsComment; - use radroots_events::kinds::{KIND_COMMENT, KIND_POST}; - use radroots_events::RadrootsNostrEventRef; - use radroots_events_codec::comment::encode::comment_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn comment_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 10); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_COMMENT, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_comment(root_id: &str, parent_id: &str, author: &str, content: &str) -> RadrootsComment { - let root = RadrootsNostrEventRef { - id: root_id.to_string(), - author: author.to_string(), - kind: KIND_POST, - d_tag: None, - relays: None, - }; - let parent = RadrootsNostrEventRef { - id: parent_id.to_string(), - author: author.to_string(), - kind: KIND_POST, - d_tag: None, - relays: None, - }; - RadrootsComment { - root, - parent, - content: content.to_string(), - } - } - - #[test] - fn comment_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let comment = sample_comment("root-1", "parent-1", pubkey, "hello"); - let tags = comment_build_tags(&comment).expect("tags"); - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let older = comment_event(&old_id, pubkey, 100, tags.clone(), &comment.content); - let newer = comment_event(&new_id, pubkey, 200, tags.clone(), &comment.content); - - let comments = build_comment_rows(vec![older, newer]); - - assert_eq!(comments.len(), 2); - assert_eq!(comments[0].id, new_id); - assert_eq!(comments[0].created_at, 200); - assert_eq!(comments[1].id, old_id); - assert_eq!(comments[1].created_at, 100); - } - - #[test] - fn comment_list_decodes_comment() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let comment = sample_comment("root-1", "parent-1", pubkey, "hello"); - let tags = comment_build_tags(&comment).expect("tags"); - let id = format!("{:064x}", 3); - let event = comment_event(&id, pubkey, 300, tags.clone(), &comment.content); - - let comments = build_comment_rows(vec![event]); - - assert_eq!(comments.len(), 1); - assert_eq!(comments[0].tags, tags); - let parsed = comments[0].comment.as_ref().expect("comment"); - assert_eq!(parsed.content, "hello"); - assert_eq!(parsed.root.id, "root-1"); - assert_eq!(parsed.parent.id, "parent-1"); - } - - #[test] - fn comment_filters_match_root_and_parent() { - let pubkey = "3bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let comment = sample_comment("root-1", "parent-1", pubkey, "hello"); - - assert!(super::comment_matches_filter(&comment, Some("root-1"), None)); - assert!(super::comment_matches_filter(&comment, None, Some("parent-1"))); - assert!(super::comment_matches_filter(&comment, Some("root-1"), Some("parent-1"))); - assert!(!super::comment_matches_filter(&comment, Some("root-2"), None)); - assert!(!super::comment_matches_filter(&comment, None, Some("parent-2"))); - } -} diff --git a/src/api/jsonrpc/methods/events/comment/mod.rs b/src/api/jsonrpc/methods/events/comment/mod.rs @@ -1,18 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod get; -pub mod list; -pub mod publish; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/comment/publish.rs b/src/api/jsonrpc/methods/events/comment/publish.rs @@ -1,51 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::comment::RadrootsComment; -use radroots_events::kinds::KIND_COMMENT; -use radroots_events_codec::comment::encode::to_wire_parts; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishCommentParams { - comment: RadrootsComment, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.comment.publish"); - m.register_async_method("events.comment.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishCommentParams { comment, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let parts = to_wire_parts(&comment) - .map_err(|e| RpcError::InvalidParams(format!("invalid comment: {e}")))?; - let mut tag_slices = parts.tags; - if let Some(extra_tags) = tags { - tag_slices.extend(extra_tags); - } - - let builder = radroots_nostr_build_event(KIND_COMMENT, parts.content, tag_slices) - .map_err(|e| RpcError::Other(format!("failed to build comment: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish comment: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/dvm_feedback/get.rs b/src/api/jsonrpc/methods/events/dvm_feedback/get.rs @@ -1,55 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_JOB_FEEDBACK; -use radroots_nostr::prelude::{RadrootsNostrEventId, RadrootsNostrFilter}; - -use super::list::{build_dvm_feedback_rows, DvmFeedbackRow}; -use crate::api::jsonrpc::methods::events::helpers::{fetch_latest_event, require_non_empty}; - -#[derive(Debug, Deserialize)] -struct DvmFeedbackGetParams { - id: String, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct DvmFeedbackGetResponse { - feedback: Option<DvmFeedbackRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.dvm_feedback.get"); - m.register_async_method("events.dvm_feedback.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let DvmFeedbackGetParams { id, timeout_secs } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let id = require_non_empty("id", id)?; - let event_id = RadrootsNostrEventId::parse(&id) - .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?; - - let filter = RadrootsNostrFilter::new().id(event_id); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let feedback = event.and_then(|event| { - let kind = event.kind.as_u16() as u32; - if kind != KIND_JOB_FEEDBACK { - return None; - } - build_dvm_feedback_rows(vec![event]).into_iter().next() - }); - - Ok::<DvmFeedbackGetResponse, RpcError>(DvmFeedbackGetResponse { feedback }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/dvm_feedback/list.rs b/src/api/jsonrpc/methods/events/dvm_feedback/list.rs @@ -1,237 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -use crate::api::jsonrpc::params::{ - apply_time_bounds, - limit_or, - parse_pubkeys_opt, - timeout_or, -}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::job_feedback::RadrootsJobFeedback; -use radroots_events::kinds::KIND_JOB_FEEDBACK; -use radroots_events_codec::job::feedback::decode::job_feedback_from_tags; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrEventId, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct DvmFeedbackRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - feedback: Option<RadrootsJobFeedback>, -} - -#[derive(Clone, Debug, Serialize)] -struct DvmFeedbackListResponse { - feedbacks: Vec<DvmFeedbackRow>, -} - -#[derive(Debug, Default, Deserialize)] -struct DvmFeedbackListParams { - #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] - limit: Option<u64>, - #[serde(default)] - since: Option<u64>, - #[serde(default)] - until: Option<u64>, - #[serde(default)] - timeout_secs: Option<u64>, - #[serde(default)] - request_id: Option<String>, -} - -pub(crate) fn build_dvm_feedback_rows<I>(events: I) -> Vec<DvmFeedbackRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let feedback = parse_dvm_feedback_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - DvmFeedbackRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - feedback, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_dvm_feedback_event( - event: &RadrootsNostrEvent, - tags: &[Vec<String>], -) -> Option<RadrootsJobFeedback> { - let kind = event.kind.as_u16() as u32; - if kind != KIND_JOB_FEEDBACK { - return None; - } - job_feedback_from_tags(kind, tags, &event.content).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.dvm_feedback.list"); - m.register_async_method("events.dvm_feedback.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let DvmFeedbackListParams { - authors, - limit, - since, - until, - timeout_secs, - request_id, - } = params - .parse::<Option<DvmFeedbackListParams>>() - .map_err(|e| RpcError::InvalidParams(e.to_string()))? - .unwrap_or_default(); - - let limit = limit_or(limit); - - let mut filter = RadrootsNostrFilter::new() - .limit(limit) - .kind(RadrootsNostrKind::Custom(KIND_JOB_FEEDBACK as u16)); - - 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); - - if let Some(request_id) = request_id { - let request_id = request_id.trim(); - if request_id.is_empty() { - return Err(RpcError::InvalidParams( - "request_id cannot be empty".to_string(), - )); - } - let request_id = RadrootsNostrEventId::parse(request_id) - .map_err(|e| RpcError::InvalidParams(format!("invalid request_id: {e}")))?; - filter = filter.event(request_id); - } - - let events = ctx - .state - .client - .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_dvm_feedback_rows(events); - - Ok::<DvmFeedbackListResponse, RpcError>(DvmFeedbackListResponse { feedbacks: items }) - })?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_dvm_feedback_rows; - use radroots_events::job::JobFeedbackStatus; - use radroots_events::job_feedback::RadrootsJobFeedback; - use radroots_events::kinds::KIND_JOB_FEEDBACK; - use radroots_events::RadrootsNostrEventPtr; - use radroots_events_codec::job::feedback::encode::job_feedback_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn dvm_feedback_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 11); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_JOB_FEEDBACK, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_feedback() -> RadrootsJobFeedback { - RadrootsJobFeedback { - kind: KIND_JOB_FEEDBACK as u16, - status: JobFeedbackStatus::Success, - extra_info: Some("ok".to_string()), - request_event: RadrootsNostrEventPtr { - id: "req".to_string(), - relays: None, - }, - customer_pubkey: None, - payment: None, - content: Some("payload".to_string()), - encrypted: false, - } - } - - #[test] - fn dvm_feedback_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let feedback = sample_feedback(); - let tags = job_feedback_build_tags(&feedback); - let older = dvm_feedback_event(&old_id, pubkey, 100, tags.clone(), "payload"); - let newer = dvm_feedback_event(&new_id, pubkey, 200, tags.clone(), "payload"); - - let feedbacks = build_dvm_feedback_rows(vec![older, newer]); - - assert_eq!(feedbacks.len(), 2); - assert_eq!(feedbacks[0].id, new_id); - assert_eq!(feedbacks[0].created_at, 200); - assert_eq!(feedbacks[1].id, old_id); - assert_eq!(feedbacks[1].created_at, 100); - } - - #[test] - fn dvm_feedback_list_decodes_feedback() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let feedback = sample_feedback(); - let content = feedback.content.clone().unwrap(); - let tags = job_feedback_build_tags(&feedback); - let id = format!("{:064x}", 3); - let event = dvm_feedback_event(&id, pubkey, 300, tags.clone(), &content); - - let feedbacks = build_dvm_feedback_rows(vec![event]); - - assert_eq!(feedbacks.len(), 1); - assert_eq!(feedbacks[0].tags, tags); - let decoded = feedbacks[0].feedback.as_ref().expect("feedback"); - assert_eq!(decoded, &feedback); - } -} diff --git a/src/api/jsonrpc/methods/events/dvm_feedback/mod.rs b/src/api/jsonrpc/methods/events/dvm_feedback/mod.rs @@ -1,18 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod get; -pub mod list; -pub mod publish; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/dvm_feedback/publish.rs b/src/api/jsonrpc/methods/events/dvm_feedback/publish.rs @@ -1,51 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::job_feedback::RadrootsJobFeedback; -use radroots_events_codec::job::encode::canonicalize_tags; -use radroots_events_codec::job::feedback::encode::to_wire_parts; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishDvmFeedbackParams { - feedback: RadrootsJobFeedback, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.dvm_feedback.publish"); - m.register_async_method("events.dvm_feedback.publish", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishDvmFeedbackParams { feedback, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let content = feedback.content.clone().unwrap_or_default(); - let mut parts = to_wire_parts(&feedback, &content) - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - if let Some(extra_tags) = tags { - parts.tags.extend(extra_tags); - canonicalize_tags(&mut parts.tags); - } - - let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags) - .map_err(|e| RpcError::Other(format!("failed to build dvm feedback event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish dvm feedback: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/dvm_request/get.rs b/src/api/jsonrpc/methods/events/dvm_request/get.rs @@ -1,55 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::is_request_kind; -use radroots_nostr::prelude::{RadrootsNostrEventId, RadrootsNostrFilter}; - -use super::list::{build_dvm_request_rows, DvmRequestRow}; -use crate::api::jsonrpc::methods::events::helpers::{fetch_latest_event, require_non_empty}; - -#[derive(Debug, Deserialize)] -struct DvmRequestGetParams { - id: String, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct DvmRequestGetResponse { - request: Option<DvmRequestRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.dvm_request.get"); - m.register_async_method("events.dvm_request.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let DvmRequestGetParams { id, timeout_secs } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let id = require_non_empty("id", id)?; - let event_id = RadrootsNostrEventId::parse(&id) - .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?; - - let filter = RadrootsNostrFilter::new().id(event_id); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let request = event.and_then(|event| { - let kind = event.kind.as_u16() as u32; - if !is_request_kind(kind) { - return None; - } - build_dvm_request_rows(vec![event]).into_iter().next() - }); - - Ok::<DvmRequestGetResponse, RpcError>(DvmRequestGetResponse { request }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/dvm_request/list.rs b/src/api/jsonrpc/methods/events/dvm_request/list.rs @@ -1,252 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -use crate::api::jsonrpc::params::{ - apply_time_bounds, - limit_or, - parse_pubkeys_opt, - timeout_or, -}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::job_request::RadrootsJobRequest; -use radroots_events::kinds::{is_request_kind, KIND_JOB_REQUEST_MAX, KIND_JOB_REQUEST_MIN}; -use radroots_events_codec::job::request::decode::job_request_from_tags; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct DvmRequestRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - request: Option<RadrootsJobRequest>, -} - -#[derive(Clone, Debug, Serialize)] -struct DvmRequestListResponse { - requests: Vec<DvmRequestRow>, -} - -#[derive(Debug, Default, Deserialize)] -struct DvmRequestListParams { - #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] - limit: Option<u64>, - #[serde(default)] - since: Option<u64>, - #[serde(default)] - until: Option<u64>, - #[serde(default)] - timeout_secs: Option<u64>, - #[serde(default)] - kinds: Option<Vec<u32>>, -} - -fn dvm_request_kinds_or(kinds: Option<Vec<u32>>) -> Result<Vec<RadrootsNostrKind>, RpcError> { - let kinds = match kinds { - Some(kinds) => { - if kinds.is_empty() { - return Err(RpcError::InvalidParams( - "dvm request kinds cannot be empty".to_string(), - )); - } - kinds - } - None => (KIND_JOB_REQUEST_MIN..=KIND_JOB_REQUEST_MAX).collect(), - }; - - let mut out = Vec::with_capacity(kinds.len()); - for kind in kinds { - if !is_request_kind(kind) { - return Err(RpcError::InvalidParams(format!( - "invalid dvm request kind: {kind}", - ))); - } - let kind = u16::try_from(kind) - .map_err(|_| RpcError::InvalidParams(format!("dvm request kind out of range: {kind}")))?; - out.push(RadrootsNostrKind::Custom(kind)); - } - Ok(out) -} - -pub(crate) fn build_dvm_request_rows<I>(events: I) -> Vec<DvmRequestRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let request = parse_dvm_request_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - DvmRequestRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - request, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_dvm_request_event( - event: &RadrootsNostrEvent, - tags: &[Vec<String>], -) -> Option<RadrootsJobRequest> { - let kind = event.kind.as_u16() as u32; - if !is_request_kind(kind) { - return None; - } - job_request_from_tags(kind, tags).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.dvm_request.list"); - m.register_async_method("events.dvm_request.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let DvmRequestListParams { - authors, - limit, - since, - until, - timeout_secs, - kinds, - } = params - .parse::<Option<DvmRequestListParams>>() - .map_err(|e| RpcError::InvalidParams(e.to_string()))? - .unwrap_or_default(); - - let limit = limit_or(limit); - let kinds = dvm_request_kinds_or(kinds)?; - - let mut filter = RadrootsNostrFilter::new().limit(limit).kinds(kinds); - - 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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_dvm_request_rows(events); - - Ok::<DvmRequestListResponse, RpcError>(DvmRequestListResponse { requests: items }) - })?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_dvm_request_rows; - use radroots_events::job::JobInputType; - use radroots_events::job_request::{RadrootsJobInput, RadrootsJobRequest}; - use radroots_events::kinds::KIND_JOB_REQUEST_MIN; - use radroots_events_codec::job::request::encode::job_request_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn dvm_request_event( - id: &str, - pubkey: &str, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 9); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": kind, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_request() -> RadrootsJobRequest { - RadrootsJobRequest { - kind: (KIND_JOB_REQUEST_MIN + 1) as u16, - inputs: vec![RadrootsJobInput { - data: "https://example.com".to_string(), - input_type: JobInputType::Url, - relay: None, - marker: None, - }], - output: None, - params: Vec::new(), - bid_sat: None, - relays: Vec::new(), - providers: Vec::new(), - topics: Vec::new(), - encrypted: false, - } - } - - #[test] - fn dvm_request_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let req = sample_request(); - let tags = job_request_build_tags(&req); - let older = dvm_request_event(&old_id, pubkey, 100, KIND_JOB_REQUEST_MIN + 1, tags.clone(), ""); - let newer = dvm_request_event(&new_id, pubkey, 200, KIND_JOB_REQUEST_MIN + 1, tags.clone(), ""); - - let requests = build_dvm_request_rows(vec![older, newer]); - - assert_eq!(requests.len(), 2); - assert_eq!(requests[0].id, new_id); - assert_eq!(requests[0].created_at, 200); - assert_eq!(requests[1].id, old_id); - assert_eq!(requests[1].created_at, 100); - } - - #[test] - fn dvm_request_list_decodes_request() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let req = sample_request(); - let tags = job_request_build_tags(&req); - let id = format!("{:064x}", 3); - let event = dvm_request_event(&id, pubkey, 300, KIND_JOB_REQUEST_MIN + 1, tags.clone(), "payload"); - - let requests = build_dvm_request_rows(vec![event]); - - assert_eq!(requests.len(), 1); - assert_eq!(requests[0].tags, tags); - let decoded = requests[0].request.as_ref().expect("request"); - assert_eq!(decoded, &req); - } -} diff --git a/src/api/jsonrpc/methods/events/dvm_request/mod.rs b/src/api/jsonrpc/methods/events/dvm_request/mod.rs @@ -1,18 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod get; -pub mod list; -pub mod publish; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/dvm_request/publish.rs b/src/api/jsonrpc/methods/events/dvm_request/publish.rs @@ -1,65 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::job_request::RadrootsJobRequest; -use radroots_events_codec::job::encode::canonicalize_tags; -use radroots_events_codec::job::request::encode::to_wire_parts; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; -use radroots_trade::listing::dvm_kinds::is_trade_listing_dvm_request_kind; - -#[derive(Debug, Deserialize)] -struct PublishDvmRequestParams { - request: RadrootsJobRequest, - #[serde(default)] - content: Option<String>, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.dvm_request.publish"); - m.register_async_method("events.dvm_request.publish", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishDvmRequestParams { - request, - content, - tags, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - if is_trade_listing_dvm_request_kind(request.kind) { - return Err(RpcError::InvalidParams( - "trade listing request kinds must use trade.listing.validate.request or trade.listing.order" - .to_string(), - )); - } - - let content = content.unwrap_or_default(); - let mut parts = to_wire_parts(&request, &content) - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - if let Some(extra_tags) = tags { - parts.tags.extend(extra_tags); - canonicalize_tags(&mut parts.tags); - } - - let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags) - .map_err(|e| RpcError::Other(format!("failed to build dvm request event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish dvm request: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/dvm_result/get.rs b/src/api/jsonrpc/methods/events/dvm_result/get.rs @@ -1,55 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::is_result_kind; -use radroots_nostr::prelude::{RadrootsNostrEventId, RadrootsNostrFilter}; - -use super::list::{build_dvm_result_rows, DvmResultRow}; -use crate::api::jsonrpc::methods::events::helpers::{fetch_latest_event, require_non_empty}; - -#[derive(Debug, Deserialize)] -struct DvmResultGetParams { - id: String, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct DvmResultGetResponse { - result: Option<DvmResultRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.dvm_result.get"); - m.register_async_method("events.dvm_result.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let DvmResultGetParams { id, timeout_secs } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let id = require_non_empty("id", id)?; - let event_id = RadrootsNostrEventId::parse(&id) - .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?; - - let filter = RadrootsNostrFilter::new().id(event_id); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let result = event.and_then(|event| { - let kind = event.kind.as_u16() as u32; - if !is_result_kind(kind) { - return None; - } - build_dvm_result_rows(vec![event]).into_iter().next() - }); - - Ok::<DvmResultGetResponse, RpcError>(DvmResultGetResponse { result }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/dvm_result/list.rs b/src/api/jsonrpc/methods/events/dvm_result/list.rs @@ -1,272 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -use crate::api::jsonrpc::params::{ - apply_time_bounds, - limit_or, - parse_pubkeys_opt, - timeout_or, -}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::job_result::RadrootsJobResult; -use radroots_events::kinds::{is_result_kind, KIND_JOB_RESULT_MAX, KIND_JOB_RESULT_MIN}; -use radroots_events_codec::job::result::decode::job_result_from_tags; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrEventId, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct DvmResultRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - result: Option<RadrootsJobResult>, -} - -#[derive(Clone, Debug, Serialize)] -struct DvmResultListResponse { - results: Vec<DvmResultRow>, -} - -#[derive(Debug, Default, Deserialize)] -struct DvmResultListParams { - #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] - limit: Option<u64>, - #[serde(default)] - since: Option<u64>, - #[serde(default)] - until: Option<u64>, - #[serde(default)] - timeout_secs: Option<u64>, - #[serde(default)] - kinds: Option<Vec<u32>>, - #[serde(default)] - request_id: Option<String>, -} - -fn dvm_result_kinds_or(kinds: Option<Vec<u32>>) -> Result<Vec<RadrootsNostrKind>, RpcError> { - let kinds = match kinds { - Some(kinds) => { - if kinds.is_empty() { - return Err(RpcError::InvalidParams( - "dvm result kinds cannot be empty".to_string(), - )); - } - kinds - } - None => (KIND_JOB_RESULT_MIN..=KIND_JOB_RESULT_MAX).collect(), - }; - - let mut out = Vec::with_capacity(kinds.len()); - for kind in kinds { - if !is_result_kind(kind) { - return Err(RpcError::InvalidParams(format!( - "invalid dvm result kind: {kind}", - ))); - } - let kind = u16::try_from(kind) - .map_err(|_| RpcError::InvalidParams(format!("dvm result kind out of range: {kind}")))?; - out.push(RadrootsNostrKind::Custom(kind)); - } - Ok(out) -} - -pub(crate) fn build_dvm_result_rows<I>(events: I) -> Vec<DvmResultRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let result = parse_dvm_result_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - DvmResultRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - result, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_dvm_result_event( - event: &RadrootsNostrEvent, - tags: &[Vec<String>], -) -> Option<RadrootsJobResult> { - let kind = event.kind.as_u16() as u32; - if !is_result_kind(kind) { - return None; - } - job_result_from_tags(kind, tags, &event.content).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.dvm_result.list"); - m.register_async_method("events.dvm_result.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let DvmResultListParams { - authors, - limit, - since, - until, - timeout_secs, - kinds, - request_id, - } = params - .parse::<Option<DvmResultListParams>>() - .map_err(|e| RpcError::InvalidParams(e.to_string()))? - .unwrap_or_default(); - - let limit = limit_or(limit); - let kinds = dvm_result_kinds_or(kinds)?; - - let mut filter = RadrootsNostrFilter::new().limit(limit).kinds(kinds); - - 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); - - if let Some(request_id) = request_id { - let request_id = request_id.trim(); - if request_id.is_empty() { - return Err(RpcError::InvalidParams( - "request_id cannot be empty".to_string(), - )); - } - let request_id = RadrootsNostrEventId::parse(request_id) - .map_err(|e| RpcError::InvalidParams(format!("invalid request_id: {e}")))?; - filter = filter.event(request_id); - } - - let events = ctx - .state - .client - .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_dvm_result_rows(events); - - Ok::<DvmResultListResponse, RpcError>(DvmResultListResponse { results: items }) - })?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_dvm_result_rows; - use radroots_events::job_request::RadrootsJobInput; - use radroots_events::job_result::RadrootsJobResult; - use radroots_events::kinds::KIND_JOB_RESULT_MIN; - use radroots_events::RadrootsNostrEventPtr; - use radroots_events_codec::job::result::encode::job_result_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn dvm_result_event( - id: &str, - pubkey: &str, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 10); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": kind, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_result() -> RadrootsJobResult { - RadrootsJobResult { - kind: (KIND_JOB_RESULT_MIN + 1) as u16, - request_event: RadrootsNostrEventPtr { - id: "req".to_string(), - relays: None, - }, - request_json: None, - inputs: vec![RadrootsJobInput { - data: "https://example.com".to_string(), - input_type: radroots_events::job::JobInputType::Url, - relay: None, - marker: None, - }], - customer_pubkey: None, - payment: None, - content: Some("payload".to_string()), - encrypted: false, - } - } - - #[test] - fn dvm_result_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let result = sample_result(); - let tags = job_result_build_tags(&result); - let older = dvm_result_event(&old_id, pubkey, 100, KIND_JOB_RESULT_MIN + 1, tags.clone(), "payload"); - let newer = dvm_result_event(&new_id, pubkey, 200, KIND_JOB_RESULT_MIN + 1, tags.clone(), "payload"); - - let results = build_dvm_result_rows(vec![older, newer]); - - assert_eq!(results.len(), 2); - assert_eq!(results[0].id, new_id); - assert_eq!(results[0].created_at, 200); - assert_eq!(results[1].id, old_id); - assert_eq!(results[1].created_at, 100); - } - - #[test] - fn dvm_result_list_decodes_result() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let result = sample_result(); - let content = result.content.clone().unwrap(); - let tags = job_result_build_tags(&result); - let id = format!("{:064x}", 3); - let event = dvm_result_event(&id, pubkey, 300, KIND_JOB_RESULT_MIN + 1, tags.clone(), &content); - - let results = build_dvm_result_rows(vec![event]); - - assert_eq!(results.len(), 1); - assert_eq!(results[0].tags, tags); - let decoded = results[0].result.as_ref().expect("result"); - assert_eq!(decoded, &result); - } -} diff --git a/src/api/jsonrpc/methods/events/dvm_result/mod.rs b/src/api/jsonrpc/methods/events/dvm_result/mod.rs @@ -1,18 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod get; -pub mod list; -pub mod publish; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/dvm_result/publish.rs b/src/api/jsonrpc/methods/events/dvm_result/publish.rs @@ -1,59 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::job_result::RadrootsJobResult; -use radroots_events_codec::job::encode::canonicalize_tags; -use radroots_events_codec::job::result::encode::to_wire_parts; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; -use radroots_trade::listing::dvm_kinds::is_trade_listing_dvm_result_kind; - -#[derive(Debug, Deserialize)] -struct PublishDvmResultParams { - result: RadrootsJobResult, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.dvm_result.publish"); - m.register_async_method("events.dvm_result.publish", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishDvmResultParams { result, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - if is_trade_listing_dvm_result_kind(result.kind) { - return Err(RpcError::InvalidParams( - "trade listing result kinds must use trade.listing.validate or trade.listing.order" - .to_string(), - )); - } - - let content = result.content.clone().unwrap_or_default(); - let mut parts = to_wire_parts(&result, &content) - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - if let Some(extra_tags) = tags { - parts.tags.extend(extra_tags); - canonicalize_tags(&mut parts.tags); - } - - let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags) - .map_err(|e| RpcError::Other(format!("failed to build dvm result event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish dvm result: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/farm/get.rs b/src/api/jsonrpc/methods/events/farm/get.rs @@ -1,61 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_FARM; -use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind}; - -use super::list::{build_farm_rows, FarmRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - parse_author_or_default, - require_non_empty, -}; - -#[derive(Debug, Deserialize)] -struct FarmGetParams { - d_tag: String, - #[serde(default)] - author: Option<String>, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct FarmGetResponse { - farm: Option<FarmRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.farm.get"); - m.register_async_method("events.farm.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let FarmGetParams { - d_tag, - author, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let author = parse_author_or_default(author, ctx.state.pubkey)?; - let d_tag = require_non_empty("d_tag", d_tag)?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(KIND_FARM as u16)) - .author(author) - .identifiers([d_tag]); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let farm = event.and_then(|event| build_farm_rows(vec![event]).into_iter().next()); - - Ok::<FarmGetResponse, RpcError>(FarmGetResponse { farm }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/farm/list.rs b/src/api/jsonrpc/methods/events/farm/list.rs @@ -1,197 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Serialize; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -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::farm::RadrootsFarm; -use radroots_events::kinds::KIND_FARM; -use radroots_events_codec::farm::decode::farm_from_event; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct FarmRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - farm: Option<RadrootsFarm>, -} - -#[derive(Clone, Debug, Serialize)] -struct FarmListResponse { - farms: Vec<FarmRow>, -} - -pub(crate) fn build_farm_rows<I>(events: I) -> Vec<FarmRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let farm = parse_farm_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - FarmRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - farm, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_farm_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsFarm> { - let kind = event.kind.as_u16() as u32; - farm_from_event(kind, tags, &event.content).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.farm.list"); - m.register_async_method("events.farm.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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() - .limit(limit) - .kind(RadrootsNostrKind::Custom(KIND_FARM as u16)); - - 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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_farm_rows(events); - - Ok::<FarmListResponse, RpcError>(FarmListResponse { farms: items }) - })?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_farm_rows; - use radroots_events::farm::RadrootsFarm; - use radroots_events::kinds::KIND_FARM; - use radroots_events_codec::farm::encode::farm_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn farm_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 7); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_FARM, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_farm(d_tag: &str, name: &str) -> RadrootsFarm { - RadrootsFarm { - d_tag: d_tag.to_string(), - name: name.to_string(), - about: None, - website: None, - picture: None, - banner: None, - location: None, - tags: None, - } - } - - #[test] - fn farm_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let farm = sample_farm("AAAAAAAAAAAAAAAAAAAAAA", "Farm One"); - let content = serde_json::to_string(&farm).expect("content"); - let tags = farm_build_tags(&farm).expect("tags"); - let older = farm_event(&old_id, pubkey, 100, tags.clone(), &content); - let newer = farm_event(&new_id, pubkey, 200, tags.clone(), &content); - - let farms = build_farm_rows(vec![older, newer]); - - assert_eq!(farms.len(), 2); - assert_eq!(farms[0].id, new_id); - assert_eq!(farms[0].created_at, 200); - assert_eq!(farms[1].id, old_id); - assert_eq!(farms[1].created_at, 100); - } - - #[test] - fn farm_list_uses_tag_d_when_missing_in_content() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let farm = sample_farm("AAAAAAAAAAAAAAAAAAAAAA", "Farm One"); - let tags = farm_build_tags(&farm).expect("tags"); - let content_farm = sample_farm("", "Farm One"); - let content = serde_json::to_string(&content_farm).expect("content"); - let id = format!("{:064x}", 3); - let event = farm_event(&id, pubkey, 300, tags.clone(), &content); - - let farms = build_farm_rows(vec![event]); - - assert_eq!(farms.len(), 1); - assert_eq!(farms[0].tags, tags); - let parsed = farms[0].farm.as_ref().expect("farm"); - assert_eq!(parsed.d_tag, "AAAAAAAAAAAAAAAAAAAAAA"); - assert_eq!(parsed.name, "Farm One"); - } -} diff --git a/src/api/jsonrpc/methods/events/farm/mod.rs b/src/api/jsonrpc/methods/events/farm/mod.rs @@ -1,16 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod publish; -pub mod list; -pub mod get; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/farm/publish.rs b/src/api/jsonrpc/methods/events/farm/publish.rs @@ -1,50 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::farm::RadrootsFarm; -use radroots_events::kinds::KIND_FARM; -use radroots_events_codec::farm::encode::farm_build_tags; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishFarmParams { - farm: RadrootsFarm, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.farm.publish"); - m.register_async_method("events.farm.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishFarmParams { farm, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let content = serde_json::to_string(&farm) - .map_err(|e| RpcError::InvalidParams(format!("invalid farm json: {e}")))?; - let mut tag_slices = - farm_build_tags(&farm).map_err(|e| RpcError::InvalidParams(e.to_string()))?; - if let Some(extra_tags) = tags { - tag_slices.extend(extra_tags); - } - - let builder = radroots_nostr_build_event(KIND_FARM, content, tag_slices) - .map_err(|e| RpcError::Other(format!("failed to build farm event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish farm: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/follow/get.rs b/src/api/jsonrpc/methods/events/follow/get.rs @@ -1,56 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind}; - -use super::list::{build_follow_rows, FollowRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - parse_author_or_default, -}; - -#[derive(Debug, Deserialize)] -struct FollowGetParams { - #[serde(default)] - author: Option<String>, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct FollowGetResponse { - follow: Option<FollowRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.follow.get"); - m.register_async_method("events.follow.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let FollowGetParams { - author, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let author = parse_author_or_default(author, ctx.state.pubkey)?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::ContactList) - .author(author); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let follow = event.and_then(|event| build_follow_rows(vec![event]).into_iter().next()); - - Ok::<FollowGetResponse, RpcError>(FollowGetResponse { follow }) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/follow/list.rs b/src/api/jsonrpc/methods/events/follow/list.rs @@ -1,214 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Serialize; -use std::collections::HashMap; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -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::follow::RadrootsFollow; -use radroots_events_codec::follow::decode::follow_from_tags; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, - RadrootsNostrPublicKey, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct FollowRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - follow: Option<RadrootsFollow>, -} - -#[derive(Clone, Debug, Serialize)] -struct FollowListResponse { - follows: Vec<FollowRow>, -} - -pub(crate) fn build_follow_rows<I>(events: I) -> Vec<FollowRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut latest_by_author: HashMap<RadrootsNostrPublicKey, RadrootsNostrEvent> = HashMap::new(); - for event in events { - match latest_by_author.get(&event.pubkey) { - Some(cur) if event.created_at <= cur.created_at => {} - _ => { - latest_by_author.insert(event.pubkey, event); - } - } - } - - let mut items = latest_by_author - .into_values() - .map(|ev| { - let tags = event_tags(&ev); - let follow = parse_follow_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - FollowRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - follow, - } - }) - .collect::<Vec<_>>(); - - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_follow_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsFollow> { - let kind = event.kind.as_u16() as u32; - let published_at = u32::try_from(event.created_at.as_secs()).ok()?; - follow_from_tags(kind, tags, published_at).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.follow.list"); - m.register_async_method("events.follow.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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::ContactList) - .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 stored = ctx - .state - .client - .database() - .query(filter.clone()) - .await - .map_err(|e| RpcError::Other(format!("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!("fetch failed: {e}")))?; - - let mut items = build_follow_rows(stored.into_iter().chain(fetched.into_iter())); - if items.len() > limit { - items.truncate(limit); - } - - Ok::<FollowListResponse, RpcError>(FollowListResponse { follows: items }) - })?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_follow_rows; - use radroots_events::follow::{RadrootsFollow, RadrootsFollowProfile}; - use radroots_events::kinds::KIND_FOLLOW; - use radroots_events_codec::follow::encode::follow_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn follow_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 9); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_FOLLOW, - "tags": tags, - "content": "", - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - #[test] - fn follow_list_picks_latest_per_author() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let tags = vec![vec!["p".to_string(), "target".to_string()]]; - let older = follow_event(&old_id, pubkey, 100, tags.clone()); - let newer = follow_event(&new_id, pubkey, 200, tags.clone()); - - let follows = build_follow_rows(vec![older, newer]); - - assert_eq!(follows.len(), 1); - assert_eq!(follows[0].id, new_id); - assert_eq!(follows[0].created_at, 200); - } - - #[test] - fn follow_list_decodes_follow_entries() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let follow = RadrootsFollow { - list: vec![RadrootsFollowProfile { - published_at: 0, - public_key: "pubkey".to_string(), - relay_url: Some("wss://relay".to_string()), - contact_name: Some("alice".to_string()), - }], - }; - let tags = follow_build_tags(&follow).expect("tags"); - let id = format!("{:064x}", 3); - let event = follow_event(&id, pubkey, 300, tags.clone()); - - let follows = build_follow_rows(vec![event]); - - assert_eq!(follows.len(), 1); - assert_eq!(follows[0].tags, tags); - let parsed = follows[0].follow.as_ref().expect("follow"); - assert_eq!(parsed.list.len(), 1); - assert_eq!(parsed.list[0].public_key, "pubkey"); - assert_eq!(parsed.list[0].relay_url.as_deref(), Some("wss://relay")); - assert_eq!(parsed.list[0].contact_name.as_deref(), Some("alice")); - assert_eq!(parsed.list[0].published_at, 300); - } -} diff --git a/src/api/jsonrpc/methods/events/follow/mod.rs b/src/api/jsonrpc/methods/events/follow/mod.rs @@ -1,20 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod get; -pub mod list; -pub mod publish; -pub mod update; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - update::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/follow/publish.rs b/src/api/jsonrpc/methods/events/follow/publish.rs @@ -1,51 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::follow::RadrootsFollow; -use radroots_events::kinds::KIND_FOLLOW; -use radroots_events_codec::follow::encode::to_wire_parts; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishFollowParams { - follow: RadrootsFollow, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.follow.publish"); - m.register_async_method("events.follow.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishFollowParams { follow, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let parts = to_wire_parts(&follow) - .map_err(|e| RpcError::InvalidParams(format!("invalid follow: {e}")))?; - let mut tag_slices = parts.tags; - if let Some(extra_tags) = tags { - tag_slices.extend(extra_tags); - } - - let builder = radroots_nostr_build_event(KIND_FOLLOW, parts.content, tag_slices) - .map_err(|e| RpcError::Other(format!("failed to build follow: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish follow: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/follow/update.rs b/src/api/jsonrpc/methods/events/follow/update.rs @@ -1,143 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{event_tags, publish_response, PublishResponse}; -use crate::api::jsonrpc::params::parse_pubkeys; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use crate::api::jsonrpc::methods::events::helpers::fetch_latest_event; -use radroots_events::follow::RadrootsFollow; -use radroots_events_codec::follow::decode::follow_from_tags; -use radroots_events_codec::follow::encode::{follow_apply, FollowMutation, to_wire_parts}; -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_send_event, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Copy, Debug, Deserialize)] -#[serde(rename_all = "snake_case")] -enum FollowUpdateAction { - Follow, - Unfollow, - Toggle, -} - -#[derive(Debug, Deserialize)] -struct FollowUpdateParams { - public_key: String, - #[serde(default)] - relay_url: Option<String>, - #[serde(default)] - contact_name: Option<String>, - #[serde(default)] - action: Option<FollowUpdateAction>, - #[serde(default)] - timeout_secs: Option<u64>, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.follow.update"); - m.register_async_method("events.follow.update", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let FollowUpdateParams { - public_key, - relay_url, - contact_name, - action, - timeout_secs, - tags, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let public_key = normalize_pubkey(public_key)?; - let relay_url = normalize_optional(relay_url); - let contact_name = normalize_optional(contact_name); - - let base_follow = load_latest_follow(&ctx, timeout_secs).await?; - - let mutation = match action.unwrap_or(FollowUpdateAction::Toggle) { - FollowUpdateAction::Follow => FollowMutation::Follow { - public_key, - relay_url, - contact_name, - }, - FollowUpdateAction::Unfollow => FollowMutation::Unfollow { public_key }, - FollowUpdateAction::Toggle => FollowMutation::Toggle { - public_key, - relay_url, - contact_name, - }, - }; - - let updated = follow_apply(&base_follow, mutation) - .map_err(|e| RpcError::InvalidParams(format!("invalid follow mutation: {e}")))?; - let mut parts = to_wire_parts(&updated) - .map_err(|e| RpcError::InvalidParams(format!("invalid follow: {e}")))?; - if let Some(extra_tags) = tags { - parts.tags.extend(extra_tags); - } - - let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags) - .map_err(|e| RpcError::Other(format!("failed to build follow: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish follow: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} - -fn normalize_optional(value: Option<String>) -> Option<String> { - value.and_then(|value| { - let trimmed = value.trim(); - if trimmed.is_empty() { - None - } else { - Some(trimmed.to_string()) - } - }) -} - -fn normalize_pubkey(value: String) -> Result<String, RpcError> { - let mut parsed = parse_pubkeys("public_key", &[value])?; - parsed - .pop() - .map(|key| key.to_string()) - .ok_or_else(|| RpcError::InvalidParams("public_key cannot be empty".to_string())) -} - -async fn load_latest_follow( - ctx: &RpcContext, - timeout_secs: Option<u64>, -) -> Result<RadrootsFollow, RpcError> { - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::ContactList) - .author(ctx.state.pubkey); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - match event { - Some(event) => { - let tags = event_tags(&event); - let published_at = u32::try_from(event.created_at.as_secs()).map_err(|_| { - RpcError::Other("follow event created_at overflow".to_string()) - })?; - follow_from_tags(event.kind.as_u16() as u32, &tags, published_at) - .map_err(|e| RpcError::Other(format!("invalid follow event: {e}"))) - } - None => Ok(RadrootsFollow { list: Vec::new() }), - } -} diff --git a/src/api/jsonrpc/methods/events/helpers.rs b/src/api/jsonrpc/methods/events/helpers.rs @@ -1,105 +0,0 @@ -#![forbid(unsafe_code)] - -use std::time::Duration; - -use crate::api::jsonrpc::params::{parse_pubkeys, timeout_or}; -use crate::api::jsonrpc::RpcError; -use radroots_nostr::prelude::{ - RadrootsNostrClient, - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrPublicKey, -}; - -pub(crate) fn parse_author_or_default( - author: Option<String>, - default: RadrootsNostrPublicKey, -) -> Result<RadrootsNostrPublicKey, RpcError> { - match author { - Some(author) => { - let authors = vec![author]; - let mut parsed = parse_pubkeys("author", &authors)?; - parsed - .pop() - .ok_or_else(|| RpcError::InvalidParams("author cannot be empty".to_string())) - } - None => Ok(default), - } -} - -pub(crate) fn require_non_empty(label: &str, value: String) -> Result<String, RpcError> { - if value.trim().is_empty() { - Err(RpcError::InvalidParams(format!("{label} cannot be empty"))) - } else { - Ok(value) - } -} - -pub(crate) async fn fetch_latest_event( - client: &RadrootsNostrClient, - filter: RadrootsNostrFilter, - timeout_secs: Option<u64>, -) -> Result<Option<RadrootsNostrEvent>, RpcError> { - let stored = client - .database() - .query(filter.clone()) - .await - .map_err(|e| RpcError::Other(format!("query failed: {e}")))?; - let fetched = client - .fetch_events(filter, Duration::from_secs(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - Ok(select_latest_event( - stored.into_iter().chain(fetched.into_iter()), - )) -} - -fn select_latest_event<I>(events: I) -> Option<RadrootsNostrEvent> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut latest: Option<RadrootsNostrEvent> = None; - for event in events { - let replace = match latest.as_ref() { - Some(current) => event.created_at > current.created_at, - None => true, - }; - if replace { - latest = Some(event); - } - } - latest -} - -#[cfg(test)] -mod tests { - use super::select_latest_event; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn event_with_created_at(id: &str, created_at: u64) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 1); - let event_json = json!({ - "id": id, - "pubkey": "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4", - "created_at": created_at, - "kind": 1, - "tags": [], - "content": "content", - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - #[test] - fn select_latest_event_picks_newest() { - let older_id = format!("{:064x}", 1); - let newer_id = format!("{:064x}", 2); - let older = event_with_created_at(&older_id, 100); - let newer = event_with_created_at(&newer_id, 200); - let latest = select_latest_event(vec![older, newer]).expect("latest"); - assert_eq!(latest.id.to_string(), newer_id); - assert_eq!(latest.created_at.as_secs(), 200); - } - -} diff --git a/src/api/jsonrpc/methods/events/list_set/get.rs b/src/api/jsonrpc/methods/events/list_set/get.rs @@ -1,73 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::{is_nip51_list_set_kind, KIND_LIST_SET_GENERIC}; -use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind}; - -use super::list::{build_list_set_rows, ListSetRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - parse_author_or_default, - require_non_empty, -}; - -#[derive(Debug, Deserialize)] -struct ListSetGetParams { - d_tag: String, - #[serde(default)] - author: Option<String>, - #[serde(default)] - kind: Option<u32>, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct ListSetGetResponse { - list_set: Option<ListSetRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.list_set.get"); - m.register_async_method("events.list_set.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let ListSetGetParams { - d_tag, - author, - kind, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let author = parse_author_or_default(author, ctx.state.pubkey)?; - let d_tag = require_non_empty("d_tag", d_tag)?; - - let kind = kind.unwrap_or(KIND_LIST_SET_GENERIC); - if !is_nip51_list_set_kind(kind) { - return Err(RpcError::InvalidParams(format!( - "invalid list_set kind: {kind}" - ))); - } - let kind = u16::try_from(kind) - .map_err(|_| RpcError::InvalidParams(format!("list_set kind out of range: {kind}")))?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(kind)) - .author(author) - .identifiers([d_tag]); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let list_set = event.and_then(|event| build_list_set_rows(vec![event]).into_iter().next()); - - Ok::<ListSetGetResponse, RpcError>(ListSetGetResponse { list_set }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/list_set/list.rs b/src/api/jsonrpc/methods/events/list_set/list.rs @@ -1,360 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -use crate::api::jsonrpc::params::{apply_time_bounds, limit_or, parse_pubkeys_opt, timeout_or}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::{is_nip51_list_set_kind, KIND_LIST_SET_GENERIC}; -use radroots_events::list_set::RadrootsListSet; -use radroots_events_codec::list_set::decode::list_set_from_tags; -use radroots_nostr::prelude::{ - RadrootsNostrClient, - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct ListSetRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - list_set: Option<RadrootsListSet>, -} - -#[derive(Clone, Debug, Serialize)] -struct ListSetListResponse { - list_sets: Vec<ListSetRow>, -} - -#[derive(Debug, Default, Deserialize)] -struct ListSetListParams { - #[serde(default)] - authors: Option<Vec<String>>, - #[serde(default)] - limit: Option<u64>, - #[serde(default)] - since: Option<u64>, - #[serde(default)] - until: Option<u64>, - #[serde(default)] - timeout_secs: Option<u64>, - #[serde(default)] - kinds: Option<Vec<u32>>, - #[serde(default)] - d_tags: Option<Vec<String>>, -} - -fn list_set_kinds_or(kinds: Option<Vec<u32>>) -> Result<Vec<RadrootsNostrKind>, RpcError> { - let kinds = kinds.unwrap_or_else(|| vec![KIND_LIST_SET_GENERIC]); - if kinds.is_empty() { - return Err(RpcError::InvalidParams( - "list_set kinds cannot be empty".to_string(), - )); - } - let mut out = Vec::with_capacity(kinds.len()); - for kind in kinds { - if !is_nip51_list_set_kind(kind) { - return Err(RpcError::InvalidParams(format!( - "invalid list_set kind: {kind}" - ))); - } - let kind = u16::try_from(kind).map_err(|_| { - RpcError::InvalidParams(format!("list_set kind out of range: {kind}")) - })?; - out.push(RadrootsNostrKind::Custom(kind)); - } - Ok(out) -} - -pub(crate) fn build_list_set_rows<I>(events: I) -> Vec<ListSetRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let list_set = parse_list_set_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - ListSetRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - list_set, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_list_set_event( - event: &RadrootsNostrEvent, - tags: &[Vec<String>], -) -> Option<RadrootsListSet> { - let kind = event.kind.as_u16() as u32; - list_set_from_tags(kind, event.content.clone(), tags).ok() -} - -fn merge_list_set_events( - stored: Vec<RadrootsNostrEvent>, - fetched: Vec<RadrootsNostrEvent>, -) -> Vec<RadrootsNostrEvent> { - let mut seen = HashSet::new(); - let mut combined = Vec::with_capacity(stored.len() + fetched.len()); - for event in stored.into_iter().chain(fetched) { - let id = event.id.to_string(); - if seen.insert(id) { - combined.push(event); - } - } - combined -} - -async fn query_list_set_events( - client: &RadrootsNostrClient, - base_filter: RadrootsNostrFilter, - d_tags: Option<Vec<String>>, -) -> Result<Vec<RadrootsNostrEvent>, RpcError> { - match d_tags { - Some(d_tags) if d_tags.len() > 1 => { - let mut events = Vec::new(); - let mut seen = HashSet::new(); - for d_tag in d_tags.into_iter().filter(|tag| !tag.trim().is_empty()) { - let filter = base_filter.clone().identifiers([d_tag]); - let items = client - .database() - .query(filter) - .await - .map_err(|e| RpcError::Other(format!("query failed: {e}")))?; - for item in items { - let id = item.id.to_string(); - if seen.insert(id) { - events.push(item); - } - } - } - Ok(events) - } - Some(d_tags) => { - let mut filter = base_filter; - if let Some(d_tag) = d_tags.into_iter().find(|tag| !tag.trim().is_empty()) { - filter = filter.identifiers([d_tag]); - } - let events = client - .database() - .query(filter) - .await - .map_err(|e| RpcError::Other(format!("query failed: {e}")))?; - Ok(events.into_iter().collect()) - } - None => { - let events = client - .database() - .query(base_filter) - .await - .map_err(|e| RpcError::Other(format!("query failed: {e}")))?; - Ok(events.into_iter().collect()) - } - } -} - -async fn fetch_list_set_events( - client: &RadrootsNostrClient, - base_filter: RadrootsNostrFilter, - d_tags: Option<Vec<String>>, - timeout: Duration, -) -> Result<Vec<RadrootsNostrEvent>, RpcError> { - match d_tags { - Some(d_tags) if d_tags.len() > 1 => { - let mut events = Vec::new(); - let mut seen = HashSet::new(); - for d_tag in d_tags.into_iter().filter(|tag| !tag.trim().is_empty()) { - let filter = base_filter.clone().identifiers([d_tag]); - let items = client - .fetch_events(filter, timeout) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - for item in items { - let id = item.id.to_string(); - if seen.insert(id) { - events.push(item); - } - } - } - Ok(events) - } - Some(d_tags) => { - let mut filter = base_filter; - if let Some(d_tag) = d_tags.into_iter().find(|tag| !tag.trim().is_empty()) { - filter = filter.identifiers([d_tag]); - } - let events = client - .fetch_events(filter, timeout) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - Ok(events) - } - None => { - let events = client - .fetch_events(base_filter, timeout) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - Ok(events) - } - } -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.list_set.list"); - m.register_async_method("events.list_set.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let ListSetListParams { - authors, - limit, - since, - until, - timeout_secs, - kinds, - d_tags, - } = params - .parse::<Option<ListSetListParams>>() - .map_err(|e| RpcError::InvalidParams(e.to_string()))? - .unwrap_or_default(); - - let limit = limit_or(limit); - let kinds = list_set_kinds_or(kinds)?; - - let mut filter = RadrootsNostrFilter::new().limit(limit).kinds(kinds); - - 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 stored = query_list_set_events(&ctx.state.client, filter.clone(), d_tags.clone()) - .await?; - let fetched = fetch_list_set_events( - &ctx.state.client, - filter, - d_tags, - Duration::from_secs(timeout_or(timeout_secs)), - ) - .await?; - - let events = merge_list_set_events(stored, fetched); - let mut items = build_list_set_rows(events); - if items.len() > limit { - items.truncate(limit); - } - - Ok::<ListSetListResponse, RpcError>(ListSetListResponse { list_sets: items }) - })?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_list_set_rows; - use radroots_events::kinds::KIND_LIST_SET_GENERIC; - use radroots_events::list::RadrootsListEntry; - use radroots_events::list_set::RadrootsListSet; - use radroots_events_codec::list_set::encode::list_set_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn list_set_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 12); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_LIST_SET_GENERIC, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_list_set(d_tag: &str, pubkey: &str) -> RadrootsListSet { - RadrootsListSet { - d_tag: d_tag.to_string(), - content: String::new(), - entries: vec![RadrootsListEntry { - tag: "p".to_string(), - values: vec![pubkey.to_string()], - }], - title: None, - description: None, - image: None, - } - } - - #[test] - fn list_set_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let list_set = sample_list_set("member_of.farms", pubkey); - let content = list_set.content.clone(); - let tags = list_set_build_tags(&list_set).expect("tags"); - let older = list_set_event(&old_id, pubkey, 100, tags.clone(), &content); - let newer = list_set_event(&new_id, pubkey, 200, tags.clone(), &content); - - let list_sets = build_list_set_rows(vec![older, newer]); - - assert_eq!(list_sets.len(), 2); - assert_eq!(list_sets[0].id, new_id); - assert_eq!(list_sets[0].created_at, 200); - assert_eq!(list_sets[1].id, old_id); - assert_eq!(list_sets[1].created_at, 100); - } - - #[test] - fn list_set_list_decodes_entries() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let list_set = sample_list_set("member_of.farms", pubkey); - let content = list_set.content.clone(); - let tags = list_set_build_tags(&list_set).expect("tags"); - let id = format!("{:064x}", 3); - let event = list_set_event(&id, pubkey, 300, tags.clone(), &content); - - let list_sets = build_list_set_rows(vec![event]); - - assert_eq!(list_sets.len(), 1); - assert_eq!(list_sets[0].tags, tags); - let parsed = list_sets[0].list_set.as_ref().expect("list set"); - assert_eq!(parsed.d_tag, "member_of.farms"); - assert_eq!(parsed.entries.len(), 1); - assert_eq!(parsed.entries[0].tag, "p"); - assert_eq!(parsed.entries[0].values, vec![pubkey.to_string()]); - } -} diff --git a/src/api/jsonrpc/methods/events/list_set/mod.rs b/src/api/jsonrpc/methods/events/list_set/mod.rs @@ -1,18 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod list; -pub mod publish; -pub mod get; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/list_set/publish.rs b/src/api/jsonrpc/methods/events/list_set/publish.rs @@ -1,64 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::{is_nip51_list_set_kind, KIND_LIST_SET_GENERIC}; -use radroots_events::list_set::RadrootsListSet; -use radroots_events_codec::list_set::encode::list_set_build_tags; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishListSetParams { - list_set: RadrootsListSet, - #[serde(default)] - kind: Option<u32>, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.list_set.publish"); - m.register_async_method("events.list_set.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishListSetParams { - list_set, - kind, - tags, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let kind = kind.unwrap_or(KIND_LIST_SET_GENERIC); - if !is_nip51_list_set_kind(kind) { - return Err(RpcError::InvalidParams(format!( - "invalid list_set kind: {kind}" - ))); - } - - let content = list_set.content.clone(); - let mut tag_slices = - list_set_build_tags(&list_set).map_err(|e| RpcError::InvalidParams(e.to_string()))?; - if let Some(extra_tags) = tags { - tag_slices.extend(extra_tags); - } - - let builder = radroots_nostr_build_event(kind, content, tag_slices) - .map_err(|e| RpcError::Other(format!("failed to build list_set event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish list_set: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/listing/get.rs b/src/api/jsonrpc/methods/events/listing/get.rs @@ -1,61 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_LISTING; -use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind}; - -use super::list::{build_listing_rows, ListingRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - parse_author_or_default, - require_non_empty, -}; - -#[derive(Debug, Deserialize)] -struct ListingGetParams { - d_tag: String, - #[serde(default)] - author: Option<String>, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct ListingGetResponse { - listing: Option<ListingRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.listing.get"); - m.register_async_method("events.listing.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let ListingGetParams { - d_tag, - author, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let author = parse_author_or_default(author, ctx.state.pubkey)?; - let d_tag = require_non_empty("d_tag", d_tag)?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(KIND_LISTING as u16)) - .author(author) - .identifiers([d_tag]); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let listing = event.and_then(|event| build_listing_rows(vec![event]).into_iter().next()); - - Ok::<ListingGetResponse, RpcError>(ListingGetResponse { listing }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/listing/list.rs b/src/api/jsonrpc/methods/events/listing/list.rs @@ -1,238 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Serialize; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -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::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; -use radroots_trade::listing::codec::listing_from_event_parts; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct ListingRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - listing: Option<RadrootsListing>, -} - -#[derive(Clone, Debug, Serialize)] -struct ListingListResponse { - listings: Vec<ListingRow>, -} - -pub(crate) fn build_listing_rows<I>(events: I) -> Vec<ListingRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let listing = parse_listing_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - ListingRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - listing, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_listing_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsListing> { - listing_from_event_parts(tags, &event.content).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.listing.list"); - m.register_async_method("events.listing.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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() - .limit(limit) - .kind(RadrootsNostrKind::Custom(KIND_LISTING as u16)); - - 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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_listing_rows(events); - - Ok::<ListingListResponse, RpcError>(ListingListResponse { listings: items }) - })?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_listing_rows; - use radroots_core::{ - RadrootsCoreCurrency, - RadrootsCoreDecimal, - RadrootsCoreMoney, - RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, - RadrootsCoreUnit, - }; - use radroots_events::kinds::KIND_LISTING; - use radroots_events::listing::{ - RadrootsListing, - RadrootsListingBin, - RadrootsListingFarmRef, - RadrootsListingProduct, - }; - use radroots_nostr::prelude::RadrootsNostrEvent; - use radroots_trade::listing::codec::listing_tags_build; - use serde_json::json; - - fn listing_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 5); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_LISTING, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_listing(farm_pubkey: &str) -> RadrootsListing { - let quantity = RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1_u64), RadrootsCoreUnit::Each); - let price = RadrootsCoreQuantityPrice::new( - RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10_u64), RadrootsCoreCurrency::USD), - quantity.clone(), - ); - let bin = RadrootsListingBin { - bin_id: "bin-1".to_string(), - quantity, - price_per_canonical_unit: price, - display_amount: None, - display_unit: None, - display_label: None, - display_price: None, - display_price_unit: None, - }; - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), - farm: RadrootsListingFarmRef { - pubkey: farm_pubkey.to_string(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), - }, - product: RadrootsListingProduct { - key: "coffee".to_string(), - title: "Coffee".to_string(), - category: "beverage".to_string(), - summary: None, - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".to_string(), - bins: vec![bin], - resource_area: None, - plot: None, - discounts: None, - inventory_available: None, - availability: None, - delivery_method: None, - location: None, - images: None, - } - } - - #[test] - fn listing_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let older = listing_event(&old_id, pubkey, 100, Vec::new(), ""); - let newer = listing_event(&new_id, pubkey, 200, Vec::new(), ""); - - let listings = build_listing_rows(vec![older, newer]); - - assert_eq!(listings.len(), 2); - assert_eq!(listings[0].id, new_id); - assert_eq!(listings[0].created_at, 200); - assert_eq!(listings[1].id, old_id); - assert_eq!(listings[1].created_at, 100); - } - - #[test] - fn listing_list_builds_from_tags_when_content_empty() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let listing = sample_listing(pubkey); - let tags = listing_tags_build(&listing).expect("tags"); - let id = format!("{:064x}", 3); - let event = listing_event(&id, pubkey, 300, tags.clone(), ""); - - let listings = build_listing_rows(vec![event]); - - assert_eq!(listings.len(), 1); - assert_eq!(listings[0].tags, tags); - let parsed = listings[0].listing.as_ref().expect("listing"); - assert_eq!(parsed.d_tag, "AAAAAAAAAAAAAAAAAAAAAg"); - assert_eq!(parsed.farm.pubkey, pubkey); - assert_eq!(parsed.primary_bin_id, "bin-1"); - } -} diff --git a/src/api/jsonrpc/methods/events/listing/mod.rs b/src/api/jsonrpc/methods/events/listing/mod.rs @@ -1,16 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod list; -pub mod publish; -pub mod get; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/listing/publish.rs b/src/api/jsonrpc/methods/events/listing/publish.rs @@ -1,46 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_LISTING; -use radroots_events::listing::RadrootsListing; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; -use radroots_trade::listing::codec::listing_tags_build; - -#[derive(Debug, Deserialize)] -struct PublishListingParams { - listing: RadrootsListing, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.listing.publish"); - m.register_async_method("events.listing.publish", |params, ctx, _| async move { - if ctx.state.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 mut tag_slices = listing_tags_build(&listing) - .map_err(|e| RpcError::InvalidParams(format!("invalid listing tags: {e}")))?; - if let Some(extra_tags) = tags { - tag_slices.extend(extra_tags); - } - let builder = radroots_nostr_build_event(KIND_LISTING, content, tag_slices) - .map_err(|e| RpcError::Other(format!("failed to build listing event: {e}")))?; - - let out = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish listing: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(out)) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/mod.rs b/src/api/jsonrpc/methods/events/mod.rs @@ -1,15 +0,0 @@ -pub mod comment; -pub mod farm; -pub mod follow; -pub mod dvm_feedback; -pub mod dvm_request; -pub mod dvm_result; -pub mod reaction; -pub mod plot; -pub mod resource_area; -pub mod resource_cap; -pub mod listing; -pub mod list_set; -pub mod post; -pub mod profile; -pub mod helpers; diff --git a/src/api/jsonrpc/methods/events/plot/get.rs b/src/api/jsonrpc/methods/events/plot/get.rs @@ -1,61 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_PLOT; -use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind}; - -use super::list::{build_plot_rows, PlotRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - parse_author_or_default, - require_non_empty, -}; - -#[derive(Debug, Deserialize)] -struct PlotGetParams { - d_tag: String, - #[serde(default)] - author: Option<String>, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct PlotGetResponse { - plot: Option<PlotRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.plot.get"); - m.register_async_method("events.plot.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let PlotGetParams { - d_tag, - author, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let author = parse_author_or_default(author, ctx.state.pubkey)?; - let d_tag = require_non_empty("d_tag", d_tag)?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(KIND_PLOT as u16)) - .author(author) - .identifiers([d_tag]); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let plot = event.and_then(|event| build_plot_rows(vec![event]).into_iter().next()); - - Ok::<PlotGetResponse, RpcError>(PlotGetResponse { plot }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/plot/list.rs b/src/api/jsonrpc/methods/events/plot/list.rs @@ -1,201 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Serialize; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -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_PLOT; -use radroots_events::plot::RadrootsPlot; -use radroots_events_codec::plot::decode::plot_from_event; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct PlotRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - plot: Option<RadrootsPlot>, -} - -#[derive(Clone, Debug, Serialize)] -struct PlotListResponse { - plots: Vec<PlotRow>, -} - -pub(crate) fn build_plot_rows<I>(events: I) -> Vec<PlotRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let plot = parse_plot_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - PlotRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - plot, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_plot_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsPlot> { - let kind = event.kind.as_u16() as u32; - plot_from_event(kind, tags, &event.content).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.plot.list"); - m.register_async_method("events.plot.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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() - .limit(limit) - .kind(RadrootsNostrKind::Custom(KIND_PLOT as u16)); - - 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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_plot_rows(events); - - Ok::<PlotListResponse, RpcError>(PlotListResponse { plots: items }) - })?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_plot_rows; - use radroots_events::farm::RadrootsFarmRef; - use radroots_events::kinds::KIND_PLOT; - use radroots_events::plot::RadrootsPlot; - use radroots_events_codec::plot::encode::plot_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn plot_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 8); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_PLOT, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_plot(d_tag: &str, name: &str, farm_pubkey: &str, farm_d_tag: &str) -> RadrootsPlot { - RadrootsPlot { - d_tag: d_tag.to_string(), - farm: RadrootsFarmRef { - pubkey: farm_pubkey.to_string(), - d_tag: farm_d_tag.to_string(), - }, - name: name.to_string(), - about: None, - location: None, - tags: None, - } - } - - #[test] - fn plot_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let farm_pubkey = pubkey; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let plot = sample_plot("AAAAAAAAAAAAAAAAAAAAAQ", "Plot One", farm_pubkey, "AAAAAAAAAAAAAAAAAAAAAA"); - let content = serde_json::to_string(&plot).expect("content"); - let tags = plot_build_tags(&plot).expect("tags"); - let older = plot_event(&old_id, pubkey, 100, tags.clone(), &content); - let newer = plot_event(&new_id, pubkey, 200, tags.clone(), &content); - - let plots = build_plot_rows(vec![older, newer]); - - assert_eq!(plots.len(), 2); - assert_eq!(plots[0].id, new_id); - assert_eq!(plots[0].created_at, 200); - assert_eq!(plots[1].id, old_id); - assert_eq!(plots[1].created_at, 100); - } - - #[test] - fn plot_list_uses_tag_fields_when_missing_in_content() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let plot = sample_plot("AAAAAAAAAAAAAAAAAAAAAQ", "Plot One", pubkey, "AAAAAAAAAAAAAAAAAAAAAA"); - let tags = plot_build_tags(&plot).expect("tags"); - let content_plot = sample_plot("", "Plot One", "", ""); - let content = serde_json::to_string(&content_plot).expect("content"); - let id = format!("{:064x}", 3); - let event = plot_event(&id, pubkey, 300, tags.clone(), &content); - - let plots = build_plot_rows(vec![event]); - - assert_eq!(plots.len(), 1); - assert_eq!(plots[0].tags, tags); - let parsed = plots[0].plot.as_ref().expect("plot"); - assert_eq!(parsed.d_tag, "AAAAAAAAAAAAAAAAAAAAAQ"); - assert_eq!(parsed.farm.pubkey, pubkey); - assert_eq!(parsed.farm.d_tag, "AAAAAAAAAAAAAAAAAAAAAA"); - } -} diff --git a/src/api/jsonrpc/methods/events/plot/mod.rs b/src/api/jsonrpc/methods/events/plot/mod.rs @@ -1,16 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod list; -pub mod publish; -pub mod get; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/plot/publish.rs b/src/api/jsonrpc/methods/events/plot/publish.rs @@ -1,50 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_PLOT; -use radroots_events::plot::RadrootsPlot; -use radroots_events_codec::plot::encode::plot_build_tags; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishPlotParams { - plot: RadrootsPlot, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.plot.publish"); - m.register_async_method("events.plot.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishPlotParams { plot, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let content = serde_json::to_string(&plot) - .map_err(|e| RpcError::InvalidParams(format!("invalid plot json: {e}")))?; - let mut tag_slices = - plot_build_tags(&plot).map_err(|e| RpcError::InvalidParams(e.to_string()))?; - if let Some(extra_tags) = tags { - tag_slices.extend(extra_tags); - } - - let builder = radroots_nostr_build_event(KIND_PLOT, content, tag_slices) - .map_err(|e| RpcError::Other(format!("failed to build plot event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish plot: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/post/get.rs b/src/api/jsonrpc/methods/events/post/get.rs @@ -1,57 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_nostr::prelude::{ - RadrootsNostrEventId, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -use super::list::{build_post_rows, PostRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - require_non_empty, -}; - -#[derive(Debug, Deserialize)] -struct PostGetParams { - id: String, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct PostGetResponse { - post: Option<PostRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.post.get"); - m.register_async_method("events.post.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let PostGetParams { id, timeout_secs } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let id = require_non_empty("id", id)?; - let event_id = RadrootsNostrEventId::parse(&id) - .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::TextNote) - .id(event_id); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let post = event.and_then(|event| build_post_rows(vec![event]).into_iter().next()); - - Ok::<PostGetResponse, RpcError>(PostGetResponse { post }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/post/list.rs b/src/api/jsonrpc/methods/events/post/list.rs @@ -1,173 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Serialize; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -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::post::RadrootsPost; -use radroots_events_codec::post::decode::post_from_content; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct PostRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - post: Option<RadrootsPost>, -} - -#[derive(Clone, Debug, Serialize)] -struct PostListResponse { - posts: Vec<PostRow>, -} - -pub(crate) fn build_post_rows<I>(events: I) -> Vec<PostRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let post = parse_post_event(&ev); - let event = event_view_with_tags(&ev, tags); - PostRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - post, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_post_event(event: &RadrootsNostrEvent) -> Option<RadrootsPost> { - let kind = event.kind.as_u16() as u32; - post_from_content(kind, &event.content).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.post.list"); - m.register_async_method("events.post.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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); - 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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_post_rows(events); - - Ok::<PostListResponse, RpcError>(PostListResponse { posts: items }) - })?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_post_rows; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn post_event( - id: &str, - pubkey: &str, - created_at: u64, - content: &str, - tags: Vec<Vec<String>>, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 4); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": 1, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - #[test] - fn post_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let older = post_event(&old_id, pubkey, 100, "old", Vec::new()); - let newer = post_event(&new_id, pubkey, 200, "new", Vec::new()); - - let posts = build_post_rows(vec![older, newer]); - - assert_eq!(posts.len(), 2); - assert_eq!(posts[0].id, new_id); - assert_eq!(posts[0].created_at, 200); - assert_eq!(posts[1].id, old_id); - assert_eq!(posts[1].created_at, 100); - } - - #[test] - fn post_list_preserves_content_and_tags() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let id = format!("{:064x}", 3); - let tags = vec![vec!["t".to_string(), "radroots".to_string()]]; - let event = post_event(&id, pubkey, 300, "hello", tags.clone()); - - let posts = build_post_rows(vec![event]); - - assert_eq!(posts.len(), 1); - assert_eq!(posts[0].content, "hello"); - assert_eq!(posts[0].tags, tags); - assert_eq!(posts[0].post.as_ref().unwrap().content, "hello"); - } -} diff --git a/src/api/jsonrpc/methods/events/post/mod.rs b/src/api/jsonrpc/methods/events/post/mod.rs @@ -1,16 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod list; -pub mod publish; -pub mod get; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/post/publish.rs b/src/api/jsonrpc/methods/events/post/publish.rs @@ -1,44 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_POST; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishPostParams { - content: String, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.post.publish"); - m.register_async_method("events.post.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishPostParams { 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 = radroots_nostr_build_event(KIND_POST, content, tags.unwrap_or_default()) - .map_err(|e| RpcError::Other(format!("failed to build note: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish note: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/profile/get.rs b/src/api/jsonrpc/methods/events/profile/get.rs @@ -1,60 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind}; - -use super::list::{build_profile_rows, ProfileListRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - parse_author_or_default, -}; - -#[derive(Debug, Deserialize)] -struct ProfileGetParams { - #[serde(default)] - author: Option<String>, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct ProfileGetResponse { - profile: Option<ProfileListRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.profile.get"); - m.register_async_method("events.profile.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let ProfileGetParams { - author, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let author = parse_author_or_default(author, ctx.state.pubkey)?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Metadata) - .author(author); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let profile = match event { - Some(event) => build_profile_rows(vec![author], vec![event])? - .into_iter() - .next(), - None => None, - }; - - Ok::<ProfileGetResponse, RpcError>(ProfileGetResponse { profile }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/profile/list.rs b/src/api/jsonrpc/methods/events/profile/list.rs @@ -1,213 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -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_npub_string, - RadrootsNostrFilter, - RadrootsNostrKind, - RadrootsNostrEvent, - RadrootsNostrPublicKey, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) 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(crate) fn build_profile_rows<I>( - authors: Vec<RadrootsNostrPublicKey>, - events: I, -) -> Result<Vec<ProfileListRow>, RpcError> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut latest_by_author: HashMap<RadrootsNostrPublicKey, RadrootsNostrEvent> = HashMap::new(); - for event in events { - match latest_by_author.get(&event.pubkey) { - Some(cur) if event.created_at <= cur.created_at => {} - _ => { - latest_by_author.insert(event.pubkey, event); - } - } - } - - 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>>() -} - -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 { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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 mut filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Metadata) - .authors(authors.clone()) - .limit(limit_or(limit)); - filter = apply_time_bounds(filter, since, until); - - 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 profiles = build_profile_rows(authors, stored.into_iter().chain(fetched.into_iter()))?; - - Ok::<ProfileListResponse, RpcError>(ProfileListResponse { profiles }) - })?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_profile_rows; - use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrPublicKey}; - use serde_json::json; - - fn parse_pubkey(hex: &str) -> RadrootsNostrPublicKey { - RadrootsNostrPublicKey::from_hex(hex).expect("pubkey") - } - - fn event_with_profile( - pubkey: &RadrootsNostrPublicKey, - created_at: u64, - name: &str, - id: &str, - ) -> RadrootsNostrEvent { - let content = serde_json::to_string(&json!({ "name": name })).expect("content"); - let sig = format!("{:0128x}", 2); - let event_json = json!({ - "id": id, - "pubkey": pubkey.to_string(), - "created_at": created_at, - "kind": 0, - "tags": [], - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - #[test] - fn profile_list_picks_latest_per_author() { - let author = parse_pubkey( - "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4", - ); - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let older = event_with_profile(&author, 100, "old", &old_id); - let newer = event_with_profile(&author, 200, "new", &new_id); - - let profiles = build_profile_rows(vec![author], vec![older, newer]).expect("profiles"); - - assert_eq!(profiles.len(), 1); - let row = &profiles[0]; - assert_eq!(row.created_at, Some(200)); - assert_eq!(row.event_id.as_deref(), Some(new_id.as_str())); - assert_eq!(row.radroots_profile.as_ref().unwrap().name, "new"); - assert_eq!(row.metadata_json.as_ref().unwrap()["name"], "new"); - } - - #[test] - fn profile_list_preserves_author_order_and_missing_rows() { - let author_a = parse_pubkey( - "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4", - ); - let author_b = parse_pubkey( - "3bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4", - ); - let event_id = format!("{:064x}", 3); - let event_b = event_with_profile(&author_b, 300, "b", &event_id); - - let profiles = - build_profile_rows(vec![author_a, author_b], vec![event_b]).expect("profiles"); - - assert_eq!(profiles.len(), 2); - assert_eq!(profiles[0].author_hex, author_a.to_string()); - assert!(profiles[0].event_id.is_none()); - assert_eq!(profiles[1].author_hex, author_b.to_string()); - assert_eq!(profiles[1].event_id.as_deref(), Some(event_id.as_str())); - } -} diff --git a/src/api/jsonrpc/methods/events/profile/mod.rs b/src/api/jsonrpc/methods/events/profile/mod.rs @@ -1,16 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod list; -pub mod publish; -pub mod get; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/profile/publish.rs b/src/api/jsonrpc/methods/events/profile/publish.rs @@ -1,43 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; - -use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; -use radroots_events_codec::profile::encode::to_wire_parts_with_profile_type; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishProfileParams { - profile: RadrootsProfile, - profile_type: RadrootsProfileType, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.profile.publish"); - m.register_async_method("events.profile.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishProfileParams { profile, profile_type } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let parts = to_wire_parts_with_profile_type(&profile, Some(profile_type)) - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - let builder = radroots_nostr_build_event(parts.kind, parts.content, parts.tags) - .map_err(|e| RpcError::Other(format!("failed to build profile event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish metadata: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/reaction/get.rs b/src/api/jsonrpc/methods/events/reaction/get.rs @@ -1,58 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_REACTION; -use radroots_nostr::prelude::{ - RadrootsNostrEventId, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -use super::list::{build_reaction_rows, ReactionRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - require_non_empty, -}; - -#[derive(Debug, Deserialize)] -struct ReactionGetParams { - id: String, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct ReactionGetResponse { - reaction: Option<ReactionRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.reaction.get"); - m.register_async_method("events.reaction.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let ReactionGetParams { id, timeout_secs } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let id = require_non_empty("id", id)?; - let event_id = RadrootsNostrEventId::parse(&id) - .map_err(|e| RpcError::InvalidParams(format!("invalid id: {e}")))?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(KIND_REACTION as u16)) - .id(event_id); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let reaction = event.and_then(|event| build_reaction_rows(vec![event]).into_iter().next()); - - Ok::<ReactionGetResponse, RpcError>(ReactionGetResponse { reaction }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/reaction/list.rs b/src/api/jsonrpc/methods/events/reaction/list.rs @@ -1,199 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Serialize; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -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_REACTION; -use radroots_events::reaction::RadrootsReaction; -use radroots_events_codec::reaction::decode::reaction_from_tags; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct ReactionRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - reaction: Option<RadrootsReaction>, -} - -#[derive(Clone, Debug, Serialize)] -struct ReactionListResponse { - reactions: Vec<ReactionRow>, -} - -pub(crate) fn build_reaction_rows<I>(events: I) -> Vec<ReactionRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let reaction = parse_reaction_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - ReactionRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - reaction, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_reaction_event(event: &RadrootsNostrEvent, tags: &[Vec<String>]) -> Option<RadrootsReaction> { - let kind = event.kind.as_u16() as u32; - reaction_from_tags(kind, tags, &event.content).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.reaction.list"); - m.register_async_method("events.reaction.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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() - .limit(limit) - .kind(RadrootsNostrKind::Custom(KIND_REACTION as u16)); - - 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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_reaction_rows(events); - - Ok::<ReactionListResponse, RpcError>(ReactionListResponse { reactions: items }) - })?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_reaction_rows; - use radroots_events::kinds::{KIND_REACTION, KIND_POST}; - use radroots_events::reaction::RadrootsReaction; - use radroots_events::RadrootsNostrEventRef; - use radroots_events_codec::reaction::encode::reaction_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn reaction_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 11); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_REACTION, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_reaction(event_id: &str, author: &str, content: &str) -> RadrootsReaction { - let root = RadrootsNostrEventRef { - id: event_id.to_string(), - author: author.to_string(), - kind: KIND_POST, - d_tag: None, - relays: None, - }; - RadrootsReaction { - root, - content: content.to_string(), - } - } - - #[test] - fn reaction_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let reaction = sample_reaction("root-1", pubkey, "+"); - let tags = reaction_build_tags(&reaction).expect("tags"); - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let older = reaction_event(&old_id, pubkey, 100, tags.clone(), &reaction.content); - let newer = reaction_event(&new_id, pubkey, 200, tags.clone(), &reaction.content); - - let reactions = build_reaction_rows(vec![older, newer]); - - assert_eq!(reactions.len(), 2); - assert_eq!(reactions[0].id, new_id); - assert_eq!(reactions[0].created_at, 200); - assert_eq!(reactions[1].id, old_id); - assert_eq!(reactions[1].created_at, 100); - } - - #[test] - fn reaction_list_decodes_reaction() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let reaction = sample_reaction("root-1", pubkey, "+"); - let tags = reaction_build_tags(&reaction).expect("tags"); - let id = format!("{:064x}", 3); - let event = reaction_event(&id, pubkey, 300, tags.clone(), &reaction.content); - - let reactions = build_reaction_rows(vec![event]); - - assert_eq!(reactions.len(), 1); - assert_eq!(reactions[0].tags, tags); - let parsed = reactions[0].reaction.as_ref().expect("reaction"); - assert_eq!(parsed.content, "+"); - assert_eq!(parsed.root.id, "root-1"); - } -} diff --git a/src/api/jsonrpc/methods/events/reaction/mod.rs b/src/api/jsonrpc/methods/events/reaction/mod.rs @@ -1,18 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod get; -pub mod list; -pub mod publish; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/reaction/publish.rs b/src/api/jsonrpc/methods/events/reaction/publish.rs @@ -1,51 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_REACTION; -use radroots_events::reaction::RadrootsReaction; -use radroots_events_codec::reaction::encode::to_wire_parts; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishReactionParams { - reaction: RadrootsReaction, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.reaction.publish"); - m.register_async_method("events.reaction.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishReactionParams { reaction, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let parts = to_wire_parts(&reaction) - .map_err(|e| RpcError::InvalidParams(format!("invalid reaction: {e}")))?; - let mut tag_slices = parts.tags; - if let Some(extra_tags) = tags { - tag_slices.extend(extra_tags); - } - - let builder = radroots_nostr_build_event(KIND_REACTION, parts.content, tag_slices) - .map_err(|e| RpcError::Other(format!("failed to build reaction: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish reaction: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/resource_area/get.rs b/src/api/jsonrpc/methods/events/resource_area/get.rs @@ -1,62 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_RESOURCE_AREA; -use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind}; - -use super::list::{build_resource_area_rows, ResourceAreaRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - parse_author_or_default, - require_non_empty, -}; - -#[derive(Debug, Deserialize)] -struct ResourceAreaGetParams { - d_tag: String, - #[serde(default)] - author: Option<String>, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct ResourceAreaGetResponse { - resource_area: Option<ResourceAreaRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.resource_area.get"); - m.register_async_method("events.resource_area.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let ResourceAreaGetParams { - d_tag, - author, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let author = parse_author_or_default(author, ctx.state.pubkey)?; - let d_tag = require_non_empty("d_tag", d_tag)?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(KIND_RESOURCE_AREA as u16)) - .author(author) - .identifiers([d_tag]); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let resource_area = - event.and_then(|event| build_resource_area_rows(vec![event]).into_iter().next()); - - Ok::<ResourceAreaGetResponse, RpcError>(ResourceAreaGetResponse { resource_area }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/resource_area/list.rs b/src/api/jsonrpc/methods/events/resource_area/list.rs @@ -1,247 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Serialize; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -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_RESOURCE_AREA; -use radroots_events::resource_area::RadrootsResourceArea; -use radroots_events_codec::resource_area::decode::resource_area_from_event; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct ResourceAreaRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - resource_area: Option<RadrootsResourceArea>, -} - -#[derive(Clone, Debug, Serialize)] -struct ResourceAreaListResponse { - resource_areas: Vec<ResourceAreaRow>, -} - -pub(crate) fn build_resource_area_rows<I>(events: I) -> Vec<ResourceAreaRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let resource_area = parse_resource_area_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - ResourceAreaRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - resource_area, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_resource_area_event( - event: &RadrootsNostrEvent, - tags: &[Vec<String>], -) -> Option<RadrootsResourceArea> { - let kind = event.kind.as_u16() as u32; - resource_area_from_event(kind, tags, &event.content).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.resource_area.list"); - m.register_async_method("events.resource_area.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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() - .limit(limit) - .kind(RadrootsNostrKind::Custom(KIND_RESOURCE_AREA as u16)); - - 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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_resource_area_rows(events); - - Ok::<ResourceAreaListResponse, RpcError>(ResourceAreaListResponse { - resource_areas: items, - }) - })?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_resource_area_rows; - use radroots_events::farm::{RadrootsGcsLocation, RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon}; - use radroots_events::kinds::KIND_RESOURCE_AREA; - use radroots_events::resource_area::{ - RadrootsResourceArea, RadrootsResourceAreaLocation, - }; - use radroots_events_codec::resource_area::encode::resource_area_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn resource_area_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 9); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_RESOURCE_AREA, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_location() -> RadrootsResourceAreaLocation { - let point = RadrootsGeoJsonPoint { - r#type: "Point".to_string(), - coordinates: [-76.9714, -6.0346], - }; - let polygon = RadrootsGeoJsonPolygon { - r#type: "Polygon".to_string(), - coordinates: vec![vec![ - [-76.9714, -6.0346], - [-76.9712, -6.0346], - [-76.9712, -6.0344], - [-76.9714, -6.0344], - [-76.9714, -6.0346], - ]], - }; - let gcs = RadrootsGcsLocation { - lat: -6.0346, - lng: -76.9714, - geohash: "6m6t5x".to_string(), - point, - polygon, - accuracy: None, - altitude: None, - tag_0: None, - label: None, - area: None, - elevation: None, - soil: None, - climate: None, - gc_id: None, - gc_name: None, - gc_admin1_id: None, - gc_admin1_name: None, - gc_country_id: None, - gc_country_name: None, - }; - RadrootsResourceAreaLocation { - primary: Some("Moyobamba".to_string()), - city: None, - region: None, - country: None, - gcs, - } - } - - fn sample_resource_area(d_tag: &str, name: &str) -> RadrootsResourceArea { - RadrootsResourceArea { - d_tag: d_tag.to_string(), - name: name.to_string(), - about: None, - location: sample_location(), - tags: None, - } - } - - #[test] - fn resource_area_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let area = sample_resource_area("AAAAAAAAAAAAAAAAAAAAAw", "Area One"); - let content = serde_json::to_string(&area).expect("content"); - let tags = resource_area_build_tags(&area).expect("tags"); - let older = resource_area_event(&old_id, pubkey, 100, tags.clone(), &content); - let newer = resource_area_event(&new_id, pubkey, 200, tags.clone(), &content); - - let areas = build_resource_area_rows(vec![older, newer]); - - assert_eq!(areas.len(), 2); - assert_eq!(areas[0].id, new_id); - assert_eq!(areas[0].created_at, 200); - assert_eq!(areas[1].id, old_id); - assert_eq!(areas[1].created_at, 100); - } - - #[test] - fn resource_area_list_uses_tag_d_when_missing_in_content() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let area = sample_resource_area("AAAAAAAAAAAAAAAAAAAAAw", "Area One"); - let tags = resource_area_build_tags(&area).expect("tags"); - let content_area = sample_resource_area("", "Area One"); - let content = serde_json::to_string(&content_area).expect("content"); - let id = format!("{:064x}", 3); - let event = resource_area_event(&id, pubkey, 300, tags.clone(), &content); - - let areas = build_resource_area_rows(vec![event]); - - assert_eq!(areas.len(), 1); - assert_eq!(areas[0].tags, tags); - let parsed = areas[0].resource_area.as_ref().expect("area"); - assert_eq!(parsed.d_tag, "AAAAAAAAAAAAAAAAAAAAAw"); - assert_eq!(parsed.name, "Area One"); - } -} diff --git a/src/api/jsonrpc/methods/events/resource_area/mod.rs b/src/api/jsonrpc/methods/events/resource_area/mod.rs @@ -1,16 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod list; -pub mod publish; -pub mod get; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/resource_area/publish.rs b/src/api/jsonrpc/methods/events/resource_area/publish.rs @@ -1,51 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_RESOURCE_AREA; -use radroots_events::resource_area::RadrootsResourceArea; -use radroots_events_codec::resource_area::encode::resource_area_build_tags; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishResourceAreaParams { - resource_area: RadrootsResourceArea, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.resource_area.publish"); - m.register_async_method("events.resource_area.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishResourceAreaParams { resource_area, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let content = serde_json::to_string(&resource_area).map_err(|e| { - RpcError::InvalidParams(format!("invalid resource_area json: {e}")) - })?; - let mut tag_slices = resource_area_build_tags(&resource_area) - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - if let Some(extra_tags) = tags { - tag_slices.extend(extra_tags); - } - - let builder = radroots_nostr_build_event(KIND_RESOURCE_AREA, content, tag_slices) - .map_err(|e| RpcError::Other(format!("failed to build resource_area event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish resource_area: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/resource_cap/get.rs b/src/api/jsonrpc/methods/events/resource_cap/get.rs @@ -1,62 +0,0 @@ -#![forbid(unsafe_code)] - -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::{Deserialize, Serialize}; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_RESOURCE_HARVEST_CAP; -use radroots_nostr::prelude::{RadrootsNostrFilter, RadrootsNostrKind}; - -use super::list::{build_resource_cap_rows, ResourceCapRow}; -use crate::api::jsonrpc::methods::events::helpers::{ - fetch_latest_event, - parse_author_or_default, - require_non_empty, -}; - -#[derive(Debug, Deserialize)] -struct ResourceCapGetParams { - d_tag: String, - #[serde(default)] - author: Option<String>, - #[serde(default)] - timeout_secs: Option<u64>, -} - -#[derive(Clone, Debug, Serialize)] -struct ResourceCapGetResponse { - resource_cap: Option<ResourceCapRow>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.resource_cap.get"); - m.register_async_method("events.resource_cap.get", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - let ResourceCapGetParams { - d_tag, - author, - timeout_secs, - } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let author = parse_author_or_default(author, ctx.state.pubkey)?; - let d_tag = require_non_empty("d_tag", d_tag)?; - - let filter = RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(KIND_RESOURCE_HARVEST_CAP as u16)) - .author(author) - .identifiers([d_tag]); - - let event = fetch_latest_event(&ctx.state.client, filter, timeout_secs).await?; - let resource_cap = - event.and_then(|event| build_resource_cap_rows(vec![event]).into_iter().next()); - - Ok::<ResourceCapGetResponse, RpcError>(ResourceCapGetResponse { resource_cap }) - })?; - Ok(()) -} diff --git a/src/api/jsonrpc/methods/events/resource_cap/list.rs b/src/api/jsonrpc/methods/events/resource_cap/list.rs @@ -1,218 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Serialize; -use std::time::Duration; - -use crate::api::jsonrpc::nostr::{event_tags, event_view_with_tags}; -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_RESOURCE_HARVEST_CAP; -use radroots_events::resource_cap::RadrootsResourceHarvestCap; -use radroots_events_codec::resource_cap::decode::resource_harvest_cap_from_event; -use radroots_nostr::prelude::{ - RadrootsNostrEvent, - RadrootsNostrFilter, - RadrootsNostrKind, -}; - -#[derive(Clone, Debug, Serialize)] -pub(crate) struct ResourceCapRow { - id: String, - author: String, - created_at: u64, - kind: u32, - tags: Vec<Vec<String>>, - content: String, - sig: String, - resource_cap: Option<RadrootsResourceHarvestCap>, -} - -#[derive(Clone, Debug, Serialize)] -struct ResourceCapListResponse { - resource_caps: Vec<ResourceCapRow>, -} - -pub(crate) fn build_resource_cap_rows<I>(events: I) -> Vec<ResourceCapRow> -where - I: IntoIterator<Item = RadrootsNostrEvent>, -{ - let mut items = events - .into_iter() - .map(|ev| { - let tags = event_tags(&ev); - let resource_cap = parse_resource_cap_event(&ev, &tags); - let event = event_view_with_tags(&ev, tags); - ResourceCapRow { - id: event.id, - author: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, - resource_cap, - } - }) - .collect::<Vec<_>>(); - items.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - items -} - -fn parse_resource_cap_event( - event: &RadrootsNostrEvent, - tags: &[Vec<String>], -) -> Option<RadrootsResourceHarvestCap> { - let kind = event.kind.as_u16() as u32; - resource_harvest_cap_from_event(kind, tags, &event.content).ok() -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.resource_cap.list"); - m.register_async_method("events.resource_cap.list", |params, ctx, _| async move { - if ctx.state.client.relays().await.is_empty() { - return Err(RpcError::NoRelays); - } - - 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() - .limit(limit) - .kind(RadrootsNostrKind::Custom(KIND_RESOURCE_HARVEST_CAP as u16)); - - 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(timeout_or(timeout_secs))) - .await - .map_err(|e| RpcError::Other(format!("fetch failed: {e}")))?; - - let items = build_resource_cap_rows(events); - - Ok::<ResourceCapListResponse, RpcError>(ResourceCapListResponse { - resource_caps: items, - }) - })?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::build_resource_cap_rows; - use radroots_core::{RadrootsCoreDecimal, RadrootsCoreQuantity, RadrootsCoreUnit}; - use radroots_events::kinds::KIND_RESOURCE_HARVEST_CAP; - use radroots_events::resource_area::RadrootsResourceAreaRef; - use radroots_events::resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestProduct}; - use radroots_events_codec::resource_cap::encode::resource_harvest_cap_build_tags; - use radroots_nostr::prelude::RadrootsNostrEvent; - use serde_json::json; - - fn resource_cap_event( - id: &str, - pubkey: &str, - created_at: u64, - tags: Vec<Vec<String>>, - content: &str, - ) -> RadrootsNostrEvent { - let sig = format!("{:0128x}", 10); - let event_json = json!({ - "id": id, - "pubkey": pubkey, - "created_at": created_at, - "kind": KIND_RESOURCE_HARVEST_CAP, - "tags": tags, - "content": content, - "sig": sig, - }); - serde_json::from_value(event_json).expect("event") - } - - fn sample_cap(d_tag: &str, area_pubkey: &str, area_d_tag: &str) -> RadrootsResourceHarvestCap { - let quantity = RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(100_u64), - RadrootsCoreUnit::MassG, - ); - RadrootsResourceHarvestCap { - d_tag: d_tag.to_string(), - resource_area: RadrootsResourceAreaRef { - pubkey: area_pubkey.to_string(), - d_tag: area_d_tag.to_string(), - }, - product: RadrootsResourceHarvestProduct { - key: "coffee".to_string(), - category: None, - }, - start: 100, - end: 200, - cap_quantity: quantity, - display_amount: None, - display_unit: None, - display_label: None, - tags: None, - } - } - - #[test] - fn resource_cap_list_sorts_by_created_at_desc() { - let pubkey = "1bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let old_id = format!("{:064x}", 1); - let new_id = format!("{:064x}", 2); - let cap = sample_cap("CAAAAAAAAAAAAAAAAAAAAA", pubkey, "AAAAAAAAAAAAAAAAAAAAAw"); - let content = serde_json::to_string(&cap).expect("content"); - let tags = resource_harvest_cap_build_tags(&cap).expect("tags"); - let older = resource_cap_event(&old_id, pubkey, 100, tags.clone(), &content); - let newer = resource_cap_event(&new_id, pubkey, 200, tags.clone(), &content); - - let caps = build_resource_cap_rows(vec![older, newer]); - - assert_eq!(caps.len(), 2); - assert_eq!(caps[0].id, new_id); - assert_eq!(caps[0].created_at, 200); - assert_eq!(caps[1].id, old_id); - assert_eq!(caps[1].created_at, 100); - } - - #[test] - fn resource_cap_list_uses_tag_d_when_missing_in_content() { - let pubkey = "2bdebe7b23fccb167fc8843280b789839dfa296ae9fd86cc9769b4813d76d8a4"; - let cap = sample_cap("CAAAAAAAAAAAAAAAAAAAAA", pubkey, "AAAAAAAAAAAAAAAAAAAAAw"); - let tags = resource_harvest_cap_build_tags(&cap).expect("tags"); - let mut content_cap = sample_cap("", pubkey, "AAAAAAAAAAAAAAAAAAAAAw"); - content_cap.display_label = Some("display".to_string()); - let content = serde_json::to_string(&content_cap).expect("content"); - let id = format!("{:064x}", 3); - let event = resource_cap_event(&id, pubkey, 300, tags.clone(), &content); - - let caps = build_resource_cap_rows(vec![event]); - - assert_eq!(caps.len(), 1); - assert_eq!(caps[0].tags, tags); - let parsed = caps[0].resource_cap.as_ref().expect("cap"); - assert_eq!(parsed.d_tag, "CAAAAAAAAAAAAAAAAAAAAA"); - assert_eq!(parsed.resource_area.d_tag, "AAAAAAAAAAAAAAAAAAAAAw"); - assert_eq!(parsed.product.key, "coffee"); - } -} diff --git a/src/api/jsonrpc/methods/events/resource_cap/mod.rs b/src/api/jsonrpc/methods/events/resource_cap/mod.rs @@ -1,16 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -pub mod list; -pub mod publish; -pub mod get; - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - list::register(&mut m, &registry)?; - publish::register(&mut m, &registry)?; - get::register(&mut m, &registry)?; - Ok(m) -} diff --git a/src/api/jsonrpc/methods/events/resource_cap/publish.rs b/src/api/jsonrpc/methods/events/resource_cap/publish.rs @@ -1,51 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Deserialize; - -use crate::api::jsonrpc::nostr::{publish_response, PublishResponse}; -use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; -use radroots_events::kinds::KIND_RESOURCE_HARVEST_CAP; -use radroots_events::resource_cap::RadrootsResourceHarvestCap; -use radroots_events_codec::resource_cap::encode::resource_harvest_cap_build_tags; -use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_send_event}; - -#[derive(Debug, Deserialize)] -struct PublishResourceCapParams { - resource_cap: RadrootsResourceHarvestCap, - #[serde(default)] - tags: Option<Vec<Vec<String>>>, -} - -pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { - registry.track("events.resource_cap.publish"); - m.register_async_method("events.resource_cap.publish", |params, ctx, _| async move { - let relays = ctx.state.client.relays().await; - if relays.is_empty() { - return Err(RpcError::NoRelays); - } - - let PublishResourceCapParams { resource_cap, tags } = params - .parse() - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - - let content = serde_json::to_string(&resource_cap).map_err(|e| { - RpcError::InvalidParams(format!("invalid resource_cap json: {e}")) - })?; - let mut tag_slices = resource_harvest_cap_build_tags(&resource_cap) - .map_err(|e| RpcError::InvalidParams(e.to_string()))?; - if let Some(extra_tags) = tags { - tag_slices.extend(extra_tags); - } - - let builder = radroots_nostr_build_event(KIND_RESOURCE_HARVEST_CAP, content, tag_slices) - .map_err(|e| RpcError::Other(format!("failed to build resource_cap event: {e}")))?; - - let output = radroots_nostr_send_event(&ctx.state.client, builder) - .await - .map_err(|e| RpcError::Other(format!("failed to publish resource_cap: {e}")))?; - - Ok::<PublishResponse, RpcError>(publish_response(output)) - })?; - - Ok(()) -} diff --git a/src/api/jsonrpc/methods/mod.rs b/src/api/jsonrpc/methods/mod.rs @@ -5,32 +5,15 @@ use jsonrpsee::server::RpcModule; use super::{context::RpcContext, registry::MethodRegistry}; -pub mod domains; -pub mod events; +pub mod nip46; pub mod relays; -pub mod system; pub fn register_all( root: &mut RpcModule<RpcContext>, ctx: RpcContext, registry: MethodRegistry, ) -> Result<()> { - root.merge(system::module(ctx.clone(), registry.clone())?)?; root.merge(relays::module(ctx.clone(), registry.clone())?)?; - root.merge(events::profile::module(ctx.clone(), registry.clone())?)?; - root.merge(events::follow::module(ctx.clone(), registry.clone())?)?; - root.merge(events::post::module(ctx.clone(), registry.clone())?)?; - root.merge(events::comment::module(ctx.clone(), registry.clone())?)?; - root.merge(events::reaction::module(ctx.clone(), registry.clone())?)?; - root.merge(events::listing::module(ctx.clone(), registry.clone())?)?; - root.merge(events::list_set::module(ctx.clone(), registry.clone())?)?; - root.merge(events::dvm_request::module(ctx.clone(), registry.clone())?)?; - root.merge(events::dvm_result::module(ctx.clone(), registry.clone())?)?; - root.merge(events::dvm_feedback::module(ctx.clone(), registry.clone())?)?; - root.merge(events::farm::module(ctx.clone(), registry.clone())?)?; - root.merge(events::plot::module(ctx.clone(), registry.clone())?)?; - root.merge(events::resource_area::module(ctx.clone(), registry.clone())?)?; - root.merge(events::resource_cap::module(ctx.clone(), registry.clone())?)?; - root.merge(domains::trade::module(ctx, registry)?)?; + root.merge(nip46::module(ctx, registry)?)?; Ok(()) } diff --git a/src/api/jsonrpc/methods/nip46/mod.rs b/src/api/jsonrpc/methods/nip46/mod.rs @@ -0,0 +1,14 @@ +#![forbid(unsafe_code)] + +use anyhow::Result; +use jsonrpsee::server::RpcModule; + +use crate::api::jsonrpc::{MethodRegistry, RpcContext}; + +pub mod status; + +pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { + let mut m = RpcModule::new(ctx); + status::register(&mut m, &registry)?; + Ok(m) +} diff --git a/src/api/jsonrpc/methods/nip46/status.rs b/src/api/jsonrpc/methods/nip46/status.rs @@ -0,0 +1,18 @@ +use anyhow::Result; +use jsonrpsee::server::RpcModule; +use serde::Serialize; + +use crate::api::jsonrpc::{MethodRegistry, RpcContext, RpcError}; + +#[derive(Debug, Serialize)] +struct Nip46StatusResponse { + ready: bool, +} + +pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { + registry.track("nip46.status"); + m.register_method("nip46.status", |_p, _ctx, _| { + Ok::<Nip46StatusResponse, RpcError>(Nip46StatusResponse { ready: false }) + })?; + Ok(()) +} diff --git a/src/api/jsonrpc/methods/system.rs b/src/api/jsonrpc/methods/system.rs @@ -1,34 +0,0 @@ -use anyhow::Result; -use jsonrpsee::server::RpcModule; -use serde::Serialize; - -use crate::api::jsonrpc::{MethodRegistry, RpcContext}; - -#[derive(Clone, Debug, Serialize)] -struct SystemInfoResponse { - version: Option<serde_json::Value>, - build: Option<serde_json::Value>, - uptime_secs: u64, -} - -pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<RpcContext>> { - let mut m = RpcModule::new(ctx); - - registry.track("system.ping"); - m.register_method("system.ping", |_p, _ctx, _| "pong")?; - - registry.track("system.get_info"); - m.register_method("system.get_info", |_p, ctx, _| { - let uptime = ctx.state.started.elapsed().as_secs(); - Ok::<SystemInfoResponse, crate::api::jsonrpc::RpcError>(SystemInfoResponse { - version: ctx.state.info.get("version").cloned(), - build: ctx.state.info.get("build").cloned(), - uptime_secs: uptime, - }) - })?; - - registry.track("system.help"); - m.register_method("system.help", |_p, ctx, _| ctx.methods.list())?; - - Ok(m) -} diff --git a/src/build/mod.rs b/src/build/mod.rs @@ -0,0 +1 @@ +#![forbid(unsafe_code)] diff --git a/src/events/mod.rs b/src/events/mod.rs @@ -0,0 +1 @@ +#![forbid(unsafe_code)] diff --git a/src/lib.rs b/src/lib.rs @@ -1,9 +1,13 @@ #![forbid(unsafe_code)] pub mod api; +pub mod build; pub mod cli; pub mod config; +pub mod events; +pub mod nip46; pub mod radrootsd; +pub mod validate; use anyhow::Result; diff --git a/src/nip46/mod.rs b/src/nip46/mod.rs @@ -0,0 +1 @@ +#![forbid(unsafe_code)] diff --git a/src/validate/mod.rs b/src/validate/mod.rs @@ -0,0 +1 @@ +#![forbid(unsafe_code)]