Skip to main content

openlark_webhook/robot/v1/
client.rs

1use crate::common::error::{Result, WebhookError};
2use crate::common::validation;
3use crate::robot::v1::send::SendWebhookMessageResponse;
4use serde_json::json;
5
6#[cfg(feature = "signature")]
7use crate::common::signature;
8
9/// Webhook 客户端。
10#[derive(Debug, Clone)]
11pub struct WebhookClient {
12    client: reqwest::Client,
13    #[cfg(feature = "signature")]
14    secret: Option<String>,
15}
16
17impl WebhookClient {
18    /// 创建新的 Webhook 客户端
19    pub fn new() -> Self {
20        Self {
21            client: reqwest::Client::new(),
22            #[cfg(feature = "signature")]
23            secret: None,
24        }
25    }
26
27    /// 使用自定义 HTTP 客户端创建 Webhook 客户端
28    ///
29    /// 允许配置连接池、超时等参数:
30    /// ```rust,no_run
31    /// use openlark_webhook::prelude::*;
32    ///
33    /// let http_client = reqwest::Client::builder()
34    ///     .timeout(std::time::Duration::from_secs(30))
35    ///     .pool_max_idle_per_host(10)
36    ///     .build()
37    ///     .expect("Failed to build HTTP client");
38    ///
39    /// let client = WebhookClient::with_client(http_client);
40    /// ```
41    pub fn with_client(client: reqwest::Client) -> Self {
42        Self {
43            client,
44            #[cfg(feature = "signature")]
45            secret: None,
46        }
47    }
48
49    /// 设置签名密钥(启用签名验证)
50    #[cfg(feature = "signature")]
51    pub fn with_secret(mut self, secret: String) -> Self {
52        self.secret = Some(secret);
53        self
54    }
55
56    /// 发送原始 JSON 负载到指定 webhook。
57    ///
58    /// `payload` 需要符合飞书自定义机器人消息协议。
59    pub async fn send(
60        &self,
61        webhook_url: &str,
62        payload: serde_json::Value,
63    ) -> Result<SendWebhookMessageResponse> {
64        validation::validate_webhook_url(webhook_url)
65            .map_err(|e| WebhookError::Http(e.to_string()))?;
66
67        #[cfg(feature = "signature")]
68        let request_builder = {
69            let mut rb = self.client.post(webhook_url).json(&payload);
70            if let Some(secret) = &self.secret {
71                let timestamp = signature::current_timestamp();
72                let sign = signature::sign(timestamp, secret);
73                rb = rb
74                    .header("X-Lark-Signature", sign)
75                    .header("X-Lark-Timestamp", timestamp.to_string());
76            }
77            rb
78        };
79
80        #[cfg(not(feature = "signature"))]
81        let request_builder = self.client.post(webhook_url).json(&payload);
82
83        let response = request_builder
84            .send()
85            .await
86            .map_err(|e| WebhookError::Http(e.to_string()))?;
87
88        let status = response.status();
89        if !status.is_success() {
90            return Err(WebhookError::Http(format!("HTTP error: {status}")));
91        }
92
93        let body = response
94            .text()
95            .await
96            .map_err(|e| WebhookError::Http(e.to_string()))?;
97
98        let result: SendWebhookMessageResponse = serde_json::from_str(&body)?;
99        Ok(result)
100    }
101
102    /// 发送文本消息。
103    pub async fn send_text(
104        &self,
105        webhook_url: &str,
106        text: String,
107    ) -> Result<SendWebhookMessageResponse> {
108        let payload = json!({
109            "msg_type": "text",
110            "content": {
111                "text": text
112            }
113        });
114        self.send(webhook_url, payload).await
115    }
116
117    /// 发送富文本消息。
118    pub async fn send_post(
119        &self,
120        webhook_url: &str,
121        post: String,
122    ) -> Result<SendWebhookMessageResponse> {
123        let payload = json!({
124            "msg_type": "post",
125            "content": {
126                "post": post
127            }
128        });
129        self.send(webhook_url, payload).await
130    }
131
132    /// 发送图片消息。
133    pub async fn send_image(
134        &self,
135        webhook_url: &str,
136        image_key: String,
137    ) -> Result<SendWebhookMessageResponse> {
138        let payload = json!({
139            "msg_type": "image",
140            "content": {
141                "image_key": image_key
142            }
143        });
144        self.send(webhook_url, payload).await
145    }
146
147    /// 发送文件消息。
148    pub async fn send_file(
149        &self,
150        webhook_url: &str,
151        file_key: String,
152    ) -> Result<SendWebhookMessageResponse> {
153        let payload = json!({
154            "msg_type": "file",
155            "content": {
156                "file_key": file_key
157            }
158        });
159        self.send(webhook_url, payload).await
160    }
161
162    /// 发送交互式卡片消息。
163    ///
164    /// 需要启用 `card` feature。
165    #[cfg(feature = "card")]
166    pub async fn send_card(
167        &self,
168        webhook_url: &str,
169        card: serde_json::Value,
170    ) -> Result<SendWebhookMessageResponse> {
171        let payload = json!({
172            "msg_type": "interactive",
173            "content": {
174                "card": card
175            }
176        });
177        self.send(webhook_url, payload).await
178    }
179}
180
181impl Default for WebhookClient {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187#[cfg(test)]
188#[allow(unused_imports)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_webhook_client_creation() {
194        let _client = WebhookClient::new();
195        let _default_client = WebhookClient::default();
196    }
197
198    #[tokio::test]
199    async fn test_send_text_message_construction() {
200        let client = WebhookClient::new();
201        // Test that the method exists and can be called
202        // (actual HTTP call would require mocking)
203        let _client_ref = &client;
204    }
205
206    #[cfg(feature = "signature")]
207    #[test]
208    fn test_webhook_client_with_secret() {
209        let client = WebhookClient::new().with_secret("my-secret".to_string());
210        let _ = client;
211    }
212}