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 99 db_pool: web::Data<Arc<Pool>>, 100 100 path: web::Path<i64>, 101 101 payload: web::Json<UpdateWebhookRequest>, 102 + app_config: web::Data<Config>, 102 103 ) -> impl Responder { 103 104 match session.get::<Did>("did").unwrap_or(None) { 104 105 Some(did) => { 105 106 let id = path.into_inner(); 106 107 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" })); 108 + if let Err(msg) = validate_url(url, &app_config) { 109 + return HttpResponse::BadRequest().json(serde_json::json!({ "error": msg })); 110 110 } 111 111 } 112 112 if let Some(events_str) = &payload.events {
+9 -4
src/webhooks.rs
··· 1 1 use async_sqlite::Pool; 2 2 use hmac::{Hmac, Mac}; 3 + use once_cell::sync::Lazy; 3 4 use reqwest::Client; 4 5 use serde::Serialize; 5 6 use sha2::Sha256; ··· 42 43 hex::encode(mac.finalize().into_bytes()) 43 44 } 44 45 46 + static HTTP: Lazy<Client> = Lazy::new(Client::new); 47 + 45 48 pub async fn send_status_event(pool: std::sync::Arc<Pool>, did: &str, event: StatusEvent<'_>) { 46 - let client = Client::new(); 47 49 let hooks = match get_user_webhooks(&pool, did).await { 48 50 Ok(h) => h, 49 51 Err(e) => { ··· 66 68 .map(|h| { 67 69 let payload = payload.clone(); 68 70 let ts = ts.clone(); 69 - let client = client.clone(); 71 + let client = HTTP.clone(); 70 72 async move { 71 73 let sig = hmac_sig_hex(&h.secret, &ts, &payload); 72 74 let res = client ··· 83 85 match res { 84 86 Ok(resp) => { 85 87 if !resp.status().is_success() { 88 + let status = resp.status(); 89 + let body = resp.text().await.unwrap_or_default(); 86 90 log::warn!( 87 - "webhook delivery failed: {} -> status {}", 91 + "webhook delivery failed: {} -> {} body={}", 88 92 &h.url, 89 - resp.status() 93 + status, 94 + body 90 95 ); 91 96 } 92 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 271 <details class="wh-guide" id="webhook-guide"> 272 272 <summary>Integration guide</summary> 273 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> 274 280 <div class="wh-grid"> 275 - <div> 281 + <div class="wh-snippet" data-lang="node"> 276 282 <h4>Request</h4> 277 283 <ul> 278 284 <li>Method: POST</li> ··· 292 298 "expires": null // created only 293 299 }</code></pre> 294 300 </div> 295 - <div> 301 + <div class="wh-snippet" data-lang="node"> 296 302 <h4>Verify signature</h4> 297 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> 298 304 <pre><code>// Node (TypeScript) ··· 308 314 return crypto.timingSafeEqual(Buffer.from(mac, 'hex'), Buffer.from(sig, 'hex')); 309 315 } 310 316 </code></pre> 317 + </div> 318 + <div class="wh-snippet" data-lang="rust"> 311 319 <pre><code>// Rust (axum-ish) 312 320 use hmac::{Hmac, Mac}; 313 321 use sha2::Sha256; ··· 326 334 } 327 335 </code></pre> 328 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> 329 383 </div> 330 384 </div> 331 385 </details> 386 + <link rel="stylesheet" href="/static/webhook_guide.css"> 387 + <script src="/static/webhook_guide.js"></script> 332 388 </div> 333 389 </div> 334 390 </div>