1use crate::errors::{Result, ZealError};
4use crate::events::*;
5use crate::webhooks::WebhooksAPI;
6use futures_util::stream::Stream;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::pin::Pin;
10use std::sync::{Arc, Mutex};
11use std::task::{Context, Poll};
12use tokio::sync::broadcast;
13
14#[derive(Debug, Clone)]
16pub struct SubscriptionOptions {
17 pub port: Option<u16>,
19 pub host: Option<String>,
21 pub path: Option<String>,
23 pub https: Option<bool>,
25 pub key: Option<String>,
27 pub cert: Option<String>,
29 pub auto_register: Option<bool>,
31 pub namespace: Option<String>,
33 pub events: Vec<String>,
35 pub buffer_size: usize,
37 pub headers: Option<HashMap<String, String>>,
39 pub verify_signature: Option<bool>,
41 pub secret_key: Option<String>,
43}
44
45impl Default for SubscriptionOptions {
46 fn default() -> Self {
47 Self {
48 port: Some(3001),
49 host: Some("0.0.0.0".to_string()),
50 path: Some("/webhooks".to_string()),
51 https: Some(false),
52 key: None,
53 cert: None,
54 auto_register: Some(true),
55 namespace: Some("default".to_string()),
56 events: vec!["*".to_string()],
57 buffer_size: 1000,
58 headers: None,
59 verify_signature: Some(false),
60 secret_key: None,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct WebhookDelivery {
68 pub webhook_id: String,
69 pub events: Vec<ZipWebhookEvent>,
70 pub metadata: WebhookMetadata,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct WebhookMetadata {
76 pub namespace: String,
77 pub delivery_id: String,
78 pub timestamp: String,
79}
80
81pub type WebhookEventCallback = Arc<
83 dyn Fn(ZipWebhookEvent) -> Pin<Box<dyn std::future::Future<Output = ()> + Send>> + Send + Sync,
84>;
85
86pub type WebhookDeliveryCallback = Arc<
88 dyn Fn(WebhookDelivery) -> Pin<Box<dyn std::future::Future<Output = ()> + Send>> + Send + Sync,
89>;
90
91pub type WebhookErrorCallback =
93 Arc<dyn Fn(ZealError) -> Pin<Box<dyn std::future::Future<Output = ()> + Send>> + Send + Sync>;
94
95#[pin_project::pin_project]
97pub struct WebhookObservable {
98 #[pin]
99 receiver: broadcast::Receiver<ZipWebhookEvent>,
100}
101
102impl Stream for WebhookObservable {
103 type Item = ZipWebhookEvent;
104
105 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
106 let mut this = self.project();
107 loop {
109 match this.receiver.try_recv() {
110 Ok(event) => return Poll::Ready(Some(event)),
111 Err(broadcast::error::TryRecvError::Empty) => {
112 cx.waker().wake_by_ref();
114 return Poll::Pending;
115 }
116 Err(broadcast::error::TryRecvError::Closed) => return Poll::Ready(None),
117 Err(broadcast::error::TryRecvError::Lagged(_)) => {
118 continue;
120 }
121 }
122 }
123 }
124}
125
126pub struct WebhookSubscription {
128 webhooks_api: WebhooksAPI,
129 options: SubscriptionOptions,
130 event_sender: broadcast::Sender<ZipWebhookEvent>,
131 event_callbacks: Arc<Mutex<Vec<WebhookEventCallback>>>,
132 delivery_callbacks: Arc<Mutex<Vec<WebhookDeliveryCallback>>>,
133 error_callbacks: Arc<Mutex<Vec<WebhookErrorCallback>>>,
134 webhook_id: Arc<Mutex<Option<String>>>,
135 is_running: Arc<Mutex<bool>>,
136 #[cfg(feature = "webhook-server")]
137 server_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
138}
139
140impl WebhookSubscription {
141 pub fn new(webhooks_api: WebhooksAPI, options: Option<SubscriptionOptions>) -> Self {
143 let options = options.unwrap_or_default();
144 let (event_sender, _) = broadcast::channel(options.buffer_size);
145
146 Self {
147 webhooks_api,
148 options,
149 event_sender,
150 event_callbacks: Arc::new(Mutex::new(Vec::new())),
151 delivery_callbacks: Arc::new(Mutex::new(Vec::new())),
152 error_callbacks: Arc::new(Mutex::new(Vec::new())),
153 webhook_id: Arc::new(Mutex::new(None)),
154 is_running: Arc::new(Mutex::new(false)),
155 #[cfg(feature = "webhook-server")]
156 server_handle: Arc::new(Mutex::new(None)),
157 }
158 }
159
160 pub fn on_event<F, Fut>(&self, callback: F) -> impl Fn() + Send + Sync
162 where
163 F: Fn(ZipWebhookEvent) -> Fut + Send + Sync + 'static,
164 Fut: std::future::Future<Output = ()> + Send + 'static,
165 {
166 let wrapped_callback: WebhookEventCallback =
167 Arc::new(move |event| Box::pin(callback(event)));
168
169 self.event_callbacks.lock().unwrap().push(wrapped_callback);
170 let callbacks = Arc::clone(&self.event_callbacks);
171 let index = callbacks.lock().unwrap().len() - 1;
172
173 move || {
174 callbacks.lock().unwrap().remove(index);
175 }
176 }
177
178 pub fn on_delivery<F, Fut>(&self, callback: F) -> impl Fn() + Send + Sync
180 where
181 F: Fn(WebhookDelivery) -> Fut + Send + Sync + 'static,
182 Fut: std::future::Future<Output = ()> + Send + 'static,
183 {
184 let wrapped_callback: WebhookDeliveryCallback =
185 Arc::new(move |delivery| Box::pin(callback(delivery)));
186
187 self.delivery_callbacks
188 .lock()
189 .unwrap()
190 .push(wrapped_callback);
191 let callbacks = Arc::clone(&self.delivery_callbacks);
192 let index = callbacks.lock().unwrap().len() - 1;
193
194 move || {
195 callbacks.lock().unwrap().remove(index);
196 }
197 }
198
199 pub fn on_error<F, Fut>(&self, callback: F) -> impl Fn() + Send + Sync
201 where
202 F: Fn(ZealError) -> Fut + Send + Sync + 'static,
203 Fut: std::future::Future<Output = ()> + Send + 'static,
204 {
205 let wrapped_callback: WebhookErrorCallback =
206 Arc::new(move |error| Box::pin(callback(error)));
207
208 self.error_callbacks.lock().unwrap().push(wrapped_callback);
209 let callbacks = Arc::clone(&self.error_callbacks);
210 let index = callbacks.lock().unwrap().len() - 1;
211
212 move || {
213 callbacks.lock().unwrap().remove(index);
214 }
215 }
216
217 pub fn as_observable(&self) -> WebhookObservable {
219 WebhookObservable {
220 receiver: self.event_sender.subscribe(),
221 }
222 }
223
224 pub async fn start(&self) -> Result<()> {
226 {
227 let mut is_running = self.is_running.lock().unwrap();
228 if *is_running {
229 return Err(ZealError::other("Webhook subscription is already running"));
230 }
231 *is_running = true;
232 }
233
234 #[cfg(feature = "webhook-server")]
235 {
236 self.start_webhook_server().await?;
237
238 if self.options.auto_register.unwrap_or(true) {
240 self.register().await?;
241 }
242 Ok(())
243 }
244
245 #[cfg(not(feature = "webhook-server"))]
246 Err(ZealError::other("Webhook server feature not enabled. Enable 'webhook-server' feature to use this functionality"))
247 }
248
249 pub async fn stop(&self) -> Result<()> {
251 {
252 let mut is_running = self.is_running.lock().unwrap();
253 if !*is_running {
254 return Ok(());
255 }
256 *is_running = false;
257 }
258
259 let webhook_id = self.webhook_id.lock().unwrap().take();
261 if let Some(webhook_id) = webhook_id {
262 if let Err(err) = self.webhooks_api.delete(&webhook_id).await {
263 tracing::error!("Failed to unregister webhook {}: {}", webhook_id, err);
264 } else {
265 tracing::info!("Unregistered webhook {}", webhook_id);
266 }
267 }
268
269 #[cfg(feature = "webhook-server")]
270 {
271 if let Some(handle) = self.server_handle.lock().unwrap().take() {
272 handle.abort();
273 let _ = handle.await;
274 tracing::info!("Webhook server stopped");
275 }
276 }
277
278 Ok(())
279 }
280
281 pub async fn register(&self) -> Result<()> {
283 if !*self.is_running.lock().unwrap() {
284 return Err(ZealError::other(
285 "Webhook server must be running before registration",
286 ));
287 }
288
289 let protocol = if self.options.https.unwrap_or(false) {
291 "https"
292 } else {
293 "http"
294 };
295 let host = self.options.host.as_deref().unwrap_or("localhost");
296 let host = if host == "0.0.0.0" { "localhost" } else { host };
297 let port = self.options.port.unwrap_or(3001);
298 let path = self.options.path.as_deref().unwrap_or("/webhooks");
299 let url = format!("{}://{}:{}{}", protocol, host, port, path);
300
301 let config = crate::types::WebhookConfig {
303 namespace: self
304 .options
305 .namespace
306 .as_deref()
307 .unwrap_or("default")
308 .to_string(),
309 url,
310 events: Some(self.options.events.clone()),
311 headers: self.options.headers.clone(),
312 metadata: None,
313 };
314
315 let result = self.webhooks_api.register(config).await?;
316 *self.webhook_id.lock().unwrap() = Some(result.webhook_id.clone());
317
318 tracing::info!("Registered webhook {} at {}", result.webhook_id, result.url);
319 Ok(())
320 }
321
322 #[cfg(feature = "webhook-server")]
323 async fn process_delivery(&self, delivery: WebhookDelivery) {
325 let delivery_callbacks = self.delivery_callbacks.lock().unwrap().clone();
327 for callback in delivery_callbacks {
328 if let Err(err) = tokio::time::timeout(
329 std::time::Duration::from_secs(30),
330 callback(delivery.clone()),
331 )
332 .await
333 {
334 tracing::error!("Delivery callback timeout: {}", err);
335 }
336 }
337
338 for event in delivery.events {
340 if let Err(err) = self.event_sender.send(event.clone()) {
342 tracing::error!("Failed to send event to broadcast channel: {}", err);
343 }
344
345 let event_callbacks = self.event_callbacks.lock().unwrap().clone();
347 for callback in event_callbacks {
348 if let Err(err) = tokio::time::timeout(
349 std::time::Duration::from_secs(30),
350 callback(event.clone()),
351 )
352 .await
353 {
354 tracing::error!("Event callback timeout: {}", err);
355 }
356 }
357 }
358 }
359
360 #[cfg(feature = "webhook-server")]
361 async fn emit_error(&self, error: ZealError) {
363 let error_callbacks = self.error_callbacks.lock().unwrap().clone();
364 for callback in error_callbacks {
365 if let Err(err) =
366 tokio::time::timeout(std::time::Duration::from_secs(30), callback(error.clone()))
367 .await
368 {
369 tracing::error!("Error callback timeout: {}", err);
370 }
371 }
372 }
373
374 pub fn filter_events<F>(&self, predicate: F) -> impl Stream<Item = ZipWebhookEvent>
376 where
377 F: Fn(&ZipWebhookEvent) -> bool + Send + Sync + 'static,
378 {
379 use futures_util::StreamExt;
380 StreamExt::filter(self.as_observable(), move |event| {
381 futures_util::future::ready(predicate(event))
382 })
383 }
384
385 pub fn on_event_type<F, Fut>(
387 &self,
388 event_types: Vec<String>,
389 callback: F,
390 ) -> impl Fn() + Send + Sync
391 where
392 F: Fn(ZipWebhookEvent) -> Fut + Send + Sync + 'static,
393 Fut: std::future::Future<Output = ()> + Send + 'static,
394 {
395 let callback = std::sync::Arc::new(callback);
396 self.on_event(move |event| {
397 let event_types = event_types.clone();
398 let callback = callback.clone();
399 async move {
400 let event_type = match &event {
401 ZipWebhookEvent::Execution(e) => e.event_type(),
402 ZipWebhookEvent::Workflow(e) => e.event_type(),
403 ZipWebhookEvent::CRDT(e) => e.event_type(),
404 };
405 if event_types.contains(&event_type.to_string()) {
406 callback(event).await
407 }
408 }
409 })
410 }
411
412 pub fn on_event_source<F, Fut>(
414 &self,
415 sources: Vec<String>,
416 callback: F,
417 ) -> impl Fn() + Send + Sync
418 where
419 F: Fn(ZipWebhookEvent) -> Fut + Send + Sync + 'static,
420 Fut: std::future::Future<Output = ()> + Send + 'static,
421 {
422 let callback = std::sync::Arc::new(callback);
423 self.on_event(move |event| {
424 let sources = sources.clone();
425 let callback = callback.clone();
426 async move {
427 let workflow_id = match &event {
428 ZipWebhookEvent::Execution(e) => e.workflow_id(),
429 ZipWebhookEvent::Workflow(e) => e.workflow_id(),
430 ZipWebhookEvent::CRDT(e) => e.workflow_id(),
431 };
432 if sources.contains(&workflow_id.to_string()) {
433 callback(event).await
434 }
435 }
436 })
437 }
438
439 pub fn webhook_id(&self) -> Option<String> {
441 self.webhook_id.lock().unwrap().clone()
442 }
443
444 pub fn is_running(&self) -> bool {
446 *self.is_running.lock().unwrap()
447 }
448
449 #[cfg(feature = "webhook-server")]
450 async fn start_webhook_server(&self) -> Result<()> {
451 use axum::{extract::State, http::StatusCode, response::Json, routing::post, Router};
452 use tower::ServiceBuilder;
453
454 let app_state = WebhookServerState {
455 subscription: self as *const WebhookSubscription,
456 };
457
458 let app = Router::new()
459 .route(
460 self.options.path.as_deref().unwrap_or("/webhooks"),
461 post(webhook_handler),
462 )
463 .layer(ServiceBuilder::new())
464 .with_state(app_state);
465
466 let addr = format!(
467 "{}:{}",
468 self.options.host.as_deref().unwrap_or("0.0.0.0"),
469 self.options.port.unwrap_or(3001)
470 );
471
472 let listener = tokio::net::TcpListener::bind(&addr).await.map_err(|e| {
473 ZealError::other(format!("Failed to bind webhook server to {}: {}", addr, e))
474 })?;
475
476 tracing::info!("Webhook server listening on {}", addr);
477
478 let server_handle = tokio::spawn(async move {
479 if let Err(err) = axum::serve(listener, app).await {
480 tracing::error!("Webhook server error: {}", err);
481 }
482 });
483
484 *self.server_handle.lock().unwrap() = Some(server_handle);
485 Ok(())
486 }
487}
488
489#[cfg(feature = "webhook-server")]
490#[derive(Clone)]
491struct WebhookServerState {
492 subscription: *const WebhookSubscription,
493}
494
495#[cfg(feature = "webhook-server")]
496unsafe impl Send for WebhookServerState {}
497#[cfg(feature = "webhook-server")]
498unsafe impl Sync for WebhookServerState {}
499
500#[cfg(feature = "webhook-server")]
501async fn webhook_handler(
502 State(state): State<WebhookServerState>,
503 Json(delivery): Json<WebhookDelivery>,
504) -> Result<StatusCode, StatusCode> {
505 let subscription = unsafe { &*state.subscription };
506
507 if subscription.options.verify_signature.unwrap_or(false) {
509 }
511
512 subscription.process_delivery(delivery).await;
513 Ok(StatusCode::OK)
514}
515
516impl Drop for WebhookSubscription {
517 fn drop(&mut self) {
518 if *self.is_running.lock().unwrap() {
519 tracing::warn!("WebhookSubscription dropped while still running. Consider calling stop() explicitly.");
520 }
521 }
522}