Skip to main content

twilight_http/request/application/interaction/
create_followup.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::{Id, marker::ApplicationMarker},
18    poll::Poll,
19};
20use twilight_validate::message::{
21    MessageValidationError, attachment as validate_attachment, components as validate_components,
22    content as validate_content, embeds as validate_embeds,
23};
24
25#[derive(Serialize)]
26struct CreateFollowupFields<'a> {
27    #[serde(skip_serializing_if = "Option::is_none")]
28    allowed_mentions: Option<Nullable<&'a AllowedMentions>>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    attachments: Option<Vec<PartialAttachment<'a>>>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    components: Option<&'a [Component]>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    content: Option<&'a str>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    embeds: Option<&'a [Embed]>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    payload_json: Option<&'a [u8]>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    tts: Option<bool>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    flags: Option<MessageFlags>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    poll: Option<Poll>,
45}
46
47/// Create a followup message to an interaction, by its token.
48///
49/// The message must include at least one of [`attachments`], [`components`],
50/// [`content`], or [`embeds`].
51///
52/// This endpoint is not bound to the application's global rate limit.
53///
54/// # Examples
55///
56/// ```no_run
57/// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
58/// use std::env;
59/// use twilight_http::Client;
60/// use twilight_model::id::Id;
61///
62/// let client = Client::new(env::var("DISCORD_TOKEN")?);
63/// let application_id = Id::new(1);
64///
65/// client
66///     .interaction(application_id)
67///     .create_followup("webhook token")
68///     .content("Pinkie...")
69///     .await?;
70/// # Ok(()) }
71/// ```
72///
73/// [`attachments`]: Self::attachments
74/// [`components`]: Self::components
75/// [`content`]: Self::content
76/// [`embeds`]: Self::embeds
77#[must_use = "requests must be configured and executed"]
78pub struct CreateFollowup<'a> {
79    application_id: Id<ApplicationMarker>,
80    attachment_manager: AttachmentManager<'a>,
81    fields: Result<CreateFollowupFields<'a>, MessageValidationError>,
82    http: &'a Client,
83    token: &'a str,
84}
85
86impl<'a> CreateFollowup<'a> {
87    pub(crate) const fn new(
88        http: &'a Client,
89        application_id: Id<ApplicationMarker>,
90        token: &'a str,
91    ) -> Self {
92        Self {
93            application_id,
94            attachment_manager: AttachmentManager::new(),
95            fields: Ok(CreateFollowupFields {
96                allowed_mentions: None,
97                attachments: None,
98                components: None,
99                content: None,
100                embeds: None,
101                payload_json: None,
102                tts: None,
103                flags: None,
104                poll: None,
105            }),
106            http,
107            token,
108        }
109    }
110
111    /// Specify the [`AllowedMentions`] for the message.
112    ///
113    /// Unless otherwise called, the request will use the client's default
114    /// allowed mentions. Set to `None` to ignore this default.
115    pub const fn allowed_mentions(mut self, allowed_mentions: Option<&'a AllowedMentions>) -> Self {
116        if let Ok(fields) = self.fields.as_mut() {
117            fields.allowed_mentions = Some(Nullable(allowed_mentions));
118        }
119
120        self
121    }
122
123    /// Attach multiple files to the message.
124    ///
125    /// Calling this method will clear any previous calls.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error of type [`AttachmentDescriptionTooLarge`] if
130    /// the attachments's description is too large.
131    ///
132    /// Returns an error of type [`AttachmentFilename`] if any filename is
133    /// invalid.
134    ///
135    /// [`AttachmentDescriptionTooLarge`]: twilight_validate::message::MessageValidationErrorType::AttachmentDescriptionTooLarge
136    /// [`AttachmentFilename`]: twilight_validate::message::MessageValidationErrorType::AttachmentFilename
137    pub fn attachments(mut self, attachments: &'a [Attachment]) -> Self {
138        if self.fields.is_ok() {
139            if let Err(source) = attachments.iter().try_for_each(validate_attachment) {
140                self.fields = Err(source);
141            } else {
142                self.attachment_manager = self
143                    .attachment_manager
144                    .set_files(attachments.iter().collect());
145            }
146        }
147
148        self
149    }
150
151    /// Add multiple [`Component`]s to a message.
152    ///
153    /// Calling this method multiple times will clear previous calls.
154    ///
155    /// # Errors
156    ///
157    /// Refer to the errors section of
158    /// [`twilight_validate::component::component`] for a list of errors that
159    /// may be returned as a result of validating each provided component.
160    pub fn components(mut self, components: &'a [Component]) -> Self {
161        self.fields = self.fields.and_then(|mut fields| {
162            validate_components(
163                components,
164                fields
165                    .flags
166                    .is_some_and(|flags| flags.contains(MessageFlags::IS_COMPONENTS_V2)),
167            )?;
168            fields.components = Some(components);
169
170            Ok(fields)
171        });
172
173        self
174    }
175
176    /// Set the message's content.
177    ///
178    /// The maximum length is 2000 UTF-16 characters.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error of type [`ContentInvalid`] if the content length is too
183    /// long.
184    ///
185    /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
186    pub fn content(mut self, content: &'a str) -> Self {
187        self.fields = self.fields.and_then(|mut fields| {
188            validate_content(content)?;
189            fields.content = Some(content);
190
191            Ok(fields)
192        });
193
194        self
195    }
196
197    /// Set the message's list of embeds.
198    ///
199    /// Calling this method will clear previous calls.
200    ///
201    /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
202    /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
203    /// characters. Additionally, the internal fields also have character
204    /// limits. Refer to [Discord Docs/Embed Limits] for more information.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
209    ///
210    /// Otherwise, refer to the errors section of
211    /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
212    ///
213    /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
214    /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
215    /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
216    /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
217    pub fn embeds(mut self, embeds: &'a [Embed]) -> Self {
218        self.fields = self.fields.and_then(|mut fields| {
219            validate_embeds(embeds)?;
220            fields.embeds = Some(embeds);
221
222            Ok(fields)
223        });
224
225        self
226    }
227
228    /// Set the message's flags.
229    ///
230    /// The only supported flags are [`EPHEMERAL`], [`SUPPRESS_EMBEDS`] and [`IS_COMPONENTS_V2`].
231    ///
232    /// [`EPHEMERAL`]: MessageFlags::EPHEMERAL
233    /// [`SUPPRESS_EMBEDS`]: MessageFlags::SUPPRESS_EMBEDS
234    /// [`IS_COMPONENTS_V2`]: MessageFlags::IS_COMPONENTS_V2
235    pub const fn flags(mut self, flags: MessageFlags) -> Self {
236        if let Ok(fields) = self.fields.as_mut() {
237            fields.flags = Some(flags);
238        }
239
240        self
241    }
242
243    /// JSON encoded body of any additional request fields.
244    ///
245    /// If this method is called, all other fields are ignored, except for
246    /// [`attachments`]. See [Discord Docs/Uploading Files].
247    ///
248    /// # Examples
249    ///
250    /// See [`ExecuteWebhook::payload_json`] for examples.
251    ///
252    /// [`attachments`]: Self::attachments
253    /// [`ExecuteWebhook::payload_json`]: crate::request::channel::webhook::ExecuteWebhook::payload_json
254    /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
255    pub const fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
256        if let Ok(fields) = self.fields.as_mut() {
257            fields.payload_json = Some(payload_json);
258        }
259
260        self
261    }
262
263    /// Specify true if the message is TTS.
264    pub const fn tts(mut self, tts: bool) -> Self {
265        if let Ok(fields) = self.fields.as_mut() {
266            fields.tts = Some(tts);
267        }
268
269        self
270    }
271
272    /// Specify the poll for this followup message.
273    pub fn poll(mut self, poll: Poll) -> Self {
274        if let Ok(fields) = self.fields.as_mut() {
275            fields.poll = Some(poll);
276        }
277
278        self
279    }
280}
281
282#[cfg(not(target_os = "wasi"))]
283impl IntoFuture for CreateFollowup<'_> {
284    type Output = Result<Response<Message>, Error>;
285
286    type IntoFuture = ResponseFuture<Message>;
287
288    fn into_future(self) -> Self::IntoFuture {
289        let http = self.http;
290
291        match self.try_into_request() {
292            Ok(request) => http.request(request),
293            Err(source) => ResponseFuture::error(source),
294        }
295    }
296}
297
298impl TryIntoRequest for CreateFollowup<'_> {
299    fn try_into_request(self) -> Result<Request, Error> {
300        let mut fields = self.fields.map_err(Error::validation)?;
301        let mut request = Request::builder(&Route::ExecuteWebhook {
302            thread_id: None,
303            token: self.token,
304            wait: None,
305            with_components: Some(
306                fields
307                    .components
308                    .is_some_and(|components| !components.is_empty()),
309            ),
310            webhook_id: self.application_id.get(),
311        });
312
313        // Interaction executions don't need the authorization token, only the
314        // interaction token.
315        request = request.use_authorization_token(false);
316
317        // Set the default allowed mentions if required.
318        if fields.allowed_mentions.is_none()
319            && let Some(allowed_mentions) = self.http.default_allowed_mentions()
320        {
321            fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
322        }
323
324        // Determine whether we need to use a multipart/form-data body or a JSON
325        // body.
326        if !self.attachment_manager.is_empty() {
327            let form = if let Some(payload_json) = fields.payload_json {
328                self.attachment_manager.build_form(payload_json)
329            } else {
330                fields.attachments = Some(self.attachment_manager.get_partial_attachments());
331
332                let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
333
334                self.attachment_manager.build_form(fields.as_ref())
335            };
336
337            request = request.form(form);
338        } else if let Some(payload_json) = fields.payload_json {
339            request = request.body(payload_json.to_vec());
340        } else {
341            request = request.json(&fields);
342        }
343
344        request.build()
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use crate::{client::Client, request::TryIntoRequest};
351    use std::error::Error;
352    use twilight_model::id::Id;
353
354    #[test]
355    fn create_followup_message() -> Result<(), Box<dyn Error>> {
356        let application_id = Id::new(1);
357        let token = "foo".to_owned();
358
359        let client = Client::new(String::new());
360        let req = client
361            .interaction(application_id)
362            .create_followup(&token)
363            .content("test")
364            .try_into_request()?;
365
366        assert!(!req.use_authorization_token());
367
368        Ok(())
369    }
370}