diff --git a/src/Disqord.Core/Discord/Limits/Entities/Discord.Limits.Guild.cs b/src/Disqord.Core/Discord/Limits/Entities/Discord.Limits.Guild.cs index f981f192e..1a642a08b 100644 --- a/src/Disqord.Core/Discord/Limits/Entities/Discord.Limits.Guild.cs +++ b/src/Disqord.Core/Discord/Limits/Entities/Discord.Limits.Guild.cs @@ -9,6 +9,16 @@ public static partial class Limits /// public static class Guild { + /// + /// The maximum seconds in the past to delete messages for when a user is banned. + /// + public const int MaxDeleteMessageSeconds = 604800; + + /// + /// The maximum amount of users that can be bulk banned. + /// + public const int MaxBulkBanUsersAmount = 200; + /// /// Represents limits for guild events. /// diff --git a/src/Disqord.Core/Models/GuildBulkBanJsonModel.cs b/src/Disqord.Core/Models/GuildBulkBanJsonModel.cs new file mode 100644 index 000000000..943be157c --- /dev/null +++ b/src/Disqord.Core/Models/GuildBulkBanJsonModel.cs @@ -0,0 +1,12 @@ +using Disqord.Serialization.Json; + +namespace Disqord.Models; + +public class GuildBulkBanJsonModel : JsonModel +{ + [JsonProperty("banned_users")] + public Snowflake[] BannedUsers = null!; + + [JsonProperty("failed_users")] + public Snowflake[] FailedUsers = null!; +} diff --git a/src/Disqord.Rest.Api/Content/Json/CreateBanJsonRestRequestContent.cs b/src/Disqord.Rest.Api/Content/Json/CreateBanJsonRestRequestContent.cs index 8b57ed0f2..b2e4cdcc6 100644 --- a/src/Disqord.Rest.Api/Content/Json/CreateBanJsonRestRequestContent.cs +++ b/src/Disqord.Rest.Api/Content/Json/CreateBanJsonRestRequestContent.cs @@ -5,14 +5,15 @@ namespace Disqord.Rest.Api; public class CreateBanJsonRestRequestContent : JsonModelRestRequestContent { - [JsonProperty("delete_message_days")] - public Optional DeleteMessageDays; + [JsonProperty("delete_message_seconds")] + public Optional DeleteMessageSeconds; [JsonProperty("reason")] public Optional Reason; protected override void OnValidate() { + OptionalGuard.CheckValue(DeleteMessageSeconds, seconds => Guard.IsBetweenOrEqualTo(seconds, 0, Discord.Limits.Guild.MaxDeleteMessageSeconds)); OptionalGuard.CheckValue(Reason, static reason => Guard.HasSizeLessThanOrEqualTo(reason, Discord.Limits.Rest.MaxAuditLogReasonLength)); } } diff --git a/src/Disqord.Rest.Api/Content/Json/CreateBansJsonRestRequestContent.cs b/src/Disqord.Rest.Api/Content/Json/CreateBansJsonRestRequestContent.cs new file mode 100644 index 000000000..04195cead --- /dev/null +++ b/src/Disqord.Rest.Api/Content/Json/CreateBansJsonRestRequestContent.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Disqord.Serialization.Json; +using Qommon; + +namespace Disqord.Rest.Api; + +public class CreateBansJsonRestRequestContent : CreateBanJsonRestRequestContent +{ + [JsonProperty("user_ids")] + public IList UserIds = null!; + + protected override void OnValidate() + { + base.OnValidate(); + + Guard.IsNotNull(UserIds); + Guard.HasSizeLessThanOrEqualTo(UserIds, Discord.Limits.Guild.MaxBulkBanUsersAmount); + } +} diff --git a/src/Disqord.Rest.Api/Methods/RestApiClientExtensions.Guild.cs b/src/Disqord.Rest.Api/Methods/RestApiClientExtensions.Guild.cs index c555988f5..ee67c63b2 100644 --- a/src/Disqord.Rest.Api/Methods/RestApiClientExtensions.Guild.cs +++ b/src/Disqord.Rest.Api/Methods/RestApiClientExtensions.Guild.cs @@ -245,6 +245,14 @@ public static Task DeleteBanAsync(this IRestApiClient client, return client.ExecuteAsync(route, null, options, cancellationToken); } + public static Task CreateBansAsync(this IRestApiClient client, + Snowflake guildId, CreateBanJsonRestRequestContent content, + IRestRequestOptions? options = null, CancellationToken cancellationToken = default) + { + var route = Format(Route.Guild.CreateBans, guildId); + return client.ExecuteAsync(route, content, options, cancellationToken); + } + public static Task FetchRolesAsync(this IRestApiClient client, Snowflake guildId, IRestRequestOptions? options = null, CancellationToken cancellationToken = default) diff --git a/src/Disqord.Rest.Api/Requests/Routing/Default/Route.Static.cs b/src/Disqord.Rest.Api/Requests/Routing/Default/Route.Static.cs index 81ba04d41..f55f1e53f 100644 --- a/src/Disqord.Rest.Api/Requests/Routing/Default/Route.Static.cs +++ b/src/Disqord.Rest.Api/Requests/Routing/Default/Route.Static.cs @@ -188,6 +188,8 @@ public static class Guild public static readonly Route DeleteBan = Delete("guilds/{0:guild_id}/bans/{1:user_id}"); + public static readonly Route CreateBans = Post("guilds/{0:guild_id}/bulk-ban"); + public static readonly Route GetRoles = Get("guilds/{0:guild_id}/roles"); public static readonly Route CreateRole = Post("guilds/{0:guild_id}/roles"); diff --git a/src/Disqord.Rest/Entities/Core/Guild/IBulkBanResponse.cs b/src/Disqord.Rest/Entities/Core/Guild/IBulkBanResponse.cs new file mode 100644 index 000000000..092a4410e --- /dev/null +++ b/src/Disqord.Rest/Entities/Core/Guild/IBulkBanResponse.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Disqord.Rest; + +public interface IBulkBanResponse +{ + IReadOnlyList BannedUserIds { get; } + + IReadOnlyList FailedUserIds { get; } +} diff --git a/src/Disqord.Rest/Entities/Transient/Guild/CombinedBulkBanResponse.cs b/src/Disqord.Rest/Entities/Transient/Guild/CombinedBulkBanResponse.cs new file mode 100644 index 000000000..b61729b04 --- /dev/null +++ b/src/Disqord.Rest/Entities/Transient/Guild/CombinedBulkBanResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Disqord.Rest; + +public class CombinedBulkBanResponse : IBulkBanResponse +{ + public IReadOnlyList BannedUserIds { get; } + public IReadOnlyList FailedUserIds { get; } + + public CombinedBulkBanResponse(IEnumerable responses) + { + BannedUserIds = responses.SelectMany(response => response.BannedUserIds).ToArray(); + FailedUserIds = responses.SelectMany(response => response.FailedUserIds).ToArray(); + } +} diff --git a/src/Disqord.Rest/Entities/Transient/Guild/TransientBulkBanResponse.cs b/src/Disqord.Rest/Entities/Transient/Guild/TransientBulkBanResponse.cs new file mode 100644 index 000000000..ba344cb59 --- /dev/null +++ b/src/Disqord.Rest/Entities/Transient/Guild/TransientBulkBanResponse.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Disqord.Models; + +namespace Disqord.Rest; + +public class TransientBulkBanResponse : TransientEntity, IBulkBanResponse +{ + public IReadOnlyList BannedUserIds => Model.BannedUsers; + + public IReadOnlyList FailedUserIds => Model.FailedUsers; + + public TransientBulkBanResponse(GuildBulkBanJsonModel model) + : base(model) + { } +} diff --git a/src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs b/src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs index 436749780..475b6ba83 100644 --- a/src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs +++ b/src/Disqord.Rest/Extensions/Entity/RestEntityExtensions.Guild.cs @@ -235,6 +235,7 @@ public static Task> FetchBansAsync(this IGuild guild, return client.FetchBanAsync(guild.Id, userId, options, cancellationToken); } + [Obsolete("Parameter deleteMessageDays is deprecated, use a TimeSpan instead.")] public static Task CreateBanAsync(this IGuild guild, Snowflake userId, string? reason = null, int? deleteMessageDays = null, IRestRequestOptions? options = null, CancellationToken cancellationToken = default) @@ -243,6 +244,14 @@ public static Task CreateBanAsync(this IGuild guild, return client.CreateBanAsync(guild.Id, userId, reason, deleteMessageDays, options, cancellationToken); } + public static Task CreateBanAsync(this IGuild guild, + Snowflake userId, string? reason = null, TimeSpan? deleteMessageTime = null, + IRestRequestOptions? options = null, CancellationToken cancellationToken = default) + { + var client = guild.GetRestClient(); + return client.CreateBanAsync(guild.Id, userId, reason, deleteMessageTime, options, cancellationToken); + } + public static Task DeleteBanAsync(this IGuild guild, Snowflake userId, IRestRequestOptions? options = null, CancellationToken cancellationToken = default) @@ -251,6 +260,22 @@ public static Task DeleteBanAsync(this IGuild guild, return client.DeleteBanAsync(guild.Id, userId, options, cancellationToken); } + public static IPagedEnumerable EnumerateBanCreation(this IGuild guild, + IEnumerable userIds, string? reason = null, TimeSpan? deleteMessageTime = null, + IRestRequestOptions? options = null) + { + var client = guild.GetRestClient(); + return client.EnumerateBanCreation(guild.Id, userIds, reason, deleteMessageTime, options); + } + + public static Task CreateBansAsync(this IGuild guild, + IEnumerable userIds, string? reason = null, TimeSpan? deleteMessageTime = null, + IRestRequestOptions? options = null, CancellationToken cancellationToken = default) + { + var client = guild.GetRestClient(); + return client.CreateBansAsync(guild.Id, userIds, reason, deleteMessageTime, options, cancellationToken); + } + public static Task> FetchRolesAsync(this IGuild guild, IRestRequestOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs b/src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs index 3f3617503..3506353d0 100644 --- a/src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs +++ b/src/Disqord.Rest/Extensions/RestClientExtensions.Guild.cs @@ -443,13 +443,23 @@ internal static async Task> InternalFetchBansAsync(this IRes } } + [Obsolete("Parameter deleteMessageDays is deprecated, use a TimeSpan instead.")] public static Task CreateBanAsync(this IRestClient client, Snowflake guildId, Snowflake userId, string? reason = null, int? deleteMessageDays = null, IRestRequestOptions? options = null, CancellationToken cancellationToken = default) + { + return client.CreateBanAsync(guildId, userId, reason, + deleteMessageDays.HasValue ? TimeSpan.FromDays(deleteMessageDays.Value) : null, + options, cancellationToken); + } + + public static Task CreateBanAsync(this IRestClient client, + Snowflake guildId, Snowflake userId, string? reason = null, TimeSpan? deleteMessageTime = null, + IRestRequestOptions? options = null, CancellationToken cancellationToken = default) { var content = new CreateBanJsonRestRequestContent { - DeleteMessageDays = Optional.FromNullable(deleteMessageDays), + DeleteMessageSeconds = Optional.Convert(Optional.FromNullable(deleteMessageTime), time => (int) time.TotalSeconds), Reason = Optional.FromNullable(reason) }; @@ -463,6 +473,53 @@ public static Task DeleteBanAsync(this IRestClient client, return client.ApiClient.DeleteBanAsync(guildId, userId, options, cancellationToken); } + public static IPagedEnumerable EnumerateBanCreation(this IRestClient client, + Snowflake guildId, IEnumerable userIds, string? reason = null, TimeSpan? deleteMessageTime = null, + IRestRequestOptions? options = null) + { + Guard.IsNotNull(userIds); + + return PagedEnumerable.Create((state, cancellationToken) => + { + var (client, guildId, userIds, reason, deleteMessageTime, options) = state; + return new CreateBansPagedEnumerator(client, guildId, userIds, reason, deleteMessageTime, options, cancellationToken); + }, (client, guildId, userIds.ToArray(), reason, deleteMessageTime, options)); + } + + public static async Task CreateBansAsync(this IRestClient client, + Snowflake guildId, IEnumerable userIds, string? reason = null, TimeSpan? deleteMessageTime = null, + IRestRequestOptions? options = null, CancellationToken cancellationToken = default) + { + Guard.IsNotNull(userIds); + + var users = userIds.ToArray(); + Guard.IsNotEmpty(users); + + if (users.Length <= Discord.Limits.Guild.MaxBulkBanUsersAmount) + { + return await client.InternalCreateBansAsync(guildId, users, reason, deleteMessageTime, options, cancellationToken).ConfigureAwait(false); + } + + var enumerable = client.EnumerateBanCreation(guildId, users, reason, deleteMessageTime, options); + var flattened = await enumerable.FlattenAsync(cancellationToken); + return new CombinedBulkBanResponse(flattened); + } + + internal static async Task InternalCreateBansAsync(this IRestClient client, + Snowflake guildId, ArraySegment userIds, string? reason = null, TimeSpan? deleteMessageTime = null, + IRestRequestOptions? options = null, CancellationToken cancellationToken = default) + { + var content = new CreateBansJsonRestRequestContent + { + UserIds = userIds, + DeleteMessageSeconds = Optional.Convert(Optional.FromNullable(deleteMessageTime), time => (int) time.TotalSeconds), + Reason = Optional.FromNullable(reason) + }; + + var model = await client.ApiClient.CreateBansAsync(guildId, content, options, cancellationToken); + return new TransientBulkBanResponse(model);; + } + public static async Task> FetchRolesAsync(this IRestClient client, Snowflake guildId, IRestRequestOptions? options = null, CancellationToken cancellationToken = default) diff --git a/src/Disqord.Rest/Requests/Pagination/Implementation/CreateBansPagedEnumerator.cs b/src/Disqord.Rest/Requests/Pagination/Implementation/CreateBansPagedEnumerator.cs new file mode 100644 index 000000000..ee33c8b47 --- /dev/null +++ b/src/Disqord.Rest/Requests/Pagination/Implementation/CreateBansPagedEnumerator.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Qommon.Collections.ReadOnly; + +namespace Disqord.Rest; + +public class CreateBansPagedEnumerator : PagedEnumerator +{ + public override int PageSize => Discord.Limits.Guild.MaxBulkBanUsersAmount; + + private readonly Snowflake _guildId; + private readonly Snowflake[] _userIds; + private readonly string? _reason; + private readonly TimeSpan? _deleteMessagesTime = null; + + private int _offset; + + public CreateBansPagedEnumerator( + IRestClient client, + Snowflake guildId, Snowflake[] userIds, string? reason = null, TimeSpan? deleteMessagesTime = null, + IRestRequestOptions? options = null, + CancellationToken cancellationToken = default) + : base(client, userIds.Length, options, cancellationToken) + { + _guildId = guildId; + _userIds = userIds; + _reason = reason; + _deleteMessagesTime = deleteMessagesTime; + } + + protected override async Task> NextPageAsync(IReadOnlyList? previousPage, IRestRequestOptions? options = null, + CancellationToken cancellationToken = default) + { + var amount = NextPageSize; + var segment = new ArraySegment(_userIds, _offset, amount); + _offset += amount; + var response = await Client.InternalCreateBansAsync(_guildId, segment, _reason, _deleteMessagesTime, options, + cancellationToken); + return Enumerable.Repeat(response, PageSize).ToReadOnlyList(); + } +} diff --git a/src/Disqord.Rest/Requests/Pagination/PagedEnumerator`1.cs b/src/Disqord.Rest/Requests/Pagination/PagedEnumerator`1.cs index 397d44dcd..f59ceffc4 100644 --- a/src/Disqord.Rest/Requests/Pagination/PagedEnumerator`1.cs +++ b/src/Disqord.Rest/Requests/Pagination/PagedEnumerator`1.cs @@ -82,16 +82,17 @@ public async ValueTask MoveNextAsync(IRestRequestOptions? options = null) return false; } - if (current.Count < PageSize) - { - // If Discord returns less entities than the page size, - // it means there are no more entities beyond the ones we just received. - RemainingCount = 0; - } - else - { + // TODO: Rework paged enumerator + // if (current.Count < PageSize) + // { + // // If Discord returns less entities than the page size, + // // it means there are no more entities beyond the ones we just received. + // RemainingCount = 0; + // } + // else + // { RemainingCount -= current.Count; - } + // } return true; } @@ -99,4 +100,4 @@ public async ValueTask MoveNextAsync(IRestRequestOptions? options = null) /// public virtual ValueTask DisposeAsync() => default; -} \ No newline at end of file +}