diff --git a/cache/in-memory/src/event/interaction.rs b/cache/in-memory/src/event/interaction.rs index 0247e2ac42a..cc833cbfcb2 100644 --- a/cache/in-memory/src/event/interaction.rs +++ b/cache/in-memory/src/event/interaction.rs @@ -104,6 +104,7 @@ mod tests { UserId::new(7).expect("non zero"), InteractionMember { avatar: None, + communication_disabled_until: None, joined_at: timestamp, nick: None, pending: false, @@ -149,6 +150,7 @@ mod tests { kind: MessageType::Regular, member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at: timestamp, mute: false, @@ -223,6 +225,7 @@ mod tests { kind: InteractionType::ApplicationCommand, member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at: timestamp, mute: false, diff --git a/cache/in-memory/src/event/member.rs b/cache/in-memory/src/event/member.rs index e5995400814..ea7d3d46b5d 100644 --- a/cache/in-memory/src/event/member.rs +++ b/cache/in-memory/src/event/member.rs @@ -33,6 +33,7 @@ impl InMemoryCache { self.cache_user(Cow::Owned(member.user), Some(guild_id)); let cached = CachedMember { avatar: member.avatar, + communication_disabled_until: member.communication_disabled_until, deaf: Some(member.deaf), guild_id, joined_at: member.joined_at, @@ -71,6 +72,7 @@ impl InMemoryCache { let cached = CachedMember { avatar: member.avatar.to_owned(), + communication_disabled_until: member.communication_disabled_until.to_owned(), deaf: Some(member.deaf), guild_id, joined_at: member.joined_at, @@ -105,6 +107,7 @@ impl InMemoryCache { let cached = CachedMember { avatar, + communication_disabled_until: member.communication_disabled_until.to_owned(), deaf, guild_id, joined_at: member.joined_at, diff --git a/cache/in-memory/src/event/message.rs b/cache/in-memory/src/event/message.rs index 802230729e6..8176746f5ee 100644 --- a/cache/in-memory/src/event/message.rs +++ b/cache/in-memory/src/event/message.rs @@ -180,6 +180,7 @@ mod tests { kind: MessageType::Regular, member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at, mute: false, diff --git a/cache/in-memory/src/event/voice_state.rs b/cache/in-memory/src/event/voice_state.rs index 824ae431afd..db00620ee2a 100644 --- a/cache/in-memory/src/event/voice_state.rs +++ b/cache/in-memory/src/event/voice_state.rs @@ -332,6 +332,7 @@ mod tests { guild_id: Some(GuildId::new(2).expect("non zero")), member: Some(Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(2).expect("non zero"), joined_at, diff --git a/cache/in-memory/src/lib.rs b/cache/in-memory/src/lib.rs index 5f097abc077..8e1fab10db5 100644 --- a/cache/in-memory/src/lib.rs +++ b/cache/in-memory/src/lib.rs @@ -920,6 +920,7 @@ mod tests { guild_id, Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id, joined_at, diff --git a/cache/in-memory/src/model/member.rs b/cache/in-memory/src/model/member.rs index e92663c019f..0408c3aac87 100644 --- a/cache/in-memory/src/model/member.rs +++ b/cache/in-memory/src/model/member.rs @@ -12,6 +12,7 @@ use twilight_model::{ #[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct CachedMember { pub(crate) avatar: Option, + pub(crate) communication_disabled_until: Option, pub(crate) deaf: Option, pub(crate) guild_id: GuildId, pub(crate) joined_at: Timestamp, @@ -29,6 +30,19 @@ impl CachedMember { self.avatar.as_deref() } + /// When the user can resume communication in a guild again. + /// + /// Checking if this value is [`Some`] is not enough to know if a used is currently + /// timed out as Discord doesn't send any events when the timeout expires, and + /// therefore the cache is not updated accordingly. You should ensure that the + /// provided [`Timestamp`] is not in the past. See [discord-api-docs#4269] for + /// more information. + /// + /// [discord-api-docs#4269]: https://github.com/discord/discord-api-docs/issues/4269 + pub const fn communication_disabled_until(&self) -> Option { + self.communication_disabled_until + } + /// Whether the member is deafened in a voice channel. pub const fn deaf(&self) -> Option { self.deaf @@ -80,6 +94,7 @@ impl PartialEq for CachedMember { fn eq(&self, other: &Member) -> bool { ( &self.avatar, + &self.communication_disabled_until, self.deaf, self.joined_at, self.mute, @@ -90,6 +105,7 @@ impl PartialEq for CachedMember { self.user_id, ) == ( &other.avatar, + &other.communication_disabled_until, Some(other.deaf), other.joined_at, Some(other.mute), @@ -105,6 +121,7 @@ impl PartialEq for CachedMember { impl PartialEq for CachedMember { fn eq(&self, other: &PartialMember) -> bool { ( + &self.communication_disabled_until, self.deaf, self.joined_at, self.mute, @@ -112,6 +129,7 @@ impl PartialEq for CachedMember { self.premium_since, &self.roles, ) == ( + &other.communication_disabled_until, Some(other.deaf), other.joined_at, Some(other.mute), @@ -162,6 +180,7 @@ mod tests { CachedMember { avatar: None, + communication_disabled_until: None, deaf: Some(false), guild_id: GuildId::new(3).expect("non zero"), joined_at, @@ -200,6 +219,7 @@ mod tests { let member = Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(3).expect("non zero"), joined_at, @@ -220,6 +240,7 @@ mod tests { let member = PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at, mute: true, diff --git a/cache/in-memory/src/test.rs b/cache/in-memory/src/test.rs index bf9b1f095f2..3b1741ee11a 100644 --- a/cache/in-memory/src/test.rs +++ b/cache/in-memory/src/test.rs @@ -50,6 +50,7 @@ pub fn cache_with_message_and_reactions() -> InMemoryCache { kind: MessageType::Regular, member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at, mute: false, @@ -84,6 +85,7 @@ pub fn cache_with_message_and_reactions() -> InMemoryCache { guild_id: Some(GuildId::new(1).expect("non zero")), member: Some(Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -118,6 +120,7 @@ pub fn cache_with_message_and_reactions() -> InMemoryCache { reaction.member.replace(Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -215,6 +218,7 @@ pub fn member(id: UserId, guild_id: GuildId) -> Member { Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id, joined_at, diff --git a/http/src/request/guild/member/update_guild_member.rs b/http/src/request/guild/member/update_guild_member.rs index e0c9860f676..4625348b022 100644 --- a/http/src/request/guild/member/update_guild_member.rs +++ b/http/src/request/guild/member/update_guild_member.rs @@ -10,7 +10,10 @@ use std::{ error::Error, fmt::{Display, Formatter, Result as FmtResult}, }; -use twilight_model::id::{ChannelId, GuildId, RoleId, UserId}; +use twilight_model::{ + datetime::Timestamp, + id::{ChannelId, GuildId, RoleId, UserId}, +}; /// The error created when the member can not be updated as configured. #[derive(Debug)] @@ -50,6 +53,9 @@ impl Display for UpdateGuildMemberError { UpdateGuildMemberErrorType::NicknameInvalid => { f.write_str("the nickname length is invalid") } + UpdateGuildMemberErrorType::TimeoutExpiryTimestampInvalid => { + f.write_str("the timeout expiry is more than 28 days from the current time") + } } } } @@ -62,6 +68,8 @@ impl Error for UpdateGuildMemberError {} pub enum UpdateGuildMemberErrorType { /// The nickname is either empty or the length is more than 32 UTF-16 characters. NicknameInvalid, + /// The timeout expiry timestamp is more than 28 days from the current timestamp + TimeoutExpiryTimestampInvalid, } #[derive(Serialize)] @@ -70,6 +78,8 @@ struct UpdateGuildMemberFields<'a> { #[serde(skip_serializing_if = "Option::is_none")] channel_id: Option>, #[serde(skip_serializing_if = "Option::is_none")] + communication_disabled_util: Option>, + #[serde(skip_serializing_if = "Option::is_none")] deaf: Option, #[serde(skip_serializing_if = "Option::is_none")] mute: Option, @@ -98,6 +108,7 @@ impl<'a> UpdateGuildMember<'a> { Self { fields: UpdateGuildMemberFields { channel_id: None, + communication_disabled_util: None, deaf: None, mute: None, nick: None, @@ -117,6 +128,36 @@ impl<'a> UpdateGuildMember<'a> { self } + /// Set the member's [Guild Timeout]. + /// + /// The timestamp indicates when the user will be able to communicate again. + /// It can be up to 28 days in the future. Set to [`None`] to remove the + /// timeout. Requires the [`MODERATE_MEMBERS`] permission. + /// + /// [Guild Timeout]: https://support.discord.com/hc/en-us/articles/4413305239191-Time-Out-FAQ + /// [`MODERATE_MEMBERS`]: twilight_model::guild::Permissions::MODERATE_MEMBERS + /// + /// # Errors + /// Returns an [`UpdateGuildMemberErrorType::TimeoutExpiryTimestampInvalid`] + /// error type if the expiry timestamp is more than 28 days from the current time. + /// + pub fn communication_disabled_until( + mut self, + timestamp: Option, + ) -> Result { + if let Some(timestamp) = timestamp { + if !validate_inner::communication_disabled_until(timestamp) { + return Err(UpdateGuildMemberError { + kind: UpdateGuildMemberErrorType::TimeoutExpiryTimestampInvalid, + }); + } + } + + self.fields.communication_disabled_util = Some(NullableField(timestamp)); + + Ok(self) + } + /// If true, restrict the member's ability to hear sound from a voice channel. pub const fn deaf(mut self, deaf: bool) -> Self { self.fields.deaf = Some(deaf); @@ -227,6 +268,7 @@ mod tests { let body = UpdateGuildMemberFields { channel_id: None, + communication_disabled_util: None, deaf: Some(true), mute: Some(true), nick: None, @@ -252,6 +294,7 @@ mod tests { let body = UpdateGuildMemberFields { channel_id: None, + communication_disabled_util: None, deaf: None, mute: None, nick: Some(NullableField(None)), @@ -276,6 +319,7 @@ mod tests { let body = UpdateGuildMemberFields { channel_id: None, + communication_disabled_util: None, deaf: None, mute: None, nick: Some(NullableField(Some("foo"))), diff --git a/http/src/request/validate.rs b/http/src/request/validate.rs index acf9bbbdd45..6d81b98257d 100644 --- a/http/src/request/validate.rs +++ b/http/src/request/validate.rs @@ -7,10 +7,12 @@ use super::{application::InteractionError, guild::sticker::StickerValidationErro use std::{ error::Error, fmt::{Display, Formatter, Result as FmtResult}, + time::{SystemTime, UNIX_EPOCH}, }; use twilight_model::{ application::component::{select_menu::SelectMenuOption, Component, ComponentType}, channel::{embed::Embed, ChannelType}, + datetime::Timestamp, }; /// A provided [`Component`] is invalid. @@ -1175,6 +1177,26 @@ fn _sticker_tags(value: &str) -> bool { .contains(&len) } +pub fn communication_disabled_until(timestamp: Timestamp) -> bool { + _communication_disabled_until(timestamp) +} + +const COMMUNICATION_DISABLED_MAX_DURATION: i64 = 28 * 24 * 60 * 60; + +#[allow(clippy::cast_possible_wrap)] // casting of unix timestamp should never wrap +fn _communication_disabled_until(timestamp: Timestamp) -> bool { + let now = SystemTime::now().duration_since(UNIX_EPOCH); + + match now { + Ok(now) => { + let end = timestamp.as_secs(); + + end - now.as_secs() as i64 <= COMMUNICATION_DISABLED_MAX_DURATION + } + Err(_) => false, + } +} + /// Validate the number of guild command permission overwrites. /// /// The maximum number of commands allowed in a guild is defined by diff --git a/model/src/application/interaction/application_command/data/resolved.rs b/model/src/application/interaction/application_command/data/resolved.rs index af2f44f1ecb..704fa8f3582 100644 --- a/model/src/application/interaction/application_command/data/resolved.rs +++ b/model/src/application/interaction/application_command/data/resolved.rs @@ -40,6 +40,7 @@ pub struct InteractionMember { /// Member's guild avatar. #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, + pub communication_disabled_until: Option, pub joined_at: Timestamp, pub nick: Option, /// Whether the user has yet to pass the guild's Membership Screening @@ -95,6 +96,7 @@ mod tests { UserId::new(300).expect("non zero"), InteractionMember { avatar: None, + communication_disabled_until: None, joined_at, nick: None, pending: false, @@ -140,6 +142,7 @@ mod tests { kind: MessageType::Regular, member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at, mute: false, @@ -243,8 +246,10 @@ mod tests { Token::Str("300"), Token::Struct { name: "InteractionMember", - len: 5, + len: 6, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("joined_at"), Token::Str("2021-08-10T12:18:37.000000+00:00"), Token::Str("nick"), @@ -317,8 +322,10 @@ mod tests { Token::Some, Token::Struct { name: "PartialMember", - len: 7, + len: 8, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("joined_at"), diff --git a/model/src/application/interaction/message_component/mod.rs b/model/src/application/interaction/message_component/mod.rs index dad868bec2a..0eeb1210575 100644 --- a/model/src/application/interaction/message_component/mod.rs +++ b/model/src/application/interaction/message_component/mod.rs @@ -152,6 +152,7 @@ mod tests { kind: InteractionType::MessageComponent, member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at: timestamp, mute: false, diff --git a/model/src/application/interaction/mod.rs b/model/src/application/interaction/mod.rs index de2b23d06cd..f8a60e517db 100644 --- a/model/src/application/interaction/mod.rs +++ b/model/src/application/interaction/mod.rs @@ -357,6 +357,7 @@ mod test { UserId::new(600).expect("non zero"), InteractionMember { avatar: None, + communication_disabled_until: None, joined_at, nick: Some("nickname".into()), pending: false, @@ -396,6 +397,7 @@ mod test { kind: InteractionType::ApplicationCommand, member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at, mute: false, @@ -477,8 +479,10 @@ mod test { Token::Str("600"), Token::Struct { name: "InteractionMember", - len: 5, + len: 6, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("joined_at"), Token::Str("2020-01-01T00:00:00.000000+00:00"), Token::Str("nick"), @@ -536,8 +540,10 @@ mod test { Token::Some, Token::Struct { name: "PartialMember", - len: 7, + len: 8, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("joined_at"), diff --git a/model/src/channel/message/mention.rs b/model/src/channel/message/mention.rs index 823cec9d418..2f6d2d50904 100644 --- a/model/src/channel/message/mention.rs +++ b/model/src/channel/message/mention.rs @@ -46,9 +46,8 @@ impl Mention { mod tests { use std::str::FromStr; - use crate::datetime::{Timestamp, TimestampParseError}; - use super::{Mention, PartialMember, UserFlags, UserId}; + use crate::datetime::{Timestamp, TimestampParseError}; use serde_test::Token; #[test] @@ -101,6 +100,7 @@ mod tests { id: UserId::new(1).expect("non zero"), member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at, mute: true, @@ -134,8 +134,10 @@ mod tests { Token::Some, Token::Struct { name: "PartialMember", - len: 7, + len: 8, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("joined_at"), diff --git a/model/src/channel/message/mod.rs b/model/src/channel/message/mod.rs index 05f264ae39c..8f203593cdd 100644 --- a/model/src/channel/message/mod.rs +++ b/model/src/channel/message/mod.rs @@ -145,6 +145,7 @@ mod tests { kind: MessageType::Regular, member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at, mute: false, @@ -231,8 +232,10 @@ mod tests { Token::Some, Token::Struct { name: "PartialMember", - len: 7, + len: 8, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("joined_at"), @@ -336,6 +339,7 @@ mod tests { kind: MessageType::Regular, member: Some(PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at, mute: false, @@ -476,8 +480,10 @@ mod tests { Token::Some, Token::Struct { name: "PartialMember", - len: 7, + len: 8, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("joined_at"), diff --git a/model/src/channel/reaction.rs b/model/src/channel/reaction.rs index 7f7586cb951..99b7ef3e792 100644 --- a/model/src/channel/reaction.rs +++ b/model/src/channel/reaction.rs @@ -199,6 +199,7 @@ mod tests { guild_id: Some(GuildId::new(1).expect("non zero")), member: Some(Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -255,8 +256,10 @@ mod tests { Token::Some, Token::Struct { name: "Member", - len: 8, + len: 9, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("guild_id"), diff --git a/model/src/gateway/payload/incoming/member_add.rs b/model/src/gateway/payload/incoming/member_add.rs index baced333dbf..9f98111cc1b 100644 --- a/model/src/gateway/payload/incoming/member_add.rs +++ b/model/src/gateway/payload/incoming/member_add.rs @@ -35,6 +35,7 @@ mod tests { let value = MemberAdd(Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -68,8 +69,10 @@ mod tests { Token::NewtypeStruct { name: "MemberAdd" }, Token::Struct { name: "Member", - len: 8, + len: 9, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("guild_id"), diff --git a/model/src/gateway/payload/incoming/member_chunk.rs b/model/src/gateway/payload/incoming/member_chunk.rs index e86d86d581e..f50333ac395 100644 --- a/model/src/gateway/payload/incoming/member_chunk.rs +++ b/model/src/gateway/payload/incoming/member_chunk.rs @@ -227,6 +227,7 @@ mod tests { "chunk_index": 0, "guild_id": "1", "members": [{ + "communication_disabled_until": null, "deaf": false, "hoisted_role": "6", "joined_at": "2020-04-04T04:04:04.000000+00:00", @@ -242,6 +243,7 @@ mod tests { "username": "test", }, }, { + "communication_disabled_until": null, "deaf": false, "hoisted_role": "6", "joined_at": "2020-04-04T04:04:04.000000+00:00", @@ -255,6 +257,7 @@ mod tests { "username": "test", }, }, { + "communication_disabled_until": null, "deaf": false, "hoisted_role": "6", "joined_at": "2020-04-04T04:04:04.000000+00:00", @@ -269,6 +272,7 @@ mod tests { "username": "test", }, }, { + "communication_disabled_until": null, "deaf": false, "hoisted_role": "6", "joined_at": "2020-04-04T04:04:04.000000+00:00", @@ -326,6 +330,7 @@ mod tests { members: Vec::from([ Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -357,6 +362,7 @@ mod tests { }, Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -385,6 +391,7 @@ mod tests { }, Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -413,6 +420,7 @@ mod tests { }, Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, diff --git a/model/src/gateway/payload/incoming/thread_members_update.rs b/model/src/gateway/payload/incoming/thread_members_update.rs index 37ac4bbcd3c..0eb04649b29 100644 --- a/model/src/gateway/payload/incoming/thread_members_update.rs +++ b/model/src/gateway/payload/incoming/thread_members_update.rs @@ -107,6 +107,7 @@ mod tests { let member = Member { avatar: Some("guild avatar".to_owned()), + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(2).expect("non zero"), joined_at, @@ -212,11 +213,13 @@ mod tests { Token::Some, Token::Struct { name: "MemberIntermediary", - len: 10, + len: 11, }, Token::Str("avatar"), Token::Some, Token::Str("guild avatar"), + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("guild_id"), diff --git a/model/src/gateway/payload/incoming/typing_start.rs b/model/src/gateway/payload/incoming/typing_start.rs index f3b24478cef..f2835e57ca9 100644 --- a/model/src/gateway/payload/incoming/typing_start.rs +++ b/model/src/gateway/payload/incoming/typing_start.rs @@ -184,6 +184,7 @@ mod tests { guild_id: Some(GuildId::new(1).expect("non zero")), member: Some(Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -232,8 +233,10 @@ mod tests { Token::Some, Token::Struct { name: "Member", - len: 8, + len: 9, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("guild_id"), diff --git a/model/src/gateway/payload/incoming/voice_state_update.rs b/model/src/gateway/payload/incoming/voice_state_update.rs index 0a4b0aaa89c..a52d98ddb19 100644 --- a/model/src/gateway/payload/incoming/voice_state_update.rs +++ b/model/src/gateway/payload/incoming/voice_state_update.rs @@ -27,6 +27,7 @@ mod tests { guild_id: Some(GuildId::new(1).expect("non zero")), member: Some(Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -86,8 +87,10 @@ mod tests { Token::Some, Token::Struct { name: "Member", - len: 8, + len: 9, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("guild_id"), @@ -162,6 +165,7 @@ mod tests { guild_id: Some(GuildId::new(999_999).expect("non zero")), member: Some(Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(999_999).expect("non zero"), joined_at, @@ -228,8 +232,10 @@ mod tests { Token::Some, Token::Struct { name: "Member", - len: 8, + len: 9, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("joined_at"), diff --git a/model/src/guild/member.rs b/model/src/guild/member.rs index 11232a8af8d..b86081d01ab 100644 --- a/model/src/guild/member.rs +++ b/model/src/guild/member.rs @@ -18,6 +18,7 @@ pub struct Member { /// Member's guild avatar. #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, + pub communication_disabled_until: Option, pub deaf: bool, pub guild_id: GuildId, pub joined_at: Timestamp, @@ -43,6 +44,7 @@ pub struct MemberIntermediary { /// Member's guild avatar. #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, + pub communication_disabled_until: Option, pub deaf: bool, pub joined_at: Timestamp, pub mute: bool, @@ -61,6 +63,7 @@ impl MemberIntermediary { pub fn into_member(self, guild_id: GuildId) -> Member { Member { avatar: self.avatar, + communication_disabled_until: self.communication_disabled_until, deaf: self.deaf, guild_id, joined_at: self.joined_at, @@ -113,6 +116,7 @@ impl<'de> Visitor<'de> for MemberVisitor { Ok(Member { avatar: member.avatar, + communication_disabled_until: member.communication_disabled_until, deaf: member.deaf, guild_id: self.0, joined_at: member.joined_at, @@ -220,6 +224,7 @@ mod tests { let value = Member { avatar: Some("guild avatar".to_owned()), + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(1).expect("non zero"), joined_at, @@ -252,11 +257,110 @@ mod tests { &[ Token::Struct { name: "Member", - len: 10, + len: 11, }, Token::Str("avatar"), Token::Some, Token::Str("guild avatar"), + Token::Str("communication_disabled_until"), + Token::None, + Token::Str("deaf"), + Token::Bool(false), + Token::Str("guild_id"), + Token::NewtypeStruct { name: "GuildId" }, + Token::Str("1"), + Token::Str("joined_at"), + Token::Str("2015-04-26T06:26:56.936000+00:00"), + Token::Str("mute"), + Token::Bool(true), + Token::Str("nick"), + Token::Some, + Token::Str("twilight"), + Token::Str("pending"), + Token::Bool(false), + Token::Str("premium_since"), + Token::Some, + Token::Str("2021-03-16T14:29:19.046000+00:00"), + Token::Str("roles"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::Str("user"), + Token::Struct { + name: "User", + len: 7, + }, + Token::Str("accent_color"), + Token::None, + Token::Str("avatar"), + Token::None, + Token::Str("banner"), + Token::None, + Token::Str("bot"), + Token::Bool(false), + Token::Str("discriminator"), + Token::Str("0001"), + Token::Str("id"), + Token::NewtypeStruct { name: "UserId" }, + Token::Str("3"), + Token::Str("username"), + Token::Str("twilight"), + Token::StructEnd, + Token::StructEnd, + ], + ); + + Ok(()) + } + + #[test] + fn test_guild_member_communication_disabled_until() -> Result<(), TimestampParseError> { + let communication_disabled_until = Timestamp::from_str("2021-12-23T14:29:19.046000+00:00")?; + let joined_at = Timestamp::from_str("2015-04-26T06:26:56.936000+00:00")?; + let premium_since = Timestamp::from_str("2021-03-16T14:29:19.046000+00:00")?; + + let value = Member { + avatar: Some("guild avatar".to_owned()), + communication_disabled_until: Some(communication_disabled_until), + deaf: false, + guild_id: GuildId::new(1).expect("non zero"), + joined_at, + mute: true, + nick: Some("twilight".to_owned()), + pending: false, + premium_since: Some(premium_since), + roles: Vec::new(), + user: User { + accent_color: None, + avatar: None, + banner: None, + bot: false, + discriminator: 1, + email: None, + flags: None, + id: UserId::new(3).expect("non zero"), + locale: None, + mfa_enabled: None, + name: "twilight".to_owned(), + premium_type: None, + public_flags: None, + system: None, + verified: None, + }, + }; + + serde_test::assert_tokens( + &value, + &[ + Token::Struct { + name: "Member", + len: 11, + }, + Token::Str("avatar"), + Token::Some, + Token::Str("guild avatar"), + Token::Str("communication_disabled_until"), + Token::Some, + Token::Str("2021-12-23T14:29:19.046000+00:00"), Token::Str("deaf"), Token::Bool(false), Token::Str("guild_id"), diff --git a/model/src/guild/partial_member.rs b/model/src/guild/partial_member.rs index 368189e9aec..4d904996c7a 100644 --- a/model/src/guild/partial_member.rs +++ b/model/src/guild/partial_member.rs @@ -6,6 +6,7 @@ pub struct PartialMember { /// Member's guild avatar. #[serde(skip_serializing_if = "Option::is_none")] pub avatar: Option, + pub communication_disabled_until: Option, pub deaf: bool, pub joined_at: Timestamp, pub mute: bool, @@ -35,6 +36,7 @@ mod tests { let value = PartialMember { avatar: None, + communication_disabled_until: None, deaf: false, joined_at, mute: true, @@ -50,8 +52,10 @@ mod tests { &[ Token::Struct { name: "PartialMember", - len: 7, + len: 8, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("joined_at"), diff --git a/model/src/guild/permissions.rs b/model/src/guild/permissions.rs index b78b0ff647e..2ec4abe2298 100644 --- a/model/src/guild/permissions.rs +++ b/model/src/guild/permissions.rs @@ -51,6 +51,14 @@ bitflags! { /// Allows for launching activities (applications with the `EMBEDDED` /// flag) in a voice channel. const START_EMBEDDED_ACTIVITIES = 1 << 39; + /// Allows for timing out users to prevent them from sending or reacting + /// to messages in chat and threads, and from speaking in voice and + /// stage channels. + /// + /// See Discord's article on [Guild Timeouts]. + /// + /// [Guild Timeouts]: https://support.discord.com/hc/en-us/articles/4413305239191-Time-Out-FAQ + const MODERATE_MEMBERS = 1 << 40; } } diff --git a/model/src/voice/voice_state.rs b/model/src/voice/voice_state.rs index cfe77a4c1a9..d3a5d2e9975 100644 --- a/model/src/voice/voice_state.rs +++ b/model/src/voice/voice_state.rs @@ -365,6 +365,7 @@ mod tests { guild_id: Some(GuildId::new(2).expect("non zero")), member: Some(Member { avatar: None, + communication_disabled_until: None, deaf: false, guild_id: GuildId::new(2).expect("non zero"), joined_at, @@ -423,8 +424,10 @@ mod tests { Token::Some, Token::Struct { name: "Member", - len: 9, + len: 10, }, + Token::Str("communication_disabled_until"), + Token::None, Token::Str("deaf"), Token::Bool(false), Token::Str("guild_id"),