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