From 4db858b1e456a622792c998f8cb7f5791b533e4d Mon Sep 17 00:00:00 2001 From: quin lynch Date: Wed, 28 Jul 2021 15:43:09 -0300 Subject: [PATCH] Threads initial --- src/Discord.Net.Core/Discord.Net.Core.xml | 115 ++++++ .../Entities/Channels/IThreadChannel.cs | 44 +++ .../Channels/ThreadArchiveDuration.cs | 40 ++ .../Channels/ThreadChannelProperties.cs | 45 +++ .../Entities/Messages/MessageFlags.cs | 12 + src/Discord.Net.Rest/API/Common/Channel.cs | 16 + .../API/Common/ChannelThreads.cs | 21 ++ src/Discord.Net.Rest/API/Common/Guild.cs | 2 + .../API/Common/ThreadMember.cs | 24 ++ .../API/Common/ThreadMetadata.cs | 24 ++ .../Rest/ModifyInteractionResponseParams.cs | 3 + .../API/Rest/StartThreadParams.cs | 21 ++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 154 ++++++++ .../Interactions/InteractionHelper.cs | 1 + .../API/Gateway/ExtendedGuild.cs | 10 +- .../API/Gateway/ThreadListSyncEvent.cs | 24 ++ .../BaseSocketClient.Events.cs | 32 ++ .../Discord.Net.WebSocket.csproj | 2 +- .../Discord.Net.WebSocket.xml | 203 ++++++++++ .../DiscordSocketClient.cs | 106 ++++++ .../Entities/Channels/SocketGuildChannel.cs | 2 + .../Entities/Channels/SocketTextChannel.cs | 6 + .../Entities/Channels/SocketThreadChannel.cs | 348 ++++++++++++++++++ .../Entities/Guilds/SocketGuild.cs | 8 + .../Entities/Users/SocketThreadUser.cs | 212 +++++++++++ 25 files changed, 1473 insertions(+), 2 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs create mode 100644 src/Discord.Net.Rest/API/Common/ChannelThreads.cs create mode 100644 src/Discord.Net.Rest/API/Common/ThreadMember.cs create mode 100644 src/Discord.Net.Rest/API/Common/ThreadMetadata.cs create mode 100644 src/Discord.Net.Rest/API/Rest/StartThreadParams.cs create mode 100644 src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs create mode 100644 src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs diff --git a/src/Discord.Net.Core/Discord.Net.Core.xml b/src/Discord.Net.Core/Discord.Net.Core.xml index 9e288d1a1..3c0e6afa2 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.xml +++ b/src/Discord.Net.Core/Discord.Net.Core.xml @@ -1982,6 +1982,41 @@ of webhooks that is available in this channel. + + + Represents a thread channel inside of a guild. + + + + + if the current thread is archived, otherwise . + + + + + Duration to automatically archive the thread after recent activity. + + + + + Timestamp when the thread's archive status was last changed, used for calculating recent activity. + + + + + if the current thread is locked, otherwise + + + + + An approximate count of users in a thread, stops counting at 50. + + + + + An approximate count of messages in a thread, stops counting at 50. + + Represents a generic voice channel in a guild. @@ -2081,6 +2116,71 @@ Thrown if the value does not fall within [0, 21600]. + + + Represents the thread auto archive duration. + + + + + One hour (60 minutes). + + + + + One day (1440 minutes). + + + + + Three days (4320 minutes). + + This option is explicity avaliable to nitro users. + + + + + + One week (10080 minutes). + + This option is explicity avaliable to nitro users. + + + + + + Gets or sets whether or not the thread is archived. + + + + + Gets or sets whether or not the thread is locked. + + + + + Gets or sets the name of the thread. + + + + + Gets or sets the auto archive duration. + + + + + Gets or sets the slow-mode ratelimit in seconds for this channel. + + + Setting this value to anything above zero will require each user to wait X seconds before + sending another message; setting this value to 0 will disable slow-mode for this channel. + + Users with or + will be exempt from slow-mode. + + + Thrown if the value does not fall within [0, 21600]. + Provides properties that are used to modify an with the specified changes. @@ -7029,6 +7129,21 @@ Flag given to messages that came from the urgent message system. + + + Flag given to messages has an associated thread, with the same id as the message + + + + + Flag given to messages that is only visible to the user who invoked the Interaction. + + + + + Flag given to messages that is an Interaction Response and the bot is "thinking" + + Properties that are used to modify an with the specified changes. diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs new file mode 100644 index 000000000..1459c954c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents a thread channel inside of a guild. + /// + public interface IThreadChannel : ITextChannel, IGuildChannel + { + /// + /// if the current thread is archived, otherwise . + /// + bool Archived { get; } + + /// + /// Duration to automatically archive the thread after recent activity. + /// + ThreadArchiveDuration AutoArchiveDuration { get; } + + /// + /// Timestamp when the thread's archive status was last changed, used for calculating recent activity. + /// + DateTimeOffset ArchiveTimestamp { get; } + + /// + /// if the current thread is locked, otherwise + /// + bool Locked { get; } + + /// + /// An approximate count of users in a thread, stops counting at 50. + /// + int MemberCount { get; } + + /// + /// An approximate count of messages in a thread, stops counting at 50. + /// + int MessageCount { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs b/src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs new file mode 100644 index 000000000..01d1574bf --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ThreadArchiveDuration.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents the thread auto archive duration. + /// + public enum ThreadArchiveDuration + { + /// + /// One hour (60 minutes). + /// + OneHour = 60, + + /// + /// One day (1440 minutes). + /// + OneDay = 1440, + + /// + /// Three days (4320 minutes). + /// + /// This option is explicity avaliable to nitro users. + /// + /// + ThreeDays = 4320, + + /// + /// One week (10080 minutes). + /// + /// This option is explicity avaliable to nitro users. + /// + /// + OneWeek = 10080, + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs new file mode 100644 index 000000000..10a0d7654 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Entities +{ + public class ThreadChannelProperties + { + /// + /// Gets or sets whether or not the thread is archived. + /// + public Optional Archived { get; set; } + + /// + /// Gets or sets whether or not the thread is locked. + /// + public Optional Locked { get; set; } + + /// + /// Gets or sets the name of the thread. + /// + public Optional Name { get; set; } + + /// + /// Gets or sets the auto archive duration. + /// + public Optional AutoArchiveDuration { get; set; } + + /// + /// Gets or sets the slow-mode ratelimit in seconds for this channel. + /// + /// + /// Setting this value to anything above zero will require each user to wait X seconds before + /// sending another message; setting this value to 0 will disable slow-mode for this channel. + /// + /// Users with or + /// will be exempt from slow-mode. + /// + /// + /// Thrown if the value does not fall within [0, 21600]. + public Optional SlowModeInterval { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs index 52d0f0e9e..6f9450372 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs @@ -32,5 +32,17 @@ namespace Discord /// Flag given to messages that came from the urgent message system. /// Urgent = 1 << 4, + /// + /// Flag given to messages has an associated thread, with the same id as the message + /// + HasThread = 1 << 5, + /// + /// Flag given to messages that is only visible to the user who invoked the Interaction. + /// + Ephemeral = 1 << 6, + /// + /// Flag given to messages that is an Interaction Response and the bot is "thinking" + /// + Loading = 1 << 7 } } diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index 57a5ce9ab..2dd004021 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -49,5 +49,21 @@ namespace Discord.API //GroupChannel [JsonProperty("icon")] public Optional Icon { get; set; } + + //ThreadChannel + [JsonProperty("member")] + public Optional ThreadMember { get; set; } + + [JsonProperty("thread_metadata")] + public Optional ThreadMetadata { get; set; } + + [JsonProperty("owner_id")] + public Optional OwnerId { get; set; } + + [JsonProperty("message_count")] + public Optional MessageCount { get; set; } + + [JsonProperty("member_count")] + public Optional MemberCount { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ChannelThreads.cs b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs new file mode 100644 index 000000000..eccac8e54 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class ChannelThreads + { + [JsonProperty("threads")] + public Channel[] Threads { get; set; } + + [JsonProperty("members")] + public ThreadMember[] Members { get; set; } + + [JsonProperty("has_more")] + public bool HasMore { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs index bd25c7e1a..ba09f9b8a 100644 --- a/src/Discord.Net.Rest/API/Common/Guild.cs +++ b/src/Discord.Net.Rest/API/Common/Guild.cs @@ -76,5 +76,7 @@ namespace Discord.API public Optional ApproximateMemberCount { get; set; } [JsonProperty("approximate_presence_count")] public Optional ApproximatePresenceCount { get; set; } + [JsonProperty("threads")] + public Optional Threads { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ThreadMember.cs b/src/Discord.Net.Rest/API/Common/ThreadMember.cs new file mode 100644 index 000000000..b4c542454 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ThreadMember.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ThreadMember + { + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("user_id")] + public Optional UserId { get; set; } + + [JsonProperty("join_timestamp")] + public DateTimeOffset JoinTimestamp { get; set; } + + [JsonProperty("flags")] + public int Flags { get; set; } // No enum type (yet?) + } +} diff --git a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs new file mode 100644 index 000000000..f6da249f7 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ThreadMetadata + { + [JsonProperty("archived")] + public bool Archived { get; set; } + + [JsonProperty("auto_archive_duration")] + public int AutoArchiveDuration { get; set; } + + [JsonProperty("archive_timestamp")] + public DateTimeOffset ArchiveTimestamp { get; set; } + + [JsonProperty("locked")] + public Optional Locked { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs index b8c7a124f..b2800a066 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyInteractionResponseParams.cs @@ -20,5 +20,8 @@ namespace Discord.API.Rest [JsonProperty("components")] public Optional Components { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs b/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs new file mode 100644 index 000000000..167d94099 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class StartThreadParams + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("duration")] + public ThreadArchiveDuration Duration { get; set; } + + [JsonProperty("type")] + public Optional Type { get; set; } + } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 36259b01a..a56b3d7f6 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -413,6 +413,160 @@ namespace Discord.API break; } } + + // Threads + public async Task StartThreadAsync(ulong channelId, ulong messageId, StartThreadParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(0, channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/messages/{messageId}/threads", args, bucket, options: options).ConfigureAwait(false); + } + + public async Task StartThreadAsync(ulong channelId, StartThreadParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(0, channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/threads", args, bucket, options: options).ConfigureAwait(false); + } + + public async Task JoinThreadAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + await SendAsync("PUT", $"channels/{channelId}/thread-members/@me", options: options).ConfigureAwait(false); + } + + public async Task AddThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(userId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + await SendAsync("PUT", $"channels/{channelId}/thread-members/{userId}", options: options).ConfigureAwait(false); + } + + public async Task LeaveThreadAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + await SendAsync("DELETE", $"channels/{channelId}/thread-members/@me", options: options).ConfigureAwait(false); + } + + public async Task RemoveThreadMemberAsync(ulong channelId, ulong userId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(userId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + await SendAsync("DELETE", $"channels/{channelId}/thread-members/{userId}", options: options).ConfigureAwait(false); + } + + public async Task ListThreadMembersAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + return await SendAsync("GET", () => $"/channels/{channelId}", bucket, options: options); + } + + public async Task GetActiveThreadsAsync(ulong channelId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + return await SendAsync("GET", $"channels/{channelId}/threads/active"); + } + + public async Task GetPublicArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + string query = ""; + + if (limit.HasValue) + { + query = $"?before={before.GetValueOrDefault(DateTimeOffset.UtcNow).ToString("O")}&limit={limit.Value}"; + } + else if (before.HasValue) + { + query = $"?before={before.Value.ToString("O")}"; + } + + return await SendAsync("GET", () => $"channels/{channelId}/threads/archived/public{query}", bucket, options: options); + } + + public async Task GetPrivateArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, + RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + string query = ""; + + if (limit.HasValue) + { + query = $"?before={before.GetValueOrDefault(DateTimeOffset.UtcNow).ToString("O")}&limit={limit.Value}"; + } + else if (before.HasValue) + { + query = $"?before={before.Value.ToString("O")}"; + } + + return await SendAsync("GET", () => $"channels/{channelId}/threads/archived/private{query}", bucket, options: options); + } + + public async Task GetJoinedPrivateArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, + RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + options = RequestOptions.CreateOrClone(options); + + var bucket = new BucketIds(channelId: channelId); + + string query = ""; + + if (limit.HasValue) + { + query = $"?before={SnowflakeUtils.ToSnowflake(before.GetValueOrDefault(DateTimeOffset.UtcNow))}&limit={limit.Value}"; + } + else if (before.HasValue) + { + query = $"?before={before.Value.ToString("O")}"; + } + + return await SendAsync("GET", () => $"channels/{channelId}/users/@me/threads/archived/private{query}", bucket, options: options); + } + + // roles public async Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index ec32623c7..f030f91ca 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -333,6 +333,7 @@ namespace Discord.Rest Embeds = args.Embed.IsSpecified ? new API.Embed[] { args.Embed.Value.ToModel() } : Optional.Create(), AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Unspecified, Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + Flags = args.Flags }; return await client.ApiClient.ModifyInteractionResponse(apiArgs, message.Token, options).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs index 910f6d909..da02b737a 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ExtendedGuild.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; using System; @@ -8,18 +8,26 @@ namespace Discord.API.Gateway { [JsonProperty("unavailable")] public bool? Unavailable { get; set; } + [JsonProperty("member_count")] public int MemberCount { get; set; } + [JsonProperty("large")] public bool Large { get; set; } [JsonProperty("presences")] public Presence[] Presences { get; set; } + [JsonProperty("members")] public GuildMember[] Members { get; set; } + [JsonProperty("channels")] public Channel[] Channels { get; set; } + [JsonProperty("joined_at")] public DateTimeOffset JoinedAt { get; set; } + + [JsonProperty("threads")] + public Channel[] Threads { get; set; } } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs new file mode 100644 index 000000000..ab7989268 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/ThreadListSyncEvent.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Gateway +{ + internal class ThreadListSyncEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("channel_ids")] + public Optional ChannelIds { get; set; } + + [JsonProperty("threads")] + public Channel[] Threads { get; set; } + + [JsonProperty("members")] + public ThreadMember[] Members { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 3b6b3ec5a..f29f13cf6 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -540,5 +540,37 @@ namespace Discord.WebSocket remove { _applicationCommandDeleted.Remove(value); } } internal readonly AsyncEvent> _applicationCommandDeleted = new AsyncEvent>(); + + /// + /// Fired when a thread is created within a guild. + /// + public event Func ThreadCreated + { + add { _threadCreated.Add(value); } + remove { _threadCreated.Remove(value); } + } + internal readonly AsyncEvent> _threadCreated = new AsyncEvent>(); + + /// + /// Fired when a thread is updated within a guild. + /// + public event Func ThreadUpdated + { + add { _threadUpdated.Add(value); } + remove { _threadUpdated.Remove(value); } + } + + internal readonly AsyncEvent> _threadUpdated = new AsyncEvent>(); + + /// + /// Fired when a thread is deleted. + /// + public event Func, Task> ThreadDeleted + { + add { _threadDeleted.Add(value); } + remove { _threadDeleted.Remove(value); } + } + internal readonly AsyncEvent, Task>> _threadDeleted = new AsyncEvent, Task>>(); + } } diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index 56932f7d8..3f1f63db3 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -8,7 +8,7 @@ net461;netstandard2.0;netstandard2.1 netstandard2.0;netstandard2.1 true - 2.4.8 + 2.4.9 https://github.com/Discord-Net-Labs/Discord.Net-Labs https://github.com/Discord-Net-Labs/Discord.Net-Labs Temporary.png diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml index 89d29ba09..e5b4c84a0 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml @@ -2355,6 +2355,209 @@ + + + Represents a thread channel inside of a guild. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Gets a message from this message channel. + + + This method follows the same behavior as described in . + Please visit its documentation for more details on this method. + + The snowflake identifier of the message. + The options to be used when sending the request. + + A task that represents an asynchronous get operation for retrieving the message. The task result contains + the retrieved message; null if no message is found with the specified identifier. + + + + + Gets the last N messages from this message channel. + + + This method follows the same behavior as described in . + Please visit its documentation for more details on this method. + + The numbers of message to be gotten from. + The options to be used when sending the request. + + Paged collection of messages. + + + + + Gets a collection of messages in this channel. + + + This method follows the same behavior as described in . + Please visit its documentation for more details on this method. + + The ID of the starting message to get the messages from. + The direction of the messages to be gotten from. + The numbers of message to be gotten from. + The options to be used when sending the request. + + Paged collection of messages. + + + + + Gets a collection of messages in this channel. + + + This method follows the same behavior as described in . + Please visit its documentation for more details on this method. + + The starting message to get the messages from. + The direction of the messages to be gotten from. + The numbers of message to be gotten from. + The options to be used when sending the request. + + Paged collection of messages. + + + + + + + + + + + + + + + + + Message content is too long, length must be less or equal to . + + + + + + + Message content is too long, length must be less or equal to . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Represents a WebSocket-based voice channel in a guild. diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f9eaeb005..dc9dedd34 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1862,6 +1862,8 @@ namespace Discord.WebSocket } } break; + + // Interactions case "INTERACTION_CREATE": { await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); @@ -1969,6 +1971,110 @@ namespace Discord.WebSocket await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false); } break; + + // Threads + case "THREAD_CREATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_CREATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId.Value); + + if(guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value); + return; + } + + var threadChannel = (SocketThreadChannel)guild.AddChannel(this.State, data); + + await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); + } + + break; + case "THREAD_UPDATE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value); + return; + } + + var channel = (SocketThreadChannel)guild.GetChannel(data.Id); + + var before = channel.Clone(); + channel.Update(State, data); + + if (!(guild?.IsSynced ?? true)) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + await TimedInvokeAsync(_threadUpdated, nameof(ThreadUpdated), before, channel).ConfigureAwait(false); + + } + break; + case "THREAD_DELETE": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_DELETE)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId.Value); + + if(guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + + var thread = (SocketThreadChannel)guild.GetChannel(data.Id); + + var cacheable = new Cacheable(thread, data.Id, thread != null, null); + + await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false); + } + break; + case "THREAD_LIST_SYNC": + { + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + var guild = State.GetGuild(data.GuildId); + + if(guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + foreach(var thread in data.Threads) + { + var entity = guild.ThreadChannels.FirstOrDefault(x => x.Id == thread.Id); + + if(entity == null) + { + guild.AddChannel(this.State, thread); + } + else + { + entity.Update(this.State, thread); + } + } + } + break; + case "THREAD_MEMBER_UPDATE": + break; + case "THREAD_MEMBERS_UPDATE": // based on intents + break; + //Ignored (User only) case "CHANNEL_PINS_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (CHANNEL_PINS_ACK)").ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 3cc8496d9..9f63ad7b4 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -56,6 +56,8 @@ namespace Discord.WebSocket return SocketVoiceChannel.Create(guild, state, model); case ChannelType.Category: return SocketCategoryChannel.Create(guild, state, model); + case ChannelType.PrivateThread or ChannelType.PublicThread or ChannelType.NewsThread: + return SocketThreadChannel.Create(guild, state, model); default: return new SocketGuildChannel(guild.Discord, model.Id, guild); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index de896be81..192d74328 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -50,6 +50,12 @@ namespace Discord.WebSocket Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), ChannelPermission.ViewChannel)).ToImmutableArray(); + /// + /// Gets a collection of threads within this text channel. + /// + public IReadOnlyCollection Threads + => Guild.ThreadChannels.Where(x => x.ParentChannel.Id == this.Id).ToImmutableArray(); + internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs new file mode 100644 index 000000000..d9583d1ea --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -0,0 +1,348 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + /// + /// Represents a thread channel inside of a guild. + /// + public class SocketThreadChannel : SocketGuildChannel, IThreadChannel, ISocketMessageChannel + { + /// + /// if this thread is private, otherwise + /// + public bool IsPrivateThread { get; private set; } + + /// + /// Gets the parent channel this thread resides in. + /// + public SocketTextChannel ParentChannel { get; private set; } + + /// + public int MessageCount { get; private set; } + + /// + public int MemberCount { get; private set; } + + /// + public bool Archived { get; private set; } + + /// + public DateTimeOffset ArchiveTimestamp { get; private set; } + + /// + public ThreadArchiveDuration AutoArchiveDuration { get; private set; } + + /// + public bool Locked { get; private set; } + + /// + public bool IsNsfw { get; private set; } + + /// + public string Topic { get; private set; } + + /// + public int SlowModeInterval { get; private set; } + + /// + public string Mention { get; private set; } + + /// + public ulong? CategoryId { get; private set; } + + /// + public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + + public new IReadOnlyCollection Users = ImmutableArray.Create(); + + private readonly MessageCache _messages; + + internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketTextChannel parent) + : base(discord, id, guild) + { + this.ParentChannel = parent; + } + + internal static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) + { + var parent = (SocketTextChannel)guild.GetChannel(model.CategoryId.Value); + var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + + this.MessageCount = model.MessageCount.GetValueOrDefault(-1); + this.MemberCount = model.MemberCount.GetValueOrDefault(-1); + + this.IsPrivateThread = model.Type == ChannelType.PrivateThread; + + if (model.ThreadMetadata.IsSpecified) + { + this.Archived = model.ThreadMetadata.Value.Archived; + this.ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; + this.AutoArchiveDuration = (ThreadArchiveDuration)model.ThreadMetadata.Value.AutoArchiveDuration; + this.Locked = model.ThreadMetadata.Value.Locked.GetValueOrDefault(false); + } + } + + /// + public virtual Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + + /// + public Task ModifyAsync(Action func, RequestOptions options = null) + => ChannelHelper.ModifyAsync(this, Discord, func, options); + + //Messages + /// + public SocketMessage GetCachedMessage(ulong id) + => _messages?.Get(id); + + /// + /// Gets a message from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The snowflake identifier of the message. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the message. The task result contains + /// the retrieved message; null if no message is found with the specified identifier. + /// + public async Task GetMessageAsync(ulong id, RequestOptions options = null) + { + IMessage msg = _messages?.Get(id); + if (msg == null) + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); + return msg; + } + + /// + /// Gets the last N messages from this message channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); + + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); + + /// + /// Gets a collection of messages in this channel. + /// + /// + /// This method follows the same behavior as described in . + /// Please visit its documentation for more details on this method. + /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. + /// + public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); + + /// + public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + + /// + public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + + /// + public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) + => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + + /// + public Task> GetPinnedMessagesAsync(RequestOptions options = null) + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, component, options); + + /// + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, component, options, isSpoiler); + + /// + /// Message content is too long, length must be less or equal to . + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, component, options, isSpoiler); + + /// + public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + /// + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); + + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + + /// + public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) + => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + + /// + public Task TriggerTypingAsync(RequestOptions options = null) + => ChannelHelper.TriggerTypingAsync(this, Discord, options); + + /// + public IDisposable EnterTypingState(RequestOptions options = null) + => ChannelHelper.EnterTypingState(this, Discord, options); + + internal void AddMessage(SocketMessage msg) + => _messages?.Add(msg); + + internal SocketMessage RemoveMessage(ulong id) + => _messages?.Remove(id); + + //Users + /// + public new SocketThreadUser GetUser(ulong id) + { + var user = Users.FirstOrDefault(x => x.Id == id); + return user; + } + + public Task GetUsersAsync() + { + + } + + private string DebuggerDisplay => $"{Name} ({Id}, Thread)"; + internal new SocketThreadChannel Clone() => MemberwiseClone() as SocketThreadChannel; + + //ITextChannel + /// + Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) + => throw new NotSupportedException("Thread channels don't support webhooks"); + /// + Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) + => throw new NotSupportedException("Thread channels don't support webhooks"); + /// + Task> ITextChannel.GetWebhooksAsync(RequestOptions options) + => throw new NotSupportedException("Thread channels don't support webhooks"); + + //IGuildChannel + /// + Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => Task.FromResult(GetUser(id)); + /// + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + + //IMessageChannel + /// + async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await GetMessageAsync(id, options).ConfigureAwait(false); + else + return GetCachedMessage(id); + } + /// + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options); + /// + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); + /// + IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); + /// + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, component).ConfigureAwait(false); + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent component) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, component).ConfigureAwait(false); + + // INestedChannel + /// + Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(this.ParentChannel.Category); + Task INestedChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => throw new NotSupportedException("Thread channels don't support invites"); + Task INestedChannel.CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => throw new NotSupportedException("Thread channels don't support invites"); + Task INestedChannel.CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) + => throw new NotSupportedException("Thread channels don't support invites"); + Task> INestedChannel.GetInvitesAsync(RequestOptions options) + => throw new NotSupportedException("Thread channels don't support invites"); + + public Task JoinAsync() + { + + } + + public Task LeaveAsync() + { + + } + + public Task AddThreadMember(IGuildUser user) + { + + } + + public Task RemoveThreadMember(IGuildUser user) + { + + } + + + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index e8748dda9..547dc6d7f 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -277,6 +277,14 @@ namespace Discord.WebSocket public IReadOnlyCollection CategoryChannels => Channels.OfType().ToImmutableArray(); /// + /// Gets a collection of all thread channels in this guild. + /// + /// + /// A read-only collection of thread channels found within this guild. + /// + public IReadOnlyCollection ThreadChannels + => Channels.OfType().ToImmutableArray(); + /// /// Gets the current logged-in user. /// public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs new file mode 100644 index 000000000..66a5579c4 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ThreadMember; +using MemberModel = Discord.API.GuildMember; +using Discord.API; +using System.Collections.Immutable; + +namespace Discord.WebSocket +{ + public class SocketThreadUser : IGuildUser + { + /// + /// Gets the this user is in. + /// + public SocketThreadChannel Thread { get; private set; } + + /// + /// Gets the timestamp for when this user joined this thread. + /// + public DateTimeOffset ThreadJoinedAt { get; private set; } + + /// + /// Gets the guild this user is in. + /// + public SocketGuild Guild { get; private set; } + + /// + public DateTimeOffset? JoinedAt + => GuildUser.JoinedAt; + + /// + public string Nickname + => GuildUser.Nickname; + + /// + public DateTimeOffset? PremiumSince + => GuildUser.PremiumSince; + + /// + public bool? IsPending + => GuildUser.IsPending; + + /// + public string AvatarId + => GuildUser.AvatarId; + + /// + public string Discriminator + => GuildUser.Discriminator; + + /// + public ushort DiscriminatorValue + => GuildUser.DiscriminatorValue; + + /// + public bool IsBot + => GuildUser.IsBot; + + /// + public bool IsWebhook + => GuildUser.IsWebhook; + + /// + public string Username + => GuildUser.Username; + + /// + public UserProperties? PublicFlags + => GuildUser.PublicFlags; + + /// + public DateTimeOffset CreatedAt + => GuildUser.CreatedAt; + + /// + public ulong Id + => GuildUser.Id; + + /// + public string Mention + => GuildUser.Mention; + + /// + public UserStatus Status + => GuildUser.Status; + + /// + public IImmutableSet ActiveClients + => GuildUser.ActiveClients; + + /// + public IImmutableList Activities + => GuildUser.Activities; + + /// + public bool IsDeafened + => GuildUser.IsDeafened; + + /// + public bool IsMuted + => GuildUser.IsMuted; + + /// + public bool IsSelfDeafened + => GuildUser.IsSelfDeafened; + + /// + public bool IsSelfMuted + => GuildUser.IsSelfMuted; + + /// + public bool IsSuppressed + => GuildUser.IsSuppressed; + + /// + public IVoiceChannel VoiceChannel + => GuildUser.VoiceChannel; + + /// + public string VoiceSessionId + => GuildUser.VoiceSessionId; + + /// + public bool IsStreaming + => GuildUser.IsStreaming; + + private SocketGuildUser GuildUser { get; set; } + + internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member) + { + this.Thread = thread; + this.Guild = guild; + this.GuildUser = member; + } + + internal SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) + { + var entity = new SocketThreadUser(guild, thread, member); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + this.ThreadJoinedAt = model.JoinTimestamp; + } + + + /// + public ChannelPermissions GetPermissions(IGuildChannel channel) => GuildUser.GetPermissions(channel); + + /// + public Task KickAsync(string reason = null, RequestOptions options = null) => GuildUser.KickAsync(reason, options); + + /// + public Task ModifyAsync(Action func, RequestOptions options = null) => GuildUser.ModifyAsync(func, options); + + /// + public Task AddRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.AddRoleAsync(roleId, options); + + /// + public Task AddRoleAsync(IRole role, RequestOptions options = null) => GuildUser.AddRoleAsync(role, options); + + /// + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.AddRolesAsync(roleIds, options); + + /// + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.AddRolesAsync(roles, options); + + /// + public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) => GuildUser.RemoveRoleAsync(roleId, options); + + /// + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) => GuildUser.RemoveRoleAsync(role, options); + + /// + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roleIds, options); + + /// + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => GuildUser.RemoveRolesAsync(roles, options); + + /// + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => GuildUser.GetAvatarUrl(format, size); + + /// + public string GetDefaultAvatarUrl() => GuildUser.GetDefaultAvatarUrl(); + + /// + public Task CreateDMChannelAsync(RequestOptions options = null) => GuildUser.CreateDMChannelAsync(options); + + /// + GuildPermissions IGuildUser.GuildPermissions => this.GuildUser.GuildPermissions; + + /// + IGuild IGuildUser.Guild => this.Guild; + + /// + ulong IGuildUser.GuildId => this.Guild.Id; + + /// + IReadOnlyCollection IGuildUser.RoleIds => this.GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); + + /// + /// Gets the guild user of this thread user. + /// + /// + public static explicit operator SocketGuildUser(SocketThreadUser user) => user.GuildUser; + } +}