slack status without the slack status.zzstoatzz.io/
quickslice

chore(clippy): fix redundant closure in HTTP client lazy init

+88 -9
+3 -3
src/api/webhooks.rs
··· 99 db_pool: web::Data<Arc<Pool>>, 100 path: web::Path<i64>, 101 payload: web::Json<UpdateWebhookRequest>, 102 ) -> impl Responder { 103 match session.get::<Did>("did").unwrap_or(None) { 104 Some(did) => { 105 let id = path.into_inner(); 106 if let Some(url) = &payload.url { 107 - if Url::parse(url).is_err() { 108 - return HttpResponse::BadRequest() 109 - .json(serde_json::json!({ "error": "Invalid URL" })); 110 } 111 } 112 if let Some(events_str) = &payload.events {
··· 99 db_pool: web::Data<Arc<Pool>>, 100 path: web::Path<i64>, 101 payload: web::Json<UpdateWebhookRequest>, 102 + app_config: web::Data<Config>, 103 ) -> impl Responder { 104 match session.get::<Did>("did").unwrap_or(None) { 105 Some(did) => { 106 let id = path.into_inner(); 107 if let Some(url) = &payload.url { 108 + if let Err(msg) = validate_url(url, &app_config) { 109 + return HttpResponse::BadRequest().json(serde_json::json!({ "error": msg })); 110 } 111 } 112 if let Some(events_str) = &payload.events {
+9 -4
src/webhooks.rs
··· 1 use async_sqlite::Pool; 2 use hmac::{Hmac, Mac}; 3 use reqwest::Client; 4 use serde::Serialize; 5 use sha2::Sha256; ··· 42 hex::encode(mac.finalize().into_bytes()) 43 } 44 45 pub async fn send_status_event(pool: std::sync::Arc<Pool>, did: &str, event: StatusEvent<'_>) { 46 - let client = Client::new(); 47 let hooks = match get_user_webhooks(&pool, did).await { 48 Ok(h) => h, 49 Err(e) => { ··· 66 .map(|h| { 67 let payload = payload.clone(); 68 let ts = ts.clone(); 69 - let client = client.clone(); 70 async move { 71 let sig = hmac_sig_hex(&h.secret, &ts, &payload); 72 let res = client ··· 83 match res { 84 Ok(resp) => { 85 if !resp.status().is_success() { 86 log::warn!( 87 - "webhook delivery failed: {} -> status {}", 88 &h.url, 89 - resp.status() 90 ); 91 } 92 }
··· 1 use async_sqlite::Pool; 2 use hmac::{Hmac, Mac}; 3 + use once_cell::sync::Lazy; 4 use reqwest::Client; 5 use serde::Serialize; 6 use sha2::Sha256; ··· 43 hex::encode(mac.finalize().into_bytes()) 44 } 45 46 + static HTTP: Lazy<Client> = Lazy::new(Client::new); 47 + 48 pub async fn send_status_event(pool: std::sync::Arc<Pool>, did: &str, event: StatusEvent<'_>) { 49 let hooks = match get_user_webhooks(&pool, did).await { 50 Ok(h) => h, 51 Err(e) => { ··· 68 .map(|h| { 69 let payload = payload.clone(); 70 let ts = ts.clone(); 71 + let client = HTTP.clone(); 72 async move { 73 let sig = hmac_sig_hex(&h.secret, &ts, &payload); 74 let res = client ··· 85 match res { 86 Ok(resp) => { 87 if !resp.status().is_success() { 88 + let status = resp.status(); 89 + let body = resp.text().await.unwrap_or_default(); 90 log::warn!( 91 + "webhook delivery failed: {} -> {} body={}", 92 &h.url, 93 + status, 94 + body 95 ); 96 } 97 }
+5
static/webhook_guide.css
···
··· 1 + .wh-tabs { display: flex; gap: 8px; margin: 10px 0; } 2 + .wh-tabs button { border: 1px solid var(--border-color, #2a2a2a); background: var(--bg-secondary, #0f0f0f); color: var(--text, #fff); padding: 6px 10px; border-radius: 8px; cursor: pointer; font-size: 12px; } 3 + .wh-tabs button.active { background: var(--accent, #1DA1F2); color: #000; border-color: var(--accent, #1DA1F2); } 4 + .wh-snippet { display: none; } 5 + .wh-snippet.active { display: block; }
+13
static/webhook_guide.js
···
··· 1 + document.addEventListener('DOMContentLoaded', () => { 2 + const tabs = document.querySelectorAll('#wh-lang-tabs [data-lang]'); 3 + const blocks = document.querySelectorAll('.wh-snippet[data-lang]'); 4 + if (!tabs.length || !blocks.length) return; 5 + const activate = (lang) => { 6 + tabs.forEach(t => t.classList.toggle('active', t.dataset.lang === lang)); 7 + blocks.forEach(b => b.classList.toggle('active', b.dataset.lang === lang)); 8 + }; 9 + tabs.forEach(btn => btn.addEventListener('click', () => activate(btn.dataset.lang))); 10 + // default 11 + activate('node'); 12 + }); 13 +
+58 -2
templates/status.html
··· 271 <details class="wh-guide" id="webhook-guide"> 272 <summary>Integration guide</summary> 273 <div class="content"> 274 <div class="wh-grid"> 275 - <div> 276 <h4>Request</h4> 277 <ul> 278 <li>Method: POST</li> ··· 292 "expires": null // created only 293 }</code></pre> 294 </div> 295 - <div> 296 <h4>Verify signature</h4> 297 <p>Compute HMAC-SHA256 over <code>timestamp + "." + rawBody</code> using your secret. Compare to header (without the <code>sha256=</code> prefix) with constant-time equality, and reject if timestamp is too old (e.g., &gt; 5 minutes).</p> 298 <pre><code>// Node (TypeScript) ··· 308 return crypto.timingSafeEqual(Buffer.from(mac, 'hex'), Buffer.from(sig, 'hex')); 309 } 310 </code></pre> 311 <pre><code>// Rust (axum-ish) 312 use hmac::{Hmac, Mac}; 313 use sha2::Sha256; ··· 326 } 327 </code></pre> 328 </div> 329 </div> 330 </div> 331 </details> 332 </div> 333 </div> 334 </div>
··· 271 <details class="wh-guide" id="webhook-guide"> 272 <summary>Integration guide</summary> 273 <div class="content"> 274 + <div id="wh-lang-tabs" class="wh-tabs" role="tablist" aria-label="language selector"> 275 + <button type="button" data-lang="node" role="tab" aria-selected="true">Node</button> 276 + <button type="button" data-lang="rust" role="tab">Rust</button> 277 + <button type="button" data-lang="python" role="tab">Python</button> 278 + <button type="button" data-lang="go" role="tab">Go</button> 279 + </div> 280 <div class="wh-grid"> 281 + <div class="wh-snippet" data-lang="node"> 282 <h4>Request</h4> 283 <ul> 284 <li>Method: POST</li> ··· 298 "expires": null // created only 299 }</code></pre> 300 </div> 301 + <div class="wh-snippet" data-lang="node"> 302 <h4>Verify signature</h4> 303 <p>Compute HMAC-SHA256 over <code>timestamp + "." + rawBody</code> using your secret. Compare to header (without the <code>sha256=</code> prefix) with constant-time equality, and reject if timestamp is too old (e.g., &gt; 5 minutes).</p> 304 <pre><code>// Node (TypeScript) ··· 314 return crypto.timingSafeEqual(Buffer.from(mac, 'hex'), Buffer.from(sig, 'hex')); 315 } 316 </code></pre> 317 + </div> 318 + <div class="wh-snippet" data-lang="rust"> 319 <pre><code>// Rust (axum-ish) 320 use hmac::{Hmac, Mac}; 321 use sha2::Sha256; ··· 334 } 335 </code></pre> 336 </div> 337 + <div class="wh-snippet" data-lang="python"> 338 + <pre><code># Python (Flask example) 339 + import hmac, hashlib, time 340 + from flask import request 341 + 342 + def verify(secret: str, raw_body: bytes) -> bool: 343 + ts = request.headers.get('X-Status-Webhook-Timestamp') 344 + sig_hdr = request.headers.get('X-Status-Webhook-Signature', '') 345 + if not ts or not sig_hdr.startswith('sha256='): 346 + return False 347 + if abs(int(time.time()) - int(ts)) > 300: 348 + return False 349 + expected = hmac.new(secret.encode(), (ts + '.').encode() + raw_body, hashlib.sha256).hexdigest() 350 + actual = sig_hdr[len('sha256='):] 351 + return hmac.compare_digest(expected, actual) 352 + </code></pre> 353 + </div> 354 + <div class="wh-snippet" data-lang="go"> 355 + <pre><code>// Go (net/http) 356 + package main 357 + 358 + import ( 359 + "crypto/hmac" 360 + "crypto/sha256" 361 + "encoding/hex" 362 + "net/http" 363 + "strconv" 364 + "time" 365 + ) 366 + 367 + func verify(r *http.Request, body []byte, secret string) bool { 368 + ts := r.Header.Get("X-Status-Webhook-Timestamp") 369 + sig := r.Header.Get("X-Status-Webhook-Signature") 370 + if ts == "" || sig == "" { return false } 371 + if len(sig) > 7 && sig[:7] == "sha256=" { sig = sig[7:] } 372 + tsv, err := strconv.ParseInt(ts, 10, 64) 373 + if err != nil || time.Now().Unix()-tsv > 300 || tsv-time.Now().Unix() > 300 { return false } 374 + mac := hmac.New(sha256.New, []byte(secret)) 375 + mac.Write([]byte(ts)) 376 + mac.Write([]byte(".")) 377 + mac.Write(body) 378 + expected := hex.EncodeToString(mac.Sum(nil)) 379 + return hmac.Equal([]byte(expected), []byte(sig)) 380 + } 381 + </code></pre> 382 + </div> 383 </div> 384 </div> 385 </details> 386 + <link rel="stylesheet" href="/static/webhook_guide.css"> 387 + <script src="/static/webhook_guide.js"></script> 388 </div> 389 </div> 390 </div>