twilight_http/request/application/interaction/
update_response.rs

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