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, content as validate_content, embeds as validate_embeds,
23    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    /// # Manual Validation
169    ///
170    /// Validation of components is not done automatically here, as we don't know which component
171    /// version is in use, you can validate them manually using the [`twilight_validate::component::component_v1`]
172    /// or [`twilight_validate::component::component_v2`] functions.
173    pub fn components(mut self, components: Option<&'a [Component]>) -> Self {
174        self.fields = self.fields.map(|mut fields| {
175            fields.components = Some(Nullable(components));
176            fields
177        });
178
179        self
180    }
181
182    /// Set the message's content.
183    ///
184    /// The maximum length is 2000 UTF-16 characters.
185    ///
186    /// # Editing
187    ///
188    /// Pass [`None`] to remove the message content. This is impossible if it
189    /// would leave the message empty of `attachments`, `content`, `embeds`, or
190    /// `sticker_ids`.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error of type [`ContentInvalid`] if the content length is too
195    /// long.
196    ///
197    /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
198    pub fn content(mut self, content: Option<&'a str>) -> Self {
199        self.fields = self.fields.and_then(|mut fields| {
200            if let Some(content) = content {
201                validate_content(content)?;
202            }
203
204            fields.content = Some(Nullable(content));
205
206            Ok(fields)
207        });
208
209        self
210    }
211
212    /// Set the message's list of embeds.
213    ///
214    /// Calling this method will clear previous calls.
215    ///
216    /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
217    /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
218    /// characters. Additionally, the internal fields also have character
219    /// limits. Refer to [Discord Docs/Embed Limits] for more information.
220    ///
221    /// # Editing
222    ///
223    /// To keep all embeds, do not call this method. To modify one or more
224    /// embeds in the message, acquire them from the previous message, mutate
225    /// them in place, then pass that list to this method. To remove all embeds,
226    /// pass [`None`]. This is impossible if it would leave the message empty of
227    /// `attachments`, `content`, `embeds`, or `sticker_ids`.
228    ///
229    /// # Errors
230    ///
231    /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
232    ///
233    /// Otherwise, refer to the errors section of
234    /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
235    ///
236    /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
237    /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
238    /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
239    /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
240    pub fn embeds(mut self, embeds: Option<&'a [Embed]>) -> Self {
241        self.fields = self.fields.and_then(|mut fields| {
242            if let Some(embeds) = embeds {
243                validate_embeds(embeds)?;
244            }
245
246            fields.embeds = Some(Nullable(embeds));
247
248            Ok(fields)
249        });
250        self
251    }
252
253    /// Set the message's flags.
254    ///
255    /// The only supported flag is [`SUPPRESS_EMBEDS`].
256    ///
257    /// [`SUPPRESS_EMBEDS`]: MessageFlags::SUPPRESS_EMBEDS
258    pub fn flags(mut self, flags: MessageFlags) -> Self {
259        if let Ok(fields) = self.fields.as_mut() {
260            fields.flags = Some(flags);
261        }
262
263        self
264    }
265
266    /// Specify multiple [`Id<AttachmentMarker>`]s already present in the target
267    /// message to keep.
268    ///
269    /// If called, all unspecified attachments (except ones added with
270    /// [`attachments`]) will be removed from the message. This is impossible if
271    /// it would leave the message empty of `attachments`, `content`, `embeds`,
272    /// or `sticker_ids`. If not called, all attachments will be kept.
273    ///
274    /// [`attachments`]: Self::attachments
275    pub fn keep_attachment_ids(mut self, attachment_ids: &'a [Id<AttachmentMarker>]) -> Self {
276        if let Ok(fields) = self.fields.as_mut() {
277            self.attachment_manager = self.attachment_manager.set_ids(attachment_ids.to_vec());
278
279            // Set an empty list. This will be overwritten in `TryIntoRequest` if
280            // the actual list is not empty.
281            fields.attachments = Some(Nullable(Some(Vec::new())));
282        }
283
284        self
285    }
286
287    /// JSON encoded body of any additional request fields.
288    ///
289    /// If this method is called, all other fields are ignored, except for
290    /// [`attachments`]. See [Discord Docs/Uploading Files].
291    ///
292    /// # Examples
293    ///
294    /// See [`ExecuteWebhook::payload_json`] for examples.
295    ///
296    /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
297    /// [`ExecuteWebhook::payload_json`]: crate::request::channel::webhook::ExecuteWebhook::payload_json
298    /// [`attachments`]: Self::attachments
299    pub fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
300        if let Ok(fields) = self.fields.as_mut() {
301            fields.payload_json = Some(payload_json);
302        }
303
304        self
305    }
306}
307
308impl IntoFuture for UpdateMessage<'_> {
309    type Output = Result<Response<Message>, Error>;
310
311    type IntoFuture = ResponseFuture<Message>;
312
313    fn into_future(self) -> Self::IntoFuture {
314        let http = self.http;
315
316        match self.try_into_request() {
317            Ok(request) => http.request(request),
318            Err(source) => ResponseFuture::error(source),
319        }
320    }
321}
322
323impl TryIntoRequest for UpdateMessage<'_> {
324    fn try_into_request(self) -> Result<Request, Error> {
325        let mut fields = self.fields.map_err(Error::validation)?;
326        let mut request = Request::builder(&Route::UpdateMessage {
327            channel_id: self.channel_id.get(),
328            message_id: self.message_id.get(),
329        });
330
331        // Set the default allowed mentions if required.
332        if fields.allowed_mentions.is_none() {
333            if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
334                fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
335            }
336        }
337
338        // Determine whether we need to use a multipart/form-data body or a JSON
339        // body.
340        if !self.attachment_manager.is_empty() {
341            let form = if let Some(payload_json) = fields.payload_json {
342                self.attachment_manager.build_form(payload_json)
343            } else {
344                fields.attachments = Some(Nullable(Some(
345                    self.attachment_manager.get_partial_attachments(),
346                )));
347
348                let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
349
350                self.attachment_manager.build_form(fields.as_ref())
351            };
352
353            request = request.form(form);
354        } else if let Some(payload_json) = fields.payload_json {
355            request = request.body(payload_json.to_vec());
356        } else {
357            request = request.json(&fields);
358        }
359
360        request.build()
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367    use std::error::Error;
368
369    #[test]
370    fn clear_attachment() -> Result<(), Box<dyn Error>> {
371        const CHANNEL_ID: Id<ChannelMarker> = Id::new(1);
372        const MESSAGE_ID: Id<MessageMarker> = Id::new(2);
373
374        let client = Client::new("token".into());
375
376        let expected = r#"{"attachments":[]}"#;
377        let actual = UpdateMessage::new(&client, CHANNEL_ID, MESSAGE_ID)
378            .keep_attachment_ids(&[])
379            .try_into_request()?;
380
381        assert_eq!(Some(expected.as_bytes()), actual.body());
382
383        let expected = r"{}";
384        let actual = UpdateMessage::new(&client, CHANNEL_ID, MESSAGE_ID).try_into_request()?;
385
386        assert_eq!(Some(expected.as_bytes()), actual.body());
387
388        Ok(())
389    }
390}