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
25pub 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
41pub 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 let (initial_success, initial_failed) = match output {
58 Ok(out) => (out.success.clone(), out.failed.clone()),
59 Err(e) => {
60 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 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 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 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}