Skip to main content

okbot_core/
webhook.rs

1//! Platform-neutral HTTP types for channels that receive events by webhook.
2//!
3//! The runtime owns the HTTP server and mounts each channel's [`WebhookHandler`] at its path, so
4//! channel crates do not depend on a web framework.
5
6use crate::async_trait;
7
8/// An HTTP request delivered to a webhook.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct WebhookRequest {
11    /// Upper-case method (`POST`).
12    pub method: String,
13    /// Path relative to the handler's mount point (usually empty).
14    pub path: String,
15    /// Headers with lower-case names.
16    pub headers: Vec<(String, String)>,
17    /// Raw body.
18    pub body: Vec<u8>,
19}
20
21impl WebhookRequest {
22    /// The first value of a header; `name` is matched case-insensitively.
23    pub fn header(&self, name: &str) -> Option<&str> {
24        self.headers
25            .iter()
26            .find(|(k, _)| k.eq_ignore_ascii_case(name))
27            .map(|(_, v)| v.as_str())
28    }
29}
30
31/// The response to a webhook request.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct WebhookResponse {
34    /// HTTP status.
35    pub status: u16,
36    /// `Content-Type` of the body.
37    pub content_type: &'static str,
38    /// Body.
39    pub body: Vec<u8>,
40}
41
42impl WebhookResponse {
43    /// `200` with a JSON body.
44    pub fn json(body: &serde_json::Value) -> Self {
45        Self {
46            status: 200,
47            content_type: "application/json",
48            body: body.to_string().into_bytes(),
49        }
50    }
51
52    /// An empty response with `status`.
53    pub fn status(status: u16) -> Self {
54        Self {
55            status,
56            content_type: "text/plain",
57            body: Vec::new(),
58        }
59    }
60}
61
62/// Receives webhook requests for one channel.
63///
64/// Implementations authenticate the request, hand events to the runtime and return quickly:
65/// platforms time out slow webhooks, and agent turns are slow.
66#[async_trait]
67pub trait WebhookHandler: Send + Sync {
68    /// Mount path, without the bot prefix (for example `zalo/shop`).
69    fn path(&self) -> String;
70
71    /// Handles one request.
72    async fn handle(&self, request: WebhookRequest) -> WebhookResponse;
73}