Skip to main content

twilight_http/request/channel/message/
update_message.rs

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