serenity/builder/execute_webhook.rs
1#[cfg(feature = "http")]
2use super::{check_overflow, Builder};
3use super::{
4 CreateActionRow,
5 CreateAllowedMentions,
6 CreateAttachment,
7 CreateEmbed,
8 EditAttachments,
9};
10#[cfg(feature = "http")]
11use crate::constants;
12#[cfg(feature = "http")]
13use crate::http::CacheHttp;
14#[cfg(feature = "http")]
15use crate::internal::prelude::*;
16use crate::model::prelude::*;
17
18/// A builder to create the content for a [`Webhook`]'s execution.
19///
20/// Refer to [`Http::execute_webhook`](crate::http::Http::execute_webhook) for restrictions and
21/// requirements on the execution payload.
22///
23/// # Examples
24///
25/// Creating two embeds, and then sending them as part of the payload using [`Webhook::execute`]:
26///
27/// ```rust,no_run
28/// use serenity::builder::{CreateEmbed, ExecuteWebhook};
29/// use serenity::http::Http;
30/// use serenity::model::webhook::Webhook;
31/// use serenity::model::Colour;
32///
33/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
34/// # let http: Http = unimplemented!();
35/// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
36/// let webhook = Webhook::from_url(&http, url).await?;
37///
38/// let website = CreateEmbed::new()
39/// .title("The Rust Language Website")
40/// .description("Rust is a systems programming language.")
41/// .colour(Colour::from_rgb(222, 165, 132));
42///
43/// let resources = CreateEmbed::new()
44/// .title("Rust Resources")
45/// .description("A few resources to help with learning Rust")
46/// .colour(0xDEA584)
47/// .field("The Rust Book", "A comprehensive resource for Rust.", false)
48/// .field("Rust by Example", "A collection of Rust examples", false);
49///
50/// let builder = ExecuteWebhook::new()
51/// .content("Here's some information on Rust:")
52/// .embeds(vec![website, resources]);
53/// webhook.execute(&http, false, builder).await?;
54/// # Ok(())
55/// # }
56/// ```
57///
58/// [Discord docs](https://discord.com/developers/docs/resources/webhook#execute-webhook)
59#[derive(Clone, Debug, Default, Serialize)]
60#[must_use]
61pub struct ExecuteWebhook {
62 #[serde(skip_serializing_if = "Option::is_none")]
63 content: Option<String>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 username: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 avatar_url: Option<String>,
68 tts: bool,
69 embeds: Vec<CreateEmbed>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 allowed_mentions: Option<CreateAllowedMentions>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 components: Option<Vec<CreateActionRow>>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 flags: Option<MessageFlags>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 thread_name: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 applied_tags: Option<Vec<ForumTagId>>,
80 attachments: EditAttachments,
81
82 #[serde(skip)]
83 thread_id: Option<ChannelId>,
84 #[serde(skip)]
85 with_components: Option<bool>,
86}
87
88impl ExecuteWebhook {
89 /// Equivalent to [`Self::default`].
90 pub fn new() -> Self {
91 Self::default()
92 }
93
94 #[cfg(feature = "http")]
95 fn check_length(&self) -> Result<()> {
96 if let Some(content) = &self.content {
97 check_overflow(content.chars().count(), constants::MESSAGE_CODE_LIMIT)
98 .map_err(|overflow| Error::Model(ModelError::MessageTooLong(overflow)))?;
99 }
100
101 check_overflow(self.embeds.len(), constants::EMBED_MAX_COUNT)
102 .map_err(|_| Error::Model(ModelError::EmbedAmount))?;
103 for embed in &self.embeds {
104 embed.check_length()?;
105 }
106
107 Ok(())
108 }
109
110 /// Override the default avatar of the webhook with an image URL.
111 ///
112 /// # Examples
113 ///
114 /// Overriding the default avatar:
115 ///
116 /// ```rust,no_run
117 /// # use serenity::builder::ExecuteWebhook;
118 /// # use serenity::http::Http;
119 /// # use serenity::model::webhook::Webhook;
120 /// #
121 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
122 /// # let http: Http = unimplemented!();
123 /// # let webhook: Webhook = unimplemented!();
124 /// let builder = ExecuteWebhook::new()
125 /// .avatar_url("https://i.imgur.com/KTs6whd.jpg")
126 /// .content("Here's a webhook");
127 /// webhook.execute(&http, false, builder).await?;
128 /// # Ok(())
129 /// # }
130 /// ```
131 pub fn avatar_url(mut self, avatar_url: impl Into<String>) -> Self {
132 self.avatar_url = Some(avatar_url.into());
133 self
134 }
135
136 /// Set the content of the message.
137 ///
138 /// Note that when setting at least one embed via [`Self::embeds`], this may be
139 /// omitted.
140 ///
141 /// # Examples
142 ///
143 /// Sending a webhook with a content of `"foo"`:
144 ///
145 /// ```rust,no_run
146 /// # use serenity::builder::ExecuteWebhook;
147 /// # use serenity::http::Http;
148 /// # use serenity::model::webhook::Webhook;
149 /// #
150 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
151 /// # let http: Http = unimplemented!();
152 /// # let webhook: Webhook = unimplemented!();
153 /// let builder = ExecuteWebhook::new().content("foo");
154 /// let execution = webhook.execute(&http, false, builder).await;
155 ///
156 /// if let Err(why) = execution {
157 /// println!("Err sending webhook: {:?}", why);
158 /// }
159 /// # Ok(())
160 /// # }
161 /// ```
162 pub fn content(mut self, content: impl Into<String>) -> Self {
163 self.content = Some(content.into());
164 self
165 }
166
167 /// Execute within a given thread. If the provided thread Id doesn't belong to the current
168 /// webhook, the API will return an error.
169 ///
170 /// **Note**: If the given thread is archived, it will automatically be unarchived.
171 ///
172 /// # Examples
173 ///
174 /// Execute a webhook with message content of `test`, in a thread with Id `12345678`:
175 ///
176 /// ```rust,no_run
177 /// # use serenity::builder::ExecuteWebhook;
178 /// # use serenity::http::Http;
179 /// # use serenity::model::webhook::Webhook;
180 /// #
181 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
182 /// # let http: Http = unimplemented!();
183 /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
184 /// let mut webhook = Webhook::from_url(&http, url).await?;
185 ///
186 /// let builder = ExecuteWebhook::new().in_thread(12345678).content("test");
187 /// webhook.execute(&http, false, builder).await?;
188 /// # Ok(())
189 /// # }
190 /// ```
191 pub fn in_thread(mut self, thread_id: impl Into<ChannelId>) -> Self {
192 self.thread_id = Some(thread_id.into());
193 self
194 }
195
196 /// Appends a file to the webhook message.
197 pub fn add_file(mut self, file: CreateAttachment) -> Self {
198 self.attachments = self.attachments.add(file);
199 self
200 }
201
202 /// Appends a list of files to the webhook message.
203 pub fn add_files(mut self, files: impl IntoIterator<Item = CreateAttachment>) -> Self {
204 for file in files {
205 self.attachments = self.attachments.add(file);
206 }
207 self
208 }
209
210 /// Sets a list of files to include in the webhook message.
211 ///
212 /// Calling this multiple times will overwrite the file list. To append files, call
213 /// [`Self::add_file`] or [`Self::add_files`] instead.
214 pub fn files(mut self, files: impl IntoIterator<Item = CreateAttachment>) -> Self {
215 self.attachments = EditAttachments::new();
216 self.add_files(files)
217 }
218
219 /// Set the allowed mentions for the message.
220 pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self {
221 self.allowed_mentions = Some(allowed_mentions);
222 self
223 }
224
225 /// Sets the components for this message. Requires an application-owned webhook, meaning either
226 /// the webhook's `kind` field is set to [`WebhookType::Application`], or it was created by an
227 /// application (and has kind [`WebhookType::Incoming`]).
228 ///
229 /// If [`Self::with_components`] is set, non-interactive components can be used on non
230 /// application-owned webhooks.
231 ///
232 /// [`WebhookType::Application`]: crate::model::webhook::WebhookType
233 /// [`WebhookType::Incoming`]: crate::model::webhook::WebhookType
234 pub fn components(mut self, components: Vec<CreateActionRow>) -> Self {
235 self.components = Some(components);
236 self
237 }
238 super::button_and_select_menu_convenience_methods!(self.components);
239
240 /// Set an embed for the message.
241 ///
242 /// Refer to the [struct-level documentation] for an example on how to use embeds.
243 ///
244 /// [struct-level documentation]: #examples
245 pub fn embed(self, embed: CreateEmbed) -> Self {
246 self.embeds(vec![embed])
247 }
248
249 /// Set multiple embeds for the message.
250 pub fn embeds(mut self, embeds: Vec<CreateEmbed>) -> Self {
251 self.embeds = embeds;
252 self
253 }
254
255 /// Whether the message is a text-to-speech message.
256 ///
257 /// # Examples
258 ///
259 /// Sending a webhook with text-to-speech enabled:
260 ///
261 /// ```rust,no_run
262 /// # use serenity::builder::ExecuteWebhook;
263 /// # use serenity::http::Http;
264 /// # use serenity::model::webhook::Webhook;
265 /// #
266 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
267 /// # let http: Http = unimplemented!();
268 /// # let webhook: Webhook = unimplemented!();
269 /// let builder = ExecuteWebhook::new().content("hello").tts(true);
270 /// let execution = webhook.execute(&http, false, builder).await;
271 ///
272 /// if let Err(why) = execution {
273 /// println!("Err sending webhook: {:?}", why);
274 /// }
275 /// # Ok(())
276 /// # }
277 /// ```
278 pub fn tts(mut self, tts: bool) -> Self {
279 self.tts = tts;
280 self
281 }
282
283 /// Override the default username of the webhook.
284 ///
285 /// # Examples
286 ///
287 /// Overriding the username to `"hakase"`:
288 ///
289 /// ```rust,no_run
290 /// # use serenity::builder::ExecuteWebhook;
291 /// # use serenity::http::Http;
292 /// # use serenity::model::webhook::Webhook;
293 /// #
294 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
295 /// # let http: Http = unimplemented!();
296 /// # let webhook: Webhook = unimplemented!();
297 /// let builder = ExecuteWebhook::new().content("hello").username("hakase");
298 /// let execution = webhook.execute(&http, false, builder).await;
299 ///
300 /// if let Err(why) = execution {
301 /// println!("Err sending webhook: {:?}", why);
302 /// }
303 /// # Ok(())
304 /// # }
305 /// ```
306 pub fn username(mut self, username: impl Into<String>) -> Self {
307 self.username = Some(username.into());
308 self
309 }
310
311 /// Sets the flags for the message.
312 ///
313 /// # Examples
314 ///
315 /// Suppressing an embed on the message.
316 ///
317 /// ```rust,no_run
318 /// # use serenity::builder::ExecuteWebhook;
319 /// # use serenity::http::Http;
320 /// # use serenity::model::channel::MessageFlags;
321 /// # use serenity::model::webhook::Webhook;
322 /// #
323 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
324 /// # let http: Http = unimplemented!();
325 /// # let webhook: Webhook = unimplemented!();
326 /// let builder = ExecuteWebhook::new()
327 /// .content("https://docs.rs/serenity/latest/serenity/")
328 /// .flags(MessageFlags::SUPPRESS_EMBEDS);
329 /// let execution = webhook.execute(&http, false, builder).await;
330 ///
331 /// if let Err(why) = execution {
332 /// println!("Err sending webhook: {:?}", why);
333 /// }
334 /// # Ok(())
335 /// # }
336 /// ```
337 pub fn flags(mut self, flags: MessageFlags) -> Self {
338 self.flags = Some(flags);
339 self
340 }
341
342 /// Name of thread to create (requires the webhook channel to be a forum channel)
343 pub fn thread_name(mut self, thread_name: String) -> Self {
344 self.thread_name = Some(thread_name);
345 self
346 }
347
348 /// Tags for thread being created (requires the webhook channel to be a forum channel)
349 pub fn applied_tags(mut self, applied_tags: Vec<ForumTagId>) -> Self {
350 self.applied_tags = Some(applied_tags);
351 self
352 }
353
354 /// Allows sending non interactive components on non application owned webhooks.
355 pub fn with_components(mut self, with_components: bool) -> Self {
356 self.with_components = Some(with_components);
357 self
358 }
359}
360
361#[cfg(feature = "http")]
362#[async_trait::async_trait]
363impl Builder for ExecuteWebhook {
364 type Context<'ctx> = (WebhookId, &'ctx str, bool);
365 type Built = Option<Message>;
366
367 /// Executes the webhook with the given content.
368 ///
369 /// # Errors
370 ///
371 /// Returns [`Error::Http`] if the content is malformed, if the token is invalid, or if
372 /// execution is attempted in a thread not belonging to the webhook's [`Channel`].
373 ///
374 /// Returns [`Error::Json`] if there is an error in deserialising Discord's response.
375 async fn execute(
376 mut self,
377 cache_http: impl CacheHttp,
378 ctx: Self::Context<'_>,
379 ) -> Result<Self::Built> {
380 self.check_length()?;
381
382 let files = self.attachments.take_files();
383
384 let http = cache_http.http();
385 if self.allowed_mentions.is_none() {
386 self.allowed_mentions.clone_from(&http.default_allowed_mentions);
387 }
388
389 if self.with_components.unwrap_or_default() {
390 http.execute_webhook_with_components(ctx.0, self.thread_id, ctx.1, ctx.2, files, &self)
391 .await
392 } else {
393 http.execute_webhook(ctx.0, self.thread_id, ctx.1, ctx.2, files, &self).await
394 }
395 }
396}