From b085621ca6be8e906ffedc6e567b33c9025e8929 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Thu, 9 Feb 2023 14:37:26 +0300 Subject: [PATCH] [Feature] Thread user member & thread members pagination (#2580) * initial commit * return & update old method * mark old method as obsolete --- src/Discord.Net.Core/DiscordConfig.cs | 8 +++++++ .../Entities/Users/IThreadUser.cs | 5 ++++ .../API/Common/ThreadMember.cs | 3 +++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 18 ++++++++++---- .../Entities/Channels/RestThreadChannel.cs | 14 ++++++++++- .../Entities/Channels/ThreadHelper.cs | 24 +++++++++++++++---- .../Entities/Users/RestThreadUser.cs | 8 +++++++ .../Entities/Channels/SocketThreadChannel.cs | 20 ++++++++++------ .../Entities/Users/SocketThreadUser.cs | 19 ++++++++++----- 9 files changed, 96 insertions(+), 23 deletions(-) diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index ebca0120c..09681dafc 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -214,5 +214,13 @@ namespace Discord /// If set to , this value will be "Discord#1234". /// public bool FormatUsersInBidirectionalUnicode { get; set; } = true; + + /// + /// Returns the max thread members allowed to be in a request. + /// + /// + /// The maximum number of thread members that can be gotten per-batch. + /// + public const int MaxThreadMembersPerBatch = 100; } } diff --git a/src/Discord.Net.Core/Entities/Users/IThreadUser.cs b/src/Discord.Net.Core/Entities/Users/IThreadUser.cs index f94ca8bb6..81cbf89d4 100644 --- a/src/Discord.Net.Core/Entities/Users/IThreadUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IThreadUser.cs @@ -21,5 +21,10 @@ namespace Discord /// Gets the guild this thread was created in. /// IGuild Guild { get; } + + /// + /// Gets the on the server this thread was created in. + /// + IGuildUser GuildUser { get; } } } diff --git a/src/Discord.Net.Rest/API/Common/ThreadMember.cs b/src/Discord.Net.Rest/API/Common/ThreadMember.cs index 30249ee44..fb89f349d 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMember.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMember.cs @@ -16,5 +16,8 @@ namespace Discord.API [JsonProperty("flags")] public int Flags { get; set; } // No enum type (yet?) + + [JsonProperty("member")] + public Optional GuildMember { get; set; } } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index c759119c2..955952d00 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -563,16 +563,23 @@ namespace Discord.API await SendAsync("DELETE", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); } - - public async Task ListThreadMembersAsync(ulong channelId, RequestOptions options = null) + + public async Task ListThreadMembersAsync(ulong channelId, ulong? after = null, int? limit = null, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); + var query = "?with_member=true"; + + if (limit.HasValue) + query += $"&limit={limit}"; + if (after.HasValue) + query += $"&after={after}"; + options = RequestOptions.CreateOrClone(options); var bucket = new BucketIds(channelId: channelId); - return await SendAsync("GET", () => $"channels/{channelId}/thread-members", bucket, options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"channels/{channelId}/thread-members{query}", bucket, options: options).ConfigureAwait(false); } public async Task GetThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null) @@ -583,8 +590,9 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var bucket = new BucketIds(channelId: channelId); + var query = "?with_member=true"; - return await SendAsync("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); + return await SendAsync("GET", () => $"channels/{channelId}/thread-members/{userId}{query}", bucket, options: options).ConfigureAwait(false); } public async Task GetActiveThreadsAsync(ulong guildId, RequestOptions options = null) @@ -2524,7 +2532,7 @@ namespace Discord.API => await SendAsync("GET", () => $"applications/{CurrentApplicationId}/role-connections/metadata", new BucketIds(), options: options).ConfigureAwait(false); public async Task UpdateApplicationRoleConnectionMetadataRecordsAsync(RoleConnectionMetadata[] roleConnections, RequestOptions options = null) - => await SendJsonAsync ("PUT", () => $"applications/{CurrentApplicationId}/role-connections/metadata", roleConnections, new BucketIds(), options: options).ConfigureAwait(false); + => await SendJsonAsync("PUT", () => $"applications/{CurrentApplicationId}/role-connections/metadata", roleConnections, new BucketIds(), options: options).ConfigureAwait(false); public async Task GetUserApplicationRoleConnectionAsync(ulong applicationId, RequestOptions options = null) => await SendAsync("GET", () => $"users/@me/applications/{applicationId}/role-connection", new BucketIds(), options: options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs index 1555b2093..7889c8b41 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -101,6 +101,17 @@ namespace Discord.Rest public new Task GetUserAsync(ulong userId, RequestOptions options = null) => ThreadHelper.GetUserAsync(userId, this, Discord, options); + /// + /// Gets a collection of users within this thread. + /// + /// Sets the limit of the user count for each request. 100 by default. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of thread + /// users found within this thread channel. + /// + public IAsyncEnumerable> GetThreadUsersAsync(int limit = DiscordConfig.MaxThreadMembersPerBatch, RequestOptions options = null) + => ThreadHelper.GetUsersAsync(this, Discord, limit, null, options); + /// /// Gets a collection of users within this thread. /// @@ -109,8 +120,9 @@ namespace Discord.Rest /// A task representing the asynchronous get operation. The task returns a /// of 's. /// + [Obsolete("Please use GetThreadUsersAsync instead of this. Will be removed in next major version.", false)] public new async Task> GetUsersAsync(RequestOptions options = null) - => (await ThreadHelper.GetUsersAsync(this, Discord, options).ConfigureAwait(false)).ToImmutableArray(); + => (await GetThreadUsersAsync(options: options).FlattenAsync()).ToImmutableArray(); /// public override async Task ModifyAsync(Action func, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index f1c6c0efb..7b6d42e22 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -85,11 +85,27 @@ namespace Discord.Rest return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); } - public static async Task GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) + public static IAsyncEnumerable> GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, int limit = DiscordConfig.MaxThreadMembersPerBatch, ulong? afterId = null, RequestOptions options = null) { - var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, options); - - return users.Select(x => RestThreadUser.Create(client, channel.Guild, x, channel)).ToArray(); + return new PagedAsyncEnumerable( + limit, + async (info, ct) => + { + if (info.Position != null) + afterId = info.Position.Value; + var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, afterId, limit, options); + return users.Select(x => RestThreadUser.Create(client, channel.Guild, x, channel)).ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != limit) + return false; + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: afterId, + count: limit + ); } public static async Task GetUserAsync(ulong userId, IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs b/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs index fe362e16e..0187e5f71 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs @@ -18,6 +18,9 @@ namespace Discord.Rest /// public IGuild Guild { get; } + /// + public RestGuildUser GuildUser { get; private set; } + /// public string Mention => MentionUtils.MentionUser(Id); @@ -38,8 +41,13 @@ namespace Discord.Rest internal void Update(Model model) { ThreadJoinedAt = model.JoinTimestamp; + if(model.GuildMember.IsSpecified) + GuildUser = RestGuildUser.Create(Discord, Guild, model.GuildMember.Value); } + /// + IGuildUser IThreadUser.GuildUser => GuildUser; + /// /// Gets the guild user for this thread user. /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 5c739c857..baf3e7529 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -172,7 +172,7 @@ namespace Discord.WebSocket return threadUsers.ToImmutableArray(); } - internal SocketThreadUser AddOrUpdateThreadMember(ThreadMember model, SocketGuildUser guildMember) + internal SocketThreadUser AddOrUpdateThreadMember(ThreadMember model, SocketGuildUser guildMember = null) { if (_members.TryGetValue(model.UserId.Value, out SocketThreadUser member)) member.Update(model); @@ -219,15 +219,21 @@ namespace Discord.WebSocket /// A task representing the asynchronous download operation. public async Task DownloadUsersAsync(RequestOptions options = null) { - var users = await Discord.ApiClient.ListThreadMembersAsync(Id, options); + var prevBatchCount = DiscordConfig.MaxThreadMembersPerBatch; + ulong? maxId = null; - lock (_downloadLock) + while (prevBatchCount == DiscordConfig.MaxThreadMembersPerBatch) { - foreach (var threadMember in users) - { - var guildUser = Guild.GetUser(threadMember.UserId.Value); + var users = await Discord.ApiClient.ListThreadMembersAsync(Id, maxId, DiscordConfig.MaxThreadMembersPerBatch, options); + prevBatchCount = users.Length; + maxId = users.Max(x => x.UserId.GetValueOrDefault()); - AddOrUpdateThreadMember(threadMember, guildUser); + lock (_downloadLock) + { + foreach (var threadMember in users) + { + AddOrUpdateThreadMember(threadMember); + } } } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index 290681d4d..96eee47a4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -134,19 +134,21 @@ namespace Discord.WebSocket public DateTimeOffset? RequestToSpeakTimestamp => GuildUser.RequestToSpeakTimestamp; - private SocketGuildUser GuildUser { get; set; } + /// + public SocketGuildUser GuildUser { get; private set; } - internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member, ulong userId) + internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, ulong userId, SocketGuildUser member = null) : base(guild.Discord, userId) { Thread = thread; Guild = guild; - GuildUser = member; + if(member is not null) + GuildUser = member; } - internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) + internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser guildUser = null) { - var entity = new SocketThreadUser(guild, thread, member, model.UserId.Value); + var entity = new SocketThreadUser(guild, thread, model.UserId.Value, guildUser); entity.Update(model); return entity; } @@ -154,7 +156,7 @@ namespace Discord.WebSocket internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) { // this is used for creating the owner of the thread. - var entity = new SocketThreadUser(guild, thread, owner, owner.Id); + var entity = new SocketThreadUser(guild, thread, owner.Id, owner); entity.Update(new Model { JoinTimestamp = thread.CreatedAt, @@ -165,6 +167,8 @@ namespace Discord.WebSocket internal void Update(Model model) { ThreadJoinedAt = model.JoinTimestamp; + if(model.GuildMember.IsSpecified) + GuildUser = Guild.AddOrUpdateUser(model.GuildMember.Value); } /// @@ -214,6 +218,9 @@ namespace Discord.WebSocket /// IGuild IGuildUser.Guild => Guild; + /// + IGuildUser IThreadUser.GuildUser => GuildUser; + /// ulong IGuildUser.GuildId => Guild.Id;