dingtalk_stream/client/stream_/
upload_resources.rs1use 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}