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}