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