Skip to main content

dingtalk_stream/frames/down_message/
callback_message.rs

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