twilight_http/request/channel/message/
update_message.rs

1use crate::{
2    client::Client,
3    error::Error,
4    request::{
5        attachment::{AttachmentManager, PartialAttachment},
6        Nullable, Request, TryIntoRequest,
7    },
8    response::{Response, ResponseFuture},
9    routing::Route,
10};
11use serde::Serialize;
12use std::future::IntoFuture;
13use twilight_model::{
14    channel::message::{AllowedMentions, Component, Embed, Message, MessageFlags},
15    http::attachment::Attachment,
16    id::{
17        marker::{AttachmentMarker, ChannelMarker, MessageMarker},
18        Id,
19    },
20};
21use twilight_validate::message::{
22    attachment as validate_attachment, components as validate_components,
23    content as validate_content, embeds as validate_embeds, MessageValidationError,
24};
25
26#[derive(Serialize)]
27struct UpdateMessageFields<'a> {
28    #[serde(skip_serializing_if = "Option::is_none")]
29    allowed_mentions: Option<Nullable<&'a AllowedMentions>>,
30    /// List of attachments to keep, and new attachments to add.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    attachments: Option<Nullable<Vec<PartialAttachment<'a>>>>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    components: Option<Nullable<&'a [Component]>>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    content: Option<Nullable<&'a str>>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    embeds: Option<Nullable<&'a [Embed]>>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    flags: Option<MessageFlags>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    payload_json: Option<&'a [u8]>,
43}
44
45/// Update a message by [`Id<ChannelMarker>`] and [`Id<MessageMarker>`].
46///
47/// You can pass [`None`] to any of the methods to remove the associated field.
48/// Pass [`None`] to [`content`] to remove the content. You must ensure that the
49/// message still contains at least one of [`attachments`], [`content`],
50/// [`embeds`], or stickers.
51///
52/// # Examples
53///
54/// Replace the content with `"test update"`:
55///
56/// ```no_run
57/// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
58/// use twilight_http::Client;
59/// use twilight_model::id::Id;
60///
61/// let client = Client::new("my token".to_owned());
62/// client
63///     .update_message(Id::new(1), Id::new(2))
64///     .content(Some("test update"))
65///     .await?;
66/// # Ok(()) }
67/// ```
68///
69/// Remove the message's content:
70///
71/// ```no_run
72/// # use twilight_http::Client;
73/// # use twilight_model::id::Id;
74/// #
75/// # #[tokio::main]
76/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
77/// # let client = Client::new("my token".to_owned());
78/// client
79///     .update_message(Id::new(1), Id::new(2))
80///     .content(None)
81///     .await?;
82/// # Ok(()) }
83/// ```
84///
85/// [`attachments`]: Self::attachments
86/// [`content`]: Self::content
87/// [`embeds`]: Self::embeds
88#[must_use = "requests must be configured and executed"]
89pub struct UpdateMessage<'a> {
90    attachment_manager: AttachmentManager<'a>,
91    channel_id: Id<ChannelMarker>,
92    fields: Result<UpdateMessageFields<'a>, MessageValidationError>,
93    http: &'a Client,
94    message_id: Id<MessageMarker>,
95}
96
97impl<'a> UpdateMessage<'a> {
98    pub(crate) const fn new(
99        http: &'a Client,
100        channel_id: Id<ChannelMarker>,
101        message_id: Id<MessageMarker>,
102    ) -> Self {
103        Self {
104            attachment_manager: AttachmentManager::new(),
105            channel_id,
106            fields: Ok(UpdateMessageFields {
107                allowed_mentions: None,
108                attachments: None,
109                components: None,
110                content: None,
111                embeds: None,
112                flags: None,
113                payload_json: None,
114            }),
115            http,
116            message_id,
117        }
118    }
119
120    /// Specify the [`AllowedMentions`] for the message.
121    ///
122    /// If not called, the request will use the client's default allowed
123    /// mentions.
124    pub fn allowed_mentions(mut self, allowed_mentions: Option<&'a AllowedMentions>) -> Self {
125        if let Ok(fields) = self.fields.as_mut() {
126            fields.allowed_mentions = Some(Nullable(allowed_mentions));
127        }
128
129        self
130    }
131
132    /// Attach multiple new files to the message.
133    ///
134    /// This method clears previous calls.
135    ///
136    /// # Errors
137    ///
138    /// Returns an error of type [`AttachmentDescriptionTooLarge`] if
139    /// the attachments's description is too large.
140    ///
141    /// Returns an error of type [`AttachmentFilename`] if any filename is
142    /// invalid.
143    ///
144    /// [`AttachmentDescriptionTooLarge`]: twilight_validate::message::MessageValidationErrorType::AttachmentDescriptionTooLarge
145    /// [`AttachmentFilename`]: twilight_validate::message::MessageValidationErrorType::AttachmentFilename
146    pub fn attachments(mut self, attachments: &'a [Attachment]) -> Self {
147        if self.fields.is_ok() {
148            if let Err(source) = attachments.iter().try_for_each(validate_attachment) {
149                self.fields = Err(source);
150            } else {
151                self.attachment_manager = self
152                    .attachment_manager
153                    .set_files(attachments.iter().collect());
154            }
155        }
156
157        self
158    }
159
160    /// Set the message's list of [`Component`]s.
161    ///
162    /// Calling this method will clear previous calls.
163    ///
164    /// # Editing
165    ///
166    /// Pass [`None`] to clear existing components.
167    ///
168    /// # Errors
169    ///
170    /// Refer to the errors section of
171    /// [`twilight_validate::component::component`] for a list of errors that
172    /// may be returned as a result of validating each provided component.
173    pub fn components(mut self, components: Option<&'a [Component]>) -> Self {
174        self.fields = self.fields.and_then(|mut fields| {
175            if let Some(components) = components {
176                validate_components(components)?;
177            }
178
179            fields.components = Some(Nullable(components));
180
181            Ok(fields)
182        });
183
184        self
185    }
186
187    /// Set the message's content.
188    ///
189    /// The maximum length is 2000 UTF-16 characters.
190    ///
191    /// # Editing
192    ///
193    /// Pass [`None`] to remove the message content. This is impossible if it
194    /// would leave the message empty of `attachments`, `content`, `embeds`, or
195    /// `sticker_ids`.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error of type [`ContentInvalid`] if the content length is too
200    /// long.
201    ///
202    /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
203    pub fn content(mut self, content: Option<&'a str>) -> Self {
204        self.fields = self.fields.and_then(|mut fields| {
205            if let Some(content) = content {
206                validate_content(content)?;
207            }
208
209            fields.content = Some(Nullable(content));
210
211            Ok(fields)
212        });
213
214        self
215    }
216
217    /// Set the message's list of embeds.
218    ///
219    /// Calling this method will clear previous calls.
220    ///
221    /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
222    /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
223    /// characters. Additionally, the internal fields also have character
224    /// limits. Refer to [Discord Docs/Embed Limits] for more information.
225    ///
226    /// # Editing
227    ///
228    /// To keep all embeds, do not call this method. To modify one or more
229    /// embeds in the message, acquire them from the previous message, mutate
230    /// them in place, then pass that list to this method. To remove all embeds,
231    /// pass [`None`]. This is impossible if it would leave the message empty of
232    /// `attachments`, `content`, `embeds`, or `sticker_ids`.
233    ///
234    /// # Errors
235    ///
236    /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
237    ///
238    /// Otherwise, refer to the errors section of
239    /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
240    ///
241    /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
242    /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
243    /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
244    /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
245    pub fn embeds(mut self, embeds: Option<&'a [Embed]>) -> Self {
246        self.fields = self.fields.and_then(|mut fields| {
247            if let Some(embeds) = embeds {
248                validate_embeds(embeds)?;
249            }
250
251            fields.embeds = Some(Nullable(embeds));
252
253            Ok(fields)
254        });
255        self
256    }
257
258    /// Set the message's flags.
259    ///
260    /// The only supported flag is [`SUPPRESS_EMBEDS`].
261    ///
262    /// [`SUPPRESS_EMBEDS`]: MessageFlags::SUPPRESS_EMBEDS
263    pub fn flags(mut self, flags: MessageFlags) -> Self {
264        if let Ok(fields) = self.fields.as_mut() {
265            fields.flags = Some(flags);
266        }
267
268        self
269    }
270
271    /// Specify multiple [`Id<AttachmentMarker>`]s already present in the target
272    /// message to keep.
273    ///
274    /// If called, all unspecified attachments (except ones added with
275    /// [`attachments`]) will be removed from the message. This is impossible if
276    /// it would leave the message empty of `attachments`, `content`, `embeds`,
277    /// or `sticker_ids`. If not called, all attachments will be kept.
278    ///
279    /// [`attachments`]: Self::attachments
280    pub fn keep_attachment_ids(mut self, attachment_ids: &'a [Id<AttachmentMarker>]) -> Self {
281        if let Ok(fields) = self.fields.as_mut() {
282            self.attachment_manager = self.attachment_manager.set_ids(attachment_ids.to_vec());
283
284            // Set an empty list. This will be overwritten in `TryIntoRequest` if
285            // the actual list is not empty.
286            fields.attachments = Some(Nullable(Some(Vec::new())));
287        }
288
289        self
290    }
291
292    /// JSON encoded body of any additional request fields.
293    ///
294    /// If this method is called, all other fields are ignored, except for
295    /// [`attachments`]. See [Discord Docs/Uploading Files].
296    ///
297    /// # Examples
298    ///
299    /// See [`ExecuteWebhook::payload_json`] for examples.
300    ///
301    /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
302    /// [`ExecuteWebhook::payload_json`]: crate::request::channel::webhook::ExecuteWebhook::payload_json
303    /// [`attachments`]: Self::attachments
304    pub fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
305        if let Ok(fields) = self.fields.as_mut() {
306            fields.payload_json = Some(payload_json);
307        }
308
309        self
310    }
311}
312
313impl IntoFuture for UpdateMessage<'_> {
314    type Output = Result<Response<Message>, Error>;
315
316    type IntoFuture = ResponseFuture<Message>;
317
318    fn into_future(self) -> Self::IntoFuture {
319        let http = self.http;
320
321        match self.try_into_request() {
322            Ok(request) => http.request(request),
323            Err(source) => ResponseFuture::error(source),
324        }
325    }
326}
327
328impl TryIntoRequest for UpdateMessage<'_> {
329    fn try_into_request(self) -> Result<Request, Error> {
330        let mut fields = self.fields.map_err(Error::validation)?;
331        let mut request = Request::builder(&Route::UpdateMessage {
332            channel_id: self.channel_id.get(),
333            message_id: self.message_id.get(),
334        });
335
336        // Set the default allowed mentions if required.
337        if fields.allowed_mentions.is_none() {
338            if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
339                fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
340            }
341        }
342
343        // Determine whether we need to use a multipart/form-data body or a JSON
344        // body.
345        if !self.attachment_manager.is_empty() {
346            let form = if let Some(payload_json) = fields.payload_json {
347                self.attachment_manager.build_form(payload_json)
348            } else {
349                fields.attachments = Some(Nullable(Some(
350                    self.attachment_manager.get_partial_attachments(),
351                )));
352
353                let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
354
355                self.attachment_manager.build_form(fields.as_ref())
356            };
357
358            request = request.form(form);
359        } else if let Some(payload_json) = fields.payload_json {
360            request = request.body(payload_json.to_vec());
361        } else {
362            request = request.json(&fields);
363        }
364
365        request.build()
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use std::error::Error;
373
374    #[test]
375    fn clear_attachment() -> Result<(), Box<dyn Error>> {
376        const CHANNEL_ID: Id<ChannelMarker> = Id::new(1);
377        const MESSAGE_ID: Id<MessageMarker> = Id::new(2);
378
379        let client = Client::new("token".into());
380
381        let expected = r#"{"attachments":[]}"#;
382        let actual = UpdateMessage::new(&client, CHANNEL_ID, MESSAGE_ID)
383            .keep_attachment_ids(&[])
384            .try_into_request()?;
385
386        assert_eq!(Some(expected.as_bytes()), actual.body());
387
388        let expected = r"{}";
389        let actual = UpdateMessage::new(&client, CHANNEL_ID, MESSAGE_ID).try_into_request()?;
390
391        assert_eq!(Some(expected.as_bytes()), actual.body());
392
393        Ok(())
394    }
395}