| @@ -214,5 +214,13 @@ namespace Discord | |||||
| /// If set to <see langword="false"/>, this value will be "Discord#1234". | /// If set to <see langword="false"/>, this value will be "Discord#1234". | ||||
| /// </remarks> | /// </remarks> | ||||
| public bool FormatUsersInBidirectionalUnicode { get; set; } = true; | public bool FormatUsersInBidirectionalUnicode { get; set; } = true; | ||||
| /// <summary> | |||||
| /// Returns the max thread members allowed to be in a request. | |||||
| /// </summary> | |||||
| /// <returns> | |||||
| /// The maximum number of thread members that can be gotten per-batch. | |||||
| /// </returns> | |||||
| public const int MaxThreadMembersPerBatch = 100; | |||||
| } | } | ||||
| } | } | ||||
| @@ -21,5 +21,10 @@ namespace Discord | |||||
| /// Gets the guild this thread was created in. | /// Gets the guild this thread was created in. | ||||
| /// </summary> | /// </summary> | ||||
| IGuild Guild { get; } | IGuild Guild { get; } | ||||
| /// <summary> | |||||
| /// Gets the <see cref="IGuildUser"/> on the server this thread was created in. | |||||
| /// </summary> | |||||
| IGuildUser GuildUser { get; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -16,5 +16,8 @@ namespace Discord.API | |||||
| [JsonProperty("flags")] | [JsonProperty("flags")] | ||||
| public int Flags { get; set; } // No enum type (yet?) | public int Flags { get; set; } // No enum type (yet?) | ||||
| [JsonProperty("member")] | |||||
| public Optional<GuildMember> GuildMember { get; set; } | |||||
| } | } | ||||
| } | } | ||||
| @@ -564,16 +564,23 @@ namespace Discord.API | |||||
| await SendAsync("DELETE", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); | await SendAsync("DELETE", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task<ThreadMember[]> ListThreadMembersAsync(ulong channelId, RequestOptions options = null) | |||||
| public async Task<ThreadMember[]> ListThreadMembersAsync(ulong channelId, ulong? after = null, int? limit = null, RequestOptions options = null) | |||||
| { | { | ||||
| Preconditions.NotEqual(channelId, 0, nameof(channelId)); | 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); | options = RequestOptions.CreateOrClone(options); | ||||
| var bucket = new BucketIds(channelId: channelId); | var bucket = new BucketIds(channelId: channelId); | ||||
| return await SendAsync<ThreadMember[]>("GET", () => $"channels/{channelId}/thread-members", bucket, options: options).ConfigureAwait(false); | |||||
| return await SendAsync<ThreadMember[]>("GET", () => $"channels/{channelId}/thread-members{query}", bucket, options: options).ConfigureAwait(false); | |||||
| } | } | ||||
| public async Task<ThreadMember> GetThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null) | public async Task<ThreadMember> GetThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null) | ||||
| @@ -584,8 +591,9 @@ namespace Discord.API | |||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| var bucket = new BucketIds(channelId: channelId); | var bucket = new BucketIds(channelId: channelId); | ||||
| var query = "?with_member=true"; | |||||
| return await SendAsync<ThreadMember>("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); | |||||
| return await SendAsync<ThreadMember>("GET", () => $"channels/{channelId}/thread-members/{userId}{query}", bucket, options: options).ConfigureAwait(false); | |||||
| } | } | ||||
| public async Task<ChannelThreads> GetActiveThreadsAsync(ulong guildId, RequestOptions options = null) | public async Task<ChannelThreads> GetActiveThreadsAsync(ulong guildId, RequestOptions options = null) | ||||
| @@ -2517,7 +2525,7 @@ namespace Discord.API | |||||
| => await SendAsync<RoleConnectionMetadata[]>("GET", () => $"applications/{CurrentApplicationId}/role-connections/metadata", new BucketIds(), options: options).ConfigureAwait(false); | => await SendAsync<RoleConnectionMetadata[]>("GET", () => $"applications/{CurrentApplicationId}/role-connections/metadata", new BucketIds(), options: options).ConfigureAwait(false); | ||||
| public async Task<RoleConnectionMetadata[]> UpdateApplicationRoleConnectionMetadataRecordsAsync(RoleConnectionMetadata[] roleConnections, RequestOptions options = null) | public async Task<RoleConnectionMetadata[]> UpdateApplicationRoleConnectionMetadataRecordsAsync(RoleConnectionMetadata[] roleConnections, RequestOptions options = null) | ||||
| => await SendJsonAsync <RoleConnectionMetadata[]>("PUT", () => $"applications/{CurrentApplicationId}/role-connections/metadata", roleConnections, new BucketIds(), options: options).ConfigureAwait(false); | |||||
| => await SendJsonAsync<RoleConnectionMetadata[]>("PUT", () => $"applications/{CurrentApplicationId}/role-connections/metadata", roleConnections, new BucketIds(), options: options).ConfigureAwait(false); | |||||
| public async Task<RoleConnection> GetUserApplicationRoleConnectionAsync(ulong applicationId, RequestOptions options = null) | public async Task<RoleConnection> GetUserApplicationRoleConnectionAsync(ulong applicationId, RequestOptions options = null) | ||||
| => await SendAsync<RoleConnection>("GET", () => $"users/@me/applications/{applicationId}/role-connection", new BucketIds(), options: options); | => await SendAsync<RoleConnection>("GET", () => $"users/@me/applications/{applicationId}/role-connection", new BucketIds(), options: options); | ||||
| @@ -104,13 +104,12 @@ namespace Discord.Rest | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a collection of users within this thread. | /// Gets a collection of users within this thread. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | /// <returns> | ||||
| /// A task representing the asynchronous get operation. The task returns a | |||||
| /// <see cref="IReadOnlyCollection{T}"/> of <see cref="RestThreadUser"/>'s. | |||||
| /// A task that represents the asynchronous get operation. The task result contains a collection of thread | |||||
| /// users found within this thread channel. | |||||
| /// </returns> | /// </returns> | ||||
| public new async Task<IReadOnlyCollection<RestThreadUser>> GetUsersAsync(RequestOptions options = null) | |||||
| => (await ThreadHelper.GetUsersAsync(this, Discord, options).ConfigureAwait(false)).ToImmutableArray(); | |||||
| public IAsyncEnumerable<IReadOnlyCollection<RestThreadUser>> GetUsersAsync(int usersPerBatch = DiscordConfig.MaxThreadMembersPerBatch, RequestOptions options = null) | |||||
| => ThreadHelper.GetUsersAsync(this, Discord, usersPerBatch, null, options); | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public override async Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null) | public override async Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null) | ||||
| @@ -85,11 +85,27 @@ namespace Discord.Rest | |||||
| return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); | return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); | ||||
| } | } | ||||
| public static async Task<RestThreadUser[]> GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) | |||||
| public static IAsyncEnumerable<IReadOnlyCollection<RestThreadUser>> GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, int? limit = null, 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<RestThreadUser>( | |||||
| DiscordConfig.MaxUsersPerBatch, | |||||
| 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<RestThreadUser> GetUserAsync(ulong userId, IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) | public static async Task<RestThreadUser> GetUserAsync(ulong userId, IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) | ||||
| @@ -18,6 +18,9 @@ namespace Discord.Rest | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public IGuild Guild { get; } | public IGuild Guild { get; } | ||||
| /// <inheritdoc cref="IThreadUser.GuildUser"/> | |||||
| public RestGuildUser GuildUser { get; private set; } | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| public string Mention => MentionUtils.MentionUser(Id); | public string Mention => MentionUtils.MentionUser(Id); | ||||
| @@ -38,8 +41,13 @@ namespace Discord.Rest | |||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| ThreadJoinedAt = model.JoinTimestamp; | ThreadJoinedAt = model.JoinTimestamp; | ||||
| if(model.GuildMember.IsSpecified) | |||||
| GuildUser = RestGuildUser.Create(Discord, Guild, model.GuildMember.Value); | |||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| IGuildUser IThreadUser.GuildUser => GuildUser; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the guild user for this thread user. | /// Gets the guild user for this thread user. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -172,7 +172,7 @@ namespace Discord.WebSocket | |||||
| return threadUsers.ToImmutableArray(); | 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)) | if (_members.TryGetValue(model.UserId.Value, out SocketThreadUser member)) | ||||
| member.Update(model); | member.Update(model); | ||||
| @@ -219,15 +219,21 @@ namespace Discord.WebSocket | |||||
| /// <returns>A task representing the asynchronous download operation.</returns> | /// <returns>A task representing the asynchronous download operation.</returns> | ||||
| public async Task DownloadUsersAsync(RequestOptions options = null) | 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); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -134,19 +134,21 @@ namespace Discord.WebSocket | |||||
| public DateTimeOffset? RequestToSpeakTimestamp | public DateTimeOffset? RequestToSpeakTimestamp | ||||
| => GuildUser.RequestToSpeakTimestamp; | => GuildUser.RequestToSpeakTimestamp; | ||||
| private SocketGuildUser GuildUser { get; set; } | |||||
| /// <inheritdoc cref="IThreadUser.GuildUser"/> | |||||
| 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) | : base(guild.Discord, userId) | ||||
| { | { | ||||
| Thread = thread; | Thread = thread; | ||||
| Guild = guild; | 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); | entity.Update(model); | ||||
| return entity; | return entity; | ||||
| } | } | ||||
| @@ -154,7 +156,7 @@ namespace Discord.WebSocket | |||||
| internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) | internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser owner) | ||||
| { | { | ||||
| // this is used for creating the owner of the thread. | // 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 | entity.Update(new Model | ||||
| { | { | ||||
| JoinTimestamp = thread.CreatedAt, | JoinTimestamp = thread.CreatedAt, | ||||
| @@ -165,6 +167,8 @@ namespace Discord.WebSocket | |||||
| internal void Update(Model model) | internal void Update(Model model) | ||||
| { | { | ||||
| ThreadJoinedAt = model.JoinTimestamp; | ThreadJoinedAt = model.JoinTimestamp; | ||||
| if(model.GuildMember.IsSpecified) | |||||
| GuildUser = Guild.AddOrUpdateUser(model.GuildMember.Value); | |||||
| } | } | ||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| @@ -214,6 +218,9 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| IGuild IGuildUser.Guild => Guild; | IGuild IGuildUser.Guild => Guild; | ||||
| /// <inheritdoc /> | |||||
| IGuildUser IThreadUser.GuildUser => GuildUser; | |||||
| /// <inheritdoc/> | /// <inheritdoc/> | ||||
| ulong IGuildUser.GuildId => Guild.Id; | ulong IGuildUser.GuildId => Guild.Id; | ||||