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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#[cfg(feature = "model")]
use reqwest::Client as ReqwestClient;
use serde_cow::CowStr;
#[cfg(feature = "model")]
use crate::internal::prelude::*;
use crate::model::prelude::*;
use crate::model::utils::is_false;
fn base64_bytes<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: serde::Deserializer<'de>,
{
use base64::Engine as _;
use serde::de::Error;
let base64 = <Option<CowStr<'de>>>::deserialize(deserializer)?;
let bytes = match base64 {
Some(CowStr(base64)) => {
Some(base64::prelude::BASE64_STANDARD.decode(&*base64).map_err(D::Error::custom)?)
},
None => None,
};
Ok(bytes)
}
/// A file uploaded with a message. Not to be confused with [`Embed`]s.
///
/// [Discord docs](https://discord.com/developers/docs/resources/channel#attachment-object).
///
/// [`Embed`]: super::Embed
#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct Attachment {
/// The unique ID given to this attachment.
pub id: AttachmentId,
/// The filename of the file that was uploaded. This is equivalent to what the uploader had
/// their file named.
pub filename: String,
/// Description for the file (max 1024 characters).
pub description: Option<String>,
/// If the attachment is an image, then the height of the image is provided.
pub height: Option<u32>,
/// The proxy URL.
pub proxy_url: String,
/// The size of the file in bytes.
pub size: u32,
/// The URL of the uploaded attachment.
pub url: String,
/// If the attachment is an image, then the width of the image is provided.
pub width: Option<u32>,
/// The attachment's [media type].
///
/// [media type]: https://en.wikipedia.org/wiki/Media_type
pub content_type: Option<String>,
/// Whether this attachment is ephemeral.
///
/// Ephemeral attachments will automatically be removed after a set period of time.
///
/// Ephemeral attachments on messages are guaranteed to be available as long as the message
/// itself exists.
#[serde(default, skip_serializing_if = "is_false")]
pub ephemeral: bool,
/// The duration of the audio file (present if [`MessageFlags::IS_VOICE_MESSAGE`]).
pub duration_secs: Option<f64>,
/// List of bytes representing a sampled waveform (present if
/// [`MessageFlags::IS_VOICE_MESSAGE`]).
///
/// The waveform is intended to be a preview of the entire voice message, with 1 byte per
/// datapoint. Clients sample the recording at most once per 100 milliseconds, but will
/// downsample so that no more than 256 datapoints are in the waveform.
///
/// The waveform details are a Discord implementation detail and may change without warning or
/// documentation.
#[serde(default, deserialize_with = "base64_bytes")]
pub waveform: Option<Vec<u8>>,
}
#[cfg(feature = "model")]
impl Attachment {
/// If this attachment is an image, then a tuple of the width and height in pixels is returned.
#[must_use]
pub fn dimensions(&self) -> Option<(u32, u32)> {
self.width.and_then(|width| self.height.map(|height| (width, height)))
}
/// Downloads the attachment, returning back a vector of bytes.
///
/// # Examples
///
/// Download all of the attachments associated with a [`Message`]:
///
/// ```rust,no_run
/// use std::io::Write;
/// use std::path::Path;
///
/// use serenity::model::prelude::*;
/// use serenity::prelude::*;
/// use tokio::fs::File;
/// use tokio::io::AsyncWriteExt;
///
/// # struct Handler;
///
/// #[serenity::async_trait]
/// # #[cfg(feature = "client")]
/// impl EventHandler for Handler {
/// async fn message(&self, context: Context, mut message: Message) {
/// for attachment in message.attachments {
/// let content = match attachment.download().await {
/// Ok(content) => content,
/// Err(why) => {
/// println!("Error downloading attachment: {:?}", why);
/// let _ =
/// message.channel_id.say(&context, "Error downloading attachment").await;
///
/// return;
/// },
/// };
///
/// let mut file = match File::create(&attachment.filename).await {
/// Ok(file) => file,
/// Err(why) => {
/// println!("Error creating file: {:?}", why);
/// let _ = message.channel_id.say(&context, "Error creating file").await;
///
/// return;
/// },
/// };
///
/// if let Err(why) = file.write_all(&content).await {
/// println!("Error writing to file: {:?}", why);
///
/// return;
/// }
///
/// let _ = message
/// .channel_id
/// .say(&context, format!("Saved {:?}", attachment.filename))
/// .await;
/// }
/// }
/// }
/// ```
///
/// # Errors
///
/// Returns an [`Error::Io`] when there is a problem reading the contents of the HTTP response.
///
/// Returns an [`Error::Http`] when there is a problem retrieving the attachment.
///
/// [`Message`]: super::Message
pub async fn download(&self) -> Result<Vec<u8>> {
let reqwest = ReqwestClient::new();
let bytes = reqwest.get(&self.url).send().await?.bytes().await?;
Ok(bytes.to_vec())
}
}