Skip to main content

twilight_http/request/channel/webhook/
update_webhook.rs

1#[cfg(not(target_os = "wasi"))]
2use crate::response::{Response, ResponseFuture};
3use crate::{
4    client::Client,
5    error::Error,
6    request::{self, AuditLogReason, Nullable, Request, TryIntoRequest},
7    routing::Route,
8};
9use serde::Serialize;
10use std::future::IntoFuture;
11use twilight_model::{
12    channel::Webhook,
13    id::{
14        Id,
15        marker::{ChannelMarker, WebhookMarker},
16    },
17};
18use twilight_validate::request::{
19    ValidationError, audit_reason as validate_audit_reason,
20    webhook_username as validate_webhook_username,
21};
22
23#[derive(Serialize)]
24struct UpdateWebhookFields<'a> {
25    #[serde(skip_serializing_if = "Option::is_none")]
26    avatar: Option<Nullable<&'a str>>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    channel_id: Option<Id<ChannelMarker>>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    name: Option<&'a str>,
31}
32
33/// Update a webhook by ID.
34#[must_use = "requests must be configured and executed"]
35pub struct UpdateWebhook<'a> {
36    fields: Result<UpdateWebhookFields<'a>, ValidationError>,
37    http: &'a Client,
38    webhook_id: Id<WebhookMarker>,
39    reason: Result<Option<&'a str>, ValidationError>,
40}
41
42/// Update a webhook by its ID.
43impl<'a> UpdateWebhook<'a> {
44    pub(crate) const fn new(http: &'a Client, webhook_id: Id<WebhookMarker>) -> Self {
45        Self {
46            fields: Ok(UpdateWebhookFields {
47                avatar: None,
48                channel_id: None,
49                name: None,
50            }),
51            http,
52            webhook_id,
53            reason: Ok(None),
54        }
55    }
56
57    /// Set the avatar of the webhook.
58    ///
59    /// This must be a Data URI, in the form of
60    /// `data:image/{type};base64,{data}` where `{type}` is the image MIME type
61    /// and `{data}` is the base64-encoded image. See [Discord Docs/Image Data].
62    ///
63    /// [Discord Docs/Image Data]: https://discord.com/developers/docs/reference#image-data
64    pub const fn avatar(mut self, avatar: Option<&'a str>) -> Self {
65        if let Ok(fields) = self.fields.as_mut() {
66            fields.avatar = Some(Nullable(avatar));
67        }
68
69        self
70    }
71
72    /// Move this webhook to a new channel.
73    pub const fn channel_id(mut self, channel_id: Id<ChannelMarker>) -> Self {
74        if let Ok(fields) = self.fields.as_mut() {
75            fields.channel_id = Some(channel_id);
76        }
77
78        self
79    }
80
81    /// Change the name of the webhook.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error of type [`WebhookUsername`] if the webhook's name is
86    /// invalid.
87    ///
88    /// [`WebhookUsername`]: twilight_validate::request::ValidationErrorType::WebhookUsername
89    pub fn name(mut self, name: &'a str) -> Self {
90        self.fields = self.fields.and_then(|mut fields| {
91            validate_webhook_username(name)?;
92            fields.name = Some(name);
93
94            Ok(fields)
95        });
96
97        self
98    }
99}
100
101impl<'a> AuditLogReason<'a> for UpdateWebhook<'a> {
102    fn reason(mut self, reason: &'a str) -> Self {
103        self.reason = validate_audit_reason(reason).and(Ok(Some(reason)));
104
105        self
106    }
107}
108
109#[cfg(not(target_os = "wasi"))]
110impl IntoFuture for UpdateWebhook<'_> {
111    type Output = Result<Response<Webhook>, Error>;
112
113    type IntoFuture = ResponseFuture<Webhook>;
114
115    fn into_future(self) -> Self::IntoFuture {
116        let http = self.http;
117
118        match self.try_into_request() {
119            Ok(request) => http.request(request),
120            Err(source) => ResponseFuture::error(source),
121        }
122    }
123}
124
125impl TryIntoRequest for UpdateWebhook<'_> {
126    fn try_into_request(self) -> Result<Request, Error> {
127        let fields = self.fields.map_err(Error::validation)?;
128        let mut request = Request::builder(&Route::UpdateWebhook {
129            token: None,
130            webhook_id: self.webhook_id.get(),
131        })
132        .json(&fields);
133
134        if let Some(reason) = self.reason.map_err(Error::validation)? {
135            request = request.headers(request::audit_header(reason)?);
136        }
137
138        request.build()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use std::error::Error;
146
147    #[test]
148    fn test_update_webhook() -> Result<(), Box<dyn Error>> {
149        const WEBHOOK_ID: Id<WebhookMarker> = Id::new(1);
150        const CHANNEL_ID: Id<ChannelMarker> = Id::new(2);
151
152        let client = Client::new("token".into());
153
154        {
155            let expected = r"{}";
156            let actual = UpdateWebhook::new(&client, WEBHOOK_ID).try_into_request()?;
157
158            assert_eq!(Some(expected.as_bytes()), actual.body());
159        }
160
161        {
162            let expected = r#"{"avatar":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI"}"#;
163            let actual = UpdateWebhook::new(&client, WEBHOOK_ID)
164            .avatar(Some("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI"))
165                .try_into_request()?;
166
167            assert_eq!(Some(expected.as_bytes()), actual.body());
168
169            let expected = r#"{"avatar":null}"#;
170            let actual = UpdateWebhook::new(&client, WEBHOOK_ID)
171                .avatar(None)
172                .try_into_request()?;
173
174            assert_eq!(Some(expected.as_bytes()), actual.body());
175        }
176
177        {
178            let expected = r#"{"channel_id":"2"}"#;
179            let actual = UpdateWebhook::new(&client, WEBHOOK_ID)
180                .channel_id(CHANNEL_ID)
181                .try_into_request()?;
182
183            assert_eq!(Some(expected.as_bytes()), actual.body());
184        }
185
186        {
187            let expected = r#"{"name":"Captain Hook"}"#;
188            let actual = UpdateWebhook::new(&client, WEBHOOK_ID)
189                .name("Captain Hook")
190                .try_into_request()?;
191
192            assert_eq!(Some(expected.as_bytes()), actual.body());
193        }
194
195        {
196            let expected = r#"{"avatar":null,"channel_id":"2","name":"Captain Hook"}"#;
197            let actual = UpdateWebhook::new(&client, WEBHOOK_ID)
198                .avatar(None)
199                .channel_id(CHANNEL_ID)
200                .name("Captain Hook")
201                .try_into_request()?;
202
203            assert_eq!(Some(expected.as_bytes()), actual.body());
204        }
205        Ok(())
206    }
207}