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(skip)]
97 pub file_content: Option<FileContent>,
98 #[serde(skip)]
104 pub video_content: Option<VideoContent>,
105 #[serde(flatten)]
107 pub extensions: HashMap<String, serde_json::Value>,
108}
109
110impl ChatbotMessage {
111 pub const TOPIC: &'static str = "/v1.0/im/bot/messages/get";
113 pub const DELEGATE_TOPIC: &'static str = "/v1.0/im/bot/messages/delegate";
115
116 pub fn from_value(value: &serde_json::Value) -> crate::Result<Self> {
118 let mut msg: Self = serde_json::from_value(value.clone())?;
119
120 if let Some(msg_type) = &msg.message_type {
122 if let Some(content) = value.get("content") {
123 match msg_type.as_str() {
124 "picture" => {
125 msg.image_content = serde_json::from_value(content.clone()).ok();
126 }
127 "richText" => {
128 msg.rich_text_content = serde_json::from_value(content.clone()).ok();
129 }
130 "audio" => {
132 msg.audio_content = serde_json::from_value(content.clone()).ok();
133 }
134 "file" => {
136 msg.file_content = serde_json::from_value(content.clone()).ok();
137 }
138 "video" => {
140 msg.video_content = serde_json::from_value(content.clone()).ok();
141 }
142 _ => {}
143 }
144 }
145 }
146
147 Ok(msg)
148 }
149
150 pub fn get_text_list(&self) -> Option<Vec<String>> {
152 match self.message_type.as_deref() {
153 Some("text") => self
154 .text
155 .as_ref()
156 .and_then(|t| t.content.clone())
157 .map(|c| vec![c]),
158 Some("richText") => self.rich_text_content.as_ref().map(|rtc| {
159 rtc.rich_text_list
160 .iter()
161 .filter_map(|item| item.get("text").and_then(|v| v.as_str()).map(String::from))
162 .collect()
163 }),
164 Some("audio") => self
166 .audio_content
167 .as_ref()
168 .and_then(|ac| ac.recognition.clone())
169 .map(|r| vec![r]),
170 _ => None,
171 }
172 }
173
174 pub fn get_image_list(&self) -> Option<Vec<String>> {
176 match self.message_type.as_deref() {
177 Some("picture") => self
178 .image_content
179 .as_ref()
180 .and_then(|ic| ic.download_code.clone())
181 .map(|dc| vec![dc]),
182 Some("richText") => self.rich_text_content.as_ref().map(|rtc| {
183 rtc.rich_text_list
184 .iter()
185 .filter_map(|item| {
186 item.get("downloadCode")
187 .and_then(|v| v.as_str())
188 .map(String::from)
189 })
190 .collect()
191 }),
192 _ => None,
193 }
194 }
195
196 pub fn get_all_download_codes(&self) -> Vec<(String, String)> {
206 let mut codes = Vec::new();
207 match self.message_type.as_deref() {
208 Some("picture") => {
209 if let Some(dc) = self
210 .image_content
211 .as_ref()
212 .and_then(|ic| ic.download_code.as_ref())
213 {
214 codes.push(("picture".to_owned(), dc.clone()));
215 }
216 }
217 Some("richText") => {
218 if let Some(rtc) = &self.rich_text_content {
219 for item in &rtc.rich_text_list {
220 if let Some(dc) = item.get("downloadCode").and_then(|v| v.as_str()) {
221 codes.push(("picture".to_owned(), dc.to_owned()));
222 }
223 }
224 }
225 }
226 Some("audio") => {
227 if let Some(dc) = self
228 .audio_content
229 .as_ref()
230 .and_then(|ac| ac.download_code.as_ref())
231 {
232 codes.push(("audio".to_owned(), dc.clone()));
233 }
234 }
235 Some("file") => {
236 if let Some(dc) = self
237 .file_content
238 .as_ref()
239 .and_then(|fc| fc.download_code.as_ref())
240 {
241 codes.push(("file".to_owned(), dc.clone()));
242 }
243 }
244 Some("video") => {
245 if let Some(dc) = self
246 .video_content
247 .as_ref()
248 .and_then(|vc| vc.download_code.as_ref())
249 {
250 codes.push(("video".to_owned(), dc.clone()));
251 }
252 }
253 _ => {}
254 }
255 codes
256 }
257}
258
259impl std::fmt::Display for ChatbotMessage {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 write!(
262 f,
263 "ChatbotMessage(message_type={:?}, text={:?}, sender_nick={:?}, conversation_title={:?})",
264 self.message_type, self.text, self.sender_nick, self.conversation_title
265 )
266 }
267}
268
269#[derive(Debug, Clone, Default, Serialize, Deserialize)]
271pub struct AtUser {
272 #[serde(rename = "dingtalkId", skip_serializing_if = "Option::is_none")]
274 pub dingtalk_id: Option<String>,
275 #[serde(rename = "staffId", skip_serializing_if = "Option::is_none")]
277 pub staff_id: Option<String>,
278 #[serde(flatten)]
280 pub extensions: HashMap<String, serde_json::Value>,
281}
282
283#[derive(Debug, Clone, Default, Serialize, Deserialize)]
285pub struct TextContent {
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub content: Option<String>,
289 #[serde(flatten)]
291 pub extensions: HashMap<String, serde_json::Value>,
292}
293
294impl std::fmt::Display for TextContent {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 write!(f, "TextContent(content={:?})", self.content)
297 }
298}
299
300#[derive(Debug, Clone, Default, Serialize, Deserialize)]
302pub struct ImageContent {
303 #[serde(rename = "downloadCode", skip_serializing_if = "Option::is_none")]
305 pub download_code: Option<String>,
306}
307
308#[derive(Debug, Clone, Default, Serialize, Deserialize)]
317pub struct AudioContent {
318 #[serde(skip_serializing_if = "Option::is_none")]
320 pub recognition: Option<String>,
321 #[serde(rename = "downloadCode", skip_serializing_if = "Option::is_none")]
323 pub download_code: Option<String>,
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub duration: Option<i64>,
327}
328
329#[derive(Debug, Clone, Default, Serialize, Deserialize)]
336pub struct FileContent {
337 #[serde(rename = "downloadCode", skip_serializing_if = "Option::is_none")]
339 pub download_code: Option<String>,
340 #[serde(rename = "fileName", skip_serializing_if = "Option::is_none")]
342 pub file_name: Option<String>,
343}
344
345#[derive(Debug, Clone, Default, Serialize, Deserialize)]
352pub struct VideoContent {
353 #[serde(rename = "downloadCode", skip_serializing_if = "Option::is_none")]
355 pub download_code: Option<String>,
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub duration: Option<i64>,
359 #[serde(rename = "videoType", skip_serializing_if = "Option::is_none")]
361 pub video_type: Option<String>,
362}
363
364#[derive(Debug, Clone, Default, Serialize, Deserialize)]
366pub struct RichTextContent {
367 #[serde(rename = "richText", default)]
369 pub rich_text_list: Vec<serde_json::Value>,
370}
371
372#[derive(Debug, Clone, Default, Serialize, Deserialize)]
374pub struct HostingContext {
375 #[serde(rename = "userId")]
377 pub user_id: String,
378 pub nick: String,
380}
381
382#[derive(Debug, Clone, Default, Serialize, Deserialize)]
384pub struct ConversationMessage {
385 #[serde(rename = "readStatus", default)]
387 pub read_status: String,
388 #[serde(rename = "senderUserId", default)]
390 pub sender_user_id: String,
391 #[serde(rename = "sendTime", default)]
393 pub send_time: i64,
394}
395
396impl ConversationMessage {
397 pub fn read_by_me(&self) -> bool {
399 self.read_status == "2"
400 }
401}
402
403pub fn reply_specified_single_chat(user_id: &str, user_nickname: &str) -> ChatbotMessage {
405 let value = serde_json::json!({
406 "senderId": user_id,
407 "senderStaffId": user_id,
408 "senderNick": user_nickname,
409 "conversationType": "1",
410 "msgId": uuid::Uuid::new_v4().to_string(),
411 });
412 serde_json::from_value(value).unwrap_or_default()
413}
414
415pub fn reply_specified_group_chat(open_conversation_id: &str) -> ChatbotMessage {
417 let value = serde_json::json!({
418 "conversationId": open_conversation_id,
419 "conversationType": "2",
420 "msgId": uuid::Uuid::new_v4().to_string(),
421 });
422 serde_json::from_value(value).unwrap_or_default()
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn test_chatbot_message_text() {
431 let json = serde_json::json!({
432 "msgtype": "text",
433 "text": {"content": "hello world"},
434 "senderNick": "test_user",
435 "conversationType": "1",
436 "senderId": "user_001",
437 "senderStaffId": "staff_001",
438 "msgId": "msg_001"
439 });
440 let msg = ChatbotMessage::from_value(&json).unwrap();
441 assert_eq!(msg.message_type.as_deref(), Some("text"));
442 assert_eq!(
443 msg.text.as_ref().and_then(|t| t.content.as_deref()),
444 Some("hello world")
445 );
446 let texts = msg.get_text_list().unwrap();
447 assert_eq!(texts, vec!["hello world"]);
448 }
449
450 #[test]
451 fn test_chatbot_message_picture() {
452 let json = serde_json::json!({
453 "msgtype": "picture",
454 "content": {"downloadCode": "dc_001"},
455 "senderId": "user_001",
456 "msgId": "msg_002"
457 });
458 let msg = ChatbotMessage::from_value(&json).unwrap();
459 assert_eq!(msg.message_type.as_deref(), Some("picture"));
460 assert_eq!(
461 msg.image_content
462 .as_ref()
463 .and_then(|ic| ic.download_code.as_deref()),
464 Some("dc_001")
465 );
466 let images = msg.get_image_list().unwrap();
467 assert_eq!(images, vec!["dc_001"]);
468 }
469
470 #[test]
471 fn test_chatbot_message_rich_text() {
472 let json = serde_json::json!({
473 "msgtype": "richText",
474 "content": {
475 "richText": [
476 {"text": "line1"},
477 {"downloadCode": "img_001"},
478 {"text": "line2"}
479 ]
480 },
481 "senderId": "user_001",
482 "msgId": "msg_003"
483 });
484 let msg = ChatbotMessage::from_value(&json).unwrap();
485 let texts = msg.get_text_list().unwrap();
486 assert_eq!(texts, vec!["line1", "line2"]);
487 let images = msg.get_image_list().unwrap();
488 assert_eq!(images, vec!["img_001"]);
489 }
490
491 #[test]
492 fn test_reply_specified_single_chat() {
493 let msg = reply_specified_single_chat("user_001", "Test User");
494 assert_eq!(msg.sender_id.as_deref(), Some("user_001"));
495 assert_eq!(msg.sender_staff_id.as_deref(), Some("user_001"));
496 assert_eq!(msg.conversation_type.as_deref(), Some("1"));
497 assert!(msg.message_id.is_some());
498 }
499
500 #[test]
501 fn test_reply_specified_group_chat() {
502 let msg = reply_specified_group_chat("conv_001");
503 assert_eq!(msg.conversation_id.as_deref(), Some("conv_001"));
504 assert_eq!(msg.conversation_type.as_deref(), Some("2"));
505 assert!(msg.message_id.is_some());
506 }
507
508 #[test]
509 fn test_conversation_message_read_by_me() {
510 let msg = ConversationMessage {
511 read_status: "2".to_owned(),
512 sender_user_id: "user_001".to_owned(),
513 send_time: 1_690_000_000,
514 };
515 assert!(msg.read_by_me());
516
517 let msg2 = ConversationMessage {
518 read_status: "1".to_owned(),
519 ..Default::default()
520 };
521 assert!(!msg2.read_by_me());
522 }
523
524 #[test]
525 fn test_at_user_serde() {
526 let json = r#"{"dingtalkId":"dt_001","staffId":"staff_001","extra":"val"}"#;
527 let user: AtUser = serde_json::from_str(json).unwrap();
528 assert_eq!(user.dingtalk_id.as_deref(), Some("dt_001"));
529 assert_eq!(user.staff_id.as_deref(), Some("staff_001"));
530 assert!(user.extensions.contains_key("extra"));
531 }
532
533 #[test]
536 fn test_chatbot_message_audio() {
537 let json = serde_json::json!({
538 "msgtype": "audio",
539 "content": {
540 "duration": 4000,
541 "downloadCode": "dc_audio_001",
542 "recognition": "钉钉,让进步发生"
543 },
544 "senderId": "user_001",
545 "senderStaffId": "staff_001",
546 "conversationType": "1",
547 "msgId": "msg_audio_001"
548 });
549 let msg = ChatbotMessage::from_value(&json).unwrap();
550 assert_eq!(msg.message_type.as_deref(), Some("audio"));
551 let ac = msg.audio_content.as_ref().unwrap();
552 assert_eq!(ac.recognition.as_deref(), Some("钉钉,让进步发生"));
553 assert_eq!(ac.download_code.as_deref(), Some("dc_audio_001"));
554 assert_eq!(ac.duration, Some(4000));
555 let texts = msg.get_text_list().unwrap();
557 assert_eq!(texts, vec!["钉钉,让进步发生"]);
558 }
559
560 #[test]
561 fn test_chatbot_message_audio_no_recognition() {
562 let json = serde_json::json!({
563 "msgtype": "audio",
564 "content": {
565 "duration": 2000,
566 "downloadCode": "dc_audio_002"
567 },
568 "senderId": "user_001",
569 "msgId": "msg_audio_002"
570 });
571 let msg = ChatbotMessage::from_value(&json).unwrap();
572 assert_eq!(msg.message_type.as_deref(), Some("audio"));
573 assert!(msg.audio_content.is_some());
574 assert!(msg.get_text_list().is_none());
576 }
577
578 #[test]
581 fn test_chatbot_message_file() {
582 let json = serde_json::json!({
583 "msgtype": "file",
584 "content": {
585 "downloadCode": "dc_file_001",
586 "fileName": "report.pdf"
587 },
588 "senderId": "user_001",
589 "conversationType": "1",
590 "msgId": "msg_file_001"
591 });
592 let msg = ChatbotMessage::from_value(&json).unwrap();
593 assert_eq!(msg.message_type.as_deref(), Some("file"));
594 let fc = msg.file_content.as_ref().unwrap();
595 assert_eq!(fc.download_code.as_deref(), Some("dc_file_001"));
596 assert_eq!(fc.file_name.as_deref(), Some("report.pdf"));
597 }
598
599 #[test]
600 fn test_chatbot_message_file_partial() {
601 let json = serde_json::json!({
602 "msgtype": "file",
603 "content": { "downloadCode": "dc_file_002" },
604 "senderId": "user_001",
605 "msgId": "msg_file_002"
606 });
607 let msg = ChatbotMessage::from_value(&json).unwrap();
608 let fc = msg.file_content.as_ref().unwrap();
609 assert_eq!(fc.download_code.as_deref(), Some("dc_file_002"));
610 assert!(fc.file_name.is_none());
611 }
612
613 #[test]
616 fn test_chatbot_message_video() {
617 let json = serde_json::json!({
618 "msgtype": "video",
619 "content": {
620 "downloadCode": "dc_video_001",
621 "duration": 15000,
622 "videoType": "mp4"
623 },
624 "senderId": "user_001",
625 "conversationType": "1",
626 "msgId": "msg_video_001"
627 });
628 let msg = ChatbotMessage::from_value(&json).unwrap();
629 assert_eq!(msg.message_type.as_deref(), Some("video"));
630 let vc = msg.video_content.as_ref().unwrap();
631 assert_eq!(vc.download_code.as_deref(), Some("dc_video_001"));
632 assert_eq!(vc.duration, Some(15000));
633 assert_eq!(vc.video_type.as_deref(), Some("mp4"));
634 }
635
636 #[test]
637 fn test_chatbot_message_video_partial() {
638 let json = serde_json::json!({
639 "msgtype": "video",
640 "content": { "downloadCode": "dc_video_002" },
641 "senderId": "user_001",
642 "msgId": "msg_video_002"
643 });
644 let msg = ChatbotMessage::from_value(&json).unwrap();
645 let vc = msg.video_content.as_ref().unwrap();
646 assert_eq!(vc.download_code.as_deref(), Some("dc_video_002"));
647 assert!(vc.duration.is_none());
648 assert!(vc.video_type.is_none());
649 }
650
651 #[test]
654 fn test_get_all_download_codes_file() {
655 let json = serde_json::json!({
656 "msgtype": "file",
657 "content": { "downloadCode": "dc_001", "fileName": "test.pdf" },
658 "senderId": "user_001",
659 "msgId": "msg_001"
660 });
661 let msg = ChatbotMessage::from_value(&json).unwrap();
662 let codes = msg.get_all_download_codes();
663 assert_eq!(codes.len(), 1);
664 assert_eq!(codes[0], ("file".to_owned(), "dc_001".to_owned()));
665 }
666
667 #[test]
668 fn test_get_all_download_codes_picture() {
669 let json = serde_json::json!({
670 "msgtype": "picture",
671 "content": { "downloadCode": "dc_pic_001" },
672 "senderId": "user_001",
673 "msgId": "msg_001"
674 });
675 let msg = ChatbotMessage::from_value(&json).unwrap();
676 let codes = msg.get_all_download_codes();
677 assert_eq!(codes, vec![("picture".to_owned(), "dc_pic_001".to_owned())]);
678 }
679
680 #[test]
681 fn test_get_all_download_codes_video() {
682 let json = serde_json::json!({
683 "msgtype": "video",
684 "content": { "downloadCode": "dc_vid_001", "duration": 5000 },
685 "senderId": "user_001",
686 "msgId": "msg_001"
687 });
688 let msg = ChatbotMessage::from_value(&json).unwrap();
689 let codes = msg.get_all_download_codes();
690 assert_eq!(codes, vec![("video".to_owned(), "dc_vid_001".to_owned())]);
691 }
692
693 #[test]
694 fn test_get_all_download_codes_text_empty() {
695 let json = serde_json::json!({
696 "msgtype": "text",
697 "text": { "content": "hello" },
698 "senderId": "user_001",
699 "msgId": "msg_001"
700 });
701 let msg = ChatbotMessage::from_value(&json).unwrap();
702 assert!(msg.get_all_download_codes().is_empty());
703 }
704
705 #[test]
706 fn test_get_all_download_codes_rich_text() {
707 let json = serde_json::json!({
708 "msgtype": "richText",
709 "content": {
710 "richText": [
711 { "text": "hello" },
712 { "downloadCode": "dc_rt_001" },
713 { "downloadCode": "dc_rt_002" }
714 ]
715 },
716 "senderId": "user_001",
717 "msgId": "msg_001"
718 });
719 let msg = ChatbotMessage::from_value(&json).unwrap();
720 let codes = msg.get_all_download_codes();
721 assert_eq!(codes.len(), 2);
722 assert_eq!(codes[0].0, "picture");
723 assert_eq!(codes[1].0, "picture");
724 }
725}