serenity/model/webhook.rs
1//! Webhook model and implementations.
2
3#[cfg(feature = "model")]
4use secrecy::ExposeSecret;
5use secrecy::SecretString;
6
7use super::utils::secret;
8#[cfg(feature = "model")]
9use crate::builder::{Builder, EditWebhook, EditWebhookMessage, ExecuteWebhook};
10#[cfg(feature = "cache")]
11use crate::cache::{Cache, GuildChannelRef, GuildRef};
12#[cfg(feature = "model")]
13use crate::http::{CacheHttp, Http};
14#[cfg(feature = "model")]
15use crate::internal::prelude::*;
16use crate::model::prelude::*;
17
18enum_number! {
19 /// A representation of a type of webhook.
20 ///
21 /// [Discord docs](https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types).
22 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
23 #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
24 #[serde(from = "u8", into = "u8")]
25 #[non_exhaustive]
26 pub enum WebhookType {
27 /// An indicator that the webhook can post messages to channels with a token.
28 Incoming = 1,
29 /// An indicator that the webhook is managed by Discord for posting new messages to
30 /// channels without a token.
31 ChannelFollower = 2,
32 /// Application webhooks are webhooks used with Interactions.
33 Application = 3,
34 _ => Unknown(u8),
35 }
36}
37
38impl WebhookType {
39 #[inline]
40 #[must_use]
41 pub fn name(&self) -> &str {
42 match self {
43 Self::Incoming => "incoming",
44 Self::ChannelFollower => "channel follower",
45 Self::Application => "application",
46 Self::Unknown(_) => "unknown",
47 }
48 }
49}
50
51/// A representation of a webhook, which is a low-effort way to post messages to channels. They do
52/// not necessarily require a bot user or authentication to use.
53///
54/// [Discord docs](https://discord.com/developers/docs/resources/webhook#webhook-object).
55#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
56#[derive(Debug, Clone, Deserialize, Serialize)]
57#[non_exhaustive]
58pub struct Webhook {
59 /// The unique Id.
60 ///
61 /// Can be used to calculate the creation date of the webhook.
62 pub id: WebhookId,
63 /// The type of the webhook.
64 #[serde(rename = "type")]
65 pub kind: WebhookType,
66 /// The Id of the guild that owns the webhook.
67 pub guild_id: Option<GuildId>,
68 /// The Id of the channel that owns the webhook.
69 pub channel_id: Option<ChannelId>,
70 /// The user that created the webhook.
71 ///
72 /// **Note**: This is not received when getting a webhook by its token.
73 pub user: Option<User>,
74 /// The default name of the webhook.
75 ///
76 /// This can be temporarily overridden via [`ExecuteWebhook::username`].
77 pub name: Option<String>,
78 /// The default avatar.
79 ///
80 /// This can be temporarily overridden via [`ExecuteWebhook::avatar_url`].
81 pub avatar: Option<ImageHash>,
82 /// The webhook's secure token.
83 #[serde(with = "secret", default)]
84 pub token: Option<SecretString>,
85 /// The bot/OAuth2 application that created this webhook.
86 pub application_id: Option<ApplicationId>,
87 /// The guild of the channel that this webhook is following (returned for
88 /// [`WebhookType::ChannelFollower`])
89 pub source_guild: Option<WebhookGuild>,
90 /// The channel that this webhook is following (returned for
91 /// [`WebhookType::ChannelFollower`]).
92 pub source_channel: Option<WebhookChannel>,
93 /// The url used for executing the webhook (returned by the webhooks OAuth2 flow).
94 #[serde(with = "secret", default)]
95 pub url: Option<SecretString>,
96}
97
98/// The guild object returned by a [`Webhook`], of type [`WebhookType::ChannelFollower`].
99#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
100#[derive(Debug, Clone, Deserialize, Serialize)]
101#[non_exhaustive]
102pub struct WebhookGuild {
103 /// The unique Id identifying the guild.
104 pub id: GuildId,
105 /// The name of the guild.
106 pub name: String,
107 /// The hash of the icon used by the guild.
108 ///
109 /// In the client, this appears on the guild list on the left-hand side.
110 pub icon: Option<ImageHash>,
111}
112
113#[cfg(feature = "model")]
114impl WebhookGuild {
115 /// Tries to find the [`Guild`] by its Id in the cache.
116 #[cfg(feature = "cache")]
117 #[inline]
118 pub fn to_guild_cached(self, cache: &impl AsRef<Cache>) -> Option<GuildRef<'_>> {
119 cache.as_ref().guild(self.id)
120 }
121
122 /// Requests [`PartialGuild`] over REST API.
123 ///
124 /// **Note**: This will not be a [`Guild`], as the REST API does not send
125 /// all data with a guild retrieval.
126 ///
127 /// # Errors
128 ///
129 /// Returns an [`Error::Http`] if the current user is not in the guild.
130 #[inline]
131 pub async fn to_partial_guild(self, cache_http: impl CacheHttp) -> Result<PartialGuild> {
132 #[cfg(feature = "cache")]
133 {
134 if let Some(cache) = cache_http.cache() {
135 if let Some(guild) = cache.guild(self.id) {
136 return Ok(guild.clone().into());
137 }
138 }
139 }
140
141 cache_http.http().get_guild(self.id).await
142 }
143
144 /// Requests [`PartialGuild`] over REST API with counts.
145 ///
146 /// **Note**: This will not be a [`Guild`], as the REST API does not send all data with a guild
147 /// retrieval.
148 ///
149 /// # Errors
150 ///
151 /// Returns an [`Error::Http`] if the current user is not in the guild.
152 #[inline]
153 pub async fn to_partial_guild_with_counts(
154 self,
155 http: impl AsRef<Http>,
156 ) -> Result<PartialGuild> {
157 http.as_ref().get_guild_with_counts(self.id).await
158 }
159}
160
161#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
162#[derive(Debug, Clone, Deserialize, Serialize)]
163#[non_exhaustive]
164pub struct WebhookChannel {
165 /// The unique Id of the channel.
166 pub id: ChannelId,
167 /// The name of the channel.
168 pub name: String,
169}
170
171#[cfg(feature = "model")]
172impl WebhookChannel {
173 /// Attempts to find a [`GuildChannel`] by its Id in the cache.
174 #[cfg(feature = "cache")]
175 #[inline]
176 #[deprecated = "Use Cache::guild and Guild::channels"]
177 pub fn to_channel_cached(self, cache: &Cache) -> Option<GuildChannelRef<'_>> {
178 #[allow(deprecated)]
179 cache.channel(self.id)
180 }
181
182 /// First attempts to retrieve the channel from the `temp_cache` if enabled, otherwise performs
183 /// a HTTP request.
184 ///
185 /// It is recommended to first check if the channel is accessible via `Cache::guild` and
186 /// `Guild::members`, although this requires a `GuildId`.
187 ///
188 /// # Errors
189 ///
190 /// Returns [`Error::Http`] if the channel retrieval request failed.
191 #[inline]
192 pub async fn to_channel(self, cache_http: impl CacheHttp) -> Result<GuildChannel> {
193 let channel = self.id.to_channel(cache_http).await?;
194 channel.guild().ok_or(Error::Model(ModelError::InvalidChannelType))
195 }
196}
197
198#[cfg(feature = "model")]
199impl Webhook {
200 /// Retrieves a webhook given its Id.
201 ///
202 /// This method requires authentication, whereas [`Webhook::from_id_with_token`] and
203 /// [`Webhook::from_url`] do not.
204 ///
205 /// # Examples
206 ///
207 /// Retrieve a webhook by Id:
208 ///
209 /// ```rust,no_run
210 /// # use serenity::http::Http;
211 /// # use serenity::model::{webhook::Webhook, id::WebhookId};
212 /// #
213 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
214 /// # let http: Http = unimplemented!();
215 /// let id = WebhookId::new(245037420704169985);
216 /// let webhook = Webhook::from_id(&http, id).await?;
217 /// # Ok(())
218 /// # }
219 /// ```
220 ///
221 /// # Errors
222 ///
223 /// Returns an [`Error::Http`] if the current user is not authenticated, or if the webhook does
224 /// not exist.
225 ///
226 /// May also return an [`Error::Json`] if there is an error in deserialising Discord's
227 /// response.
228 pub async fn from_id(http: impl AsRef<Http>, webhook_id: impl Into<WebhookId>) -> Result<Self> {
229 http.as_ref().get_webhook(webhook_id.into()).await
230 }
231
232 /// Retrieves a webhook given its Id and unique token.
233 ///
234 /// This method does _not_ require authentication.
235 ///
236 /// # Examples
237 ///
238 /// Retrieve a webhook by Id and its unique token:
239 ///
240 /// ```rust,no_run
241 /// # use serenity::http::Http;
242 /// # use serenity::model::{webhook::Webhook, id::WebhookId};
243 /// #
244 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
245 /// # let http: Http = unimplemented!();
246 /// let id = WebhookId::new(245037420704169985);
247 /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
248 ///
249 /// let webhook = Webhook::from_id_with_token(&http, id, token).await?;
250 /// # Ok(())
251 /// # }
252 /// ```
253 ///
254 /// # Errors
255 ///
256 /// Returns an [`Error::Http`] if the webhook does not exist, or if the token is invalid.
257 ///
258 /// May also return an [`Error::Json`] if there is an error in deserialising Discord's
259 /// response.
260 pub async fn from_id_with_token(
261 http: impl AsRef<Http>,
262 webhook_id: impl Into<WebhookId>,
263 token: &str,
264 ) -> Result<Self> {
265 http.as_ref().get_webhook_with_token(webhook_id.into(), token).await
266 }
267
268 /// Retrieves a webhook given its url.
269 ///
270 /// This method does _not_ require authentication.
271 ///
272 /// # Examples
273 ///
274 /// Retrieve a webhook by url:
275 ///
276 /// ```rust,no_run
277 /// # use serenity::http::Http;
278 /// # use serenity::model::webhook::Webhook;
279 /// #
280 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
281 /// # let http: Http = unimplemented!();
282 /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
283 /// let webhook = Webhook::from_url(&http, url).await?;
284 /// # Ok(())
285 /// # }
286 /// ```
287 ///
288 /// # Errors
289 ///
290 /// Returns an [`Error::Http`] if the url is malformed, or otherwise if the webhook does not
291 /// exist, or if the token is invalid.
292 ///
293 /// May also return an [`Error::Json`] if there is an error in deserialising Discord's
294 /// response.
295 pub async fn from_url(http: impl AsRef<Http>, url: &str) -> Result<Self> {
296 http.as_ref().get_webhook_from_url(url).await
297 }
298
299 /// Deletes the webhook.
300 ///
301 /// If [`Self::token`] is set, then authentication is _not_ required. Otherwise, if it is
302 /// [`None`], then authentication _is_ required.
303 ///
304 /// # Errors
305 ///
306 /// Returns [`Error::Http`] if the webhook does not exist, the token is invalid, or if the
307 /// webhook could not otherwise be deleted.
308 #[inline]
309 pub async fn delete(&self, http: impl AsRef<Http>) -> Result<()> {
310 let http = http.as_ref();
311 match &self.token {
312 Some(token) => {
313 http.delete_webhook_with_token(self.id, token.expose_secret(), None).await
314 },
315 None => http.delete_webhook(self.id, None).await,
316 }
317 }
318
319 /// Edits the webhook.
320 ///
321 /// If [`Self::token`] is set, then authentication is _not_ required. Otherwise, if it is
322 /// [`None`], then authentication _is_ required.
323 ///
324 /// # Examples
325 ///
326 /// ```rust,no_run
327 /// # use serenity::http::Http;
328 /// # use serenity::builder::EditWebhook;
329 /// # use serenity::model::webhook::Webhook;
330 /// #
331 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
332 /// # let http: Http = unimplemented!();
333 /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
334 /// let mut webhook = Webhook::from_url(&http, url).await?;
335 ///
336 /// let builder = EditWebhook::new().name("new name");
337 /// webhook.edit(&http, builder).await?;
338 /// # Ok(())
339 /// # }
340 /// ```
341 ///
342 /// # Errors
343 ///
344 /// Returns an [`Error::Model`] if [`Self::token`] is [`None`].
345 ///
346 /// May also return an [`Error::Http`] if the content is malformed, or if the token is invalid.
347 ///
348 /// Or may return an [`Error::Json`] if there is an error in deserialising Discord's response.
349 pub async fn edit(
350 &mut self,
351 cache_http: impl CacheHttp,
352 builder: EditWebhook<'_>,
353 ) -> Result<()> {
354 let token = self.token.as_ref().map(ExposeSecret::expose_secret).map(String::as_str);
355 *self = builder.execute(cache_http, (self.id, token)).await?;
356 Ok(())
357 }
358
359 /// Executes a webhook with the fields set via the given builder.
360 ///
361 /// # Examples
362 ///
363 /// Execute a webhook with message content of `test`:
364 ///
365 /// ```rust,no_run
366 /// # use serenity::builder::ExecuteWebhook;
367 /// # use serenity::http::Http;
368 /// # use serenity::model::webhook::Webhook;
369 /// #
370 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
371 /// # let http: Http = unimplemented!();
372 /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
373 /// let mut webhook = Webhook::from_url(&http, url).await?;
374 ///
375 /// let builder = ExecuteWebhook::new().content("test");
376 /// webhook.execute(&http, false, builder).await?;
377 /// # Ok(())
378 /// # }
379 /// ```
380 ///
381 /// Execute a webhook with message content of `test`, overriding the username to `serenity`,
382 /// and sending an embed:
383 ///
384 /// ```rust,no_run
385 /// # use serenity::http::Http;
386 /// # use serenity::model::webhook::Webhook;
387 /// #
388 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
389 /// # let http: Http = unimplemented!();
390 /// use serenity::builder::{CreateEmbed, ExecuteWebhook};
391 ///
392 /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
393 /// let mut webhook = Webhook::from_url(&http, url).await?;
394 ///
395 /// let embed = CreateEmbed::new()
396 /// .title("Rust's website")
397 /// .description(
398 /// "Rust is a systems programming language that runs blazingly fast, prevents \
399 /// segfaults, and guarantees thread safety.",
400 /// )
401 /// .url("https://rust-lang.org");
402 ///
403 /// let builder = ExecuteWebhook::new().content("test").username("serenity").embed(embed);
404 /// webhook.execute(&http, false, builder).await?;
405 /// # Ok(())
406 /// # }
407 /// ```
408 ///
409 /// # Errors
410 ///
411 /// Returns an [`Error::Model`] if [`Self::token`] is [`None`].
412 ///
413 /// May also return an [`Error::Http`] if the content is malformed, or if the webhook's token
414 /// is invalid.
415 ///
416 /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response.
417 #[inline]
418 pub async fn execute(
419 &self,
420 cache_http: impl CacheHttp,
421 wait: bool,
422 builder: ExecuteWebhook,
423 ) -> Result<Option<Message>> {
424 let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret();
425 builder.execute(cache_http, (self.id, token, wait)).await
426 }
427
428 /// Gets a previously sent message from the webhook.
429 ///
430 /// # Errors
431 ///
432 /// Returns an [`Error::Model`] if the [`Self::token`] is [`None`].
433 ///
434 /// May also return [`Error::Http`] if the webhook's token is invalid, or the given message Id
435 /// does not belong to the current webhook.
436 ///
437 /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response.
438 pub async fn get_message(
439 &self,
440 http: impl AsRef<Http>,
441 thread_id: Option<ChannelId>,
442 message_id: MessageId,
443 ) -> Result<Message> {
444 let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret();
445 http.as_ref().get_webhook_message(self.id, thread_id, token, message_id).await
446 }
447
448 /// Edits a webhook message with the fields set via the given builder.
449 ///
450 /// **Note**: Message contents must be under 2000 unicode code points.
451 ///
452 /// # Errors
453 ///
454 /// Returns an [`Error::Model`] if [`Self::token`] is [`None`], or if the message content is
455 /// too long.
456 ///
457 /// May also return an [`Error::Http`] if the content is malformed, the webhook's token is
458 /// invalid, or the given message Id does not belong to the current webhook.
459 ///
460 /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response.
461 pub async fn edit_message(
462 &self,
463 cache_http: impl CacheHttp,
464 message_id: MessageId,
465 builder: EditWebhookMessage,
466 ) -> Result<Message> {
467 let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret();
468 builder.execute(cache_http, (self.id, token, message_id)).await
469 }
470
471 /// Deletes a webhook message.
472 ///
473 /// # Errors
474 ///
475 /// Returns an [`Error::Model`] if the [`Self::token`] is [`None`].
476 ///
477 /// May also return an [`Error::Http`] if the webhook's token is invalid or the given message
478 /// Id does not belong to the current webhook.
479 pub async fn delete_message(
480 &self,
481 http: impl AsRef<Http>,
482 thread_id: Option<ChannelId>,
483 message_id: MessageId,
484 ) -> Result<()> {
485 let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret();
486 http.as_ref().delete_webhook_message(self.id, thread_id, token, message_id).await
487 }
488
489 /// Retrieves the latest information about the webhook, editing the webhook in-place.
490 ///
491 /// As this calls the [`Http::get_webhook_with_token`] function, authentication is not
492 /// required.
493 ///
494 /// # Errors
495 ///
496 /// Returns an [`Error::Model`] if the [`Self::token`] is [`None`].
497 ///
498 /// May also return an [`Error::Http`] if the http client errors or if Discord returns an
499 /// error. Such as if the [`Webhook`] was deleted.
500 ///
501 /// Or may return an [`Error::Json`] if there is an error deserialising Discord's response.
502 pub async fn refresh(&mut self, http: impl AsRef<Http>) -> Result<()> {
503 let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret();
504 http.as_ref().get_webhook_with_token(self.id, token).await.map(|replacement| {
505 *self = replacement;
506 })
507 }
508
509 /// Returns the url of the webhook.
510 ///
511 /// ```rust,ignore
512 /// assert_eq!(hook.url(), "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV")
513 /// ```
514 ///
515 /// # Errors
516 ///
517 /// Returns an [`Error::Model`] if the [`Self::token`] is [`None`].
518 pub fn url(&self) -> Result<String> {
519 let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret();
520 Ok(format!("https://discord.com/api/webhooks/{}/{token}", self.id))
521 }
522}
523
524#[cfg(feature = "model")]
525impl WebhookId {
526 /// Requests [`Webhook`] over REST API.
527 ///
528 /// **Note**: Requires the [Manage Webhooks] permission.
529 ///
530 /// # Errors
531 ///
532 /// Returns an [`Error::Http`] if the http client errors or if Discord returns an error. Such
533 /// as if the [`WebhookId`] does not exist.
534 ///
535 /// May also return an [`Error::Json`] if there is an error in deserialising the response.
536 ///
537 /// [Manage Webhooks]: super::permissions::Permissions::MANAGE_WEBHOOKS
538 #[inline]
539 pub async fn to_webhook(self, http: impl AsRef<Http>) -> Result<Webhook> {
540 http.as_ref().get_webhook(self).await
541 }
542}