@@ -214,5 +214,13 @@ namespace Discord | |||
/// If set to <see langword="false"/>, this value will be "Discord#1234". | |||
/// </remarks> | |||
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. | |||
/// </summary> | |||
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")] | |||
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); | |||
} | |||
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)); | |||
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<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) | |||
@@ -584,8 +591,9 @@ namespace Discord.API | |||
options = RequestOptions.CreateOrClone(options); | |||
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) | |||
@@ -2517,7 +2525,7 @@ namespace Discord.API | |||
=> 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) | |||
=> 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) | |||
=> await SendAsync<RoleConnection>("GET", () => $"users/@me/applications/{applicationId}/role-connection", new BucketIds(), options: options); | |||
@@ -104,13 +104,12 @@ namespace Discord.Rest | |||
/// <summary> | |||
/// Gets a collection of users within this thread. | |||
/// </summary> | |||
/// <param name="options">The options to be used when sending the request.</param> | |||
/// <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> | |||
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/> | |||
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(); | |||
} | |||
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) | |||
@@ -18,6 +18,9 @@ namespace Discord.Rest | |||
/// <inheritdoc/> | |||
public IGuild Guild { get; } | |||
/// <inheritdoc cref="IThreadUser.GuildUser"/> | |||
public RestGuildUser GuildUser { get; private set; } | |||
/// <inheritdoc/> | |||
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); | |||
} | |||
/// <inheritdoc /> | |||
IGuildUser IThreadUser.GuildUser => GuildUser; | |||
/// <summary> | |||
/// Gets the guild user for this thread user. | |||
/// </summary> | |||
@@ -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 | |||
/// <returns>A task representing the asynchronous download operation.</returns> | |||
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 | |||
=> 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) | |||
{ | |||
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); | |||
} | |||
/// <inheritdoc/> | |||
@@ -214,6 +218,9 @@ namespace Discord.WebSocket | |||
/// <inheritdoc/> | |||
IGuild IGuildUser.Guild => Guild; | |||
/// <inheritdoc /> | |||
IGuildUser IThreadUser.GuildUser => GuildUser; | |||
/// <inheritdoc/> | |||
ulong IGuildUser.GuildId => Guild.Id; | |||