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    attachments: EditAttachments,
79
80    #[serde(skip)]
81    thread_id: Option<ChannelId>,
82}
83
84impl ExecuteWebhook {
85    /// Equivalent to [`Self::default`].
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    #[cfg(feature = "http")]
91    fn check_length(&self) -> Result<()> {
92        if let Some(content) = &self.content {
93            check_overflow(content.chars().count(), constants::MESSAGE_CODE_LIMIT)
94                .map_err(|overflow| Error::Model(ModelError::MessageTooLong(overflow)))?;
95        }
96
97        check_overflow(self.embeds.len(), constants::EMBED_MAX_COUNT)
98            .map_err(|_| Error::Model(ModelError::EmbedAmount))?;
99        for embed in &self.embeds {
100            embed.check_length()?;
101        }
102
103        Ok(())
104    }
105
106    /// Override the default avatar of the webhook with an image URL.
107    ///
108    /// # Examples
109    ///
110    /// Overriding the default avatar:
111    ///
112    /// ```rust,no_run
113    /// # use serenity::builder::ExecuteWebhook;
114    /// # use serenity::http::Http;
115    /// # use serenity::model::webhook::Webhook;
116    /// #
117    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
118    /// # let http: Http = unimplemented!();
119    /// # let webhook: Webhook = unimplemented!();
120    /// let builder = ExecuteWebhook::new()
121    ///     .avatar_url("https://i.imgur.com/KTs6whd.jpg")
122    ///     .content("Here's a webhook");
123    /// webhook.execute(&http, false, builder).await?;
124    /// # Ok(())
125    /// # }
126    /// ```
127    pub fn avatar_url(mut self, avatar_url: impl Into<String>) -> Self {
128        self.avatar_url = Some(avatar_url.into());
129        self
130    }
131
132    /// Set the content of the message.
133    ///
134    /// Note that when setting at least one embed via [`Self::embeds`], this may be
135    /// omitted.
136    ///
137    /// # Examples
138    ///
139    /// Sending a webhook with a content of `"foo"`:
140    ///
141    /// ```rust,no_run
142    /// # use serenity::builder::ExecuteWebhook;
143    /// # use serenity::http::Http;
144    /// # use serenity::model::webhook::Webhook;
145    /// #
146    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
147    /// # let http: Http = unimplemented!();
148    /// # let webhook: Webhook = unimplemented!();
149    /// let builder = ExecuteWebhook::new().content("foo");
150    /// let execution = webhook.execute(&http, false, builder).await;
151    ///
152    /// if let Err(why) = execution {
153    ///     println!("Err sending webhook: {:?}", why);
154    /// }
155    /// # Ok(())
156    /// # }
157    /// ```
158    pub fn content(mut self, content: impl Into<String>) -> Self {
159        self.content = Some(content.into());
160        self
161    }
162
163    /// Execute within a given thread. If the provided thread Id doesn't belong to the current
164    /// webhook, the API will return an error.
165    ///
166    /// **Note**: If the given thread is archived, it will automatically be unarchived.
167    ///
168    /// # Examples
169    ///
170    /// Execute a webhook with message content of `test`, in a thread with Id `12345678`:
171    ///
172    /// ```rust,no_run
173    /// # use serenity::builder::ExecuteWebhook;
174    /// # use serenity::http::Http;
175    /// # use serenity::model::webhook::Webhook;
176    /// #
177    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
178    /// # let http: Http = unimplemented!();
179    /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
180    /// let mut webhook = Webhook::from_url(&http, url).await?;
181    ///
182    /// let builder = ExecuteWebhook::new().in_thread(12345678).content("test");
183    /// webhook.execute(&http, false, builder).await?;
184    /// # Ok(())
185    /// # }
186    /// ```
187    pub fn in_thread(mut self, thread_id: impl Into<ChannelId>) -> Self {
188        self.thread_id = Some(thread_id.into());
189        self
190    }
191
192    /// Appends a file to the webhook message.
193    pub fn add_file(mut self, file: CreateAttachment) -> Self {
194        self.attachments = self.attachments.add(file);
195        self
196    }
197
198    /// Appends a list of files to the webhook message.
199    pub fn add_files(mut self, files: impl IntoIterator<Item = CreateAttachment>) -> Self {
200        for file in files {
201            self.attachments = self.attachments.add(file);
202        }
203        self
204    }
205
206    /// Sets a list of files to include in the webhook message.
207    ///
208    /// Calling this multiple times will overwrite the file list. To append files, call
209    /// [`Self::add_file`] or [`Self::add_files`] instead.
210    pub fn files(mut self, files: impl IntoIterator<Item = CreateAttachment>) -> Self {
211        self.attachments = EditAttachments::new();
212        self.add_files(files)
213    }
214
215    /// Set the allowed mentions for the message.
216    pub fn allowed_mentions(mut self, allowed_mentions: CreateAllowedMentions) -> Self {
217        self.allowed_mentions = Some(allowed_mentions);
218        self
219    }
220
221    /// Sets the components for this message. Requires an application-owned webhook, meaning either
222    /// the webhook's `kind` field is set to [`WebhookType::Application`], or it was created by an
223    /// application (and has kind [`WebhookType::Incoming`]).
224    ///
225    /// [`WebhookType::Application`]: crate::model::webhook::WebhookType
226    /// [`WebhookType::Incoming`]: crate::model::webhook::WebhookType
227    pub fn components(mut self, components: Vec<CreateActionRow>) -> Self {
228        self.components = Some(components);
229        self
230    }
231    super::button_and_select_menu_convenience_methods!(self.components);
232
233    /// Set an embed for the message.
234    ///
235    /// Refer to the [struct-level documentation] for an example on how to use embeds.
236    ///
237    /// [struct-level documentation]: #examples
238    pub fn embed(self, embed: CreateEmbed) -> Self {
239        self.embeds(vec![embed])
240    }
241
242    /// Set multiple embeds for the message.
243    pub fn embeds(mut self, embeds: Vec<CreateEmbed>) -> Self {
244        self.embeds = embeds;
245        self
246    }
247
248    /// Whether the message is a text-to-speech message.
249    ///
250    /// # Examples
251    ///
252    /// Sending a webhook with text-to-speech enabled:
253    ///
254    /// ```rust,no_run
255    /// # use serenity::builder::ExecuteWebhook;
256    /// # use serenity::http::Http;
257    /// # use serenity::model::webhook::Webhook;
258    /// #
259    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
260    /// # let http: Http = unimplemented!();
261    /// # let webhook: Webhook = unimplemented!();
262    /// let builder = ExecuteWebhook::new().content("hello").tts(true);
263    /// let execution = webhook.execute(&http, false, builder).await;
264    ///
265    /// if let Err(why) = execution {
266    ///     println!("Err sending webhook: {:?}", why);
267    /// }
268    /// # Ok(())
269    /// # }
270    /// ```
271    pub fn tts(mut self, tts: bool) -> Self {
272        self.tts = tts;
273        self
274    }
275
276    /// Override the default username of the webhook.
277    ///
278    /// # Examples
279    ///
280    /// Overriding the username to `"hakase"`:
281    ///
282    /// ```rust,no_run
283    /// # use serenity::builder::ExecuteWebhook;
284    /// # use serenity::http::Http;
285    /// # use serenity::model::webhook::Webhook;
286    /// #
287    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
288    /// # let http: Http = unimplemented!();
289    /// # let webhook: Webhook = unimplemented!();
290    /// let builder = ExecuteWebhook::new().content("hello").username("hakase");
291    /// let execution = webhook.execute(&http, false, builder).await;
292    ///
293    /// if let Err(why) = execution {
294    ///     println!("Err sending webhook: {:?}", why);
295    /// }
296    /// # Ok(())
297    /// # }
298    /// ```
299    pub fn username(mut self, username: impl Into<String>) -> Self {
300        self.username = Some(username.into());
301        self
302    }
303
304    /// Sets the flags for the message.
305    ///
306    /// # Examples
307    ///
308    /// Suppressing an embed on the message.
309    ///
310    /// ```rust,no_run
311    /// # use serenity::builder::ExecuteWebhook;
312    /// # use serenity::http::Http;
313    /// # use serenity::model::channel::MessageFlags;
314    /// # use serenity::model::webhook::Webhook;
315    /// #
316    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
317    /// # let http: Http = unimplemented!();
318    /// # let webhook: Webhook = unimplemented!();
319    /// let builder = ExecuteWebhook::new()
320    ///     .content("https://docs.rs/serenity/latest/serenity/")
321    ///     .flags(MessageFlags::SUPPRESS_EMBEDS);
322    /// let execution = webhook.execute(&http, false, builder).await;
323    ///
324    /// if let Err(why) = execution {
325    ///     println!("Err sending webhook: {:?}", why);
326    /// }
327    /// # Ok(())
328    /// # }
329    /// ```
330    pub fn flags(mut self, flags: MessageFlags) -> Self {
331        self.flags = Some(flags);
332        self
333    }
334
335    /// Name of thread to create (requires the webhook channel to be a forum channel)
336    pub fn thread_name(mut self, thread_name: String) -> Self {
337        self.thread_name = Some(thread_name);
338        self
339    }
340}
341
342#[cfg(feature = "http")]
343#[async_trait::async_trait]
344impl Builder for ExecuteWebhook {
345    type Context<'ctx> = (WebhookId, &'ctx str, bool);
346    type Built = Option<Message>;
347
348    /// Executes the webhook with the given content.
349    ///
350    /// # Errors
351    ///
352    /// Returns [`Error::Http`] if the content is malformed, if the token is invalid, or if
353    /// execution is attempted in a thread not belonging to the webhook's [`Channel`].
354    ///
355    /// Returns [`Error::Json`] if there is an error in deserialising Discord's response.
356    async fn execute(
357        mut self,
358        cache_http: impl CacheHttp,
359        ctx: Self::Context<'_>,
360    ) -> Result<Self::Built> {
361        self.check_length()?;
362
363        let files = self.attachments.take_files();
364
365        let http = cache_http.http();
366        if self.allowed_mentions.is_none() {
367            self.allowed_mentions.clone_from(&http.default_allowed_mentions);
368        }
369
370        http.execute_webhook(ctx.0, self.thread_id, ctx.1, ctx.2, files, &self).await
371    }
372}