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(
186 components,
187 fields
188 .flags
189 .is_some_and(|flags| flags.contains(MessageFlags::IS_COMPONENTS_V2)),
190 )?;
191 fields.components = Some(components);
192
193 Ok(fields)
194 });
195
196 self
197 }
198
199 /// Set the message's content.
200 ///
201 /// The maximum length is 2000 UTF-16 characters.
202 ///
203 /// # Errors
204 ///
205 /// Returns an error of type [`ContentInvalid`] if the content length is too
206 /// long.
207 ///
208 /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
209 pub fn content(mut self, content: &'a str) -> Self {
210 self.fields = self.fields.and_then(|mut fields| {
211 validate_content(content)?;
212 fields.content = Some(content);
213
214 Ok(fields)
215 });
216
217 self
218 }
219
220 /// Set the message's list of embeds.
221 ///
222 /// Calling this method will clear previous calls.
223 ///
224 /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
225 /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
226 /// characters. Additionally, the internal fields also have character
227 /// limits. Refer to [Discord Docs/Embed Limits] for more information.
228 ///
229 /// # Errors
230 ///
231 /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
232 ///
233 /// Otherwise, refer to the errors section of
234 /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
235 ///
236 /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
237 /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
238 /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
239 /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
240 pub fn embeds(mut self, embeds: &'a [Embed]) -> Self {
241 self.fields = self.fields.and_then(|mut fields| {
242 validate_embeds(embeds)?;
243 fields.embeds = Some(embeds);
244
245 Ok(fields)
246 });
247
248 self
249 }
250
251 /// Set the message's flags.
252 ///
253 /// The only supported flags are [`SUPPRESS_EMBEDS`], [`SUPPRESS_NOTIFICATIONS`] and
254 /// [`IS_COMPONENTS_V2`]
255 ///
256 /// [`SUPPRESS_EMBEDS`]: MessageFlags::SUPPRESS_EMBEDS
257 /// [`SUPPRESS_NOTIFICATIONS`]: MessageFlags::SUPPRESS_NOTIFICATIONS
258 /// [`IS_COMPONENTS_V2`]: MessageFlags::IS_COMPONENTS_V2
259 pub fn flags(mut self, flags: MessageFlags) -> Self {
260 if let Ok(fields) = self.fields.as_mut() {
261 fields.flags = Some(flags);
262 }
263
264 self
265 }
266
267 /// JSON encoded body of any additional request fields.
268 ///
269 /// If this method is called, all other fields are ignored, except for
270 /// [`attachments`]. See [Discord Docs/Uploading Files].
271 ///
272 /// Without [`payload_json`]:
273 ///
274 /// ```no_run
275 /// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
276 /// use twilight_http::Client;
277 /// use twilight_model::id::Id;
278 /// use twilight_util::builder::embed::EmbedBuilder;
279 ///
280 /// let client = Client::new("token".to_owned());
281 ///
282 /// let message = client
283 /// .execute_webhook(Id::new(1), "token here")
284 /// .content("some content")
285 /// .embeds(&[EmbedBuilder::new().title("title").validate()?.build()])
286 /// .wait()
287 /// .await?
288 /// .model()
289 /// .await?;
290 ///
291 /// assert_eq!(message.content, "some content");
292 /// # Ok(()) }
293 /// ```
294 ///
295 /// With [`payload_json`]:
296 ///
297 /// ```no_run
298 /// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
299 /// use twilight_http::Client;
300 /// use twilight_model::id::Id;
301 /// use twilight_util::builder::embed::EmbedBuilder;
302 ///
303 /// let client = Client::new("token".to_owned());
304 ///
305 /// let message = client
306 /// .execute_webhook(Id::new(1), "token here")
307 /// .content("some content")
308 /// .payload_json(br#"{ "content": "other content", "embeds": [ { "title": "title" } ] }"#)
309 /// .wait()
310 /// .await?
311 /// .model()
312 /// .await?;
313 ///
314 /// assert_eq!(message.content, "other content");
315 /// # Ok(()) }
316 /// ```
317 ///
318 /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
319 /// [`attachments`]: Self::attachments
320 /// [`payload_json`]: Self::payload_json
321 pub fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
322 if let Ok(fields) = self.fields.as_mut() {
323 fields.payload_json = Some(payload_json);
324 }
325
326 self
327 }
328
329 /// Execute in a thread belonging to the channel instead of the channel itself.
330 pub fn thread_id(mut self, thread_id: Id<ChannelMarker>) -> Self {
331 self.thread_id.replace(thread_id);
332
333 self
334 }
335
336 /// Set the name of the created thread when used in a forum channel.
337 pub fn thread_name(mut self, thread_name: &'a str) -> Self {
338 self.fields = self.fields.map(|mut fields| {
339 fields.thread_name = Some(thread_name);
340
341 fields
342 });
343
344 self
345 }
346
347 /// Specify true if the message is TTS.
348 pub fn tts(mut self, tts: bool) -> Self {
349 if let Ok(fields) = self.fields.as_mut() {
350 fields.tts = Some(tts);
351 }
352
353 self
354 }
355
356 /// Specify the username of the webhook's message.
357 ///
358 /// # Errors
359 ///
360 /// Returns an error of type [`WebhookUsername`] if the webhook's name is
361 /// invalid.
362 ///
363 /// [`WebhookUsername`]: twilight_validate::request::ValidationErrorType::WebhookUsername
364 pub fn username(mut self, username: &'a str) -> Self {
365 self.fields = self.fields.and_then(|mut fields| {
366 validate_webhook_username(username).map_err(|source| {
367 MessageValidationError::from_validation_error(
368 MessageValidationErrorType::WebhookUsername,
369 source,
370 )
371 })?;
372 fields.username = Some(username);
373
374 Ok(fields)
375 });
376
377 self
378 }
379
380 /// Wait for the message to send before sending a response. See
381 /// [Discord Docs/Execute Webhook].
382 ///
383 /// Using this will result in receiving the created message.
384 ///
385 /// [Discord Docs/Execute Webhook]: https://discord.com/developers/docs/resources/webhook#execute-webhook-querystring-params
386 pub const fn wait(mut self) -> ExecuteWebhookAndWait<'a> {
387 self.wait = true;
388
389 ExecuteWebhookAndWait::new(self.http, self)
390 }
391}
392
393impl IntoFuture for ExecuteWebhook<'_> {
394 type Output = Result<Response<EmptyBody>, Error>;
395
396 type IntoFuture = ResponseFuture<EmptyBody>;
397
398 fn into_future(self) -> Self::IntoFuture {
399 let http = self.http;
400
401 match self.try_into_request() {
402 Ok(request) => http.request(request),
403 Err(source) => ResponseFuture::error(source),
404 }
405 }
406}
407
408impl TryIntoRequest for ExecuteWebhook<'_> {
409 fn try_into_request(self) -> Result<Request, Error> {
410 let mut fields = self.fields.map_err(Error::validation)?;
411 let mut request = Request::builder(&Route::ExecuteWebhook {
412 thread_id: self.thread_id.map(Id::get),
413 token: self.token,
414 wait: Some(self.wait),
415 with_components: Some(
416 fields
417 .components
418 .is_some_and(|components| !components.is_empty()),
419 ),
420 webhook_id: self.webhook_id.get(),
421 });
422
423 // Webhook executions don't need the authorization token, only the
424 // webhook token.
425 request = request.use_authorization_token(false);
426
427 // Set the default allowed mentions if required.
428 if fields.allowed_mentions.is_none() {
429 if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
430 fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
431 }
432 }
433
434 // Determine whether we need to use a multipart/form-data body or a JSON
435 // body.
436 if !self.attachment_manager.is_empty() {
437 let form = if let Some(payload_json) = fields.payload_json {
438 self.attachment_manager.build_form(payload_json)
439 } else {
440 fields.attachments = Some(self.attachment_manager.get_partial_attachments());
441
442 let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
443
444 self.attachment_manager.build_form(fields.as_ref())
445 };
446
447 request = request.form(form);
448 } else if let Some(payload_json) = fields.payload_json {
449 request = request.body(payload_json.to_vec());
450 } else {
451 request = request.json(&fields);
452 }
453
454 request.build()
455 }
456}