upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/health.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/health.rs')
-rw-r--r--src/health.rs136
1 files changed, 136 insertions, 0 deletions
diff --git a/src/health.rs b/src/health.rs
new file mode 100644
index 0000000..2853239
--- /dev/null
+++ b/src/health.rs
@@ -0,0 +1,136 @@
1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Nip11Info {
7 pub name: Option<String>,
8 pub description: Option<String>,
9 pub supported_nips: Option<Vec<u64>>,
10 pub supported_grasps: Option<Vec<String>>,
11 pub software: Option<String>,
12 pub version: Option<String>,
13}
14
15#[derive(Debug, Clone)]
16pub struct GraspServer {
17 pub domain: String,
18 pub relay_url: String,
19 pub clone_url_prefix: String,
20 pub nip11: Option<Nip11Info>,
21 pub healthy: bool,
22}
23
24impl GraspServer {
25 pub fn from_domain(domain: &str) -> Self {
26 let clean = domain
27 .trim_start_matches("https://")
28 .trim_start_matches("http://")
29 .trim_end_matches('/')
30 .to_string();
31 Self {
32 relay_url: format!("wss://{}", clean),
33 clone_url_prefix: format!("https://{}", clean),
34 nip11: None,
35 healthy: false,
36 domain: clean,
37 }
38 }
39
40 pub fn clone_url(&self, npub_hex: &str, identifier: &str) -> String {
41 format!("{}/{}/{}.git", self.clone_url_prefix, npub_hex, identifier)
42 }
43
44 pub fn is_grasp_server(&self) -> bool {
45 self.nip11
46 .as_ref()
47 .map(|info| info.supported_grasps.is_some())
48 .unwrap_or(false)
49 }
50}
51
52pub async fn verify_grasp_server(domain: &str) -> Result<GraspServer> {
53 let mut server = GraspServer::from_domain(domain);
54 let nip11_url = format!("https://{}", server.domain);
55
56 let client = reqwest::Client::builder()
57 .timeout(std::time::Duration::from_secs(10))
58 .build()?;
59
60 let resp = client
61 .get(&nip11_url)
62 .header("Accept", "application/nostr+json")
63 .send()
64 .await;
65
66 match resp {
67 Ok(resp) if resp.status().is_success() => {
68 match resp.json::<Nip11Info>().await {
69 Ok(info) => {
70 let is_grasp = info.supported_grasps.is_some();
71 if is_grasp {
72 tracing::info!(
73 domain = %server.domain,
74 grasps = ?info.supported_grasps,
75 version = ?info.version,
76 "verified GRASP server"
77 );
78 } else {
79 tracing::warn!(
80 domain = %server.domain,
81 "server responded to NIP-11 but has no supported_grasps"
82 );
83 }
84 server.healthy = is_grasp;
85 server.nip11 = Some(info);
86 }
87 Err(e) => {
88 tracing::warn!(domain = %server.domain, error = %e, "failed to parse NIP-11 response");
89 }
90 }
91 }
92 Ok(resp) => {
93 tracing::warn!(
94 domain = %server.domain,
95 status = %resp.status(),
96 "NIP-11 check returned non-success"
97 );
98 }
99 Err(e) => {
100 tracing::warn!(domain = %server.domain, error = %e, "NIP-11 check failed");
101 }
102 }
103
104 Ok(server)
105}
106
107pub async fn verify_all_servers(domains: &[String]) -> HashMap<String, GraspServer> {
108 let mut servers = HashMap::new();
109 let mut set = tokio::task::JoinSet::new();
110
111 for d in domains {
112 let domain = d.clone();
113 set.spawn(async move {
114 let result = verify_grasp_server(&domain).await;
115 (domain, result)
116 });
117 }
118
119 while let Some(res) = set.join_next().await {
120 match res {
121 Ok((domain, result)) => match result {
122 Ok(server) => {
123 servers.insert(domain, server);
124 }
125 Err(e) => {
126 tracing::error!(domain = %domain, error = %e, "failed to verify server");
127 }
128 },
129 Err(e) => {
130 tracing::error!(error = %e, "task panicked during server verification");
131 }
132 }
133 }
134
135 servers
136}