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(flatten)]
86 pub extensions: HashMap<String, serde_json::Value>,
87}
88
89impl ChatbotMessage {
90 pub const TOPIC: &'static str = "/v1.0/im/bot/messages/get";
92 pub const DELEGATE_TOPIC: &'static str = "/v1.0/im/bot/messages/delegate";
94
95 pub fn from_value(value: &serde_json::Value) -> crate::Result<Self> {
97 let mut msg: Self = serde_json::from_value(value.clone())?;
98
99 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 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
170pub struct AtUser {
171 #[serde(rename = "dingtalkId", skip_serializing_if = "Option::is_none")]
173 pub dingtalk_id: Option<String>,
174 #[serde(rename = "staffId", skip_serializing_if = "Option::is_none")]
176 pub staff_id: Option<String>,
177 #[serde(flatten)]
179 pub extensions: HashMap<String, serde_json::Value>,
180}
181
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
184pub struct TextContent {
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub content: Option<String>,
188 #[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
201pub struct ImageContent {
202 #[serde(rename = "downloadCode", skip_serializing_if = "Option::is_none")]
204 pub download_code: Option<String>,
205}
206
207#[derive(Debug, Clone, Default, Serialize, Deserialize)]
209pub struct RichTextContent {
210 #[serde(rename = "richText", default)]
212 pub rich_text_list: Vec<serde_json::Value>,
213}
214
215#[derive(Debug, Clone, Default, Serialize, Deserialize)]
217pub struct HostingContext {
218 #[serde(rename = "userId")]
220 pub user_id: String,
221 pub nick: String,
223}
224
225#[derive(Debug, Clone, Default, Serialize, Deserialize)]
227pub struct ConversationMessage {
228 #[serde(rename = "readStatus", default)]
230 pub read_status: String,
231 #[serde(rename = "senderUserId", default)]
233 pub sender_user_id: String,
234 #[serde(rename = "sendTime", default)]
236 pub send_time: i64,
237}
238
239impl ConversationMessage {
240 pub fn read_by_me(&self) -> bool {
242 self.read_status == "2"
243 }
244}
245
246pub 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
258pub 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}