serenity/builder/
create_attachment.rs

1use std::path::Path;
2
3use tokio::fs::File;
4use tokio::io::AsyncReadExt;
5#[cfg(feature = "http")]
6use url::Url;
7
8use crate::all::Message;
9#[cfg(feature = "http")]
10use crate::error::Error;
11use crate::error::Result;
12#[cfg(feature = "http")]
13use crate::http::Http;
14use crate::model::id::AttachmentId;
15
16/// A builder for creating a new attachment from a file path, file data, or URL.
17///
18/// [Discord docs](https://discord.com/developers/docs/resources/channel#attachment-object-attachment-structure).
19#[derive(Clone, Debug, Serialize, PartialEq)]
20#[non_exhaustive]
21#[must_use]
22pub struct CreateAttachment {
23    pub(crate) id: u64, // Placeholder ID will be filled in when sending the request
24    pub filename: String,
25    pub description: Option<String>,
26
27    #[serde(skip)]
28    pub data: Vec<u8>,
29}
30
31impl CreateAttachment {
32    /// Builds an [`CreateAttachment`] from the raw attachment data.
33    pub fn bytes(data: impl Into<Vec<u8>>, filename: impl Into<String>) -> CreateAttachment {
34        CreateAttachment {
35            data: data.into(),
36            filename: filename.into(),
37            description: None,
38            id: 0,
39        }
40    }
41
42    /// Builds an [`CreateAttachment`] by reading a local file.
43    ///
44    /// # Errors
45    ///
46    /// [`Error::Io`] if reading the file fails.
47    pub async fn path(path: impl AsRef<Path>) -> Result<CreateAttachment> {
48        let mut file = File::open(path.as_ref()).await?;
49        let mut data = Vec::new();
50        file.read_to_end(&mut data).await?;
51
52        let filename = path.as_ref().file_name().ok_or_else(|| {
53            std::io::Error::new(
54                std::io::ErrorKind::Other,
55                "attachment path must not be a directory",
56            )
57        })?;
58
59        Ok(CreateAttachment::bytes(data, filename.to_string_lossy().to_string()))
60    }
61
62    /// Builds an [`CreateAttachment`] by reading from a file handler.
63    ///
64    /// # Errors
65    ///
66    /// [`Error::Io`] error if reading the file fails.
67    pub async fn file(file: &File, filename: impl Into<String>) -> Result<CreateAttachment> {
68        let mut data = Vec::new();
69        file.try_clone().await?.read_to_end(&mut data).await?;
70
71        Ok(CreateAttachment::bytes(data, filename))
72    }
73
74    /// Builds an [`CreateAttachment`] by downloading attachment data from a URL.
75    ///
76    /// # Errors
77    ///
78    /// [`Error::Url`] if the URL is invalid, [`Error::Http`] if downloading the data fails.
79    #[cfg(feature = "http")]
80    pub async fn url(http: impl AsRef<Http>, url: &str) -> Result<CreateAttachment> {
81        let url = Url::parse(url).map_err(|_| Error::Url(url.to_string()))?;
82
83        let response = http.as_ref().client.get(url.clone()).send().await?;
84        let data = response.bytes().await?.to_vec();
85
86        let filename = url
87            .path_segments()
88            .and_then(Iterator::last)
89            .ok_or_else(|| Error::Url(url.to_string()))?;
90
91        Ok(CreateAttachment::bytes(data, filename))
92    }
93
94    /// Converts the stored data to the base64 representation.
95    ///
96    /// This is used in the library internally because Discord expects image data as base64 in many
97    /// places.
98    #[must_use]
99    pub fn to_base64(&self) -> String {
100        let mut encoded = {
101            use base64::Engine;
102            base64::prelude::BASE64_STANDARD.encode(&self.data)
103        };
104        encoded.insert_str(0, "data:image/png;base64,");
105        encoded
106    }
107
108    /// Sets a description for the file (max 1024 characters).
109    pub fn description(mut self, description: impl Into<String>) -> Self {
110        self.description = Some(description.into());
111        self
112    }
113}
114
115#[derive(Debug, Clone, serde::Serialize, PartialEq)]
116struct ExistingAttachment {
117    id: AttachmentId,
118}
119
120#[derive(Debug, Clone, serde::Serialize, PartialEq)]
121#[serde(untagged)]
122enum NewOrExisting {
123    New(CreateAttachment),
124    Existing(ExistingAttachment),
125}
126
127/// You can add new attachments and edit existing ones using this builder.
128///
129/// When this builder is _not_ supplied in a message edit, Discord keeps the attachments intact.
130/// However, as soon as a builder is supplied, Discord removes all attachments from the message. If
131/// you want to keep old attachments, you must specify this either using [`Self::keep_all`], or
132/// individually for each attachment using [`Self::keep`].
133///
134/// # Examples
135///
136/// ## Removing all attachments
137///
138/// ```rust,no_run
139/// # use serenity::all::*;
140/// # async fn foo_(ctx: Http, mut msg: Message) -> Result<(), Error> {
141/// msg.edit(ctx, EditMessage::new().attachments(EditAttachments::new())).await?;
142/// # Ok(()) }
143/// ```
144///
145/// ## Adding a new attachment without deleting existing attachments
146///
147/// ```rust,no_run
148/// # use serenity::all::*;
149/// # async fn foo_(ctx: Http, mut msg: Message, my_attachment: CreateAttachment) -> Result<(), Error> {
150/// msg.edit(ctx, EditMessage::new().attachments(
151///     EditAttachments::keep_all(&msg).add(my_attachment)
152/// )).await?;
153/// # Ok(()) }
154/// ```
155///
156/// ## Delete all but the first attachment
157///
158/// ```rust,no_run
159/// # use serenity::all::*;
160/// # async fn foo_(ctx: Http, mut msg: Message, my_attachment: CreateAttachment) -> Result<(), Error> {
161/// msg.edit(ctx, EditMessage::new().attachments(
162///     EditAttachments::new().keep(msg.attachments[0].id)
163/// )).await?;
164/// # Ok(()) }
165/// ```
166///
167/// ## Delete only the first attachment
168///
169/// ```rust,no_run
170/// # use serenity::all::*;
171/// # async fn foo_(ctx: Http, mut msg: Message, my_attachment: CreateAttachment) -> Result<(), Error> {
172/// msg.edit(ctx, EditMessage::new().attachments(
173///     EditAttachments::keep_all(&msg).remove(msg.attachments[0].id)
174/// )).await?;
175/// # Ok(()) }
176/// ```
177///
178/// # Notes
179///
180/// Internally, this type is used not just for message editing endpoints, but also for message
181/// creation endpoints.
182#[derive(Default, Debug, Clone, serde::Serialize, PartialEq)]
183#[serde(transparent)]
184#[must_use]
185pub struct EditAttachments {
186    new_and_existing_attachments: Vec<NewOrExisting>,
187}
188
189impl EditAttachments {
190    /// An empty attachments builder.
191    ///
192    /// Existing attachments are not kept by default, either. See [`Self::keep_all()`] or
193    /// [`Self::keep()`].
194    pub fn new() -> Self {
195        Self::default()
196    }
197
198    /// Creates a new attachments builder that keeps all existing attachments.
199    ///
200    /// Shorthand for [`Self::new()`] and calling [`Self::keep()`] for every [`AttachmentId`] in
201    /// [`Message::attachments`].
202    ///
203    /// If you only want to keep a subset of attachments from the message, either implement this
204    /// method manually, or use [`Self::remove()`].
205    ///
206    /// **Note: this EditAttachments must be run on the same message as is supplied here, or else
207    /// Discord will throw an error!**
208    pub fn keep_all(msg: &Message) -> Self {
209        Self {
210            new_and_existing_attachments: msg
211                .attachments
212                .iter()
213                .map(|a| {
214                    NewOrExisting::Existing(ExistingAttachment {
215                        id: a.id,
216                    })
217                })
218                .collect(),
219        }
220    }
221
222    /// This method adds an existing attachment to the list of attachments that are kept after
223    /// editing.
224    ///
225    /// Opposite of [`Self::remove`].
226    pub fn keep(mut self, id: AttachmentId) -> Self {
227        self.new_and_existing_attachments.push(NewOrExisting::Existing(ExistingAttachment {
228            id,
229        }));
230        self
231    }
232
233    /// This method removes an existing attachment from the list of attachments that are kept after
234    /// editing.
235    ///
236    /// Opposite of [`Self::keep`].
237    pub fn remove(mut self, id: AttachmentId) -> Self {
238        #[allow(clippy::match_like_matches_macro)] // `matches!` is less clear here
239        self.new_and_existing_attachments.retain(|a| match a {
240            NewOrExisting::Existing(a) if a.id == id => false,
241            _ => true,
242        });
243        self
244    }
245
246    /// Adds a new attachment to the attachment list.
247    #[allow(clippy::should_implement_trait)] // Clippy thinks add == std::ops::Add::add
248    pub fn add(mut self, attachment: CreateAttachment) -> Self {
249        self.new_and_existing_attachments.push(NewOrExisting::New(attachment));
250        self
251    }
252
253    /// Clones all new attachments into a new Vec, keeping only data and filename, because those
254    /// are needed for the multipart form data. The data is taken out of `self` in the process, so
255    /// this method can only be called once.
256    pub(crate) fn take_files(&mut self) -> Vec<CreateAttachment> {
257        let mut id_placeholder = 0;
258
259        let mut files = Vec::new();
260        for attachment in &mut self.new_and_existing_attachments {
261            if let NewOrExisting::New(attachment) = attachment {
262                let mut cloned_attachment = CreateAttachment::bytes(
263                    std::mem::take(&mut attachment.data),
264                    attachment.filename.clone(),
265                );
266
267                // Assign placeholder IDs so Discord can match metadata to file contents
268                attachment.id = id_placeholder;
269                cloned_attachment.id = id_placeholder;
270                files.push(cloned_attachment);
271
272                id_placeholder += 1;
273            }
274        }
275        files
276    }
277
278    #[cfg(feature = "cache")]
279    pub(crate) fn is_empty(&self) -> bool {
280        self.new_and_existing_attachments.is_empty()
281    }
282}