cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit c180b47d48b1e8efc16ff340be76491588452792
parent 5e41f8a06adcf359a88b21b9c5c8bf4a7e7c2745
Author: triesap <tyson@radroots.org>
Date:   Thu,  7 May 2026 05:44:39 +0000

listing: preserve relay failure event ids

- carry direct relay publish event ids into failure views
- expose signed event ids in structured machine output
- update listing update README mutation posture
- cover relay failure and approval documentation paths

Diffstat:
Msrc/runtime/listing.rs | 115++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
1 file changed, 85 insertions(+), 30 deletions(-)

diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs @@ -1821,24 +1821,74 @@ fn direct_relay_error_view( operation: ListingMutationOperation, canonical: &CanonicalListingDraft, listing_addr: String, - event_preview: ListingMutationEventView, + mut event_preview: ListingMutationEventView, error: DirectRelayPublishError, ) -> ListingMutationView { - let (reason, target_relays, connected_relays, failed_relays) = match error { + let parts = direct_relay_error_view_parts(config.relay.urls.as_slice(), error); + if let Some(event_id) = parts.event_id.as_ref() { + event_preview.event_id = Some(event_id.clone()); + } + + ListingMutationView { + state: "unavailable".to_owned(), + operation: operation.as_str().to_owned(), + source: listing_write_source(config).to_owned(), + file: args.file.display().to_string(), + listing_id: canonical.listing_id.clone(), + listing_addr: listing_addr.clone(), + seller_pubkey: canonical.seller_pubkey.clone(), + event_kind: KIND_LISTING, + dry_run: false, + deduplicated: false, + target_relays: parts.target_relays, + connected_relays: parts.connected_relays, + acknowledged_relays: Vec::new(), + failed_relays: parts.failed_relays, + job_id: None, + job_status: None, + signer_mode: Some(config.signer.backend.as_str().to_owned()), + event_id: parts.event_id, + event_addr: Some(listing_addr), + idempotency_key: args.idempotency_key.clone(), + signer_session_id: None, + requested_signer_session_id: args.signer_session_id.clone(), + reason: Some(parts.reason), + job: None, + event: args.print_event.then_some(event_preview), + actions: Vec::new(), + } +} + +#[derive(Debug, Clone)] +struct DirectRelayErrorViewParts { + reason: String, + target_relays: Vec<String>, + connected_relays: Vec<String>, + failed_relays: Vec<RelayFailureView>, + event_id: Option<String>, +} + +fn direct_relay_error_view_parts( + configured_relays: &[String], + error: DirectRelayPublishError, +) -> DirectRelayErrorViewParts { + let (reason, target_relays, connected_relays, failed_relays, event_id) = match error { DirectRelayPublishError::MissingRelays => ( "direct relay publish requires at least one configured relay".to_owned(), - config.relay.urls.clone(), + configured_relays.to_vec(), Vec::new(), Vec::new(), + None, ), DirectRelayPublishError::RelayConfig { relay, source } => ( format!("failed to configure relay `{relay}` for direct relay publish: {source}"), - config.relay.urls.clone(), + configured_relays.to_vec(), Vec::new(), vec![RelayFailureView { relay, reason: source.to_string(), }], + None, ), DirectRelayPublishError::Connect { reason, @@ -1850,6 +1900,7 @@ fn direct_relay_error_view( target_relays, connected_relays, relay_failures(failed_relays), + None, ), DirectRelayPublishError::Publish { event_id, @@ -1862,39 +1913,18 @@ fn direct_relay_error_view( target_relays, connected_relays, relay_failures(failed_relays), + Some(event_id), ), DirectRelayPublishError::Runtime(_) | DirectRelayPublishError::Build(_) | DirectRelayPublishError::Sign(_) => unreachable!(), }; - - ListingMutationView { - state: "unavailable".to_owned(), - operation: operation.as_str().to_owned(), - source: listing_write_source(config).to_owned(), - file: args.file.display().to_string(), - listing_id: canonical.listing_id.clone(), - listing_addr: listing_addr.clone(), - seller_pubkey: canonical.seller_pubkey.clone(), - event_kind: KIND_LISTING, - dry_run: false, - deduplicated: false, + DirectRelayErrorViewParts { + reason, target_relays, connected_relays, - acknowledged_relays: Vec::new(), failed_relays, - job_id: None, - job_status: None, - signer_mode: Some(config.signer.backend.as_str().to_owned()), - event_id: None, - event_addr: Some(listing_addr), - idempotency_key: args.idempotency_key.clone(), - signer_session_id: None, - requested_signer_session_id: args.signer_session_id.clone(), - reason: Some(reason), - job: None, - event: args.print_event.then_some(event_preview), - actions: Vec::new(), + event_id, } } @@ -2356,7 +2386,11 @@ fn encode_base64url_no_pad(bytes: [u8; 16]) -> String { #[cfg(test)] mod tests { - use super::{DRAFT_KIND, ListingDraftDocument, encode_base64url_no_pad, generate_d_tag}; + use super::{ + DRAFT_KIND, ListingDraftDocument, direct_relay_error_view_parts, encode_base64url_no_pad, + generate_d_tag, + }; + use crate::runtime::direct_relay::{DirectRelayFailure, DirectRelayPublishError}; use radroots_events_codec::d_tag::is_d_tag_base64url; #[test] @@ -2373,6 +2407,27 @@ mod tests { } #[test] + fn direct_relay_publish_error_parts_preserve_event_id() { + let parts = direct_relay_error_view_parts( + &["ws://127.0.0.1:19000".to_owned()], + DirectRelayPublishError::Publish { + event_id: "e".repeat(64), + reason: "relay rejected event".to_owned(), + target_relays: vec!["ws://127.0.0.1:19000".to_owned()], + connected_relays: vec!["ws://127.0.0.1:19000".to_owned()], + failed_relays: vec![DirectRelayFailure { + relay: "ws://127.0.0.1:19000".to_owned(), + reason: "relay rejected event".to_owned(), + }], + }, + ); + + assert_eq!(parts.event_id, Some("e".repeat(64))); + assert!(parts.reason.contains("direct relay publish failed")); + assert_eq!(parts.failed_relays.len(), 1); + } + + #[test] fn listing_draft_kind_constant_is_stable() { let document = ListingDraftDocument { version: 1,