From 144741e7c4b64766e142e4c2190244db397cf690 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Fri, 24 Dec 2021 15:57:41 -0400 Subject: [PATCH] Guilduser timeouts and MODERATE_MEMBERS permission (#2003) Co-Authored-By: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> --- .../Entities/Permissions/GuildPermission.cs | 7 +++-- .../Entities/Permissions/GuildPermissions.cs | 18 ++++++++---- .../Entities/Users/GuildUserProperties.cs | 11 ++++++- .../Entities/Users/IGuildUser.cs | 28 ++++++++++++++++++ .../API/Common/GuildMember.cs | 2 ++ .../API/Rest/ModifyGuildMemberParams.cs | 3 ++ .../Entities/Users/RestGuildUser.cs | 21 ++++++++++++++ .../Entities/Users/RestWebhookUser.cs | 8 +++++ .../Entities/Users/UserHelper.cs | 29 ++++++++++++++++++- .../Entities/Users/SocketGuildUser.cs | 21 +++++++++++++- .../Entities/Users/SocketThreadUser.cs | 8 +++++ .../Entities/Users/SocketWebhookUser.cs | 10 +++++++ .../GuildPermissionsTests.cs | 2 ++ 13 files changed, 157 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index 5a5827c1d..299ff3bd8 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -214,7 +214,10 @@ namespace Discord /// /// Allows for launching activities (applications with the EMBEDDED flag) in a voice channel. /// - StartEmbeddedActivities = 0x80_00_00_00_00 - + StartEmbeddedActivities = 0x80_00_00_00_00, + /// + /// Allows for timing out users. + /// + ModerateMembers = 0x01_00_00_00_00_00 } } diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index 8a4ad2189..649944ede 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -102,7 +102,8 @@ namespace Discord public bool SendMessagesInThreads => Permissions.GetValue(RawValue, GuildPermission.SendMessagesInThreads); /// If true, a user launch application activities in voice channels in this guild. public bool StartEmbeddedActivities => Permissions.GetValue(RawValue, GuildPermission.StartEmbeddedActivities); - + /// If true, a user can timeout other users in this guild. + public bool ModerateMembers => Permissions.GetValue(RawValue, GuildPermission.ModerateMembers); /// Creates a new with the provided packed value. public GuildPermissions(ulong rawValue) { RawValue = rawValue; } @@ -149,7 +150,8 @@ namespace Discord bool? createPrivateThreads = null, bool? useExternalStickers = null, bool? sendMessagesInThreads = null, - bool? startEmbeddedActivities = null) + bool? startEmbeddedActivities = null, + bool? moderateMembers = null) { ulong value = initialValue; @@ -193,6 +195,7 @@ namespace Discord Permissions.SetValue(ref value, useExternalStickers, GuildPermission.UseExternalStickers); Permissions.SetValue(ref value, sendMessagesInThreads, GuildPermission.SendMessagesInThreads); Permissions.SetValue(ref value, startEmbeddedActivities, GuildPermission.StartEmbeddedActivities); + Permissions.SetValue(ref value, moderateMembers, GuildPermission.ModerateMembers); RawValue = value; } @@ -238,7 +241,8 @@ namespace Discord bool createPrivateThreads = false, bool useExternalStickers = false, bool sendMessagesInThreads = false, - bool startEmbeddedActivities = false) + bool startEmbeddedActivities = false, + bool moderateMembers = false) : this(0, createInstantInvite: createInstantInvite, manageRoles: manageRoles, @@ -279,7 +283,8 @@ namespace Discord createPrivateThreads: createPrivateThreads, useExternalStickers: useExternalStickers, sendMessagesInThreads: sendMessagesInThreads, - startEmbeddedActivities: startEmbeddedActivities) + startEmbeddedActivities: startEmbeddedActivities, + moderateMembers: moderateMembers) { } /// Creates a new from this one, changing the provided non-null permissions. @@ -323,13 +328,14 @@ namespace Discord bool? createPrivateThreads = null, bool? useExternalStickers = null, bool? sendMessagesInThreads = null, - bool? startEmbeddedActivities = null) + bool? startEmbeddedActivities = null, + bool? moderateMembers = null) => new GuildPermissions(RawValue, createInstantInvite, kickMembers, banMembers, administrator, manageChannels, manageGuild, addReactions, viewAuditLog, viewGuildInsights, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, prioritySpeaker, stream, changeNickname, manageNicknames, manageRoles, manageWebhooks, manageEmojisAndStickers, useApplicationCommands, requestToSpeak, manageEvents, manageThreads, createPublicThreads, createPrivateThreads, useExternalStickers, sendMessagesInThreads, - startEmbeddedActivities); + startEmbeddedActivities, moderateMembers); /// /// Returns a value that indicates if a specific is enabled diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 8f2d2111e..935b956c3 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace Discord @@ -72,6 +73,14 @@ namespace Discord /// /// This user MUST already be in a for this to work. /// - public Optional ChannelId { get; set; } // TODO: v3 breaking change, change ChannelId to ulong? to allow for kicking users from voice + public Optional ChannelId { get; set; } + + /// + /// Sets a timestamp how long a user should be timed out for. + /// + /// + /// or a time in the past to clear a currently existing timeout. + /// + public Optional TimedOutUntil { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 947ff8521..95896eef0 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -85,6 +85,17 @@ namespace Discord /// int Hierarchy { get; } + /// + /// Gets the date and time that indicates if and for how long a user has been timed out. + /// + /// + /// or a timestamp in the past if the user is not timed out. + /// + /// + /// A indicating how long the user will be timed out for. + /// + DateTimeOffset? TimedOutUntil { get; } + /// /// Gets the level permissions granted to this user to a given channel. /// @@ -211,5 +222,22 @@ namespace Discord /// A task that represents the asynchronous role removal operation. /// Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null); + /// + /// Sets a timeout based on provided to this user in the guild. + /// + /// The indicating how long a user should be timed out for. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous timeout creation operation. + /// + Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null); + /// + /// Removes the current timeout from the user in this guild if one exists. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous timeout removal operation. + /// + Task RemoveTimeOutAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index 9b888e86a..cd3101224 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -23,5 +23,7 @@ namespace Discord.API public Optional Pending { get; set; } [JsonProperty("premium_since")] public Optional PremiumSince { get; set; } + [JsonProperty("communication_disabled_until")] + public Optional TimedOutUntil { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs index 37625de09..eb7c944d1 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildMemberParams.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System; namespace Discord.API.Rest { @@ -15,5 +16,7 @@ namespace Discord.API.Rest public Optional RoleIds { get; set; } [JsonProperty("channel_id")] public Optional ChannelId { get; set; } + [JsonProperty("communication_disabled_until")] + public Optional TimedOutUntil { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 2e184d32e..09e7ec03a 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -16,6 +16,7 @@ namespace Discord.Rest { #region RestGuildUser private long? _premiumSinceTicks; + private long? _timedOutTicks; private long? _joinedAtTicks; private ImmutableArray _roleIds; @@ -47,6 +48,18 @@ namespace Discord.Rest } } + /// + public DateTimeOffset? TimedOutUntil + { + get + { + if (!_timedOutTicks.HasValue || _timedOutTicks.Value < 0) + return null; + else + return DateTimeUtils.FromTicks(_timedOutTicks); + } + } + /// /// Resolving permissions requires the parent guild to be downloaded. public GuildPermissions GuildPermissions @@ -92,6 +105,8 @@ namespace Discord.Rest UpdateRoles(model.Roles.Value); if (model.PremiumSince.IsSpecified) _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; + if (model.TimedOutUntil.IsSpecified) + _timedOutTicks = model.TimedOutUntil.Value?.UtcTicks; if (model.Pending.IsSpecified) IsPending = model.Pending.Value; } @@ -152,6 +167,12 @@ namespace Discord.Rest /// public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => RemoveRolesAsync(roles.Select(x => x.Id)); + /// + public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) + => UserHelper.SetTimeoutAsync(this, Discord, span, options); + /// + public Task RemoveTimeOutAsync(RequestOptions options = null) + => UserHelper.RemoveTimeOutAsync(this, Discord, options); /// /// Resolving permissions requires the parent guild to be downloaded. diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index 2cd19da41..4ef84c508 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -62,6 +62,8 @@ namespace Discord.Rest /// int IGuildUser.Hierarchy => 0; /// + DateTimeOffset? IGuildUser.TimedOutUntil => null; + /// GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; /// @@ -97,6 +99,12 @@ namespace Discord.Rest /// Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); + /// + Task IGuildUser.SetTimeOutAsync(TimeSpan span, RequestOptions options) => + throw new NotSupportedException("Timeouts are not supported on webhook users."); + /// + Task IGuildUser.RemoveTimeOutAsync(RequestOptions options) => + throw new NotSupportedException("Timeouts are not supported on webhook users."); #endregion #region IVoiceState diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 3a19fcfc1..393effb2e 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -31,11 +31,16 @@ namespace Discord.Rest { var args = new GuildUserProperties(); func(args); + + if (args.TimedOutUntil.IsSpecified && args.TimedOutUntil.Value.Value.Offset > (new TimeSpan(28, 0, 0, 0))) + throw new ArgumentOutOfRangeException(nameof(args.TimedOutUntil), "Offset cannot be more than 28 days from the current date."); + var apiArgs = new API.Rest.ModifyGuildMemberParams { Deaf = args.Deaf, Mute = args.Mute, - Nickname = args.Nickname + Nickname = args.Nickname, + TimedOutUntil = args.TimedOutUntil }; if (args.Channel.IsSpecified) @@ -84,5 +89,27 @@ namespace Discord.Rest foreach (var roleId in roleIds) await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, roleId, options).ConfigureAwait(false); } + + public static async Task SetTimeoutAsync(IGuildUser user, BaseDiscordClient client, TimeSpan span, RequestOptions options) + { + if (span.TotalDays > 28) // As its double, an exact value of 28 can be accepted. + throw new ArgumentOutOfRangeException(nameof(span), "Offset cannot be more than 28 days from the current date."); + if (span.Ticks <= 0) + throw new ArgumentOutOfRangeException(nameof(span), "Offset cannot hold no value or have a negative value."); + var apiArgs = new API.Rest.ModifyGuildMemberParams() + { + TimedOutUntil = DateTimeOffset.UtcNow.Add(span) + }; + await client.ApiClient.ModifyGuildMemberAsync(user.Guild.Id, user.Id, apiArgs, options).ConfigureAwait(false); + } + + public static async Task RemoveTimeOutAsync(IGuildUser user, BaseDiscordClient client, RequestOptions options) + { + var apiArgs = new API.Rest.ModifyGuildMemberParams() + { + TimedOutUntil = null + }; + await client.ApiClient.ModifyGuildMemberAsync(user.Guild.Id, user.Id, apiArgs, options).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index d64597501..1e5a98aeb 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -20,6 +20,7 @@ namespace Discord.WebSocket { #region SocketGuildUser private long? _premiumSinceTicks; + private long? _timedOutTicks; private long? _joinedAtTicks; private ImmutableArray _roleIds; @@ -89,6 +90,17 @@ namespace Discord.WebSocket public AudioInStream AudioStream => Guild.GetAudioStream(Id); /// public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); + /// + public DateTimeOffset? TimedOutUntil + { + get + { + if (!_timedOutTicks.HasValue || _timedOutTicks.Value < 0) + return null; + else + return DateTimeUtils.FromTicks(_timedOutTicks); + } + } /// /// Returns the position of the user within the role hierarchy. @@ -157,6 +169,8 @@ namespace Discord.WebSocket UpdateRoles(model.Roles.Value); if (model.PremiumSince.IsSpecified) _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; + if (model.TimedOutUntil.IsSpecified) + _timedOutTicks = model.TimedOutUntil.Value?.UtcTicks; if (model.Pending.IsSpecified) IsPending = model.Pending.Value; } @@ -221,7 +235,12 @@ namespace Discord.WebSocket /// public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => RemoveRolesAsync(roles.Select(x => x.Id)); - + /// + public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) + => UserHelper.SetTimeoutAsync(this, Discord, span, options); + /// + public Task RemoveTimeOutAsync(RequestOptions options = null) + => UserHelper.RemoveTimeOutAsync(this, Discord, options); /// public ChannelPermissions GetPermissions(IGuildChannel channel) => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index 407fcb8b5..461cdded0 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -39,6 +39,10 @@ namespace Discord.WebSocket public DateTimeOffset? PremiumSince => GuildUser.PremiumSince; + /// + public DateTimeOffset? TimedOutUntil + => GuildUser.TimedOutUntil; + /// public bool? IsPending => GuildUser.IsPending; @@ -171,7 +175,11 @@ namespace Discord.WebSocket /// public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options); + /// + public Task SetTimeOutAsync(TimeSpan span, RequestOptions options = null) => GuildUser.SetTimeOutAsync(span, options); + /// + public Task RemoveTimeOutAsync(RequestOptions options = null) => GuildUser.RemoveTimeOutAsync(options); /// GuildPermissions IGuildUser.GuildPermissions => GuildUser.GuildPermissions; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index bccfe1a29..cf820b80e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -72,6 +72,8 @@ namespace Discord.WebSocket /// DateTimeOffset? IGuildUser.PremiumSince => null; /// + DateTimeOffset? IGuildUser.TimedOutUntil => null; + /// bool? IGuildUser.IsPending => null; /// int IGuildUser.Hierarchy => 0; @@ -129,6 +131,14 @@ namespace Discord.WebSocket /// Roles are not supported on webhook users. Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); + /// + /// Timeouts are not supported on webhook users. + Task IGuildUser.SetTimeOutAsync(TimeSpan span, RequestOptions options) => + throw new NotSupportedException("Timeouts are not supported on webhook users."); + /// + /// Timeouts are not supported on webhook users. + Task IGuildUser.RemoveTimeOutAsync(RequestOptions options) => + throw new NotSupportedException("Timeouts are not supported on webhook users."); #endregion #region IVoiceState diff --git a/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs b/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs index f0b0b2db7..ce5d0eb84 100644 --- a/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs +++ b/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs @@ -99,6 +99,7 @@ namespace Discord AssertFlag(() => new GuildPermissions(createPublicThreads: true), GuildPermission.CreatePublicThreads); AssertFlag(() => new GuildPermissions(createPrivateThreads: true), GuildPermission.CreatePrivateThreads); AssertFlag(() => new GuildPermissions(useExternalStickers: true), GuildPermission.UseExternalStickers); + AssertFlag(() => new GuildPermissions(moderateMembers: true), GuildPermission.ModerateMembers); } /// @@ -176,6 +177,7 @@ namespace Discord AssertUtil(GuildPermission.CreatePublicThreads, x => x.CreatePublicThreads, (p, enable) => p.Modify(createPublicThreads: enable)); AssertUtil(GuildPermission.CreatePrivateThreads, x => x.CreatePrivateThreads, (p, enable) => p.Modify(createPrivateThreads: enable)); AssertUtil(GuildPermission.UseExternalStickers, x => x.UseExternalStickers, (p, enable) => p.Modify(useExternalStickers: enable)); + AssertUtil(GuildPermission.ModerateMembers, x => x.ModerateMembers, (p, enable) => p.Modify(moderateMembers: enable)); } } }