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