Skip to main content

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