1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct ChatbotMessage {
9 #[serde(rename = "isInAtList", skip_serializing_if = "Option::is_none")]
11 pub is_in_at_list: Option<bool>,
12 #[serde(rename = "sessionWebhook", skip_serializing_if = "Option::is_none")]
14 pub session_webhook: Option<String>,
15 #[serde(rename = "senderNick", skip_serializing_if = "Option::is_none")]
17 pub sender_nick: Option<String>,
18 #[serde(rename = "robotCode", skip_serializing_if = "Option::is_none")]
20 pub robot_code: Option<String>,
21 #[serde(
23 rename = "sessionWebhookExpiredTime",
24 skip_serializing_if = "Option::is_none"
25 )]
26 pub session_webhook_expired_time: Option<i64>,
27 #[serde(rename = "msgId", skip_serializing_if = "Option::is_none")]
29 pub message_id: Option<String>,
30 #[serde(rename = "senderId", skip_serializing_if = "Option::is_none")]
32 pub sender_id: Option<String>,
33 #[serde(rename = "chatbotUserId", skip_serializing_if = "Option::is_none")]
35 pub chatbot_user_id: Option<String>,
36 #[serde(rename = "conversationId", skip_serializing_if = "Option::is_none")]
38 pub conversation_id: Option<String>,
39 #[serde(rename = "isAdmin", skip_serializing_if = "Option::is_none")]
41 pub is_admin: Option<bool>,
42 #[serde(rename = "createAt", skip_serializing_if = "Option::is_none")]
44 pub create_at: Option<i64>,
45 #[serde(rename = "conversationType", skip_serializing_if = "Option::is_none")]
47 pub conversation_type: Option<String>,
48 #[serde(rename = "atUsers", skip_serializing_if = "Option::is_none")]
50 pub at_users: Option<Vec<AtUser>>,
51 #[serde(rename = "chatbotCorpId", skip_serializing_if = "Option::is_none")]
53 pub chatbot_corp_id: Option<String>,
54 #[serde(rename = "senderCorpId", skip_serializing_if = "Option::is_none")]
56 pub sender_corp_id: Option<String>,
57 #[serde(rename = "conversationTitle", skip_serializing_if = "Option::is_none")]
59 pub conversation_title: Option<String>,
60 #[serde(rename = "msgtype", skip_serializing_if = "Option::is_none")]
62 pub message_type: Option<String>,
63 #[serde(rename = "text", skip_serializing_if = "Option::is_none")]
65 pub text: Option<TextContent>,
66 #[serde(rename = "senderStaffId", skip_serializing_if = "Option::is_none")]
68 pub sender_staff_id: Option<String>,
69 #[serde(rename = "hostingContext", skip_serializing_if = "Option::is_none")]
71 pub hosting_context: Option<HostingContext>,
72 #[serde(
74 rename = "conversationMsgContext",
75 skip_serializing_if = "Option::is_none"
76 )]
77 pub conversation_msg_context: Option<Vec<ConversationMessage>>,
78 #[serde(skip)]
80 pub image_content: Option<ImageContent>,
81 #[serde(skip)]
83 pub rich_text_content: Option<RichTextContent>,
84 #[serde(skip)]
90 pub audio_content: Option<AudioContent>,
91 #[serde(flatten)]
93 pub extensions: HashMap<String, serde_json::Value>,
94}
95
96impl ChatbotMessage {
97 pub const TOPIC: &'static str = "/v1.0/im/bot/messages/get";
99 pub const DELEGATE_TOPIC: &'static str = "/v1.0/im/bot/messages/delegate";
101
102 pub fn from_value(value: &serde_json::Value) -> crate::Result<Self> {
104 let mut msg: Self = serde_json::from_value(value.clone())?;
105
106 if let Some(msg_type) = &msg.message_type {
108 if let Some(content) = value.get("content") {
109 match msg_type.as_str() {
110 "picture" => {
111 msg.image_content = serde_json::from_value(content.clone()).ok();
112 }
113 "richText" => {
114 msg.rich_text_content = serde_json::from_value(content.clone()).ok();
115 }
116 "audio" => {
118 msg.audio_content = serde_json::from_value(content.clone()).ok();
119 }
120 _ => {}
121 }
122 }
123 }
124
125 Ok(msg)
126 }
127
128 pub fn get_text_list(&self) -> Option<Vec<String>> {
130 match self.message_type.as_deref() {
131 Some("text") => self
132 .text
133 .as_ref()
134 .and_then(|t| t.content.clone())
135 .map(|c| vec![c]),
136 Some("richText") => self.rich_text_content.as_ref().map(|rtc| {
137 rtc.rich_text_list
138 .iter()
139 .filter_map(|item| item.get("text").and_then(|v| v.as_str()).map(String::from))
140 .collect()
141 }),
142 Some("audio") => self
144 .audio_content
145 .as_ref()
146 .and_then(|ac| ac.recognition.clone())
147 .map(|r| vec![r]),
148 _ => None,
149 }
150 }
151
152 pub fn get_image_list(&self) -> Option<Vec<String>> {
154 match self.message_type.as_deref() {
155 Some("picture") => self
156 .image_content
157 .as_ref()
158 .and_then(|ic| ic.download_code.clone())
159 .map(|dc| vec![dc]),
160 Some("richText") => self.rich_text_content.as_ref().map(|rtc| {
161 rtc.rich_text_list
162 .iter()
163 .filter_map(|item| {
164 item.get("downloadCode")
165 .and_then(|v| v.as_str())
166 .map(String::from)
167 })
168 .collect()
169 }),
170 _ => None,
171 }
172 }
173}
174
175impl std::fmt::Display for ChatbotMessage {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 write!(
178 f,
179 "ChatbotMessage(message_type={:?}, text={:?}, sender_nick={:?}, conversation_title={:?})",
180 self.message_type, self.text, self.sender_nick, self.conversation_title
181 )
182 }
183}
184
185#[derive(Debug, Clone, Default, Serialize, Deserialize)]
187pub struct AtUser {
188 #[serde(rename = "dingtalkId", skip_serializing_if = "Option::is_none")]
190 pub dingtalk_id: Option<String>,
191 #[serde(rename = "staffId", skip_serializing_if = "Option::is_none")]
193 pub staff_id: Option<String>,
194 #[serde(flatten)]
196 pub extensions: HashMap<String, serde_json::Value>,
197}
198
199#[derive(Debug, Clone, Default, Serialize, Deserialize)]
201pub struct TextContent {
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub content: Option<String>,
205 #[serde(flatten)]
207 pub extensions: HashMap<String, serde_json::Value>,
208}
209
210impl std::fmt::Display for TextContent {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 write!(f, "TextContent(content={:?})", self.content)
213 }
214}
215
216#[derive(Debug, Clone, Default, Serialize, Deserialize)]
218pub struct ImageContent {
219 #[serde(rename = "downloadCode", skip_serializing_if = "Option::is_none")]
221 pub download_code: Option<String>,
222}
223
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
233pub struct AudioContent {
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub recognition: Option<String>,
237 #[serde(rename = "downloadCode", skip_serializing_if = "Option::is_none")]
239 pub download_code: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub duration: Option<i64>,
243}
244
245#[derive(Debug, Clone, Default, Serialize, Deserialize)]
247pub struct RichTextContent {
248 #[serde(rename = "richText", default)]
250 pub rich_text_list: Vec<serde_json::Value>,
251}
252
253#[derive(Debug, Clone, Default, Serialize, Deserialize)]
255pub struct HostingContext {
256 #[serde(rename = "userId")]
258 pub user_id: String,
259 pub nick: String,
261}
262
263#[derive(Debug, Clone, Default, Serialize, Deserialize)]
265pub struct ConversationMessage {
266 #[serde(rename = "readStatus", default)]
268 pub read_status: String,
269 #[serde(rename = "senderUserId", default)]
271 pub sender_user_id: String,
272 #[serde(rename = "sendTime", default)]
274 pub send_time: i64,
275}
276
277impl ConversationMessage {
278 pub fn read_by_me(&self) -> bool {
280 self.read_status == "2"
281 }
282}
283
284pub fn reply_specified_single_chat(user_id: &str, user_nickname: &str) -> ChatbotMessage {
286 let value = serde_json::json!({
287 "senderId": user_id,
288 "senderStaffId": user_id,
289 "senderNick": user_nickname,
290 "conversationType": "1",
291 "msgId": uuid::Uuid::new_v4().to_string(),
292 });
293 serde_json::from_value(value).unwrap_or_default()
294}
295
296pub fn reply_specified_group_chat(open_conversation_id: &str) -> ChatbotMessage {
298 let value = serde_json::json!({
299 "conversationId": open_conversation_id,
300 "conversationType": "2",
301 "msgId": uuid::Uuid::new_v4().to_string(),
302 });
303 serde_json::from_value(value).unwrap_or_default()
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_chatbot_message_text() {
312 let json = serde_json::json!({
313 "msgtype": "text",
314 "text": {"content": "hello world"},
315 "senderNick": "test_user",
316 "conversationType": "1",
317 "senderId": "user_001",
318 "senderStaffId": "staff_001",
319 "msgId": "msg_001"
320 });
321 let msg = ChatbotMessage::from_value(&json).unwrap();
322 assert_eq!(msg.message_type.as_deref(), Some("text"));
323 assert_eq!(
324 msg.text.as_ref().and_then(|t| t.content.as_deref()),
325 Some("hello world")
326 );
327 let texts = msg.get_text_list().unwrap();
328 assert_eq!(texts, vec!["hello world"]);
329 }
330
331 #[test]
332 fn test_chatbot_message_picture() {
333 let json = serde_json::json!({
334 "msgtype": "picture",
335 "content": {"downloadCode": "dc_001"},
336 "senderId": "user_001",
337 "msgId": "msg_002"
338 });
339 let msg = ChatbotMessage::from_value(&json).unwrap();
340 assert_eq!(msg.message_type.as_deref(), Some("picture"));
341 assert_eq!(
342 msg.image_content
343 .as_ref()
344 .and_then(|ic| ic.download_code.as_deref()),
345 Some("dc_001")
346 );
347 let images = msg.get_image_list().unwrap();
348 assert_eq!(images, vec!["dc_001"]);
349 }
350
351 #[test]
352 fn test_chatbot_message_rich_text() {
353 let json = serde_json::json!({
354 "msgtype": "richText",
355 "content": {
356 "richText": [
357 {"text": "line1"},
358 {"downloadCode": "img_001"},
359 {"text": "line2"}
360 ]
361 },
362 "senderId": "user_001",
363 "msgId": "msg_003"
364 });
365 let msg = ChatbotMessage::from_value(&json).unwrap();
366 let texts = msg.get_text_list().unwrap();
367 assert_eq!(texts, vec!["line1", "line2"]);
368 let images = msg.get_image_list().unwrap();
369 assert_eq!(images, vec!["img_001"]);
370 }
371
372 #[test]
373 fn test_reply_specified_single_chat() {
374 let msg = reply_specified_single_chat("user_001", "Test User");
375 assert_eq!(msg.sender_id.as_deref(), Some("user_001"));
376 assert_eq!(msg.sender_staff_id.as_deref(), Some("user_001"));
377 assert_eq!(msg.conversation_type.as_deref(), Some("1"));
378 assert!(msg.message_id.is_some());
379 }
380
381 #[test]
382 fn test_reply_specified_group_chat() {
383 let msg = reply_specified_group_chat("conv_001");
384 assert_eq!(msg.conversation_id.as_deref(), Some("conv_001"));
385 assert_eq!(msg.conversation_type.as_deref(), Some("2"));
386 assert!(msg.message_id.is_some());
387 }
388
389 #[test]
390 fn test_conversation_message_read_by_me() {
391 let msg = ConversationMessage {
392 read_status: "2".to_owned(),
393 sender_user_id: "user_001".to_owned(),
394 send_time: 1_690_000_000,
395 };
396 assert!(msg.read_by_me());
397
398 let msg2 = ConversationMessage {
399 read_status: "1".to_owned(),
400 ..Default::default()
401 };
402 assert!(!msg2.read_by_me());
403 }
404
405 #[test]
406 fn test_at_user_serde() {
407 let json = r#"{"dingtalkId":"dt_001","staffId":"staff_001","extra":"val"}"#;
408 let user: AtUser = serde_json::from_str(json).unwrap();
409 assert_eq!(user.dingtalk_id.as_deref(), Some("dt_001"));
410 assert_eq!(user.staff_id.as_deref(), Some("staff_001"));
411 assert!(user.extensions.contains_key("extra"));
412 }
413
414 #[test]
417 fn test_chatbot_message_audio() {
418 let json = serde_json::json!({
419 "msgtype": "audio",
420 "content": {
421 "duration": 4000,
422 "downloadCode": "dc_audio_001",
423 "recognition": "钉钉,让进步发生"
424 },
425 "senderId": "user_001",
426 "senderStaffId": "staff_001",
427 "conversationType": "1",
428 "msgId": "msg_audio_001"
429 });
430 let msg = ChatbotMessage::from_value(&json).unwrap();
431 assert_eq!(msg.message_type.as_deref(), Some("audio"));
432 let ac = msg.audio_content.as_ref().unwrap();
433 assert_eq!(ac.recognition.as_deref(), Some("钉钉,让进步发生"));
434 assert_eq!(ac.download_code.as_deref(), Some("dc_audio_001"));
435 assert_eq!(ac.duration, Some(4000));
436 let texts = msg.get_text_list().unwrap();
438 assert_eq!(texts, vec!["钉钉,让进步发生"]);
439 }
440
441 #[test]
442 fn test_chatbot_message_audio_no_recognition() {
443 let json = serde_json::json!({
444 "msgtype": "audio",
445 "content": {
446 "duration": 2000,
447 "downloadCode": "dc_audio_002"
448 },
449 "senderId": "user_001",
450 "msgId": "msg_audio_002"
451 });
452 let msg = ChatbotMessage::from_value(&json).unwrap();
453 assert_eq!(msg.message_type.as_deref(), Some("audio"));
454 assert!(msg.audio_content.is_some());
455 assert!(msg.get_text_list().is_none());
457 }
458}