Skip to main content

am_core/
client.rs

1use std::collections::HashSet;
2
3use nostr_sdk::prelude::*;
4use serde::Serialize;
5
6use crate::error::{AmError, AmResult};
7
8#[derive(Debug, Clone, Serialize)]
9pub struct RelayResult {
10    pub relay: String,
11    pub status: RelayStatus,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub error: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub attempts: Option<u8>,
16}
17
18#[derive(Debug, Clone, Serialize)]
19#[serde(rename_all = "lowercase")]
20pub enum RelayStatus {
21    Ok,
22    Failed,
23}
24
25/// Create a connected client with the given keys and relays.
26pub async fn connect(keys: Keys, relays: &[String]) -> AmResult<Client> {
27    let client = Client::new(keys);
28    for relay in relays {
29        client
30            .add_relay(relay.as_str())
31            .await
32            .map_err(|e| AmError::Network(e.to_string()))?;
33    }
34    client.connect().await;
35    client
36        .wait_for_connection(std::time::Duration::from_secs(5))
37        .await;
38    Ok(client)
39}
40
41/// Send an event to all relays with per-relay retry on failure.
42///
43/// Returns the per-relay results and the set of relay URLs that succeeded.
44pub async fn send_with_retry(
45    client: &Client,
46    event: &Event,
47    relays: &[String],
48    max_retries: u8,
49    verbosity: u8,
50) -> (Vec<RelayResult>, HashSet<String>) {
51    let output = client.send_event(event).await;
52
53    let mut results = Vec::new();
54    let mut succeeded = HashSet::new();
55
56    // Parse initial output
57    let (initial_success, initial_failed) = match output {
58        Ok(out) => (out.success.clone(), out.failed.clone()),
59        Err(e) => {
60            // Total failure — mark all relays as failed
61            tracing::warn!("send_event failed: {e}");
62            let failed: std::collections::HashMap<RelayUrl, String> = relays
63                .iter()
64                .filter_map(|r| RelayUrl::parse(r).ok().map(|url| (url, e.to_string())))
65                .collect();
66            (HashSet::new(), failed)
67        }
68    };
69
70    // Record successes
71    for url in &initial_success {
72        let relay_str = url.to_string();
73        succeeded.insert(relay_str.clone());
74        results.push(RelayResult {
75            relay: relay_str,
76            status: RelayStatus::Ok,
77            error: None,
78            attempts: if verbosity >= 1 { Some(1) } else { None },
79        });
80    }
81
82    // Retry failed relays
83    for (url, err) in &initial_failed {
84        let relay_str = url.to_string();
85        let mut last_error = err.clone();
86        let mut attempt = 1u8;
87        let mut ok = false;
88
89        while attempt < max_retries {
90            attempt += 1;
91            tracing::debug!("retrying {relay_str} (attempt {attempt}/{max_retries})");
92
93            match client.send_event_to([url.clone()], event).await {
94                Ok(out) if !out.success.is_empty() => {
95                    ok = true;
96                    break;
97                }
98                Ok(out) => {
99                    if let Some((_, e)) = out.failed.into_iter().next() {
100                        last_error = e;
101                    }
102                }
103                Err(e) => {
104                    last_error = e.to_string();
105                }
106            }
107        }
108
109        if ok {
110            succeeded.insert(relay_str.clone());
111            results.push(RelayResult {
112                relay: relay_str,
113                status: RelayStatus::Ok,
114                error: None,
115                attempts: if verbosity >= 1 { Some(attempt) } else { None },
116            });
117        } else {
118            results.push(RelayResult {
119                relay: relay_str,
120                status: RelayStatus::Failed,
121                error: if verbosity >= 1 {
122                    Some(last_error)
123                } else {
124                    None
125                },
126                attempts: if verbosity >= 1 { Some(attempt) } else { None },
127            });
128        }
129    }
130
131    // Relays that weren't in either set (not connected)
132    for relay in relays {
133        let in_results = results.iter().any(|r| r.relay == *relay);
134        if !in_results {
135            results.push(RelayResult {
136                relay: relay.clone(),
137                status: RelayStatus::Failed,
138                error: if verbosity >= 1 {
139                    Some("not connected".into())
140                } else {
141                    None
142                },
143                attempts: if verbosity >= 1 { Some(0) } else { None },
144            });
145        }
146    }
147
148    (results, succeeded)
149}