twilight_http/request/guild/member/
update_guild_member.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    guild::Member,
12    id::{
13        marker::{ChannelMarker, GuildMarker, RoleMarker, UserMarker},
14        Id,
15    },
16    util::Timestamp,
17};
18use twilight_validate::request::{
19    audit_reason as validate_audit_reason,
20    communication_disabled_until as validate_communication_disabled_until,
21    nickname as validate_nickname, ValidationError,
22};
23
24#[derive(Serialize)]
25struct UpdateGuildMemberFields<'a> {
26    #[allow(clippy::option_option)]
27    #[serde(skip_serializing_if = "Option::is_none")]
28    channel_id: Option<Nullable<Id<ChannelMarker>>>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    communication_disabled_until: Option<Nullable<Timestamp>>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    deaf: Option<bool>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    mute: Option<bool>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    nick: Option<Nullable<&'a str>>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    roles: Option<&'a [Id<RoleMarker>]>,
39}
40
41/// Update a guild member.
42///
43/// All fields are optional. See [Discord Docs/Modify Guild Member].
44///
45/// [Discord Docs/Modify Guild Member]: https://discord.com/developers/docs/resources/guild#modify-guild-member
46#[must_use = "requests must be configured and executed"]
47pub struct UpdateGuildMember<'a> {
48    fields: Result<UpdateGuildMemberFields<'a>, ValidationError>,
49    guild_id: Id<GuildMarker>,
50    http: &'a Client,
51    user_id: Id<UserMarker>,
52    reason: Result<Option<&'a str>, ValidationError>,
53}
54
55impl<'a> UpdateGuildMember<'a> {
56    pub(crate) const fn new(
57        http: &'a Client,
58        guild_id: Id<GuildMarker>,
59        user_id: Id<UserMarker>,
60    ) -> Self {
61        Self {
62            fields: Ok(UpdateGuildMemberFields {
63                channel_id: None,
64                communication_disabled_until: None,
65                deaf: None,
66                mute: None,
67                nick: None,
68                roles: None,
69            }),
70            guild_id,
71            http,
72            user_id,
73            reason: Ok(None),
74        }
75    }
76
77    /// Move the member to a different voice channel.
78    pub fn channel_id(mut self, channel_id: Option<Id<ChannelMarker>>) -> Self {
79        if let Ok(fields) = self.fields.as_mut() {
80            fields.channel_id = Some(Nullable(channel_id));
81        }
82
83        self
84    }
85
86    /// Set the member's [Guild Timeout].
87    ///
88    /// The timestamp indicates when the user will be able to communicate again.
89    /// It can be up to 28 days in the future. Set to [`None`] to remove the
90    /// timeout. Requires the [`MODERATE_MEMBERS`] permission. If this is set,
91    /// and if the target member is an administrator or the owner of the guild,
92    /// the response status code will be 403.
93    ///
94    /// # Errors
95    ///
96    /// Returns an error of type [`CommunicationDisabledUntil`] if the expiry
97    /// timestamp is more than 28 days from the current time.
98    ///
99    /// [Guild Timeout]: https://support.discord.com/hc/en-us/articles/4413305239191-Time-Out-FAQ
100    /// [`CommunicationDisabledUntil`]: twilight_validate::request::ValidationErrorType::CommunicationDisabledUntil
101    /// [`MODERATE_MEMBERS`]: twilight_model::guild::Permissions::MODERATE_MEMBERS
102    pub fn communication_disabled_until(mut self, timestamp: Option<Timestamp>) -> Self {
103        self.fields = self.fields.and_then(|mut fields| {
104            if let Some(timestamp) = timestamp {
105                validate_communication_disabled_until(timestamp)?;
106            }
107
108            fields.communication_disabled_until = Some(Nullable(timestamp));
109
110            Ok(fields)
111        });
112
113        self
114    }
115
116    /// If true, restrict the member's ability to hear sound from a voice channel.
117    pub fn deaf(mut self, deaf: bool) -> Self {
118        if let Ok(fields) = self.fields.as_mut() {
119            fields.deaf = Some(deaf);
120        }
121
122        self
123    }
124
125    /// If true, restrict the member's ability to speak in a voice channel.
126    pub fn mute(mut self, mute: bool) -> Self {
127        if let Ok(fields) = self.fields.as_mut() {
128            fields.mute = Some(mute);
129        }
130
131        self
132    }
133
134    /// Set the nickname.
135    ///
136    /// The minimum length is 1 UTF-16 character and the maximum is 32 UTF-16 characters.
137    ///
138    /// # Errors
139    ///
140    /// Returns an error of type [`Nickname`] if the nickname length is too
141    /// short or too long.
142    ///
143    /// [`Nickname`]: twilight_validate::request::ValidationErrorType::Nickname
144    pub fn nick(mut self, nick: Option<&'a str>) -> Self {
145        self.fields = self.fields.and_then(|mut fields| {
146            if let Some(nick) = nick {
147                validate_nickname(nick)?;
148            }
149
150            fields.nick = Some(Nullable(nick));
151
152            Ok(fields)
153        });
154
155        self
156    }
157
158    /// Set the new list of roles for a member.
159    pub fn roles(mut self, roles: &'a [Id<RoleMarker>]) -> Self {
160        if let Ok(fields) = self.fields.as_mut() {
161            fields.roles = Some(roles);
162        }
163
164        self
165    }
166}
167
168impl<'a> AuditLogReason<'a> for UpdateGuildMember<'a> {
169    fn reason(mut self, reason: &'a str) -> Self {
170        self.reason = validate_audit_reason(reason).and(Ok(Some(reason)));
171
172        self
173    }
174}
175
176impl IntoFuture for UpdateGuildMember<'_> {
177    type Output = Result<Response<Member>, Error>;
178
179    type IntoFuture = ResponseFuture<Member>;
180
181    fn into_future(self) -> Self::IntoFuture {
182        let http = self.http;
183
184        match self.try_into_request() {
185            Ok(request) => http.request(request),
186            Err(source) => ResponseFuture::error(source),
187        }
188    }
189}
190
191impl TryIntoRequest for UpdateGuildMember<'_> {
192    fn try_into_request(self) -> Result<Request, Error> {
193        let fields = self.fields.map_err(Error::validation)?;
194        let mut request = Request::builder(&Route::UpdateMember {
195            guild_id: self.guild_id.get(),
196            user_id: self.user_id.get(),
197        })
198        .json(&fields);
199
200        if let Some(reason) = self.reason.map_err(Error::validation)? {
201            request = request.headers(request::audit_header(reason)?);
202        }
203
204        request.build()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::{UpdateGuildMember, UpdateGuildMemberFields};
211    use crate::{
212        request::{Nullable, Request, TryIntoRequest},
213        routing::Route,
214        Client,
215    };
216    use std::error::Error;
217    use twilight_model::id::{
218        marker::{GuildMarker, UserMarker},
219        Id,
220    };
221
222    const GUILD_ID: Id<GuildMarker> = Id::new(1);
223    const USER_ID: Id<UserMarker> = Id::new(1);
224
225    #[test]
226    fn request() -> Result<(), Box<dyn Error>> {
227        let client = Client::new("foo".to_owned());
228        let builder = UpdateGuildMember::new(&client, GUILD_ID, USER_ID)
229            .deaf(true)
230            .mute(true);
231        let actual = builder.try_into_request()?;
232
233        let body = UpdateGuildMemberFields {
234            channel_id: None,
235            communication_disabled_until: None,
236            deaf: Some(true),
237            mute: Some(true),
238            nick: None,
239            roles: None,
240        };
241        let route = Route::UpdateMember {
242            guild_id: GUILD_ID.get(),
243            user_id: USER_ID.get(),
244        };
245        let expected = Request::builder(&route).json(&body).build()?;
246
247        assert_eq!(actual.body, expected.body);
248        assert_eq!(actual.path, expected.path);
249
250        Ok(())
251    }
252
253    #[test]
254    fn nick_set_null() -> Result<(), Box<dyn Error>> {
255        let client = Client::new("foo".to_owned());
256        let builder = UpdateGuildMember::new(&client, GUILD_ID, USER_ID).nick(None);
257        let actual = builder.try_into_request()?;
258
259        let body = UpdateGuildMemberFields {
260            channel_id: None,
261            communication_disabled_until: None,
262            deaf: None,
263            mute: None,
264            nick: Some(Nullable(None)),
265            roles: None,
266        };
267        let route = Route::UpdateMember {
268            guild_id: GUILD_ID.get(),
269            user_id: USER_ID.get(),
270        };
271        let expected = Request::builder(&route).json(&body).build()?;
272
273        assert_eq!(actual.body, expected.body);
274
275        Ok(())
276    }
277
278    #[test]
279    fn nick_set_value() -> Result<(), Box<dyn Error>> {
280        let client = Client::new("foo".to_owned());
281        let builder = UpdateGuildMember::new(&client, GUILD_ID, USER_ID).nick(Some("foo"));
282        let actual = builder.try_into_request()?;
283
284        let body = UpdateGuildMemberFields {
285            channel_id: None,
286            communication_disabled_until: None,
287            deaf: None,
288            mute: None,
289            nick: Some(Nullable(Some("foo"))),
290            roles: None,
291        };
292        let route = Route::UpdateMember {
293            guild_id: GUILD_ID.get(),
294            user_id: USER_ID.get(),
295        };
296        let expected = Request::builder(&route).json(&body).build()?;
297
298        assert_eq!(actual.body, expected.body);
299
300        Ok(())
301    }
302}