twilight_http/request/channel/webhook/
execute_webhook.rs

1use crate::{
2    client::Client,
3    error::Error,
4    request::{
5        attachment::{AttachmentManager, PartialAttachment},
6        channel::webhook::ExecuteWebhookAndWait,
7        Nullable, Request, TryIntoRequest,
8    },
9    response::{marker::EmptyBody, Response, ResponseFuture},
10    routing::Route,
11};
12use serde::Serialize;
13use std::future::IntoFuture;
14use twilight_model::{
15    channel::message::{AllowedMentions, Component, Embed, MessageFlags},
16    http::attachment::Attachment,
17    id::{
18        marker::{ChannelMarker, WebhookMarker},
19        Id,
20    },
21};
22use twilight_validate::{
23    message::{
24        attachment as validate_attachment, components as validate_components,
25        content as validate_content, embeds as validate_embeds, MessageValidationError,
26        MessageValidationErrorType,
27    },
28    request::webhook_username as validate_webhook_username,
29};
30
31#[derive(Serialize)]
32pub(crate) struct ExecuteWebhookFields<'a> {
33    #[serde(skip_serializing_if = "Option::is_none")]
34    allowed_mentions: Option<Nullable<&'a AllowedMentions>>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    attachments: Option<Vec<PartialAttachment<'a>>>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    avatar_url: Option<&'a str>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    components: Option<&'a [Component]>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    content: Option<&'a str>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    embeds: Option<&'a [Embed]>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    flags: Option<MessageFlags>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    payload_json: Option<&'a [u8]>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    thread_name: Option<&'a str>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    tts: Option<bool>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    username: Option<&'a str>,
55}
56
57/// Execute a webhook, sending a message to its channel.
58///
59/// The message must include at least one of [`attachments`], [`components`],
60/// [`content`], or [`embeds`].
61///
62/// # Examples
63///
64/// ```no_run
65/// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
66/// use twilight_http::Client;
67/// use twilight_model::id::Id;
68///
69/// let client = Client::new("my token".to_owned());
70/// let id = Id::new(432);
71///
72/// client
73///     .execute_webhook(id, "webhook token")
74///     .content("Pinkie...")
75///     .await?;
76/// # Ok(()) }
77/// ```
78///
79/// [`attachments`]: Self::attachments
80/// [`components`]: Self::components
81/// [`content`]: Self::content
82/// [`embeds`]: Self::embeds
83#[must_use = "requests must be configured and executed"]
84pub struct ExecuteWebhook<'a> {
85    attachment_manager: AttachmentManager<'a>,
86    fields: Result<ExecuteWebhookFields<'a>, MessageValidationError>,
87    http: &'a Client,
88    thread_id: Option<Id<ChannelMarker>>,
89    token: &'a str,
90    wait: bool,
91    webhook_id: Id<WebhookMarker>,
92}
93
94impl<'a> ExecuteWebhook<'a> {
95    pub(crate) const fn new(
96        http: &'a Client,
97        webhook_id: Id<WebhookMarker>,
98        token: &'a str,
99    ) -> Self {
100        Self {
101            attachment_manager: AttachmentManager::new(),
102            fields: Ok(ExecuteWebhookFields {
103                attachments: None,
104                avatar_url: None,
105                components: None,
106                content: None,
107                embeds: None,
108                flags: None,
109                payload_json: None,
110                thread_name: None,
111                tts: None,
112                username: None,
113                allowed_mentions: None,
114            }),
115            http,
116            thread_id: None,
117            token,
118            wait: false,
119            webhook_id,
120        }
121    }
122
123    /// Specify the [`AllowedMentions`] for the message.
124    ///
125    /// Unless otherwise called, the request will use the client's default
126    /// allowed mentions. Set to `None` to ignore this default.
127    pub fn allowed_mentions(mut self, allowed_mentions: Option<&'a AllowedMentions>) -> Self {
128        if let Ok(fields) = self.fields.as_mut() {
129            fields.allowed_mentions = Some(Nullable(allowed_mentions));
130        }
131
132        self
133    }
134
135    /// Attach multiple files to the message.
136    ///
137    /// Calling this method will clear any previous calls.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error of type [`AttachmentDescriptionTooLarge`] if
142    /// the attachments's description is too large.
143    ///
144    /// Returns an error of type [`AttachmentFilename`] if any filename is
145    /// invalid.
146    ///
147    /// [`AttachmentDescriptionTooLarge`]: twilight_validate::message::MessageValidationErrorType::AttachmentDescriptionTooLarge
148    /// [`AttachmentFilename`]: twilight_validate::message::MessageValidationErrorType::AttachmentFilename
149    pub fn attachments(mut self, attachments: &'a [Attachment]) -> Self {
150        if self.fields.is_ok() {
151            if let Err(source) = attachments.iter().try_for_each(validate_attachment) {
152                self.fields = Err(source);
153            } else {
154                self.attachment_manager = self
155                    .attachment_manager
156                    .set_files(attachments.iter().collect());
157            }
158        }
159
160        self
161    }
162
163    /// The URL of the avatar of the webhook.
164    pub fn avatar_url(mut self, avatar_url: &'a str) -> Self {
165        if let Ok(fields) = self.fields.as_mut() {
166            fields.avatar_url = Some(avatar_url);
167        }
168
169        self
170    }
171
172    /// Set the message's list of [`Component`]s.
173    ///
174    /// Calling this method will clear previous calls.
175    ///
176    /// Requires a webhook owned by the application.
177    ///
178    /// # Errors
179    ///
180    /// Refer to the errors section of
181    /// [`twilight_validate::component::component`] for a list of errors that
182    /// may be returned as a result of validating each provided component.
183    pub fn components(mut self, components: &'a [Component]) -> Self {
184        self.fields = self.fields.and_then(|mut fields| {
185            validate_components(components)?;
186            fields.components = Some(components);
187
188            Ok(fields)
189        });
190
191        self
192    }
193
194    /// Set the message's content.
195    ///
196    /// The maximum length is 2000 UTF-16 characters.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error of type [`ContentInvalid`] if the content length is too
201    /// long.
202    ///
203    /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
204    pub fn content(mut self, content: &'a str) -> Self {
205        self.fields = self.fields.and_then(|mut fields| {
206            validate_content(content)?;
207            fields.content = Some(content);
208
209            Ok(fields)
210        });
211
212        self
213    }
214
215    /// Set the message's list of embeds.
216    ///
217    /// Calling this method will clear previous calls.
218    ///
219    /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
220    /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
221    /// characters. Additionally, the internal fields also have character
222    /// limits. Refer to [Discord Docs/Embed Limits] for more information.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
227    ///
228    /// Otherwise, refer to the errors section of
229    /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
230    ///
231    /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
232    /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
233    /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
234    /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
235    pub fn embeds(mut self, embeds: &'a [Embed]) -> Self {
236        self.fields = self.fields.and_then(|mut fields| {
237            validate_embeds(embeds)?;
238            fields.embeds = Some(embeds);
239
240            Ok(fields)
241        });
242
243        self
244    }
245
246    /// Set the message's flags.
247    ///
248    /// The only supported flag is [`SUPPRESS_EMBEDS`].
249    ///
250    /// [`SUPPRESS_EMBEDS`]: MessageFlags::SUPPRESS_EMBEDS
251    pub fn flags(mut self, flags: MessageFlags) -> Self {
252        if let Ok(fields) = self.fields.as_mut() {
253            fields.flags = Some(flags);
254        }
255
256        self
257    }
258
259    /// JSON encoded body of any additional request fields.
260    ///
261    /// If this method is called, all other fields are ignored, except for
262    /// [`attachments`]. See [Discord Docs/Uploading Files].
263    ///
264    /// Without [`payload_json`]:
265    ///
266    /// ```no_run
267    /// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
268    /// use twilight_http::Client;
269    /// use twilight_model::id::Id;
270    /// use twilight_util::builder::embed::EmbedBuilder;
271    ///
272    /// let client = Client::new("token".to_owned());
273    ///
274    /// let message = client
275    ///     .execute_webhook(Id::new(1), "token here")
276    ///     .content("some content")
277    ///     .embeds(&[EmbedBuilder::new().title("title").validate()?.build()])
278    ///     .wait()
279    ///     .await?
280    ///     .model()
281    ///     .await?;
282    ///
283    /// assert_eq!(message.content, "some content");
284    /// # Ok(()) }
285    /// ```
286    ///
287    /// With [`payload_json`]:
288    ///
289    /// ```no_run
290    /// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
291    /// use twilight_http::Client;
292    /// use twilight_model::id::Id;
293    /// use twilight_util::builder::embed::EmbedBuilder;
294    ///
295    /// let client = Client::new("token".to_owned());
296    ///
297    /// let message = client
298    ///     .execute_webhook(Id::new(1), "token here")
299    ///     .content("some content")
300    ///     .payload_json(br#"{ "content": "other content", "embeds": [ { "title": "title" } ] }"#)
301    ///     .wait()
302    ///     .await?
303    ///     .model()
304    ///     .await?;
305    ///
306    /// assert_eq!(message.content, "other content");
307    /// # Ok(()) }
308    /// ```
309    ///
310    /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
311    /// [`attachments`]: Self::attachments
312    /// [`payload_json`]: Self::payload_json
313    pub fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
314        if let Ok(fields) = self.fields.as_mut() {
315            fields.payload_json = Some(payload_json);
316        }
317
318        self
319    }
320
321    /// Execute in a thread belonging to the channel instead of the channel itself.
322    pub fn thread_id(mut self, thread_id: Id<ChannelMarker>) -> Self {
323        self.thread_id.replace(thread_id);
324
325        self
326    }
327
328    /// Set the name of the created thread when used in a forum channel.
329    pub fn thread_name(mut self, thread_name: &'a str) -> Self {
330        self.fields = self.fields.map(|mut fields| {
331            fields.thread_name = Some(thread_name);
332
333            fields
334        });
335
336        self
337    }
338
339    /// Specify true if the message is TTS.
340    pub fn tts(mut self, tts: bool) -> Self {
341        if let Ok(fields) = self.fields.as_mut() {
342            fields.tts = Some(tts);
343        }
344
345        self
346    }
347
348    /// Specify the username of the webhook's message.
349    ///
350    /// # Errors
351    ///
352    /// Returns an error of type [`WebhookUsername`] if the webhook's name is
353    /// invalid.
354    ///
355    /// [`WebhookUsername`]: twilight_validate::request::ValidationErrorType::WebhookUsername
356    pub fn username(mut self, username: &'a str) -> Self {
357        self.fields = self.fields.and_then(|mut fields| {
358            validate_webhook_username(username).map_err(|source| {
359                MessageValidationError::from_validation_error(
360                    MessageValidationErrorType::WebhookUsername,
361                    source,
362                )
363            })?;
364            fields.username = Some(username);
365
366            Ok(fields)
367        });
368
369        self
370    }
371
372    /// Wait for the message to send before sending a response. See
373    /// [Discord Docs/Execute Webhook].
374    ///
375    /// Using this will result in receiving the created message.
376    ///
377    /// [Discord Docs/Execute Webhook]: https://discord.com/developers/docs/resources/webhook#execute-webhook-querystring-params
378    pub const fn wait(mut self) -> ExecuteWebhookAndWait<'a> {
379        self.wait = true;
380
381        ExecuteWebhookAndWait::new(self.http, self)
382    }
383}
384
385impl IntoFuture for ExecuteWebhook<'_> {
386    type Output = Result<Response<EmptyBody>, Error>;
387
388    type IntoFuture = ResponseFuture<EmptyBody>;
389
390    fn into_future(self) -> Self::IntoFuture {
391        let http = self.http;
392
393        match self.try_into_request() {
394            Ok(request) => http.request(request),
395            Err(source) => ResponseFuture::error(source),
396        }
397    }
398}
399
400impl TryIntoRequest for ExecuteWebhook<'_> {
401    fn try_into_request(self) -> Result<Request, Error> {
402        let mut fields = self.fields.map_err(Error::validation)?;
403        let mut request = Request::builder(&Route::ExecuteWebhook {
404            thread_id: self.thread_id.map(Id::get),
405            token: self.token,
406            wait: Some(self.wait),
407            webhook_id: self.webhook_id.get(),
408        });
409
410        // Webhook executions don't need the authorization token, only the
411        // webhook token.
412        request = request.use_authorization_token(false);
413
414        // Set the default allowed mentions if required.
415        if fields.allowed_mentions.is_none() {
416            if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
417                fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
418            }
419        }
420
421        // Determine whether we need to use a multipart/form-data body or a JSON
422        // body.
423        if !self.attachment_manager.is_empty() {
424            let form = if let Some(payload_json) = fields.payload_json {
425                self.attachment_manager.build_form(payload_json)
426            } else {
427                fields.attachments = Some(self.attachment_manager.get_partial_attachments());
428
429                let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
430
431                self.attachment_manager.build_form(fields.as_ref())
432            };
433
434            request = request.form(form);
435        } else if let Some(payload_json) = fields.payload_json {
436            request = request.body(payload_json.to_vec());
437        } else {
438            request = request.json(&fields);
439        }
440
441        request.build()
442    }
443}