Skip to main content

weft_core/api/
package_webhook.rs

1use crate::api::openai_compat::AppState;
2use axum::{
3    body::Bytes,
4    extract::{Path, State},
5    http::{HeaderMap, StatusCode},
6    response::{IntoResponse, Response},
7};
8
9/// POST /api/packages/{package_name}/webhook
10pub async fn package_webhook_no_channel(
11    Path(package_name): Path<String>,
12    State(state): State<AppState>,
13    headers: HeaderMap,
14    body: Bytes,
15) -> Response {
16    do_webhook(&package_name, "", state, headers, body).await
17}
18
19/// POST /api/packages/{package_name}/webhook/{channel_type}
20pub async fn package_webhook_with_channel(
21    Path((package_name, channel_type)): Path<(String, String)>,
22    State(state): State<AppState>,
23    headers: HeaderMap,
24    body: Bytes,
25) -> Response {
26    do_webhook(&package_name, &channel_type, state, headers, body).await
27}
28
29async fn do_webhook(
30    package_name: &str,
31    channel_type: &str,
32    state: AppState,
33    headers: HeaderMap,
34    body: Bytes,
35) -> Response {
36    if package_name.is_empty() {
37        return (StatusCode::BAD_REQUEST, "Missing package name").into_response();
38    }
39
40    // Collect headers into a JSON object
41    let headers_map: serde_json::Map<String, serde_json::Value> = headers
42        .iter()
43        .map(|(k, v)| {
44            (
45                k.as_str().to_string(),
46                serde_json::Value::String(v.to_str().unwrap_or("").to_string()),
47            )
48        })
49        .collect();
50
51    // Build the payload for the WASM package
52    let raw_body = String::from_utf8_lossy(&body).to_string();
53    let payload = serde_json::json!({
54        "channel": channel_type,
55        "headers": headers_map,
56        "body": raw_body,
57    });
58
59    let payload_str = payload.to_string();
60
61    // Call the WASM package's handle_webhook export
62    let wasm_handle = state.wasm_handle.read().await.clone();
63    let Some(handle) = wasm_handle else {
64        return (
65            StatusCode::INTERNAL_SERVER_ERROR,
66            "No WASM runtime available",
67        )
68            .into_response();
69    };
70
71    match handle.call(package_name, "handle_webhook", &payload_str) {
72        Ok(result_str) => build_response_from_plugin(&result_str),
73        Err(e) => {
74            let msg = e.to_string();
75            if msg.contains("not loaded") || msg.contains("not found") {
76                (
77                    StatusCode::NOT_FOUND,
78                    format!("Package '{}' not found: {}", package_name, msg),
79                )
80                    .into_response()
81            } else {
82                tracing::error!(
83                    "[webhook] Package '{}' handle_webhook error: {}",
84                    package_name,
85                    e
86                );
87                (
88                    StatusCode::INTERNAL_SERVER_ERROR,
89                    format!("Package error: {}", msg),
90                )
91                    .into_response()
92            }
93        }
94    }
95}
96
97/// Parse the plugin's JSON response and build an HTTP response.
98/// Expected format: {"status_code": 200, "body": "..."}
99fn build_response_from_plugin(result_str: &str) -> Response {
100    let parsed: serde_json::Value = match serde_json::from_str(result_str) {
101        Ok(v) => v,
102        // If the package returned non-JSON, wrap it as a plain 200 response
103        Err(_) => {
104            return (StatusCode::OK, result_str.to_string()).into_response();
105        }
106    };
107
108    let status_code = parsed["status_code"].as_u64().unwrap_or(200) as u16;
109
110    let body = match &parsed["body"] {
111        serde_json::Value::String(s) => s.clone(),
112        serde_json::Value::Null => String::new(),
113        other => other.to_string(),
114    };
115
116    let status = StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
117    (status, body).into_response()
118}