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