upleb.uk

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

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/client.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-19 17:01:36 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-19 17:01:36 +0000
commitbf7f4d5381203d5c27b2811d62c5b1781533aa2b (patch)
tree26903bbf535d83abd7242370d8b6932eb80e3389 /grasp-audit/src/client.rs
parentfa065ad128882755f2a988d6203b59a2ab5e38ff (diff)
fix some clippy fmt warnings
Diffstat (limited to 'grasp-audit/src/client.rs')
-rw-r--r--grasp-audit/src/client.rs174
1 files changed, 104 insertions, 70 deletions
diff --git a/grasp-audit/src/client.rs b/grasp-audit/src/client.rs
index 1f6f0fb..019f4cb 100644
--- a/grasp-audit/src/client.rs
+++ b/grasp-audit/src/client.rs
@@ -24,32 +24,32 @@ impl AuditClient {
24 keys, 24 keys,
25 } 25 }
26 } 26 }
27 27
28 /// Create a new audit client 28 /// Create a new audit client
29 pub async fn new(relay_url: &str, config: AuditConfig) -> Result<Self> { 29 pub async fn new(relay_url: &str, config: AuditConfig) -> Result<Self> {
30 let keys = Keys::generate(); 30 let keys = Keys::generate();
31 let client = Client::new(keys.clone()); 31 let client = Client::new(keys.clone());
32 32
33 // Add relay and connect 33 // Add relay and connect
34 client.add_relay(relay_url).await?; 34 client.add_relay(relay_url).await?;
35 client.connect().await; 35 client.connect().await;
36 36
37 // Wait for connection to establish (with retries) 37 // Wait for connection to establish (with retries)
38 let mut attempts = 0; 38 let mut attempts = 0;
39 let mut connected = false; 39 let mut connected = false;
40 while attempts < 20 { 40 while attempts < 20 {
41 tokio::time::sleep(Duration::from_millis(100)).await; 41 tokio::time::sleep(Duration::from_millis(100)).await;
42 42
43 let relays = client.relays().await; 43 let relays = client.relays().await;
44 connected = relays.values().any(|r| r.is_connected()); 44 connected = relays.values().any(|r| r.is_connected());
45 45
46 if connected { 46 if connected {
47 break; 47 break;
48 } 48 }
49 49
50 attempts += 1; 50 attempts += 1;
51 } 51 }
52 52
53 // Verify we actually connected 53 // Verify we actually connected
54 if !connected { 54 if !connected {
55 return Err(anyhow!( 55 return Err(anyhow!(
@@ -68,22 +68,22 @@ impl AuditClient {
68 relay_url 68 relay_url
69 )); 69 ));
70 } 70 }
71 71
72 // Give it a bit more time to stabilize 72 // Give it a bit more time to stabilize
73 tokio::time::sleep(Duration::from_millis(200)).await; 73 tokio::time::sleep(Duration::from_millis(200)).await;
74 74
75 Ok(Self { 75 Ok(Self {
76 client, 76 client,
77 config, 77 config,
78 keys, 78 keys,
79 }) 79 })
80 } 80 }
81 81
82 /// Get the public key for this audit client 82 /// Get the public key for this audit client
83 pub fn public_key(&self) -> PublicKey { 83 pub fn public_key(&self) -> PublicKey {
84 self.keys.public_key() 84 self.keys.public_key()
85 } 85 }
86 86
87 /// Check if connected to relay 87 /// Check if connected to relay
88 pub async fn is_connected(&self) -> bool { 88 pub async fn is_connected(&self) -> bool {
89 // Check if we have any connected relays 89 // Check if we have any connected relays
@@ -95,29 +95,29 @@ impl AuditClient {
95 } 95 }
96 false 96 false
97 } 97 }
98 98
99 /// Send an event (with audit tags automatically added) 99 /// Send an event (with audit tags automatically added)
100 pub async fn send_event(&self, event: Event) -> Result<EventId> { 100 pub async fn send_event(&self, event: Event) -> Result<EventId> {
101 if self.config.read_only { 101 if self.config.read_only {
102 return Err(anyhow!("Client is in read-only mode")); 102 return Err(anyhow!("Client is in read-only mode"));
103 } 103 }
104 104
105 let output = self.client.send_event(&event).await?; 105 let output = self.client.send_event(&event).await?;
106 let event_id = *output.id(); 106 let event_id = *output.id();
107 107
108 // Check if any relay rejected the event and return the error message 108 // Check if any relay rejected the event and return the error message
109 if !output.failed.is_empty() { 109 if !output.failed.is_empty() {
110 // Get the first failed relay error message 110 // Get the first failed relay error message
111 let (relay_url, error) = output.failed.iter().next().unwrap(); 111 let (relay_url, error) = output.failed.iter().next().unwrap();
112 return Err(anyhow!("Relay {} rejected event: {}", relay_url, error)); 112 return Err(anyhow!("Relay {} rejected event: {}", relay_url, error));
113 } 113 }
114 114
115 // Wait a bit for event to propagate 115 // Wait a bit for event to propagate
116 tokio::time::sleep(Duration::from_millis(100)).await; 116 tokio::time::sleep(Duration::from_millis(100)).await;
117 117
118 Ok(event_id) 118 Ok(event_id)
119 } 119 }
120 120
121 /// Create an event builder that automatically includes audit tags 121 /// Create an event builder that automatically includes audit tags
122 /// 122 ///
123 /// All events built through this method will automatically have audit tags appended 123 /// All events built through this method will automatically have audit tags appended
@@ -153,11 +153,11 @@ impl AuditClient {
153 pub fn event_builder(&self, kind: Kind, content: impl Into<String>) -> AuditEventBuilder { 153 pub fn event_builder(&self, kind: Kind, content: impl Into<String>) -> AuditEventBuilder {
154 AuditEventBuilder::new(kind, content, self.config.clone()) 154 AuditEventBuilder::new(kind, content, self.config.clone())
155 } 155 }
156 156
157 /// Query events, optionally filtered to this audit run 157 /// Query events, optionally filtered to this audit run
158 pub async fn query(&self, mut filter: Filter) -> Result<Vec<Event>> { 158 pub async fn query(&self, mut filter: Filter) -> Result<Vec<Event>> {
159 use nostr_sdk::prelude::{Alphabet, SingleLetterTag}; 159 use nostr_sdk::prelude::{Alphabet, SingleLetterTag};
160 160
161 if self.config.mode == AuditMode::CI { 161 if self.config.mode == AuditMode::CI {
162 // In CI mode, only see our own audit events 162 // In CI mode, only see our own audit events
163 // Filter by "t" tags (hashtags) 163 // Filter by "t" tags (hashtags)
@@ -167,14 +167,15 @@ impl AuditClient {
167 .custom_tag(t_tag, format!("audit-{}", self.config.run_id)); 167 .custom_tag(t_tag, format!("audit-{}", self.config.run_id));
168 } 168 }
169 // In Production mode, see all events (no filter modification) 169 // In Production mode, see all events (no filter modification)
170 170
171 let events = self.client 171 let events = self
172 .client
172 .fetch_events(filter, Duration::from_secs(5)) 173 .fetch_events(filter, Duration::from_secs(5))
173 .await?; 174 .await?;
174 175
175 Ok(events.into_iter().collect()) 176 Ok(events.into_iter().collect())
176 } 177 }
177 178
178 /// Subscribe to events with a callback 179 /// Subscribe to events with a callback
179 pub async fn subscribe( 180 pub async fn subscribe(
180 &self, 181 &self,
@@ -183,27 +184,25 @@ impl AuditClient {
183 ) -> Result<Vec<Event>> { 184 ) -> Result<Vec<Event>> {
184 let timeout = timeout.unwrap_or(Duration::from_secs(5)); 185 let timeout = timeout.unwrap_or(Duration::from_secs(5));
185 let mut all_events = Vec::new(); 186 let mut all_events = Vec::new();
186 187
187 for filter in filters { 188 for filter in filters {
188 let events = self.client 189 let events = self.client.fetch_events(filter, timeout).await?;
189 .fetch_events(filter, timeout)
190 .await?;
191 all_events.extend(events.into_iter()); 190 all_events.extend(events.into_iter());
192 } 191 }
193 192
194 Ok(all_events) 193 Ok(all_events)
195 } 194 }
196 195
197 /// Get the underlying nostr client (for advanced usage) 196 /// Get the underlying nostr client (for advanced usage)
198 pub fn client(&self) -> &Client { 197 pub fn client(&self) -> &Client {
199 &self.client 198 &self.client
200 } 199 }
201 200
202 /// Get the keys (for signing custom events) 201 /// Get the keys (for signing custom events)
203 pub fn keys(&self) -> &Keys { 202 pub fn keys(&self) -> &Keys {
204 &self.keys 203 &self.keys
205 } 204 }
206 205
207 /// Create a NIP-34 repository announcement event 206 /// Create a NIP-34 repository announcement event
208 /// 207 ///
209 /// This helper creates a properly formatted NIP-34 announcement that will be 208 /// This helper creates a properly formatted NIP-34 announcement that will be
@@ -216,37 +215,58 @@ impl AuditClient {
216 /// A built and signed Event ready to be sent to the relay 215 /// A built and signed Event ready to be sent to the relay
217 pub async fn create_repo_announcement(&self, test_name: &str) -> Result<Event> { 216 pub async fn create_repo_announcement(&self, test_name: &str) -> Result<Event> {
218 // Get relay URL from client 217 // Get relay URL from client
219 let relay_url = self.client.relays().await 218 let relay_url = self
219 .client
220 .relays()
221 .await
220 .keys() 222 .keys()
221 .next() 223 .next()
222 .ok_or_else(|| anyhow!("No relay connected"))? 224 .ok_or_else(|| anyhow!("No relay connected"))?
223 .to_string(); 225 .to_string();
224 226
225 // Convert WebSocket URL to HTTP URL for clone tag 227 // Convert WebSocket URL to HTTP URL for clone tag
226 let http_url = relay_url 228 let http_url = relay_url
227 .replace("ws://", "http://") 229 .replace("ws://", "http://")
228 .replace("wss://", "https://"); 230 .replace("wss://", "https://");
229 231
230 // Create unique repository identifier using UUID for consistency 232 // Create unique repository identifier using UUID for consistency
231 let repo_id = format!("{}-{}", test_name, &uuid::Uuid::new_v4().to_string()[..8]); 233 let repo_id = format!("{}-{}", test_name, &uuid::Uuid::new_v4().to_string()[..8]);
232 234
233 // Get npub for clone URL 235 // Get npub for clone URL
234 let npub = self.public_key().to_bech32() 236 let npub = self
237 .public_key()
238 .to_bech32()
235 .map_err(|e| anyhow!("Failed to convert public key to bech32 npub format: {}", e))?; 239 .map_err(|e| anyhow!("Failed to convert public key to bech32 npub format: {}", e))?;
236 240
237 // Build kind 30617 repository announcement 241 // Build kind 30617 repository announcement
238 let event = self.event_builder(Kind::GitRepoAnnouncement, format!("Test repository for {}", test_name)) 242 let event = self
243 .event_builder(
244 Kind::GitRepoAnnouncement,
245 format!("Test repository for {}", test_name),
246 )
239 .tag(Tag::identifier(&repo_id)) 247 .tag(Tag::identifier(&repo_id))
240 .tag(Tag::custom(TagKind::custom("name"), vec![format!("{} Test Repository", test_name)])) 248 .tag(Tag::custom(
241 .tag(Tag::custom(TagKind::custom("description"), vec![format!("Repository for {} testing", test_name)])) 249 TagKind::custom("name"),
242 .tag(Tag::custom(TagKind::custom("clone"), vec![format!("{}/{}/{}.git", http_url, npub, repo_id)])) 250 vec![format!("{} Test Repository", test_name)],
243 .tag(Tag::custom(TagKind::custom("relays"), vec![relay_url.clone()])) 251 ))
252 .tag(Tag::custom(
253 TagKind::custom("description"),
254 vec![format!("Repository for {} testing", test_name)],
255 ))
256 .tag(Tag::custom(
257 TagKind::custom("clone"),
258 vec![format!("{}/{}/{}.git", http_url, npub, repo_id)],
259 ))
260 .tag(Tag::custom(
261 TagKind::custom("relays"),
262 vec![relay_url.clone()],
263 ))
244 .build(self.keys()) 264 .build(self.keys())
245 .map_err(|e| anyhow!("Failed to build repository announcement event: {}", e))?; 265 .map_err(|e| anyhow!("Failed to build repository announcement event: {}", e))?;
246 266
247 Ok(event) 267 Ok(event)
248 } 268 }
249 269
250 /// Create an issue (kind 1621) that references a repository 270 /// Create an issue (kind 1621) that references a repository
251 /// 271 ///
252 /// # Arguments 272 /// # Arguments
@@ -265,29 +285,31 @@ impl AuditClient {
265 additional_tags: Vec<Tag>, 285 additional_tags: Vec<Tag>,
266 ) -> Result<Event> { 286 ) -> Result<Event> {
267 // Extract repo_id from the d tag 287 // Extract repo_id from the d tag
268 let repo_id = repo_event.tags.iter() 288 let repo_id = repo_event
289 .tags
290 .iter()
269 .find(|t| t.kind() == TagKind::d()) 291 .find(|t| t.kind() == TagKind::d())
270 .and_then(|t| t.content()) 292 .and_then(|t| t.content())
271 .ok_or_else(|| anyhow!("Repository event must have a 'd' tag"))? 293 .ok_or_else(|| anyhow!("Repository event must have a 'd' tag"))?
272 .to_string(); 294 .to_string();
273 295
274 let repo_pubkey = repo_event.pubkey; 296 let repo_pubkey = repo_event.pubkey;
275 let a_tag_value = format!("30617:{}:{}", repo_pubkey, repo_id); 297 let a_tag_value = format!("30617:{}:{}", repo_pubkey, repo_id);
276 298
277 let mut tags = vec![ 299 let mut tags = vec![
278 Tag::custom(TagKind::custom("a"), vec![a_tag_value]), 300 Tag::custom(TagKind::custom("a"), vec![a_tag_value]),
279 Tag::custom(TagKind::custom("subject"), vec![issue_title]), 301 Tag::custom(TagKind::custom("subject"), vec![issue_title]),
280 ]; 302 ];
281 303
282 // Add any additional tags 304 // Add any additional tags
283 tags.extend(additional_tags); 305 tags.extend(additional_tags);
284 306
285 self.event_builder(Kind::Custom(1621), content) 307 self.event_builder(Kind::Custom(1621), content)
286 .tags(tags) 308 .tags(tags)
287 .build(self.keys()) 309 .build(self.keys())
288 .map_err(|e| anyhow!("Failed to build issue event: {}", e)) 310 .map_err(|e| anyhow!("Failed to build issue event: {}", e))
289 } 311 }
290 312
291 /// Create a NIP-22 comment (kind 1111) for an event 313 /// Create a NIP-22 comment (kind 1111) for an event
292 /// 314 ///
293 /// # Arguments 315 /// # Arguments
@@ -306,17 +328,20 @@ impl AuditClient {
306 let event_kind = event.kind; 328 let event_kind = event.kind;
307 let event_pubkey = event.pubkey; 329 let event_pubkey = event.pubkey;
308 let event_id = event.id; 330 let event_id = event.id;
309 331
310 let mut tags = vec![ 332 let mut tags = vec![
311 Tag::custom(TagKind::custom("E"), vec![event_id.to_hex(), "".to_string(), "root".to_string()]), 333 Tag::custom(
334 TagKind::custom("E"),
335 vec![event_id.to_hex(), "".to_string(), "root".to_string()],
336 ),
312 Tag::event(event_id), 337 Tag::event(event_id),
313 Tag::custom(TagKind::custom("K"), vec![event_kind.as_u16().to_string()]), 338 Tag::custom(TagKind::custom("K"), vec![event_kind.as_u16().to_string()]),
314 Tag::public_key(event_pubkey), 339 Tag::public_key(event_pubkey),
315 ]; 340 ];
316 341
317 // Add any additional tags 342 // Add any additional tags
318 tags.extend(additional_tags); 343 tags.extend(additional_tags);
319 344
320 self.event_builder(Kind::Custom(1111), content) 345 self.event_builder(Kind::Custom(1111), content)
321 .tags(tags) 346 .tags(tags)
322 .build(self.keys()) 347 .build(self.keys())
@@ -327,22 +352,22 @@ impl AuditClient {
327#[cfg(test)] 352#[cfg(test)]
328mod tests { 353mod tests {
329 use super::*; 354 use super::*;
330 355
331 #[tokio::test] 356 #[tokio::test]
332 async fn test_client_creation() { 357 async fn test_client_creation() {
333 let config = AuditConfig::ci(); 358 let config = AuditConfig::ci();
334 359
335 // This will fail if no relay is running, which is expected in tests 360 // This will fail if no relay is running, which is expected in tests
336 // In real usage, there should be a relay at the URL 361 // In real usage, there should be a relay at the URL
337 let result = AuditClient::new("ws://localhost:7000", config).await; 362 let result = AuditClient::new("ws://localhost:7000", config).await;
338 363
339 // We can't test connection without a running relay 364 // We can't test connection without a running relay
340 // But we can test that the client is created 365 // But we can test that the client is created
341 if let Ok(client) = result { 366 if let Ok(client) = result {
342 assert_eq!(client.config.mode, AuditMode::CI); 367 assert_eq!(client.config.mode, AuditMode::CI);
343 } 368 }
344 } 369 }
345 370
346 #[test] 371 #[test]
347 fn test_event_builder() { 372 fn test_event_builder() {
348 let config = AuditConfig::ci(); 373 let config = AuditConfig::ci();
@@ -352,13 +377,13 @@ mod tests {
352 config: config.clone(), 377 config: config.clone(),
353 keys: keys.clone(), 378 keys: keys.clone(),
354 }; 379 };
355 380
356 let _builder = client.event_builder(Kind::TextNote, "test content"); 381 let _builder = client.event_builder(Kind::TextNote, "test content");
357 382
358 // Builder should be created successfully 383 // Builder should be created successfully
359 // (We can't test the internal config field as it's private, which is correct) 384 // (We can't test the internal config field as it's private, which is correct)
360 } 385 }
361 386
362 #[test] 387 #[test]
363 fn test_audit_tags_automatically_added() { 388 fn test_audit_tags_automatically_added() {
364 let config = AuditConfig::ci(); 389 let config = AuditConfig::ci();
@@ -368,21 +393,28 @@ mod tests {
368 config: config.clone(), 393 config: config.clone(),
369 keys: keys.clone(), 394 keys: keys.clone(),
370 }; 395 };
371 396
372 // Create an event with a custom tag 397 // Create an event with a custom tag
373 let event = client.event_builder(Kind::TextNote, "test content") 398 let event = client
399 .event_builder(Kind::TextNote, "test content")
374 .tag(Tag::custom(TagKind::custom("custom"), vec!["value"])) 400 .tag(Tag::custom(TagKind::custom("custom"), vec!["value"]))
375 .build(&keys) 401 .build(&keys)
376 .unwrap(); 402 .unwrap();
377 403
378 // Should have custom tag (1) + 3 audit tags = at least 4 tags 404 // Should have custom tag (1) + 3 audit tags = at least 4 tags
379 assert!(event.tags.len() >= 4, "Expected at least 4 tags, got {}", event.tags.len()); 405 assert!(
380 406 event.tags.len() >= 4,
407 "Expected at least 4 tags, got {}",
408 event.tags.len()
409 );
410
381 // Verify audit tags are present by checking tag content 411 // Verify audit tags are present by checking tag content
382 let tag_contents: Vec<String> = event.tags.iter() 412 let tag_contents: Vec<String> = event
413 .tags
414 .iter()
383 .filter_map(|t| t.content().map(|s| s.to_string())) 415 .filter_map(|t| t.content().map(|s| s.to_string()))
384 .collect(); 416 .collect();
385 417
386 // Check for the three required audit tags 418 // Check for the three required audit tags
387 assert!( 419 assert!(
388 tag_contents.contains(&"grasp-audit-test-event".to_string()), 420 tag_contents.contains(&"grasp-audit-test-event".to_string()),
@@ -393,10 +425,12 @@ mod tests {
393 "Missing 'audit-ci-*' tag" 425 "Missing 'audit-ci-*' tag"
394 ); 426 );
395 assert!( 427 assert!(
396 tag_contents.iter().any(|t| t.starts_with("audit-cleanup-after-")), 428 tag_contents
429 .iter()
430 .any(|t| t.starts_with("audit-cleanup-after-")),
397 "Missing 'audit-cleanup-after-*' tag" 431 "Missing 'audit-cleanup-after-*' tag"
398 ); 432 );
399 433
400 // Verify the custom tag is also present 434 // Verify the custom tag is also present
401 assert!( 435 assert!(
402 tag_contents.contains(&"value".to_string()), 436 tag_contents.contains(&"value".to_string()),