dingtalk_stream/frames/down_message/
callback_message.rs1use crate::frames::{DingTalkGroupConversationId, DingTalkPrivateConversationId, DingTalkUserId};
2use anyhow::anyhow;
3use chrono::{TimeZone, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fmt::{Display, Formatter};
7use std::ops::Deref;
8use std::str::FromStr;
9use std::time::Duration;
10use crate::frames::down_message::{DownStreamMessage, MessageHeaders};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CallbackMessage {
15 #[serde(rename = "specVersion")]
16 pub spec_version: Option<String>,
17 #[serde(rename = "type")]
18 pub headers: MessageHeaders,
19 pub data: Option<MessageData>,
20 #[serde(flatten)]
21 pub extensions: HashMap<String, serde_json::Value>,
22}
23
24impl TryFrom<DownStreamMessage> for CallbackMessage {
25 type Error = anyhow::Error;
26
27 fn try_from(
28 DownStreamMessage {
29 spec_version,
30 headers,
31 r#type,
32 data,
33 extensions,
34 }: DownStreamMessage,
35 ) -> crate::Result<Self> {
36 if let super::MessageType::Callback = r#type {
37 Ok(Self {
38 spec_version,
39 headers,
40 data: if let Some(data) = data {
41 serde_json::from_str(&data)?
42 } else {
43 None
44 },
45 extensions,
46 })
47 } else {
48 Err(anyhow!("expected callback message"))
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct MessageData {
55 #[serde(rename = "msgId")]
56 pub msg_id: String,
57 #[serde(flatten)]
58 pub conversation: Conversation,
59 #[serde(flatten)]
60 pub sender: MessageSender,
61 #[serde(flatten)]
62 pub session_webhook: Option<SessionWebhook>,
63 #[serde(flatten)]
64 pub chatbot: Chatbot,
65 #[serde(rename = "isAdmin")]
66 pub is_admin: Option<bool>,
67 #[serde(rename = "openThreadId")]
68 pub open_thread_id: Option<String>,
69 #[serde(rename = "senderPlatform")]
70 pub sender_platform: Option<String>,
71 #[serde(flatten)]
72 pub payload: Option<MessagePayload>,
73 #[serde(rename = "atUsers")]
74 pub at_users: Option<Vec<AtUser>>,
75 #[serde(rename = "isInAtList")]
76 pub is_in_at_list: Option<bool>,
77 #[serde(rename = "createAt")]
78 pub create_at: i64,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(tag = "conversationType")]
83pub enum Conversation {
84 #[serde(rename = "1")]
85 Private {
86 #[serde(rename = "conversationId")]
87 id: DingTalkPrivateConversationId,
88 },
89 #[serde(rename = "2")]
90 Group {
91 #[serde(rename = "conversationId")]
92 id: DingTalkGroupConversationId,
93 #[serde(rename = "conversationTitle")]
94 title: Option<String>,
95 },
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct MessageSender {
100 #[serde(rename = "senderId")]
101 pub sender_id: String,
102 #[serde(rename = "senderNick")]
103 pub sender_nick: String,
104 #[serde(rename = "senderCorpId")]
105 pub sender_corp_id: Option<String>,
106 #[serde(rename = "senderStaffId")]
107 pub sender_staff_id: Option<DingTalkUserId>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct SessionWebhook {
112 #[serde(rename = "sessionWebhook")]
113 url: String,
114 #[serde(rename = "sessionWebhookExpiredTime")]
115 expired_time: i64,
116}
117
118impl SessionWebhook {
119 pub fn webhook_url(&self) -> crate::Result<url::Url> {
120 Ok(url::Url::from_str(&self.url)?)
121 }
122
123 pub fn timeout(&self) -> Option<Duration> {
124 if let chrono::LocalResult::Single(expired_time) =
125 Utc.timestamp_millis_opt(self.expired_time)
126 {
127 let now = Utc::now();
128 if expired_time > now {
129 if let Ok(duration) = (expired_time - now).to_std() {
130 return Some(duration);
131 }
132 }
133 }
134 None
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct Chatbot {
140 #[serde(rename = "chatbotCorpId")]
141 pub chatbot_corp_id: Option<String>,
142 #[serde(rename = "chatbotUserId")]
143 pub chatbot_user_id: String,
144 #[serde(rename = "robotCode")]
145 pub robot_code: String,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct AtUser {
150 #[serde(rename = "dingtalkId")]
151 pub dingtalk_id: Option<String>,
152 #[serde(rename = "staffId")]
153 pub staff_id: Option<String>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(tag = "msgtype")]
158pub enum MessagePayload {
159 #[serde(rename = "text")]
160 Text { text: PayloadText },
161 #[serde(rename = "picture")]
162 Picture { content: PayloadPicture },
163 #[serde(rename = "file")]
164 File { content: PayloadFile },
165 #[serde(rename = "richText")]
166 RichText { content: PayloadRichText },
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct PayloadText {
171 #[serde(rename = "content", alias = "text")]
172 pub content: String,
173}
174
175impl Display for PayloadText {
176 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
177 f.write_str(&self.content)
178 }
179}
180
181impl Deref for PayloadText {
182 type Target = str;
183
184 fn deref(&self) -> &Self::Target {
185 &self.content
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct PayloadPicture {
191 #[serde(rename = "downloadCode")]
192 pub download_code: String,
193 #[serde(rename = "pictureDownloadCode")]
194 pub picture_download_code: String,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct PayloadFile {
199 #[serde(rename = "downloadCode")]
200 pub download_code: String,
201 #[serde(rename = "fileId")]
202 pub file_id: String,
203 #[serde(rename = "fileName")]
204 pub file_name: String,
205 #[serde(rename = "spaceId")]
206 pub space_id: String,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct PayloadRichText {
211 #[serde(rename = "richText")]
212 pub content: Vec<RichTextItem>,
213}
214
215impl Deref for PayloadRichText {
216 type Target = [RichTextItem];
217
218 fn deref(&self) -> &Self::Target {
219 &self.content
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(untagged)]
225pub enum RichTextItem {
226 #[serde(rename = "picture")]
227 Picture(PayloadPicture),
228 #[serde(rename = "text", alias = "content")]
229 Text(PayloadText),
230}
231
232#[cfg(test)]
233mod tests {
234 use super::{
235 MessageData, MessagePayload, PayloadFile, PayloadPicture, PayloadRichText, PayloadText,
236 RichTextItem,
237 };
238
239 #[test]
240 fn test_text_parse() {
241 let data: MessageData = serde_json::from_str(TEXT_JSON).unwrap();
242 assert_eq!(data.msg_id.as_str(), "msgBjXREkdlZkfTfrIiQomjAw==");
243 if let Some(MessagePayload::Text {
244 text: PayloadText { content },
245 }) = data.payload
246 {
247 assert_eq!(content, "hello");
248 } else {
249 panic!("Expected text payload but got {:?}", data.payload);
250 }
251 }
252 #[test]
253 fn test_picture_parse() {
254 let data: MessageData = serde_json::from_str(PICTURE_JSON).unwrap();
255 assert_eq!(data.msg_id.as_str(), "msgmJpewjjmDF5LPJdRs9n/ZA==");
256 if let Some(MessagePayload::Picture {
257 content: PayloadPicture { download_code, .. },
258 }) = data.payload
259 {
260 assert!(download_code.starts_with("mIofN681YE3f/+m+NntqpSkhBVXbzJynU"));
261 } else {
262 panic!("Expected picture payload but got {:?}", data.payload);
263 }
264 }
265
266 #[test]
267 fn test_file_parse() {
268 let data: MessageData = serde_json::from_str(FILE_JSON).unwrap();
269 assert_eq!(data.msg_id.as_str(), "msgBCO626EXCHXfZoDioTCPxg==");
270 if let Some(MessagePayload::File {
271 content: PayloadFile { file_id, .. },
272 }) = data.payload
273 {
274 assert!(file_id.eq_ignore_ascii_case("214980176385"));
275 } else {
276 panic!("Expected picture payload but got {:?}", data.payload);
277 }
278 }
279
280 #[test]
281 fn test_rich_text_parse() {
282 let data: MessageData = serde_json::from_str(RICH_TEXT_JSON).unwrap();
283 assert_eq!(data.msg_id.as_str(), "msgGDkZWYZlvw7rFtTHcDIFWw==");
284 if let Some(MessagePayload::RichText {
285 content: PayloadRichText { content: rich_text },
286 ..
287 }) = &data.payload
288 {
289 assert!(rich_text.len() > 0);
290 if let RichTextItem::Picture(PayloadPicture { download_code, .. }) =
291 rich_text.get(0).unwrap()
292 {
293 assert!(download_code
294 .starts_with("mIofN681YE3f/+m+NntqpeLZQiMFIZMEPWAhjFjD1g5L/SdG/3lCmLWzq"));
295 } else {
296 panic!("Expected picture payload but got {:?}", data.payload);
297 }
298 if let RichTextItem::Text(PayloadText { content }) = rich_text.get(2).unwrap() {
299 assert!(content.eq("abc"));
300 } else {
301 panic!("Expected text payload but got {:?}", data.payload);
302 }
303 } else {
304 panic!("Expected picture payload but got {:?}", data.payload);
305 }
306 }
307
308 const TEXT_JSON: &str = include_str!("../../../test_resources/cb_msg_text.json");
309 const PICTURE_JSON: &str = include_str!("../../../test_resources/cb_msg_picture.json");
310 const FILE_JSON: &str = include_str!("../../../test_resources/cb_msg_file.json");
311 const RICH_TEXT_JSON: &str = include_str!("../../../test_resources/cb_msg_rich_text.json");
312}