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