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(components)?;
162            fields.components = Some(components);
163
164            Ok(fields)
165        });
166
167        self
168    }
169
170    /// Set the message's content.
171    ///
172    /// The maximum length is 2000 UTF-16 characters.
173    ///
174    /// # Errors
175    ///
176    /// Returns an error of type [`ContentInvalid`] if the content length is too
177    /// long.
178    ///
179    /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
180    pub fn content(mut self, content: &'a str) -> Self {
181        self.fields = self.fields.and_then(|mut fields| {
182            validate_content(content)?;
183            fields.content = Some(content);
184
185            Ok(fields)
186        });
187
188        self
189    }
190
191    /// Set the message's list of embeds.
192    ///
193    /// Calling this method will clear previous calls.
194    ///
195    /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
196    /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
197    /// characters. Additionally, the internal fields also have character
198    /// limits. Refer to [Discord Docs/Embed Limits] for more information.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
203    ///
204    /// Otherwise, refer to the errors section of
205    /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
206    ///
207    /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
208    /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
209    /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
210    /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
211    pub fn embeds(mut self, embeds: &'a [Embed]) -> Self {
212        self.fields = self.fields.and_then(|mut fields| {
213            validate_embeds(embeds)?;
214            fields.embeds = Some(embeds);
215
216            Ok(fields)
217        });
218
219        self
220    }
221
222    /// Set the message's flags.
223    ///
224    /// The only supported flags are [`EPHEMERAL`] and [`SUPPRESS_EMBEDS`].
225    ///
226    /// [`EPHEMERAL`]: MessageFlags::EPHEMERAL
227    /// [`SUPPRESS_EMBEDS`]: twilight_model::channel::message::MessageFlags::SUPPRESS_EMBEDS
228    pub fn flags(mut self, flags: MessageFlags) -> Self {
229        if let Ok(fields) = self.fields.as_mut() {
230            fields.flags = Some(flags);
231        }
232
233        self
234    }
235
236    /// JSON encoded body of any additional request fields.
237    ///
238    /// If this method is called, all other fields are ignored, except for
239    /// [`attachments`]. See [Discord Docs/Uploading Files].
240    ///
241    /// # Examples
242    ///
243    /// See [`ExecuteWebhook::payload_json`] for examples.
244    ///
245    /// [`attachments`]: Self::attachments
246    /// [`ExecuteWebhook::payload_json`]: crate::request::channel::webhook::ExecuteWebhook::payload_json
247    /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
248    pub fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
249        if let Ok(fields) = self.fields.as_mut() {
250            fields.payload_json = Some(payload_json);
251        }
252
253        self
254    }
255
256    /// Specify true if the message is TTS.
257    pub fn tts(mut self, tts: bool) -> Self {
258        if let Ok(fields) = self.fields.as_mut() {
259            fields.tts = Some(tts);
260        }
261
262        self
263    }
264
265    /// Specify the poll for this followup message.
266    pub fn poll(mut self, poll: Poll) -> Self {
267        if let Ok(fields) = self.fields.as_mut() {
268            fields.poll = Some(poll);
269        }
270
271        self
272    }
273}
274
275impl IntoFuture for CreateFollowup<'_> {
276    type Output = Result<Response<Message>, Error>;
277
278    type IntoFuture = ResponseFuture<Message>;
279
280    fn into_future(self) -> Self::IntoFuture {
281        let http = self.http;
282
283        match self.try_into_request() {
284            Ok(request) => http.request(request),
285            Err(source) => ResponseFuture::error(source),
286        }
287    }
288}
289
290impl TryIntoRequest for CreateFollowup<'_> {
291    fn try_into_request(self) -> Result<Request, Error> {
292        let mut fields = self.fields.map_err(Error::validation)?;
293        let mut request = Request::builder(&Route::ExecuteWebhook {
294            thread_id: None,
295            token: self.token,
296            wait: None,
297            webhook_id: self.application_id.get(),
298        });
299
300        // Interaction executions don't need the authorization token, only the
301        // interaction token.
302        request = request.use_authorization_token(false);
303
304        // Set the default allowed mentions if required.
305        if fields.allowed_mentions.is_none() {
306            if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
307                fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
308            }
309        }
310
311        // Determine whether we need to use a multipart/form-data body or a JSON
312        // body.
313        if !self.attachment_manager.is_empty() {
314            let form = if let Some(payload_json) = fields.payload_json {
315                self.attachment_manager.build_form(payload_json)
316            } else {
317                fields.attachments = Some(self.attachment_manager.get_partial_attachments());
318
319                let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
320
321                self.attachment_manager.build_form(fields.as_ref())
322            };
323
324            request = request.form(form);
325        } else if let Some(payload_json) = fields.payload_json {
326            request = request.body(payload_json.to_vec());
327        } else {
328            request = request.json(&fields);
329        }
330
331        request.build()
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use crate::{client::Client, request::TryIntoRequest};
338    use std::error::Error;
339    use twilight_http_ratelimiting::Path;
340    use twilight_model::id::Id;
341
342    #[test]
343    fn create_followup_message() -> Result<(), Box<dyn Error>> {
344        let application_id = Id::new(1);
345        let token = "foo".to_owned();
346
347        let client = Client::new(String::new());
348        let req = client
349            .interaction(application_id)
350            .create_followup(&token)
351            .content("test")
352            .try_into_request()?;
353
354        assert!(!req.use_authorization_token());
355        assert_eq!(
356            &Path::WebhooksIdToken(application_id.get(), token),
357            req.ratelimit_path()
358        );
359
360        Ok(())
361    }
362}