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}