Skip to main content

dingtalk_stream/client/stream_/
upload_resources.rs

1use crate::DingTalkStream;
2use anyhow::anyhow;
3use async_trait::async_trait;
4use reqwest::header::{HeaderMap, HeaderValue};
5use reqwest::multipart::Part;
6use serde::{Deserialize, Serialize};
7use std::fmt::{Display, Formatter};
8use std::ops::Deref;
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11use url::Url;
12
13#[async_trait]
14pub trait DingTalkMedia {
15    async fn upload(&self, dingtalk: &DingTalkStream) -> crate::Result<MediaUploadResult>;
16}
17
18#[async_trait]
19impl<M> DingTalkMedia for M
20where
21    M: TryInto<DingTalkMedia_> + Clone + Send + Sync,
22{
23    async fn upload(&self, dingtalk: &DingTalkStream) -> crate::Result<MediaUploadResult> {
24        let media = self
25            .clone()
26            .try_into()
27            .map_err(|_| anyhow!("Failed to convert to DingTalkMedia_"))?;
28        Ok(media.upload_(dingtalk).await?)
29    }
30}
31#[derive(Debug, Clone)]
32pub enum DingTalkMedia_ {
33    Image(MediaImage),
34    Voice(MediaVoice),
35    File(MediaFile),
36    Video(MediaVideo),
37}
38
39impl Deref for DingTalkMedia_ {
40    type Target = MediaContent;
41
42    fn deref(&self) -> &Self::Target {
43        match self {
44            DingTalkMedia_::Image(content) => content,
45            DingTalkMedia_::Voice(content) => content,
46            DingTalkMedia_::File(content) => content,
47            DingTalkMedia_::Video(content) => content,
48        }
49    }
50}
51
52impl DingTalkMedia_ {
53    pub fn type_(&self) -> MediaType {
54        match self {
55            DingTalkMedia_::Image(_) => MediaType::Image,
56            DingTalkMedia_::Voice(_) => MediaType::Voice,
57            DingTalkMedia_::File(_) => MediaType::File,
58            DingTalkMedia_::Video(_) => MediaType::Video,
59        }
60    }
61
62    async fn as_bytes(&self) -> crate::Result<Vec<u8>> {
63        match self {
64            DingTalkMedia_::Image(content) => content.as_bytes().await,
65            DingTalkMedia_::Voice(content) => content.as_bytes().await,
66            DingTalkMedia_::File(content) => content.as_bytes().await,
67            DingTalkMedia_::Video(content) => content.as_bytes().await,
68        }
69    }
70}
71
72impl DingTalkMedia_ {
73    async fn upload_(&self, dingtalk: &DingTalkStream) -> crate::Result<MediaUploadResult> {
74        let access_token = dingtalk.get_access_token().await?;
75        let bytes = self.as_bytes().await?;
76
77        let filename = self.filename()?;
78        let form = reqwest::multipart::Form::new()
79            .text("type", self.type_().to_string())
80            .part(
81                "media",
82                Part::bytes(bytes).file_name(filename).headers({
83                    let mut headers = HeaderMap::new();
84                    headers.insert(
85                        "Content-Type",
86                        HeaderValue::from_static("application/octet-stream"),
87                    );
88                    headers
89                }),
90            );
91        let result = dingtalk
92            .http_client
93            .post(crate::MEDIA_UPLOAD_URL)
94            .query(&[("access_token", access_token.deref())])
95            .multipart(form)
96            .send()
97            .await?
98            .json::<MediaUploadResult>()
99            .await?;
100        Ok(result)
101    }
102}
103
104#[derive(Debug, Clone)]
105pub struct MediaImage(MediaContent);
106
107impl<C: Into<MediaContent>> From<C> for MediaImage {
108    fn from(content: C) -> Self {
109        MediaImage(content.into())
110    }
111}
112
113impl Deref for MediaImage {
114    type Target = MediaContent;
115
116    fn deref(&self) -> &Self::Target {
117        &self.0
118    }
119}
120
121impl From<MediaImage> for DingTalkMedia_ {
122    fn from(value: MediaImage) -> Self {
123        Self::Image(value)
124    }
125}
126
127#[derive(Debug, Clone)]
128pub struct MediaVoice(MediaContent);
129impl<C: Into<MediaContent>> From<C> for MediaVoice {
130    fn from(content: C) -> Self {
131        MediaVoice(content.into())
132    }
133}
134
135impl Deref for MediaVoice {
136    type Target = MediaContent;
137
138    fn deref(&self) -> &Self::Target {
139        &self.0
140    }
141}
142
143impl From<MediaVoice> for DingTalkMedia_ {
144    fn from(value: MediaVoice) -> Self {
145        Self::Voice(value)
146    }
147}
148
149#[derive(Debug, Clone)]
150pub struct MediaFile(MediaContent);
151
152impl<C: Into<MediaContent>> From<C> for MediaFile {
153    fn from(content: C) -> Self {
154        MediaFile(content.into())
155    }
156}
157
158impl Deref for MediaFile {
159    type Target = MediaContent;
160
161    fn deref(&self) -> &Self::Target {
162        &self.0
163    }
164}
165
166impl From<MediaFile> for DingTalkMedia_ {
167    fn from(value: MediaFile) -> Self {
168        Self::File(value)
169    }
170}
171
172#[derive(Debug, Clone)]
173pub struct MediaVideo(MediaContent);
174impl<C: Into<MediaContent>> From<C> for MediaVideo {
175    fn from(content: C) -> Self {
176        MediaVideo(content.into())
177    }
178}
179
180impl Deref for MediaVideo {
181    type Target = MediaContent;
182
183    fn deref(&self) -> &Self::Target {
184        &self.0
185    }
186}
187
188impl From<MediaVideo> for DingTalkMedia_ {
189    fn from(value: MediaVideo) -> Self {
190        Self::Video(value)
191    }
192}
193
194#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
195pub enum MediaType {
196    #[serde(rename = "image")]
197    Image,
198    #[serde(rename = "voice")]
199    Voice,
200    #[serde(rename = "file")]
201    File,
202    #[serde(rename = "video")]
203    Video,
204}
205
206impl FromStr for MediaType {
207    type Err = anyhow::Error;
208
209    fn from_str(s: &str) -> Result<Self, Self::Err> {
210        let s = s.to_lowercase();
211        match s.as_str() {
212            "image" | "img" => Ok(MediaType::Image),
213            "voice" => Ok(MediaType::Voice),
214            "file" => Ok(MediaType::File),
215            "video" => Ok(MediaType::Video),
216            _ => Err(anyhow::anyhow!("Invalid media type: {}", s)),
217        }
218    }
219}
220
221impl Deref for MediaType {
222    type Target = str;
223
224    fn deref(&self) -> &Self::Target {
225        match self {
226            MediaType::Image => "image",
227            MediaType::Voice => "voice",
228            MediaType::File => "file",
229            MediaType::Video => "video",
230        }
231    }
232}
233
234impl Display for MediaType {
235    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
236        write!(f, "{}", self.deref())
237    }
238}
239
240#[derive(Debug, Clone)]
241pub enum MediaContent {
242    Bytes { filename: String, bytes: Vec<u8> },
243    Filepath(PathBuf),
244    Url { filename: String, url: Url },
245}
246
247impl MediaContent {
248    fn filename(&self) -> crate::Result<String> {
249        match self {
250            MediaContent::Bytes { filename, .. } => Ok(filename.to_string()),
251            MediaContent::Filepath(filepath) => filepath
252                .file_name()
253                .map(|it| it.to_string_lossy().to_string())
254                .ok_or(anyhow!("parse filename failed")),
255            MediaContent::Url { filename, .. } => Ok(filename.to_string()),
256        }
257    }
258}
259
260impl MediaContent {
261    async fn as_bytes(&self) -> crate::Result<Vec<u8>> {
262        match self {
263            MediaContent::Bytes { bytes, .. } => Ok(bytes.clone()),
264            MediaContent::Filepath(path) => Ok(tokio::fs::read(path).await?),
265            MediaContent::Url { url, .. } => {
266                let response = reqwest::get(url.as_str()).await?;
267                Ok(response.bytes().await.map(|bytes| bytes.to_vec())?)
268            }
269        }
270    }
271}
272
273impl<Filename: AsRef<str>> From<(Filename, Vec<u8>)> for MediaContent {
274    fn from((filename, bytes): (Filename, Vec<u8>)) -> Self {
275        MediaContent::Bytes {
276            filename: filename.as_ref().to_string(),
277            bytes,
278        }
279    }
280}
281
282impl From<PathBuf> for MediaContent {
283    fn from(value: PathBuf) -> Self {
284        MediaContent::Filepath(value.into())
285    }
286}
287
288impl From<&Path> for MediaContent {
289    fn from(value: &Path) -> Self {
290        value.to_path_buf().into()
291    }
292}
293
294impl<Filename: AsRef<str>> From<(Filename, Url)> for MediaContent {
295    fn from((filename, value): (Filename, Url)) -> Self {
296        MediaContent::Url {
297            filename: filename.as_ref().to_string(),
298            url: value,
299        }
300    }
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304#[serde(default)]
305pub struct MediaUploadResult {
306    pub errcode: i32,
307    pub errmsg: String,
308    #[serde(flatten)]
309    pub media: Option<Media>,
310}
311
312impl Default for MediaUploadResult {
313    fn default() -> Self {
314        Self {
315            errcode: -1,
316            errmsg: "unknown".to_string(),
317            media: None,
318        }
319    }
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct Media {
324    pub media_id: String,
325    #[serde(rename = "type")]
326    pub r#type: MediaType,
327    pub created_at: u64,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct MediaId(String);
332
333impl<T, C> TryFrom<(T, C)> for DingTalkMedia_
334where
335    T: TryInto<MediaType>,
336    C: Into<MediaContent>,
337{
338    type Error = anyhow::Error;
339
340    fn try_from((type_, content): (T, C)) -> Result<Self, Self::Error> {
341        let type_ = type_
342            .try_into()
343            .map_err(|_| anyhow!("unexpected media-type"))?;
344        let content = content.into();
345        match type_ {
346            MediaType::Image => Ok(Self::Image(content.into())),
347            MediaType::Voice => Ok(Self::Voice(content.into())),
348            MediaType::File => Ok(Self::File(content.into())),
349            MediaType::Video => Ok(Self::Video(content.into())),
350        }
351    }
352}