1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
use std::borrow::Cow;
use reqwest::multipart::{Form, Part};
use reqwest::Client;
use super::AttachmentType;
use crate::internal::prelude::*;
use crate::json;
/// Holder for multipart body. Contains files, multipart fields, and
/// payload_json for creating requests with attachments.
#[derive(Clone, Debug)]
pub struct Multipart<'a> {
pub files: Vec<AttachmentType<'a>>,
/// Multipart text fields that are sent with the form data as individual
/// fields. If a certain endpoint does not support passing JSON body via
/// `payload_json`, this must be used instead.
pub fields: Vec<(Cow<'static, str>, Cow<'static, str>)>,
/// JSON body that will be stringified and set as the form value as
/// `payload_json`.
pub payload_json: Option<Value>,
}
impl<'a> Multipart<'a> {
pub(crate) async fn build_form(&mut self, client: &Client) -> Result<Form> {
let mut multipart = Form::new();
for (file_num, file) in self.files.iter_mut().enumerate() {
// For endpoints that require a single file (e.g. create sticker),
// it will error if the part name is not `file`.
// https://github.com/discord/discord-api-docs/issues/2064#issuecomment-691650970
let part_name =
if file_num == 0 { "file".to_string() } else { format!("file{}", file_num) };
let data = file.data(client).await?;
let filename = file.filename()?;
// Modify current AttachmentType to Bytes variant to prevent the
// need for another disk read or network request when retrying
if let AttachmentType::Path(_) | AttachmentType::Image(_) = file {
*file = AttachmentType::Bytes {
data: data.clone().into(),
filename: filename.clone().unwrap_or_default(),
};
}
let mut part = Part::bytes(data);
if let Some(filename) = filename {
part = guess_mime_str(part, &filename)?;
part = part.file_name(filename);
}
multipart = multipart.part(part_name, part);
}
for (name, value) in &self.fields {
multipart = multipart.text(name.clone(), value.clone());
}
if let Some(ref payload_json) = self.payload_json {
multipart = multipart.text("payload_json", json::to_string(payload_json)?);
}
Ok(multipart)
}
}
fn guess_mime_str(part: Part, filename: &str) -> Result<Part> {
// This is required for certain endpoints like create sticker, otherwise
// the Discord API will respond with a 500 Internal Server Error.
// The mime type chosen is the same as what reqwest does internally when
// using Part::file(), but it is not done for any of the other methods we
// use.
// https://datatracker.ietf.org/doc/html/rfc7578#section-4.4
let mime_type = mime_guess::from_path(filename).first_or_octet_stream();
part.mime_str(mime_type.essence_str()).map_err(Into::into)
}