Skip to main content

openlark_webhook/robot/v1/
send.rs

1use crate::common::error::{Result, WebhookError};
2use crate::common::validation;
3use crate::models::{FileContent, ImageContent, PostContent, TextContent};
4use serde_json::json;
5
6#[cfg(feature = "signature")]
7use crate::common::signature;
8
9#[cfg(feature = "card")]
10use crate::models::InteractiveContent;
11
12/// 发送 Webhook 消息请求构建器。
13#[derive(Debug, Clone)]
14pub struct SendWebhookMessageRequest {
15    webhook_url: String,
16    msg_type: String,
17    content: serde_json::Value,
18    #[cfg(feature = "signature")]
19    secret: Option<String>,
20}
21
22impl SendWebhookMessageRequest {
23    /// 创建新的发送请求
24    pub fn new(webhook_url: String) -> Self {
25        Self {
26            webhook_url,
27            msg_type: "text".to_string(),
28            content: json!({}),
29            #[cfg(feature = "signature")]
30            secret: None,
31        }
32    }
33
34    /// 设置签名密钥(启用签名验证)
35    #[cfg(feature = "signature")]
36    pub fn with_secret(mut self, secret: String) -> Self {
37        self.secret = Some(secret);
38        self
39    }
40
41    /// 将请求内容设置为文本消息。
42    pub fn text(mut self, text: String) -> Self {
43        self.msg_type = "text".to_string();
44        self.content = serde_json::to_value(TextContent::new(text)).unwrap_or_else(|_| json!({}));
45        self
46    }
47
48    /// 将请求内容设置为富文本消息。
49    pub fn post(mut self, post: String) -> Self {
50        self.msg_type = "post".to_string();
51        self.content = serde_json::to_value(PostContent::new(post)).unwrap_or_else(|_| json!({}));
52        self
53    }
54
55    /// 将请求内容设置为图片消息。
56    pub fn image(mut self, image_key: String) -> Self {
57        self.msg_type = "image".to_string();
58        self.content =
59            serde_json::to_value(ImageContent::new(image_key)).unwrap_or_else(|_| json!({}));
60        self
61    }
62
63    /// 将请求内容设置为文件消息。
64    pub fn file(mut self, file_key: String) -> Self {
65        self.msg_type = "file".to_string();
66        self.content =
67            serde_json::to_value(FileContent::new(file_key)).unwrap_or_else(|_| json!({}));
68        self
69    }
70
71    /// 将请求内容设置为交互式卡片消息。
72    ///
73    /// 需要启用 `card` feature。
74    #[cfg(feature = "card")]
75    pub fn card(mut self, card: serde_json::Value) -> Self {
76        self.msg_type = "interactive".to_string();
77        self.content =
78            serde_json::to_value(InteractiveContent::new(card)).unwrap_or_else(|_| json!({}));
79        self
80    }
81
82    /// 执行发送请求并返回飞书响应。
83    pub async fn execute(self) -> Result<SendWebhookMessageResponse> {
84        validation::validate_webhook_url(&self.webhook_url)
85            .map_err(|e| WebhookError::Http(e.to_string()))?;
86
87        let payload = json!(
88        {
89            "msg_type": self.msg_type,
90            "content": self.content,
91        });
92
93        #[cfg(feature = "signature")]
94        let request_builder = {
95            let mut rb = reqwest::Client::new()
96                .post(&self.webhook_url)
97                .json(&payload);
98            if let Some(secret) = &self.secret {
99                let timestamp = signature::current_timestamp();
100                let sign = signature::sign(timestamp, secret);
101                rb = rb
102                    .header("X-Lark-Signature", sign)
103                    .header("X-Lark-Timestamp", timestamp.to_string());
104            }
105            rb
106        };
107
108        #[cfg(not(feature = "signature"))]
109        let request_builder = reqwest::Client::new()
110            .post(&self.webhook_url)
111            .json(&payload);
112
113        let response = request_builder
114            .send()
115            .await
116            .map_err(|e| WebhookError::Http(e.to_string()))?;
117
118        let status = response.status();
119        if !status.is_success() {
120            return Err(WebhookError::Http(format!("HTTP error: {status}")));
121        }
122
123        let body = response
124            .text()
125            .await
126            .map_err(|e| WebhookError::Http(e.to_string()))?;
127
128        let result: SendWebhookMessageResponse = serde_json::from_str(&body)?;
129        Ok(result)
130    }
131}
132
133/// Webhook 消息发送响应
134#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
135pub struct SendWebhookMessageResponse {
136    /// 返回码
137    pub code: i32,
138    /// 返回信息
139    pub msg: String,
140}
141
142#[cfg(test)]
143#[allow(unused_imports)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_send_webhook_message_request_text() {
149        let req = SendWebhookMessageRequest::new("https://example.com/webhook".to_string())
150            .text("Hello, World!".to_string());
151
152        assert_eq!(req.msg_type, "text");
153        assert_eq!(req.webhook_url, "https://example.com/webhook");
154    }
155
156    #[test]
157    fn test_send_webhook_message_request_post() {
158        let req = SendWebhookMessageRequest::new("https://example.com/webhook".to_string())
159            .post(r#"{"title":"Test"}"#.to_string());
160
161        assert_eq!(req.msg_type, "post");
162    }
163
164    #[test]
165    fn test_send_webhook_message_request_image() {
166        let req = SendWebhookMessageRequest::new("https://example.com/webhook".to_string())
167            .image("img_abc123".to_string());
168
169        assert_eq!(req.msg_type, "image");
170    }
171
172    #[test]
173    fn test_send_webhook_message_request_file() {
174        let req = SendWebhookMessageRequest::new("https://example.com/webhook".to_string())
175            .file("file_xyz789".to_string());
176
177        assert_eq!(req.msg_type, "file");
178    }
179
180    #[cfg(feature = "card")]
181    #[test]
182    fn test_send_webhook_message_request_card() {
183        let card = serde_json::json!({
184            "type": "template",
185            "data": {
186                "template_id": "test_template"
187            }
188        });
189        let req =
190            SendWebhookMessageRequest::new("https://example.com/webhook".to_string()).card(card);
191
192        assert_eq!(req.msg_type, "interactive");
193    }
194
195    #[test]
196    fn test_send_webhook_message_response_serialization() {
197        let json = r#"{"code":0,"msg":"ok"}"#;
198        let response: SendWebhookMessageResponse =
199            serde_json::from_str(json).expect("JSON 反序列化失败");
200        assert_eq!(response.code, 0);
201        assert_eq!(response.msg, "ok");
202    }
203
204    #[cfg(feature = "signature")]
205    #[test]
206    fn test_send_webhook_message_request_with_secret() {
207        let req = SendWebhookMessageRequest::new("https://example.com/webhook".to_string())
208            .text("Hello".to_string())
209            .with_secret("my-secret".to_string());
210
211        assert!(req.secret.is_some());
212        assert_eq!(req.secret.unwrap(), "my-secret");
213    }
214}