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}