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    /// Extra headers, with lower-case names.
41    pub headers: Vec<(String, String)>,
42}
43
44impl WebhookResponse {
45    /// Adds a header.
46    pub fn with_header(mut self, name: &str, value: impl Into<String>) -> Self {
47        self.headers.push((name.to_ascii_lowercase(), value.into()));
48        self
49    }
50
51    /// `200` with a JSON body.
52    pub fn json(body: &serde_json::Value) -> Self {
53        Self {
54            status: 200,
55            content_type: "application/json",
56            body: body.to_string().into_bytes(),
57            headers: Vec::new(),
58        }
59    }
60
61    /// An empty response with `status`.
62    pub fn status(status: u16) -> Self {
63        Self {
64            status,
65            content_type: "text/plain",
66            body: Vec::new(),
67            headers: Vec::new(),
68        }
69    }
70}
71
72/// Receives webhook requests for one channel.
73///
74/// Implementations authenticate the request, hand events to the runtime and return quickly:
75/// platforms time out slow webhooks, and agent turns are slow.
76#[async_trait]
77pub trait WebhookHandler: Send + Sync {
78    /// Mount path, without the bot prefix (for example `zalo/shop`).
79    fn path(&self) -> String;
80
81    /// Handles one request.
82    async fn handle(&self, request: WebhookRequest) -> WebhookResponse;
83}