Skip to main content

dingtalk_stream/messages/
chatbot.rs

1//! 聊天机器人消息类型,对齐 Python chatbot.py
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// 聊天机器人消息
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct ChatbotMessage {
9    /// 是否在 @列表中
10    #[serde(rename = "isInAtList", skip_serializing_if = "Option::is_none")]
11    pub is_in_at_list: Option<bool>,
12    /// Session Webhook URL
13    #[serde(rename = "sessionWebhook", skip_serializing_if = "Option::is_none")]
14    pub session_webhook: Option<String>,
15    /// 发送者昵称
16    #[serde(rename = "senderNick", skip_serializing_if = "Option::is_none")]
17    pub sender_nick: Option<String>,
18    /// 机器人代码
19    #[serde(rename = "robotCode", skip_serializing_if = "Option::is_none")]
20    pub robot_code: Option<String>,
21    /// Session Webhook 过期时间
22    #[serde(
23        rename = "sessionWebhookExpiredTime",
24        skip_serializing_if = "Option::is_none"
25    )]
26    pub session_webhook_expired_time: Option<i64>,
27    /// 消息 ID
28    #[serde(rename = "msgId", skip_serializing_if = "Option::is_none")]
29    pub message_id: Option<String>,
30    /// 发送者 ID
31    #[serde(rename = "senderId", skip_serializing_if = "Option::is_none")]
32    pub sender_id: Option<String>,
33    /// 机器人用户 ID
34    #[serde(rename = "chatbotUserId", skip_serializing_if = "Option::is_none")]
35    pub chatbot_user_id: Option<String>,
36    /// 会话 ID
37    #[serde(rename = "conversationId", skip_serializing_if = "Option::is_none")]
38    pub conversation_id: Option<String>,
39    /// 是否管理员
40    #[serde(rename = "isAdmin", skip_serializing_if = "Option::is_none")]
41    pub is_admin: Option<bool>,
42    /// 创建时间
43    #[serde(rename = "createAt", skip_serializing_if = "Option::is_none")]
44    pub create_at: Option<i64>,
45    /// 会话类型: "1"=单聊, "2"=群聊
46    #[serde(rename = "conversationType", skip_serializing_if = "Option::is_none")]
47    pub conversation_type: Option<String>,
48    /// @的用户列表
49    #[serde(rename = "atUsers", skip_serializing_if = "Option::is_none")]
50    pub at_users: Option<Vec<AtUser>>,
51    /// 机器人所属企业 ID
52    #[serde(rename = "chatbotCorpId", skip_serializing_if = "Option::is_none")]
53    pub chatbot_corp_id: Option<String>,
54    /// 发送者所属企业 ID
55    #[serde(rename = "senderCorpId", skip_serializing_if = "Option::is_none")]
56    pub sender_corp_id: Option<String>,
57    /// 会话标题
58    #[serde(rename = "conversationTitle", skip_serializing_if = "Option::is_none")]
59    pub conversation_title: Option<String>,
60    /// 消息类型: text, picture, richText
61    #[serde(rename = "msgtype", skip_serializing_if = "Option::is_none")]
62    pub message_type: Option<String>,
63    /// 文本内容
64    #[serde(rename = "text", skip_serializing_if = "Option::is_none")]
65    pub text: Option<TextContent>,
66    /// 发送者员工 ID
67    #[serde(rename = "senderStaffId", skip_serializing_if = "Option::is_none")]
68    pub sender_staff_id: Option<String>,
69    /// 托管上下文
70    #[serde(rename = "hostingContext", skip_serializing_if = "Option::is_none")]
71    pub hosting_context: Option<HostingContext>,
72    /// 会话消息上下文
73    #[serde(
74        rename = "conversationMsgContext",
75        skip_serializing_if = "Option::is_none"
76    )]
77    pub conversation_msg_context: Option<Vec<ConversationMessage>>,
78    /// 图片内容(从 content 字段解析,msgtype=picture 时)
79    #[serde(skip)]
80    pub image_content: Option<ImageContent>,
81    /// 富文本内容(从 content 字段解析,msgtype=richText 时)
82    #[serde(skip)]
83    pub rich_text_content: Option<RichTextContent>,
84    /// 扩展字段
85    #[serde(flatten)]
86    pub extensions: HashMap<String, serde_json::Value>,
87}
88
89impl ChatbotMessage {
90    /// 机器人消息回调主题
91    pub const TOPIC: &'static str = "/v1.0/im/bot/messages/get";
92    /// 机器人消息委托主题
93    pub const DELEGATE_TOPIC: &'static str = "/v1.0/im/bot/messages/delegate";
94
95    /// 从 JSON Value 构造(处理 content 字段的特殊解析逻辑)
96    pub fn from_value(value: &serde_json::Value) -> crate::Result<Self> {
97        let mut msg: Self = serde_json::from_value(value.clone())?;
98
99        // 根据 msgtype 解析 content 字段
100        if let Some(msg_type) = &msg.message_type {
101            if let Some(content) = value.get("content") {
102                match msg_type.as_str() {
103                    "picture" => {
104                        msg.image_content = serde_json::from_value(content.clone()).ok();
105                    }
106                    "richText" => {
107                        msg.rich_text_content = serde_json::from_value(content.clone()).ok();
108                    }
109                    _ => {}
110                }
111            }
112        }
113
114        Ok(msg)
115    }
116
117    /// 获取文本列表
118    pub fn get_text_list(&self) -> Option<Vec<String>> {
119        match self.message_type.as_deref() {
120            Some("text") => self
121                .text
122                .as_ref()
123                .and_then(|t| t.content.clone())
124                .map(|c| vec![c]),
125            Some("richText") => self.rich_text_content.as_ref().map(|rtc| {
126                rtc.rich_text_list
127                    .iter()
128                    .filter_map(|item| item.get("text").and_then(|v| v.as_str()).map(String::from))
129                    .collect()
130            }),
131            _ => None,
132        }
133    }
134
135    /// 获取图片下载码列表
136    pub fn get_image_list(&self) -> Option<Vec<String>> {
137        match self.message_type.as_deref() {
138            Some("picture") => self
139                .image_content
140                .as_ref()
141                .and_then(|ic| ic.download_code.clone())
142                .map(|dc| vec![dc]),
143            Some("richText") => self.rich_text_content.as_ref().map(|rtc| {
144                rtc.rich_text_list
145                    .iter()
146                    .filter_map(|item| {
147                        item.get("downloadCode")
148                            .and_then(|v| v.as_str())
149                            .map(String::from)
150                    })
151                    .collect()
152            }),
153            _ => None,
154        }
155    }
156}
157
158impl std::fmt::Display for ChatbotMessage {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        write!(
161            f,
162            "ChatbotMessage(message_type={:?}, text={:?}, sender_nick={:?}, conversation_title={:?})",
163            self.message_type, self.text, self.sender_nick, self.conversation_title
164        )
165    }
166}
167
168/// @用户信息
169#[derive(Debug, Clone, Default, Serialize, Deserialize)]
170pub struct AtUser {
171    /// 钉钉 ID
172    #[serde(rename = "dingtalkId", skip_serializing_if = "Option::is_none")]
173    pub dingtalk_id: Option<String>,
174    /// 员工 ID
175    #[serde(rename = "staffId", skip_serializing_if = "Option::is_none")]
176    pub staff_id: Option<String>,
177    /// 扩展字段
178    #[serde(flatten)]
179    pub extensions: HashMap<String, serde_json::Value>,
180}
181
182/// 文本内容
183#[derive(Debug, Clone, Default, Serialize, Deserialize)]
184pub struct TextContent {
185    /// 文本内容
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub content: Option<String>,
188    /// 扩展字段
189    #[serde(flatten)]
190    pub extensions: HashMap<String, serde_json::Value>,
191}
192
193impl std::fmt::Display for TextContent {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        write!(f, "TextContent(content={:?})", self.content)
196    }
197}
198
199/// 图片内容
200#[derive(Debug, Clone, Default, Serialize, Deserialize)]
201pub struct ImageContent {
202    /// 下载码
203    #[serde(rename = "downloadCode", skip_serializing_if = "Option::is_none")]
204    pub download_code: Option<String>,
205}
206
207/// 富文本内容
208#[derive(Debug, Clone, Default, Serialize, Deserialize)]
209pub struct RichTextContent {
210    /// 富文本列表
211    #[serde(rename = "richText", default)]
212    pub rich_text_list: Vec<serde_json::Value>,
213}
214
215/// 托管上下文
216#[derive(Debug, Clone, Default, Serialize, Deserialize)]
217pub struct HostingContext {
218    /// 用户 ID
219    #[serde(rename = "userId")]
220    pub user_id: String,
221    /// 昵称
222    pub nick: String,
223}
224
225/// 会话消息上下文
226#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227pub struct ConversationMessage {
228    /// 已读状态
229    #[serde(rename = "readStatus", default)]
230    pub read_status: String,
231    /// 发送者用户 ID
232    #[serde(rename = "senderUserId", default)]
233    pub sender_user_id: String,
234    /// 发送时间
235    #[serde(rename = "sendTime", default)]
236    pub send_time: i64,
237}
238
239impl ConversationMessage {
240    /// 消息是否被我已读
241    pub fn read_by_me(&self) -> bool {
242        self.read_status == "2"
243    }
244}
245
246/// 构造指定单聊的 `ChatbotMessage`(用于主动发送卡片到单聊)
247pub fn reply_specified_single_chat(user_id: &str, user_nickname: &str) -> ChatbotMessage {
248    let value = serde_json::json!({
249        "senderId": user_id,
250        "senderStaffId": user_id,
251        "senderNick": user_nickname,
252        "conversationType": "1",
253        "msgId": uuid::Uuid::new_v4().to_string(),
254    });
255    serde_json::from_value(value).unwrap_or_default()
256}
257
258/// 构造指定群聊的 `ChatbotMessage`(用于主动发送卡片到群聊)
259pub fn reply_specified_group_chat(open_conversation_id: &str) -> ChatbotMessage {
260    let value = serde_json::json!({
261        "conversationId": open_conversation_id,
262        "conversationType": "2",
263        "msgId": uuid::Uuid::new_v4().to_string(),
264    });
265    serde_json::from_value(value).unwrap_or_default()
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_chatbot_message_text() {
274        let json = serde_json::json!({
275            "msgtype": "text",
276            "text": {"content": "hello world"},
277            "senderNick": "test_user",
278            "conversationType": "1",
279            "senderId": "user_001",
280            "senderStaffId": "staff_001",
281            "msgId": "msg_001"
282        });
283        let msg = ChatbotMessage::from_value(&json).unwrap();
284        assert_eq!(msg.message_type.as_deref(), Some("text"));
285        assert_eq!(
286            msg.text.as_ref().and_then(|t| t.content.as_deref()),
287            Some("hello world")
288        );
289        let texts = msg.get_text_list().unwrap();
290        assert_eq!(texts, vec!["hello world"]);
291    }
292
293    #[test]
294    fn test_chatbot_message_picture() {
295        let json = serde_json::json!({
296            "msgtype": "picture",
297            "content": {"downloadCode": "dc_001"},
298            "senderId": "user_001",
299            "msgId": "msg_002"
300        });
301        let msg = ChatbotMessage::from_value(&json).unwrap();
302        assert_eq!(msg.message_type.as_deref(), Some("picture"));
303        assert_eq!(
304            msg.image_content
305                .as_ref()
306                .and_then(|ic| ic.download_code.as_deref()),
307            Some("dc_001")
308        );
309        let images = msg.get_image_list().unwrap();
310        assert_eq!(images, vec!["dc_001"]);
311    }
312
313    #[test]
314    fn test_chatbot_message_rich_text() {
315        let json = serde_json::json!({
316            "msgtype": "richText",
317            "content": {
318                "richText": [
319                    {"text": "line1"},
320                    {"downloadCode": "img_001"},
321                    {"text": "line2"}
322                ]
323            },
324            "senderId": "user_001",
325            "msgId": "msg_003"
326        });
327        let msg = ChatbotMessage::from_value(&json).unwrap();
328        let texts = msg.get_text_list().unwrap();
329        assert_eq!(texts, vec!["line1", "line2"]);
330        let images = msg.get_image_list().unwrap();
331        assert_eq!(images, vec!["img_001"]);
332    }
333
334    #[test]
335    fn test_reply_specified_single_chat() {
336        let msg = reply_specified_single_chat("user_001", "Test User");
337        assert_eq!(msg.sender_id.as_deref(), Some("user_001"));
338        assert_eq!(msg.sender_staff_id.as_deref(), Some("user_001"));
339        assert_eq!(msg.conversation_type.as_deref(), Some("1"));
340        assert!(msg.message_id.is_some());
341    }
342
343    #[test]
344    fn test_reply_specified_group_chat() {
345        let msg = reply_specified_group_chat("conv_001");
346        assert_eq!(msg.conversation_id.as_deref(), Some("conv_001"));
347        assert_eq!(msg.conversation_type.as_deref(), Some("2"));
348        assert!(msg.message_id.is_some());
349    }
350
351    #[test]
352    fn test_conversation_message_read_by_me() {
353        let msg = ConversationMessage {
354            read_status: "2".to_owned(),
355            sender_user_id: "user_001".to_owned(),
356            send_time: 1_690_000_000,
357        };
358        assert!(msg.read_by_me());
359
360        let msg2 = ConversationMessage {
361            read_status: "1".to_owned(),
362            ..Default::default()
363        };
364        assert!(!msg2.read_by_me());
365    }
366
367    #[test]
368    fn test_at_user_serde() {
369        let json = r#"{"dingtalkId":"dt_001","staffId":"staff_001","extra":"val"}"#;
370        let user: AtUser = serde_json::from_str(json).unwrap();
371        assert_eq!(user.dingtalk_id.as_deref(), Some("dt_001"));
372        assert_eq!(user.staff_id.as_deref(), Some("staff_001"));
373        assert!(user.extensions.contains_key("extra"));
374    }
375}