1use crate::Client;
4use anyhow::{bail, Result};
5use futures::{stream::SplitSink, SinkExt};
6use log::debug;
7use reqwest::{
8 multipart::{Form, Part},
9 Response,
10};
11use serde::{de::DeserializeOwned, Deserialize, Serialize};
12use serde_json::Value;
13use std::{ffi::OsStr, path::Path, sync::Arc};
14use strum::Display;
15use tokio::{fs::File, net::TcpStream};
16use tokio_tungstenite::{tungstenite::Message, MaybeTlsStream, WebSocketStream};
17
18pub(crate) type Sink = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
19impl Client {
20 pub(crate) async fn send<T: Serialize>(&self, msg: T) -> Result<()> {
21 let msg = serde_json::to_string(&msg)?;
22 self.send_message(Message::text(msg)).await
23 }
24
25 pub(crate) async fn ping(&self) -> Result<()> {
26 self.send_message(Message::Ping(Vec::new())).await
27 }
28
29 pub(crate) async fn send_message(&self, msg: Message) -> Result<()> {
30 let mut sink = self.sink.lock().await;
31 let Some(sink) = sink.as_mut() else {
32 bail!("stream not connected");
33 };
34 sink.send(msg).await?;
35
36 Ok(())
37 }
38
39 pub(crate) async fn post_raw<T: Serialize>(
40 &self,
41 url: impl AsRef<str>,
42 data: T,
43 ) -> Result<Response> {
44 let access_token = self.token().await?;
45 debug!("post with access token: {}", access_token);
46 let response = self
47 .client
48 .post(url.as_ref())
49 .header("x-acs-dingtalk-access-token", access_token)
50 .json(&data)
51 .send()
52 .await?;
53
54 if !response.status().is_success() {
55 bail!(
56 "post error: [{}] {:?}",
57 response.status(),
58 response.text().await?
59 );
60 }
61
62 Ok(response)
63 }
64
65 pub(crate) async fn post<T, U>(&self, url: impl AsRef<str>, data: T) -> Result<U>
66 where
67 T: Serialize,
68 U: DeserializeOwned,
69 {
70 let response = self.post_raw(url, data).await?;
71 let status = response.status();
72 let text = response.text().await?;
73 debug!("post ok: [{}] {}", status, text);
74 Ok(serde_json::from_str(&text)?)
75 }
76
77 pub async fn upload(&self, file: impl AsRef<Path>, file_type: UploadType) -> Result<String> {
82 let access_token = self.token().await?;
83 let file = file.as_ref();
84 let filename = file
85 .file_name()
86 .unwrap_or(OsStr::new("<unknown>"))
87 .to_string_lossy()
88 .to_string();
89 let file = File::open(file).await?;
90 let form = Form::new()
91 .part("media", Part::stream(file).file_name(filename))
92 .text("type", file_type.to_string());
93 let response = self
94 .client
95 .post(format!("{}?access_token={}", UPLOAD_URL, access_token))
96 .multipart(form)
97 .send()
98 .await?;
99
100 if !response.status().is_success() {
101 bail!(
102 "upload http error: {} - {}",
103 response.status(),
104 response.text().await?
105 );
106 }
107
108 let res: UploadResult = response.json().await?;
109 if res.errcode != 0 {
110 bail!("upload error: {} - {}", res.errcode, res.errmsg);
111 }
112
113 Ok(res.media_id)
114 }
115}
116
117#[derive(Deserialize)]
118struct UploadResult {
119 errcode: u32,
120 errmsg: String,
121 #[serde(default)]
122 media_id: String,
123 #[allow(dead_code)]
124 #[serde(default)]
125 created_at: u64,
126 #[allow(dead_code)]
127 #[serde(default)]
128 r#type: String,
129}
130
131#[derive(Display)]
133#[strum(serialize_all = "snake_case")]
134pub enum UploadType {
135 Image,
136 Voice,
137 Video,
138 File,
139}
140
141#[derive(Debug, Default, Serialize)]
142#[serde(rename_all = "camelCase")]
143pub(crate) struct ClientUpStream {
144 pub code: u32,
145 pub headers: StreamUpHeader,
146 pub message: String,
147 pub data: String,
148}
149
150impl ClientUpStream {
151 pub fn new(data: impl Into<String>, message_id: impl Into<String>) -> Self {
152 let data = data.into();
153 let message_id = message_id.into();
154
155 Self {
156 code: 200,
157 headers: StreamUpHeader {
158 message_id,
159 content_type: "application/json".to_owned(),
160 },
161 message: "OK".to_owned(),
162 data,
163 }
164 }
165}
166
167#[derive(Debug, Default, Serialize)]
168#[serde(rename_all = "camelCase")]
169pub(crate) struct StreamUpHeader {
170 pub content_type: String, pub message_id: String, }
173
174#[derive(Serialize)]
179#[serde(rename_all = "camelCase")]
180pub struct RobotSendMessage {
181 robot_code: String,
182 #[serde(flatten)]
183 target: SendMessageTarget,
184 msg_key: String,
185 msg_param: String,
186
187 #[serde(skip_serializing)]
188 client: Arc<Client>,
189}
190
191const BATCH_SEND_URL: &str = "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend";
192const GROUP_SEND_URL: &str = "https://api.dingtalk.com/v1.0/robot/groupMessages/send";
193const UPLOAD_URL: &str = "https://oapi.dingtalk.com/media/upload";
194
195impl RobotSendMessage {
196 pub fn group(
198 client: Arc<Client>,
199 conversation_id: impl Into<String>,
200 message: MessageTemplate,
201 ) -> Result<Self> {
202 let client_id = client.config.lock().unwrap().client_id.clone();
203 Ok(Self {
204 robot_code: client_id,
205 target: SendMessageTarget::Group {
206 open_conversation_id: conversation_id.into(),
207 },
208 msg_key: message.to_string(),
209 msg_param: message.try_into()?,
210 client,
211 })
212 }
213
214 pub async fn send(&self) -> Result<()> {
216 debug!("send: {}", serde_json::to_string(self).unwrap());
217 let _: Value = self
218 .client
219 .post(
220 {
221 match self.target {
222 SendMessageTarget::Batch { .. } => BATCH_SEND_URL,
223 SendMessageTarget::Group { .. } => GROUP_SEND_URL,
224 }
225 },
226 self,
227 )
228 .await?;
229
230 Ok(())
231 }
232
233 pub fn batch(
235 client: Arc<Client>,
236 user_ids: Vec<String>,
237 message: MessageTemplate,
238 ) -> Result<Self> {
239 let client_id = client.config.lock().unwrap().client_id.clone();
240 Ok(Self {
241 robot_code: client_id,
242 target: SendMessageTarget::Batch { user_ids },
243 msg_key: message.to_string(),
244 msg_param: message.try_into()?,
245 client,
246 })
247 }
248
249 pub fn single(
251 client: Arc<Client>,
252 user_id: impl Into<String>,
253 message: MessageTemplate,
254 ) -> Result<Self> {
255 Self::batch(client, vec![user_id.into()], message)
256 }
257}
258
259#[derive(Serialize)]
263pub struct EventAckData {
264 pub status: &'static str,
265 #[serde(default)]
266 pub message: String,
267}
268
269impl Default for EventAckData {
270 fn default() -> Self {
271 Self {
272 status: EventAckData::SUCCESS,
273 message: Default::default(),
274 }
275 }
276}
277
278impl EventAckData {
279 pub const SUCCESS: &'static str = "SUCCESS";
280 pub const LATER: &'static str = "LATER";
281}
282
283#[derive(Serialize)]
284#[serde(rename_all = "camelCase", untagged)]
285enum SendMessageTarget {
286 #[serde(rename_all = "camelCase")]
287 Group { open_conversation_id: String },
288 #[serde(rename_all = "camelCase")]
289 Batch { user_ids: Vec<String> },
290}
291
292#[derive(Serialize, strum::Display, Clone)]
296#[serde(rename_all = "camelCase", untagged)]
297#[strum(serialize_all = "camelCase")]
298pub enum MessageTemplate {
299 #[serde(rename_all = "camelCase")]
300 SampleText { content: String },
301 #[serde(rename_all = "camelCase")]
302 SampleMarkdown { title: String, text: String },
303 #[serde(rename_all = "camelCase")]
304 SampleImageMsg {
305 #[serde(rename = "photoURL")]
306 photo_url: String,
307 },
308 #[serde(rename_all = "camelCase")]
309 SampleLink {
310 text: String,
311 title: String,
312 pic_url: String,
313 message_url: String,
314 },
315 #[serde(rename_all = "camelCase")]
316 SampleActionCard {
317 title: String,
318 text: String,
319 single_title: String,
320 #[serde(rename = "singleURL")]
321 single_url: String,
322 },
323 #[serde(rename_all = "camelCase")]
324 SampleActionCard2 {
325 title: String,
326 text: String,
327 action_title_1: String,
328 #[serde(rename = "actionURL1")]
329 action_url_1: String,
330 action_title_2: String,
331 #[serde(rename = "actionURL2")]
332 action_url_2: String,
333 },
334 #[serde(rename_all = "camelCase")]
335 SampleActionCard3 {
336 title: String,
337 text: String,
338 action_title_1: String,
339 #[serde(rename = "actionURL1")]
340 action_url_1: String,
341 action_title_2: String,
342 #[serde(rename = "actionURL2")]
343 action_url_2: String,
344 action_title_3: String,
345 #[serde(rename = "actionURL3")]
346 action_url_3: String,
347 },
348 #[serde(rename_all = "camelCase")]
349 SampleActionCard4 {
350 title: String,
351 text: String,
352 action_title_1: String,
353 #[serde(rename = "actionURL1")]
354 action_url_1: String,
355 action_title_2: String,
356 #[serde(rename = "actionURL2")]
357 action_url_2: String,
358 action_title_3: String,
359 #[serde(rename = "actionURL3")]
360 action_url_3: String,
361 action_title_4: String,
362 #[serde(rename = "actionURL4")]
363 action_url_4: String,
364 },
365 #[serde(rename_all = "camelCase")]
366 SampleActionCard5 {
367 title: String,
368 text: String,
369 action_title_1: String,
370 #[serde(rename = "actionURL1")]
371 action_url_1: String,
372 action_title_2: String,
373 #[serde(rename = "actionURL2")]
374 action_url_2: String,
375 action_title_3: String,
376 #[serde(rename = "actionURL3")]
377 action_url_3: String,
378 action_title_4: String,
379 #[serde(rename = "actionURL4")]
380 action_url_4: String,
381 action_title_5: String,
382 #[serde(rename = "actionURL5")]
383 action_url_5: String,
384 },
385 #[serde(rename_all = "camelCase")]
386 SampleActionCard6 {
387 title: String,
388 text: String,
389 button_title_1: String,
390 button_url_1: String,
391 button_title_2: String,
392 button_url_2: String,
393 },
394 #[serde(rename_all = "camelCase")]
395 SampleAudio { media_id: String, duration: String },
396 #[serde(rename_all = "camelCase")]
397 SampleFile {
398 media_id: String,
399 file_name: String,
400 file_type: String,
401 },
402 #[serde(rename_all = "camelCase")]
403 SampleVideo {
404 duration: String,
405 video_media_id: String,
406 video_type: String,
407 pic_media_id: String,
408 },
409}
410
411impl TryInto<String> for MessageTemplate {
412 type Error = serde_json::Error;
413
414 fn try_into(self) -> std::result::Result<String, Self::Error> {
415 serde_json::to_string(&self)
416 }
417}