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}