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