twilight_http/request/channel/webhook/
update_webhook.rs

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