Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cache,http,model): guild timeouts #1342

Merged
merged 28 commits into from
Dec 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
74ad224
feature: add support for guild timeouts
HTGAzureX1212 Dec 21, 2021
d36638f
fix: fix build errors
HTGAzureX1212 Dec 21, 2021
b0d08e9
fix: fix build errors again
HTGAzureX1212 Dec 21, 2021
a88d4a6
fix: fix build errors again
HTGAzureX1212 Dec 21, 2021
81afde5
fix: fix build errors again
HTGAzureX1212 Dec 21, 2021
bdcfdf1
fix: fix code mistake of using none for communication_disabled_until …
HTGAzureX1212 Dec 21, 2021
ba040d6
enhancement: resolve some comments
HTGAzureX1212 Dec 22, 2021
4f57ea4
rustfmt
HTGAzureX1212 Dec 22, 2021
db462a8
fix: fix model tests
HTGAzureX1212 Dec 22, 2021
aff0ce1
enhancement: add timestamp validation
HTGAzureX1212 Dec 22, 2021
1c8eb39
implement membertimeoutstate structure
HTGAzureX1212 Dec 23, 2021
48a0ef3
migrate everything to membertimeoutstate structure
HTGAzureX1212 Dec 23, 2021
c5208b5
fix some tests
HTGAzureX1212 Dec 23, 2021
5294508
fix lints
HTGAzureX1212 Dec 23, 2021
f590fe8
fix more lints
HTGAzureX1212 Dec 23, 2021
f5f3ba8
fix remaining tests
HTGAzureX1212 Dec 23, 2021
8a609e8
fix build errors
HTGAzureX1212 Dec 23, 2021
c801779
Merge branch 'main' into feature/timeout
HTGAzureX1212 Dec 23, 2021
0aafe43
remove duplicate imports
HTGAzureX1212 Dec 23, 2021
82b6c9b
fix cache
HTGAzureX1212 Dec 23, 2021
bade956
fix rustfmt and test
HTGAzureX1212 Dec 23, 2021
80ef748
fix tests again
HTGAzureX1212 Dec 23, 2021
40ab135
improve documentation and minor code nits
HTGAzureX1212 Dec 23, 2021
3a3b72a
remove membertimeoutstate
HTGAzureX1212 Dec 24, 2021
2f78ff9
rustfmt
HTGAzureX1212 Dec 24, 2021
552bc82
remark the insufficiency of communication_disabled_until
HTGAzureX1212 Dec 24, 2021
3be9efc
use match instead of early return
HTGAzureX1212 Dec 25, 2021
caa8d86
fix stupid mistake
HTGAzureX1212 Dec 25, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cache/in-memory/src/event/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions cache/in-memory/src/event/member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions cache/in-memory/src/event/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ mod tests {
kind: MessageType::Regular,
member: Some(PartialMember {
avatar: None,
communication_disabled_until: None,
deaf: false,
joined_at,
mute: false,
Expand Down
1 change: 1 addition & 0 deletions cache/in-memory/src/event/voice_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions cache/in-memory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,7 @@ mod tests {
guild_id,
Member {
avatar: None,
communication_disabled_until: None,
deaf: false,
guild_id,
joined_at,
Expand Down
21 changes: 21 additions & 0 deletions cache/in-memory/src/model/member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use twilight_model::{
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct CachedMember {
pub(crate) avatar: Option<String>,
pub(crate) communication_disabled_until: Option<Timestamp>,
pub(crate) deaf: Option<bool>,
pub(crate) guild_id: GuildId,
pub(crate) joined_at: Timestamp,
Expand All @@ -29,6 +30,19 @@ impl CachedMember {
self.avatar.as_deref()
}

/// When the user can resume communication in a guild again.
HTGAzureX1212 marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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<Timestamp> {
self.communication_disabled_until
}

/// Whether the member is deafened in a voice channel.
pub const fn deaf(&self) -> Option<bool> {
self.deaf
Expand Down Expand Up @@ -80,6 +94,7 @@ impl PartialEq<Member> for CachedMember {
fn eq(&self, other: &Member) -> bool {
(
&self.avatar,
&self.communication_disabled_until,
self.deaf,
self.joined_at,
self.mute,
Expand All @@ -90,6 +105,7 @@ impl PartialEq<Member> for CachedMember {
self.user_id,
) == (
&other.avatar,
&other.communication_disabled_until,
Some(other.deaf),
other.joined_at,
Some(other.mute),
Expand All @@ -105,13 +121,15 @@ impl PartialEq<Member> for CachedMember {
impl PartialEq<PartialMember> for CachedMember {
fn eq(&self, other: &PartialMember) -> bool {
(
&self.communication_disabled_until,
self.deaf,
self.joined_at,
self.mute,
&self.nick,
self.premium_since,
&self.roles,
) == (
&other.communication_disabled_until,
Some(other.deaf),
other.joined_at,
Some(other.mute),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -220,6 +240,7 @@ mod tests {

let member = PartialMember {
avatar: None,
communication_disabled_until: None,
deaf: false,
joined_at,
mute: true,
Expand Down
4 changes: 4 additions & 0 deletions cache/in-memory/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
46 changes: 45 additions & 1 deletion http/src/request/guild/member/update_guild_member.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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")
}
}
}
}
Expand All @@ -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)]
Expand All @@ -70,6 +78,8 @@ struct UpdateGuildMemberFields<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
channel_id: Option<NullableField<ChannelId>>,
#[serde(skip_serializing_if = "Option::is_none")]
communication_disabled_util: Option<NullableField<Timestamp>>,
#[serde(skip_serializing_if = "Option::is_none")]
deaf: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
mute: Option<bool>,
Expand Down Expand Up @@ -98,6 +108,7 @@ impl<'a> UpdateGuildMember<'a> {
Self {
fields: UpdateGuildMemberFields {
channel_id: None,
communication_disabled_util: None,
deaf: None,
mute: None,
nick: None,
Expand All @@ -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.
///
HTGAzureX1212 marked this conversation as resolved.
Show resolved Hide resolved
/// [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<Timestamp>,
) -> Result<Self, UpdateGuildMemberError> {
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);
Expand Down Expand Up @@ -227,6 +268,7 @@ mod tests {

let body = UpdateGuildMemberFields {
channel_id: None,
communication_disabled_util: None,
deaf: Some(true),
mute: Some(true),
nick: None,
Expand All @@ -252,6 +294,7 @@ mod tests {

let body = UpdateGuildMemberFields {
channel_id: None,
communication_disabled_util: None,
deaf: None,
mute: None,
nick: Some(NullableField(None)),
Expand All @@ -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"))),
Expand Down
22 changes: 22 additions & 0 deletions http/src/request/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
HTGAzureX1212 marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub struct InteractionMember {
/// Member's guild avatar.
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>,
pub communication_disabled_until: Option<Timestamp>,
HTGAzureX1212 marked this conversation as resolved.
Show resolved Hide resolved
pub joined_at: Timestamp,
pub nick: Option<String>,
/// Whether the user has yet to pass the guild's Membership Screening
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -140,6 +142,7 @@ mod tests {
kind: MessageType::Regular,
member: Some(PartialMember {
avatar: None,
communication_disabled_until: None,
deaf: false,
joined_at,
mute: false,
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions model/src/application/interaction/message_component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading