Skip to main content

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}