llm_optimizer_integrations/jira/
webhooks.rs1use super::types::WebhookEvent;
6use anyhow::{anyhow, Context, Result};
7use async_trait::async_trait;
8use serde_json::Value;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use tracing::{debug, error, info, warn};
12
13#[async_trait]
15pub trait WebhookHandler: Send + Sync {
16 async fn on_issue_created(&self, event: &WebhookEvent) -> Result<()>;
18
19 async fn on_issue_updated(&self, event: &WebhookEvent) -> Result<()>;
21
22 async fn on_issue_deleted(&self, event: &WebhookEvent) -> Result<()>;
24
25 async fn on_other_event(&self, event: &WebhookEvent) -> Result<()>;
27}
28
29pub struct WebhookProcessor {
31 handlers: Arc<RwLock<Vec<Arc<dyn WebhookHandler>>>>,
32 webhook_secret: Option<String>,
34}
35
36impl WebhookProcessor {
37 pub fn new(webhook_secret: Option<String>) -> Self {
43 Self {
44 handlers: Arc::new(RwLock::new(Vec::new())),
45 webhook_secret,
46 }
47 }
48
49 pub async fn register_handler(&self, handler: Arc<dyn WebhookHandler>) {
55 let mut handlers = self.handlers.write().await;
56 handlers.push(handler);
57 info!("Registered webhook handler");
58 }
59
60 pub fn verify_signature(&self, signature: Option<&str>, payload: &[u8]) -> Result<()> {
71 if let Some(secret) = &self.webhook_secret {
72 let sig = signature.ok_or_else(|| anyhow!("Missing webhook signature"))?;
73
74 use sha2::{Digest, Sha256};
76 let mut hasher = Sha256::new();
77 hasher.update(secret.as_bytes());
78 hasher.update(payload);
79 let expected = format!("sha256={:x}", hasher.finalize());
80
81 if !constant_time_compare(sig.as_bytes(), expected.as_bytes()) {
83 error!("Invalid webhook signature");
84 return Err(anyhow!("Invalid webhook signature"));
85 }
86
87 debug!("Webhook signature verified");
88 } else {
89 debug!("Webhook signature verification disabled (no secret configured)");
90 }
91
92 Ok(())
93 }
94
95 pub async fn process_event(
106 &self,
107 payload: &str,
108 signature: Option<&str>,
109 ) -> Result<()> {
110 self.verify_signature(signature, payload.as_bytes())?;
112
113 let event: WebhookEvent = serde_json::from_str(payload)
115 .context("Failed to parse webhook event")?;
116
117 info!(
118 "Processing webhook event: {} at {}",
119 event.webhook_event, event.timestamp
120 );
121
122 let handlers = self.handlers.read().await;
124
125 if handlers.is_empty() {
126 warn!("No webhook handlers registered");
127 return Ok(());
128 }
129
130 let event_type = event.webhook_event.as_str();
131 let mut errors = Vec::new();
132
133 for handler in handlers.iter() {
134 let result = match event_type {
135 "jira:issue_created" => handler.on_issue_created(&event).await,
136 "jira:issue_updated" => handler.on_issue_updated(&event).await,
137 "jira:issue_deleted" => handler.on_issue_deleted(&event).await,
138 _ => handler.on_other_event(&event).await,
139 };
140
141 if let Err(e) = result {
142 error!("Handler error for {}: {}", event_type, e);
143 errors.push(e);
144 }
145 }
146
147 if !errors.is_empty() {
148 return Err(anyhow!(
149 "Some handlers failed: {} errors",
150 errors.len()
151 ));
152 }
153
154 info!("Successfully processed webhook event: {}", event_type);
155 Ok(())
156 }
157
158 pub async fn process_raw_event(
169 &self,
170 payload: &str,
171 signature: Option<&str>,
172 ) -> Result<Value> {
173 self.verify_signature(signature, payload.as_bytes())?;
175
176 let value: Value = serde_json::from_str(payload)
177 .context("Failed to parse webhook payload")?;
178
179 debug!("Received raw webhook event: {}", value["webhookEvent"].as_str().unwrap_or("unknown"));
180
181 Ok(value)
182 }
183
184 pub fn validate_event(&self, payload: &str) -> Result<()> {
194 let value: Value = serde_json::from_str(payload)
195 .context("Invalid JSON payload")?;
196
197 if !value.get("webhookEvent").and_then(|v| v.as_str()).is_some() {
199 return Err(anyhow!("Missing webhookEvent field"));
200 }
201
202 if !value.get("timestamp").and_then(|v| v.as_i64()).is_some() {
203 return Err(anyhow!("Missing or invalid timestamp field"));
204 }
205
206 debug!("Webhook event structure is valid");
207 Ok(())
208 }
209}
210
211fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
213 if a.len() != b.len() {
214 return false;
215 }
216
217 let mut result = 0u8;
218 for (x, y) in a.iter().zip(b.iter()) {
219 result |= x ^ y;
220 }
221
222 result == 0
223}
224
225pub struct LoggingWebhookHandler;
227
228#[async_trait]
229impl WebhookHandler for LoggingWebhookHandler {
230 async fn on_issue_created(&self, event: &WebhookEvent) -> Result<()> {
231 if let Some(issue) = &event.issue {
232 info!(
233 "Issue created: {} - {}",
234 issue.key, issue.fields.summary
235 );
236 }
237 Ok(())
238 }
239
240 async fn on_issue_updated(&self, event: &WebhookEvent) -> Result<()> {
241 if let Some(issue) = &event.issue {
242 info!(
243 "Issue updated: {} - {}",
244 issue.key, issue.fields.summary
245 );
246
247 if let Some(changelog) = &event.changelog {
248 for item in &changelog.items {
249 debug!(
250 " {} changed from '{}' to '{}'",
251 item.field,
252 item.from_string.as_deref().unwrap_or(""),
253 item.to_string.as_deref().unwrap_or("")
254 );
255 }
256 }
257 }
258 Ok(())
259 }
260
261 async fn on_issue_deleted(&self, event: &WebhookEvent) -> Result<()> {
262 if let Some(issue) = &event.issue {
263 info!("Issue deleted: {}", issue.key);
264 }
265 Ok(())
266 }
267
268 async fn on_other_event(&self, event: &WebhookEvent) -> Result<()> {
269 info!("Other event: {}", event.webhook_event);
270 Ok(())
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[tokio::test]
279 async fn test_webhook_processor_creation() {
280 let processor = WebhookProcessor::new(Some("secret".to_string()));
281 assert!(processor.webhook_secret.is_some());
282 }
283
284 #[tokio::test]
285 async fn test_constant_time_compare() {
286 assert!(constant_time_compare(b"test", b"test"));
287 assert!(!constant_time_compare(b"test", b"fail"));
288 assert!(!constant_time_compare(b"test", b"testing"));
289 }
290
291 #[tokio::test]
292 async fn test_validate_event() {
293 let processor = WebhookProcessor::new(None);
294
295 let valid_payload = r#"{
296 "webhookEvent": "jira:issue_created",
297 "timestamp": 1234567890
298 }"#;
299
300 assert!(processor.validate_event(valid_payload).is_ok());
301
302 let invalid_payload = r#"{"other": "data"}"#;
303 assert!(processor.validate_event(invalid_payload).is_err());
304 }
305
306 #[tokio::test]
307 async fn test_process_event() {
308 let processor = WebhookProcessor::new(None);
309 let handler = Arc::new(LoggingWebhookHandler);
310 processor.register_handler(handler).await;
311
312 let payload = r#"{
313 "webhookEvent": "jira:issue_created",
314 "timestamp": 1234567890,
315 "issue": {
316 "id": "10001",
317 "key": "TEST-1",
318 "self": "https://test.atlassian.net/rest/api/3/issue/10001",
319 "fields": {
320 "summary": "Test Issue",
321 "description": null,
322 "issuetype": {
323 "id": "1",
324 "name": "Task",
325 "description": null
326 },
327 "status": {
328 "id": "1",
329 "name": "To Do",
330 "description": null,
331 "statusCategory": {
332 "id": 1,
333 "key": "new",
334 "name": "To Do",
335 "colorName": "blue-gray"
336 }
337 },
338 "project": {
339 "id": "10000",
340 "key": "TEST",
341 "name": "Test Project",
342 "description": null,
343 "projectTypeKey": "software"
344 },
345 "created": "2024-01-01T00:00:00.000+0000",
346 "updated": "2024-01-01T00:00:00.000+0000",
347 "labels": [],
348 "components": []
349 }
350 }
351 }"#;
352
353 let result = processor.process_event(payload, None).await;
354 assert!(result.is_ok());
355 }
356}