From d8757a5afaed78b75c5705c5688d1e151c348d5d Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Tue, 5 Apr 2022 19:13:16 +0200 Subject: [PATCH] feature: Update bans to support pagination (#2223) * Cacheless impl * Ignore cache impl * Update src/Discord.Net.Core/Entities/Channels/Direction.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Channels/Direction.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Channels/Direction.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Update src/Discord.Net.Core/Entities/Guilds/IGuild.cs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> * Implement xmldoc consistency Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/DiscordConfig.cs | 7 ++ .../Entities/Channels/Direction.cs | 10 +-- .../Entities/Guilds/IGuild.cs | 65 +++++++++++++++++-- .../API/Rest/GetGuildBansParams.cs | 9 +++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 20 +++++- .../Entities/Guilds/GuildHelper.cs | 50 ++++++++++++-- .../Entities/Guilds/RestGuild.cs | 37 ++++++----- .../Entities/Guilds/SocketGuild.cs | 34 ++++++---- 8 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 006a1ca17..067c55225 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -97,6 +97,13 @@ namespace Discord /// public const int MaxUsersPerBatch = 1000; /// + /// Returns the max bans allowed to be in a request. + /// + /// + /// The maximum number of bans that can be gotten per-batch. + /// + public const int MaxBansPerBatch = 1000; + /// /// Returns the max users allowed to be in a request for guild event users. /// /// diff --git a/src/Discord.Net.Core/Entities/Channels/Direction.cs b/src/Discord.Net.Core/Entities/Channels/Direction.cs index efdf4ff42..4149617d8 100644 --- a/src/Discord.Net.Core/Entities/Channels/Direction.cs +++ b/src/Discord.Net.Core/Entities/Channels/Direction.cs @@ -1,10 +1,10 @@ namespace Discord { /// - /// Specifies the direction of where message(s) should be retrieved from. + /// Specifies the direction of where entities (e.g. bans/messages) should be retrieved from. /// /// - /// This enum is used to specify the direction for retrieving messages. + /// This enum is used to specify the direction for retrieving entities. /// /// At the time of writing, is not yet implemented into /// . @@ -15,15 +15,15 @@ namespace Discord public enum Direction { /// - /// The message(s) should be retrieved before a message. + /// The entity(s) should be retrieved before an entity. /// Before, /// - /// The message(s) should be retrieved after a message. + /// The entity(s) should be retrieved after an entity. /// After, /// - /// The message(s) should be retrieved around a message. + /// The entity(s) should be retrieved around an entity. /// Around } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index b4625abbf..4706b629e 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -409,17 +409,70 @@ namespace Discord /// A task that represents the asynchronous leave operation. /// Task LeaveAsync(RequestOptions options = null); - /// - /// Gets a collection of all users banned in this guild. + /// Gets amount of bans from the guild ordered by user ID. /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// + /// The amount of bans to get from the guild. /// The options to be used when sending the request. /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection of - /// ban objects that this guild currently possesses, with each object containing the user banned and reason - /// behind the ban. + /// A paged collection of bans. + /// + IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null); + /// + /// Gets amount of bans from the guild starting at the provided ordered by user ID. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// + /// The ID of the user to start to get bans from. + /// The direction of the bans to be gotten. + /// The number of bans to get. + /// The options to be used when sending the request. + /// + /// A paged collection of bans. + /// + IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null); + /// + /// Gets amount of bans from the guild starting at the provided ordered by user ID. + /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the individual messages as a + /// collection. + /// + /// + /// Do not fetch too many bans at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// + /// The user to start to get bans from. + /// The direction of the bans to be gotten. + /// The number of bans to get. + /// The options to be used when sending the request. + /// + /// A paged collection of bans. /// - Task> GetBansAsync(RequestOptions options = null); + IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null); /// /// Gets a ban object for a banned user. /// diff --git a/src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs b/src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs new file mode 100644 index 000000000..6a1e430c3 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetGuildBansParams.cs @@ -0,0 +1,9 @@ +namespace Discord.API.Rest +{ + internal class GetGuildBansParams + { + public Optional Limit { get; set; } + public Optional RelativeDirection { get; set; } + public Optional RelativeUserId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 645e6711c..3b829ee17 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1545,13 +1545,29 @@ namespace Discord.API #endregion #region Guild Bans - public async Task> GetGuildBansAsync(ulong guildId, RequestOptions options = null) + public async Task> GetGuildBansAsync(ulong guildId, GetGuildBansParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.AtLeast(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxBansPerBatch, nameof(args.Limit)); options = RequestOptions.CreateOrClone(options); + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxBansPerBatch); + ulong? relativeId = args.RelativeUserId.IsSpecified ? args.RelativeUserId.Value : (ulong?)null; + var relativeDir = args.RelativeDirection.GetValueOrDefault(Direction.Before) switch + { + Direction.After => "after", + Direction.Around => "around", + _ => "before", + }; var ids = new BucketIds(guildId: guildId); - return await SendAsync>("GET", () => $"guilds/{guildId}/bans", ids, options: options).ConfigureAwait(false); + Expression> endpoint; + if (relativeId != null) + endpoint = () => $"guilds/{guildId}/bans?limit={limit}&{relativeDir}={relativeId}"; + else + endpoint = () => $"guilds/{guildId}/bans?limit={limit}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } public async Task GetGuildBanAsync(ulong guildId, ulong userId, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 7dbe20881..469e93db4 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -142,12 +142,54 @@ namespace Discord.Rest #endregion #region Bans - public static async Task> GetBansAsync(IGuild guild, BaseDiscordClient client, - RequestOptions options) + public static IAsyncEnumerable> GetBansAsync(IGuild guild, BaseDiscordClient client, + ulong? fromUserId, Direction dir, int limit, RequestOptions options) { - var models = await client.ApiClient.GetGuildBansAsync(guild.Id, options).ConfigureAwait(false); - return models.Select(x => RestBan.Create(client, x)).ToImmutableArray(); + if (dir == Direction.Around && limit > DiscordConfig.MaxBansPerBatch) + { + int around = limit / 2; + if (fromUserId.HasValue) + return GetBansAsync(guild, client, fromUserId.Value + 1, Direction.Before, around + 1, options) + .Concat(GetBansAsync(guild, client, fromUserId.Value, Direction.After, around, options)); + else + return GetBansAsync(guild, client, null, Direction.Before, around + 1, options); + } + + return new PagedAsyncEnumerable( + DiscordConfig.MaxBansPerBatch, + async (info, ct) => + { + var args = new GetGuildBansParams + { + RelativeDirection = dir, + Limit = info.PageSize + }; + if (info.Position != null) + args.RelativeUserId = info.Position.Value; + + var models = await client.ApiClient.GetGuildBansAsync(guild.Id, args, options).ConfigureAwait(false); + var builder = ImmutableArray.CreateBuilder(); + + foreach (var model in models) + builder.Add(RestBan.Create(client, model)); + + return builder.ToImmutable(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + return false; + if (dir == Direction.Before) + info.Position = lastPage.Min(x => x.User.Id); + else + info.Position = lastPage.Max(x => x.User.Id); + return true; + }, + start: fromUserId, + count: limit + ); } + public static async Task GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) { var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index d7ab65a55..92d598466 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -333,17 +333,18 @@ namespace Discord.Rest #endregion #region Bans - /// - /// Gets a collection of all users banned in this guild. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection of - /// ban objects that this guild currently possesses, with each object containing the user banned and reason - /// behind the ban. - /// - public Task> GetBansAsync(RequestOptions options = null) - => GuildHelper.GetBansAsync(this, Discord, options); + + /// + public IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, null, Direction.Before, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUserId, dir, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUser.Id, dir, limit, options); /// /// Gets a ban object for a banned user. /// @@ -1193,22 +1194,24 @@ namespace Discord.Rest IReadOnlyCollection IGuild.Roles => Roles; IReadOnlyCollection IGuild.Stickers => Stickers; - /// async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, Image? coverImage, RequestOptions options) => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, coverImage, options).ConfigureAwait(false); - /// async Task IGuild.GetEventAsync(ulong id, RequestOptions options) => await GetEventAsync(id, options).ConfigureAwait(false); - /// async Task> IGuild.GetEventsAsync(RequestOptions options) => await GetEventsAsync(options).ConfigureAwait(false); - /// - async Task> IGuild.GetBansAsync(RequestOptions options) - => await GetBansAsync(options).ConfigureAwait(false); + IAsyncEnumerable> IGuild.GetBansAsync(int limit, RequestOptions options) + => GetBansAsync(limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUserId, dir, limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(IUser fromUser, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUser, dir, limit, options); /// async Task IGuild.GetBanAsync(IUser user, RequestOptions options) => await GetBanAsync(user, options).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 47bd57552..49d2cd3bd 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -621,17 +621,19 @@ namespace Discord.WebSocket #endregion #region Bans - /// - /// Gets a collection of all users banned in this guild. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection of - /// ban objects that this guild currently possesses, with each object containing the user banned and reason - /// behind the ban. - /// - public Task> GetBansAsync(RequestOptions options = null) - => GuildHelper.GetBansAsync(this, Discord, options); + + /// + public IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, null, Direction.Before, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUserId, dir, limit, options); + + /// + public IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) + => GuildHelper.GetBansAsync(this, Discord, fromUser.Id, dir, limit, options); + /// /// Gets a ban object for a banned user. /// @@ -1810,8 +1812,14 @@ namespace Discord.WebSocket async Task> IGuild.GetEventsAsync(RequestOptions options) => await GetEventsAsync(options).ConfigureAwait(false); /// - async Task> IGuild.GetBansAsync(RequestOptions options) - => await GetBansAsync(options).ConfigureAwait(false); + IAsyncEnumerable> IGuild.GetBansAsync(int limit, RequestOptions options) + => GetBansAsync(limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUserId, dir, limit, options); + /// + IAsyncEnumerable> IGuild.GetBansAsync(IUser fromUser, Direction dir, int limit, RequestOptions options) + => GetBansAsync(fromUser, dir, limit, options); /// async Task IGuild.GetBanAsync(IUser user, RequestOptions options) => await GetBanAsync(user, options).ConfigureAwait(false);