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}