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}