From 29a3e8783523a26627234d8e0468091761b4caf6 Mon Sep 17 00:00:00 2001 From: quin lynch Date: Sat, 31 Jul 2021 14:37:12 -0300 Subject: [PATCH] Threads pt3 --- src/Discord.Net.Core/Discord.Net.Core.xml | 175 +++++-- .../Entities/Channels/ITextChannel.cs | 33 ++ .../Entities/Channels/IThreadChannel.cs | 39 +- .../Channels/TextChannelProperties.cs | 16 + .../Channels/ThreadChannelProperties.cs | 45 -- .../Entities/Channels/ThreadType.cs | 29 ++ .../Entities/Permissions/GuildPermission.cs | 102 ++-- .../Entities/Permissions/GuildPermissions.cs | 8 +- .../API/Common/ThreadMember.cs | 6 + .../API/Common/ThreadMetadata.cs | 2 +- .../API/Rest/ModifyThreadParams.cs | 28 ++ .../API/Rest/StartThreadParams.cs | 2 +- src/Discord.Net.Rest/Discord.Net.Rest.xml | 218 +++++++++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 13 +- .../Entities/Channels/RestTextChannel.cs | 60 ++- .../Entities/Channels/RestThreadChannel.cs | 230 +++++++++ .../Entities/Channels/ThreadHelper.cs | 66 +++ .../Entities/Users/RestThreadUser.cs | 60 +++ .../BaseSocketClient.Events.cs | 20 + .../Discord.Net.WebSocket.xml | 300 ++++++------ .../DiscordSocketClient.cs | 148 ++++-- .../Entities/Channels/SocketChannelHelper.cs | 1 + .../Entities/Channels/SocketTextChannel.cs | 56 ++- .../Entities/Channels/SocketThreadChannel.cs | 437 +++++++++--------- .../Entities/Guilds/SocketGuild.cs | 8 + .../Entities/Users/SocketThreadUser.cs | 93 ++-- .../GuildPermissionsTests.cs | 2 +- .../MockedEntities/MockedTextChannel.cs | 2 + 28 files changed, 1572 insertions(+), 627 deletions(-) delete mode 100644 src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ThreadType.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs create mode 100644 src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs diff --git a/src/Discord.Net.Core/Discord.Net.Core.xml b/src/Discord.Net.Core/Discord.Net.Core.xml index dc1641bcd..5213edd3c 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.xml +++ b/src/Discord.Net.Core/Discord.Net.Core.xml @@ -1982,11 +1982,48 @@ of webhooks that is available in this channel. + + + Creates a thread within this . + + + When is the thread type will be based off of the + channel its created in. When called on a , it creates a . + When called on a , it creates a . The id of the created + thread will be the same as the id of the message, and as such a message can only have a + single thread created from it. + + The name of the thread. + + The type of the thread. + + Note: This parameter is not used if the parameter is not specified. + + + + The duration on which this thread archives after. + + Note: Options and + are only available for guilds that are boosted. You can check in the to see if the + guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. + + + The message which to start the thread from. + The options to be used when sending the request. + + A task that represents the asynchronous create operation. The task result contains a + + Represents a thread channel inside of a guild. + + + Gets the type of the current thread channel. + + if the current user has joined this thread, otherwise . @@ -2022,17 +2059,43 @@ An approximate count of messages in a thread, stops counting at 50. - + Joins the current thread. - + The options to be used when sending the request. + + A task that represents the asynchronous join operation. + - + Leaves the current thread. - + The options to be used when sending the request. + + A task that represents the asynchronous leave operation. + + + + + Adds a user to this thread. + + The to add. + The options to be used when sending the request. + + A task that represents the asynchronous operation of adding a member to a thread. + + + + + Removes a user from this thread. + + The to remove from this thread. + The options to be used when sending the request. + + A task that represents the asynchronous operation of removing a user from this thread. + @@ -2133,6 +2196,21 @@ Thrown if the value does not fall within [0, 21600]. + + + Gets or sets whether or not the thread is archived. + + + + + Gets or sets whether or not the thread is locked. + + + + + Gets or sets the auto archive duration. + + Represents the thread auto archive duration. @@ -2164,40 +2242,26 @@ - + - Gets or sets whether or not the thread is archived. + Represents types of threads. - + - Gets or sets whether or not the thread is locked. + Represents a temporary sub-channel within a GUILD_NEWS channel. - + - Gets or sets the name of the thread. + Represents a temporary sub-channel within a GUILD_TEXT channel. - + - Gets or sets the auto archive duration. + Represents a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission - - - 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. @@ -4747,7 +4811,7 @@ A button must contain either a or a , but not both. A button must have an or a . A link button must contain a URL. - A link must include a protocol (http or https). + A URL must include a protocol (http or https). A non-link button must contain a custom id @@ -6041,7 +6105,7 @@ The built embed object. Total embed length exceeds . - Any Url must include protocols (i.e http:// or https://). + Any Url must be well formatted include its protocols (i.e http:// or https://). @@ -8024,6 +8088,16 @@ Allows for viewing of audit logs. + + + Allows guild members to view a channel, which includes reading messages in text channels. + + + + + Allows for sending messages in a channel + + Allows for sending of text-to-speech messages. @@ -8094,6 +8168,11 @@ Allows for using voice-activity-detection in a voice channel. + + + Allows for using priority speaker in a voice channel. + + Allows video streaming in a voice channel. @@ -8127,15 +8206,49 @@ authentication when used on a guild that has server-wide 2FA enabled. - + - Allows management and editing of emojis. + Allows management and editing of emojis and stickers. + + + This permission requires the owner account to use two-factor + authentication when used on a guild that has server-wide 2FA enabled. + + + + + Allows members to use slash commands in text channels. + + + + + Allows for requesting to speak in stage channels. (This permission is under active development and may be changed or removed.). + + + + + Allows for deleting and archiving threads, and viewing all private threads. This permission requires the owner account to use two-factor authentication when used on a guild that has server-wide 2FA enabled. + + + Allows for creating and participating in threads. + + + + + Allows for creating and participating in private threads. + + + + + Allows the usage of custom stickers from other servers. + + Gets a blank that grants no permissions. @@ -8238,7 +8351,7 @@ If true, a user may edit the webhooks for this guild. - + If true, a user may edit the emojis for this guild. diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index a2baf6990..70ba872fc 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -114,5 +114,38 @@ namespace Discord /// of webhooks that is available in this channel. /// Task> GetWebhooksAsync(RequestOptions options = null); + + /// + /// Creates a thread within this . + /// + /// + /// When is the thread type will be based off of the + /// channel its created in. When called on a , it creates a . + /// When called on a , it creates a . The id of the created + /// thread will be the same as the id of the message, and as such a message can only have a + /// single thread created from it. + /// + /// The name of the thread. + /// + /// The type of the thread. + /// + /// Note: This parameter is not used if the parameter is not specified. + /// + /// + /// + /// The duration on which this thread archives after. + /// + /// Note: Options and + /// are only available for guilds that are boosted. You can check in the to see if the + /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. + /// + /// + /// The message which to start the thread from. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. The task result contains a + /// + Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, + IMessage message = null, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs index f801d3fd8..b5d6eea20 100644 --- a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs @@ -11,6 +11,11 @@ namespace Discord /// public interface IThreadChannel : ITextChannel, IGuildChannel { + /// + /// Gets the type of the current thread channel. + /// + ThreadType Type { get; } + /// /// if the current user has joined this thread, otherwise . /// @@ -49,13 +54,39 @@ namespace Discord /// /// Joins the current thread. /// - /// - Task JoinAsync(); + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous join operation. + /// + Task JoinAsync(RequestOptions options = null); /// /// Leaves the current thread. /// - /// - Task LeaveAsync(); + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous leave operation. + /// + Task LeaveAsync(RequestOptions options = null); + + /// + /// Adds a user to this thread. + /// + /// The to add. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation of adding a member to a thread. + /// + Task AddUserAsync(IGuildUser user, RequestOptions options = null); + + /// + /// Removes a user from this thread. + /// + /// The to remove from this thread. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation of removing a user from this thread. + /// + Task RemoveUserAsync(IGuildUser user, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs index 821f358f5..2dceb025c 100644 --- a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs @@ -38,5 +38,21 @@ namespace Discord /// /// Thrown if the value does not fall within [0, 21600]. public Optional SlowModeInterval { get; set; } + + /// + /// 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 auto archive duration. + /// + public Optional AutoArchiveDuration { get; set; } + } } diff --git a/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs deleted file mode 100644 index 10a0d7654..000000000 --- a/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs +++ /dev/null @@ -1,45 +0,0 @@ -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/Channels/ThreadType.cs b/src/Discord.Net.Core/Entities/Channels/ThreadType.cs new file mode 100644 index 000000000..2db09bcb9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ThreadType.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// Represents types of threads. + /// + public enum ThreadType + { + /// + /// Represents a temporary sub-channel within a GUILD_NEWS channel. + /// + NewsThread = 10, + + /// + /// Represents a temporary sub-channel within a GUILD_TEXT channel. + /// + PublicThread = 11, + + /// + /// Represents a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission + /// + PrivateThread = 12, + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index 31bd6164a..89ee20b9f 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -10,7 +10,7 @@ namespace Discord /// /// Allows creation of instant invites. /// - CreateInstantInvite = 0x00_00_00_01, + CreateInstantInvite = 0x00_00_00_01, /// /// Allows kicking members. /// @@ -18,7 +18,7 @@ namespace Discord /// This permission requires the owner account to use two-factor /// authentication when used on a guild that has server-wide 2FA enabled. /// - KickMembers = 0x00_00_00_02, + KickMembers = 0x00_00_00_02, /// /// Allows banning members. /// @@ -26,7 +26,7 @@ namespace Discord /// This permission requires the owner account to use two-factor /// authentication when used on a guild that has server-wide 2FA enabled. /// - BanMembers = 0x00_00_00_04, + BanMembers = 0x00_00_00_04, /// /// Allows all permissions and bypasses channel permission overwrites. /// @@ -34,7 +34,7 @@ namespace Discord /// This permission requires the owner account to use two-factor /// authentication when used on a guild that has server-wide 2FA enabled. /// - Administrator = 0x00_00_00_08, + Administrator = 0x00_00_00_08, /// /// Allows management and editing of channels. /// @@ -42,7 +42,7 @@ namespace Discord /// This permission requires the owner account to use two-factor /// authentication when used on a guild that has server-wide 2FA enabled. /// - ManageChannels = 0x00_00_00_10, + ManageChannels = 0x00_00_00_10, /// /// Allows management and editing of the guild. /// @@ -50,27 +50,33 @@ namespace Discord /// This permission requires the owner account to use two-factor /// authentication when used on a guild that has server-wide 2FA enabled. /// - ManageGuild = 0x00_00_00_20, + ManageGuild = 0x00_00_00_20, /// /// Allows for viewing of guild insights /// - ViewGuildInsights = 0x00_08_00_00, + ViewGuildInsights = 0x00_08_00_00, // Text /// /// Allows for the addition of reactions to messages. /// - AddReactions = 0x00_00_00_40, + AddReactions = 0x00_00_00_40, /// /// Allows for viewing of audit logs. /// - ViewAuditLog = 0x00_00_00_80, - ViewChannel = 0x00_00_04_00, - SendMessages = 0x00_00_08_00, + ViewAuditLog = 0x00_00_00_80, + /// + /// Allows guild members to view a channel, which includes reading messages in text channels. + /// + ViewChannel = 0x00_00_04_00, + /// + /// Allows for sending messages in a channel + /// + SendMessages = 0x00_00_08_00, /// /// Allows for sending of text-to-speech messages. /// - SendTTSMessages = 0x00_00_10_00, + SendTTSMessages = 0x00_00_10_00, /// /// Allows for deletion of other users messages. /// @@ -78,70 +84,73 @@ namespace Discord /// This permission requires the owner account to use two-factor /// authentication when used on a guild that has server-wide 2FA enabled. /// - ManageMessages = 0x00_00_20_00, + ManageMessages = 0x00_00_20_00, /// /// Allows links sent by users with this permission will be auto-embedded. /// - EmbedLinks = 0x00_00_40_00, + EmbedLinks = 0x00_00_40_00, /// /// Allows for uploading images and files. /// - AttachFiles = 0x00_00_80_00, + AttachFiles = 0x00_00_80_00, /// /// Allows for reading of message history. /// - ReadMessageHistory = 0x00_01_00_00, + ReadMessageHistory = 0x00_01_00_00, /// /// Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all /// online users in a channel. /// - MentionEveryone = 0x00_02_00_00, + MentionEveryone = 0x00_02_00_00, /// /// Allows the usage of custom emojis from other servers. /// - UseExternalEmojis = 0x00_04_00_00, + UseExternalEmojis = 0x00_04_00_00, // Voice /// /// Allows for joining of a voice channel. /// - Connect = 0x00_10_00_00, + Connect = 0x00_10_00_00, /// /// Allows for speaking in a voice channel. /// - Speak = 0x00_20_00_00, + Speak = 0x00_20_00_00, /// /// Allows for muting members in a voice channel. /// - MuteMembers = 0x00_40_00_00, + MuteMembers = 0x00_40_00_00, /// /// Allows for deafening of members in a voice channel. /// - DeafenMembers = 0x00_80_00_00, + DeafenMembers = 0x00_80_00_00, /// /// Allows for moving of members between voice channels. /// - MoveMembers = 0x01_00_00_00, + MoveMembers = 0x01_00_00_00, /// /// Allows for using voice-activity-detection in a voice channel. /// - UseVAD = 0x02_00_00_00, - PrioritySpeaker = 0x00_00_01_00, + UseVAD = 0x02_00_00_00, + /// + /// Allows for using priority speaker in a voice channel. + /// + PrioritySpeaker = 0x00_00_01_00, /// /// Allows video streaming in a voice channel. /// - Stream = 0x00_00_02_00, + Stream = 0x00_00_02_00, // General 2 /// /// Allows for modification of own nickname. /// - ChangeNickname = 0x04_00_00_00, + ChangeNickname = 0x04_00_00_00, /// /// Allows for modification of other users nicknames. /// - ManageNicknames = 0x08_00_00_00, + ManageNicknames = 0x08_00_00_00, /// /// Allows management and editing of roles. /// @@ -149,7 +158,7 @@ namespace Discord /// This permission requires the owner account to use two-factor /// authentication when used on a guild that has server-wide 2FA enabled. /// - ManageRoles = 0x10_00_00_00, + ManageRoles = 0x10_00_00_00, /// /// Allows management and editing of webhooks. /// @@ -157,14 +166,43 @@ namespace Discord /// This permission requires the owner account to use two-factor /// authentication when used on a guild that has server-wide 2FA enabled. /// - ManageWebhooks = 0x20_00_00_00, + ManageWebhooks = 0x20_00_00_00, /// - /// Allows management and editing of emojis. + /// Allows management and editing of emojis and stickers. /// /// /// This permission requires the owner account to use two-factor /// authentication when used on a guild that has server-wide 2FA enabled. /// - ManageEmojis = 0x40_00_00_00 + ManageEmojisAndStickers = 0x40_00_00_00, + /// + /// Allows members to use slash commands in text channels. + /// + UseSlashCommands = 0x80_00_00_00, + /// + /// Allows for requesting to speak in stage channels. (This permission is under active development and may be changed or removed.). + /// + RequestToSpeak = 0x01_00_00_00_00, + /// + /// Allows for deleting and archiving threads, and viewing all private threads. + /// + /// + /// This permission requires the owner account to use two-factor + /// authentication when used on a guild that has server-wide 2FA enabled. + /// + ManageThreads = 0x04_00_00_00_00, + /// + /// Allows for creating and participating in threads. + /// + UsePublicThreads = 0x08_00_00_00_00, + /// + /// Allows for creating and participating in private threads. + /// + UsePrivateThreads = 0x10_00_00_00_00, + /// + /// Allows the usage of custom stickers from other servers. + /// + UseExternalStickers = 0x20_00_00_00_00 + } } diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index b03c0e1a8..1914a6f86 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -82,7 +82,7 @@ namespace Discord /// If true, a user may edit the webhooks for this guild. public bool ManageWebhooks => Permissions.GetValue(RawValue, GuildPermission.ManageWebhooks); /// If true, a user may edit the emojis for this guild. - public bool ManageEmojis => Permissions.GetValue(RawValue, GuildPermission.ManageEmojis); + public bool ManageEmojisAndStickers => Permissions.GetValue(RawValue, GuildPermission.ManageEmojisAndStickers); /// Creates a new with the provided packed value. public GuildPermissions(ulong rawValue) { RawValue = rawValue; } @@ -121,7 +121,7 @@ namespace Discord bool? manageNicknames = null, bool? manageRoles = null, bool? manageWebhooks = null, - bool? manageEmojis = null) + bool? manageEmojisAndStickers = null) { ulong value = initialValue; @@ -155,7 +155,7 @@ namespace Discord Permissions.SetValue(ref value, manageNicknames, GuildPermission.ManageNicknames); Permissions.SetValue(ref value, manageRoles, GuildPermission.ManageRoles); Permissions.SetValue(ref value, manageWebhooks, GuildPermission.ManageWebhooks); - Permissions.SetValue(ref value, manageEmojis, GuildPermission.ManageEmojis); + Permissions.SetValue(ref value, manageEmojisAndStickers, GuildPermission.ManageEmojisAndStickers); RawValue = value; } @@ -224,7 +224,7 @@ namespace Discord changeNickname: changeNickname, manageNicknames: manageNicknames, manageWebhooks: manageWebhooks, - manageEmojis: manageEmojis) + manageEmojisAndStickers: manageEmojis) { } /// Creates a new from this one, changing the provided non-null permissions. diff --git a/src/Discord.Net.Rest/API/Common/ThreadMember.cs b/src/Discord.Net.Rest/API/Common/ThreadMember.cs index b4c542454..d8ce15e92 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMember.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMember.cs @@ -18,6 +18,12 @@ namespace Discord.API [JsonProperty("join_timestamp")] public DateTimeOffset JoinTimestamp { get; set; } + [JsonProperty("presense")] + public Optional Presence { get; set; } + + [JsonProperty("member")] + public Optional Member { 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 index f6da249f7..0bc068c1c 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs @@ -13,7 +13,7 @@ namespace Discord.API public bool Archived { get; set; } [JsonProperty("auto_archive_duration")] - public int AutoArchiveDuration { get; set; } + public ThreadArchiveDuration AutoArchiveDuration { get; set; } [JsonProperty("archive_timestamp")] public DateTimeOffset ArchiveTimestamp { get; set; } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs new file mode 100644 index 000000000..c62b3bfbb --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + internal class ModifyThreadParams + { + [JsonProperty("name")] + public Optional Name { get; set; } + + [JsonProperty("archived")] + public Optional Archived { get; set; } + + [JsonProperty("auto_archive_duration")] + public Optional AutoArchiveDuration { get; set; } + + [JsonProperty("locked")] + public Optional Locked { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional Slowmode { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs b/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs index 167d94099..c41398d93 100644 --- a/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs +++ b/src/Discord.Net.Rest/API/Rest/StartThreadParams.cs @@ -16,6 +16,6 @@ namespace Discord.API.Rest public ThreadArchiveDuration Duration { get; set; } [JsonProperty("type")] - public Optional Type { get; set; } + public Optional Type { get; set; } } } diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.xml b/src/Discord.Net.Rest/Discord.Net.Rest.xml index 465d5085f..eda180f01 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.xml +++ b/src/Discord.Net.Rest/Discord.Net.Rest.xml @@ -2458,6 +2458,38 @@ + + + Creates a thread within this . + + + When is the thread type will be based off of the + channel its created in. When called on a , it creates a . + When called on a , it creates a . The id of the created + thread will be the same as the id of the message, and as such a message can only have a + single thread created from it. + + The name of the thread. + + The type of the thread. + + Note: This parameter is not used if the parameter is not specified. + + + + The duration on which this thread archives after. + + Note: Options and + are only available for guilds that are boosted. You can check in the to see if the + guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. + + + The message which to start the thread from. + The options to be used when sending the request. + + A task that represents the asynchronous create operation. The task result contains a + + @@ -2506,6 +2538,163 @@ + + + Represents a thread channel recieved over REST. + + + + + + + + + + + + + + + + + + + + + + + + + + Gets the parent text channel id. + + + + + Gets a user within this thread. + + The id of the user to fetch. + The options to be used when sending the request. + + A task representing the asyncronous get operation. The task returns a + if found, otherwise . + + + + + Gets a collection of users within this thread. + + The options to be used when sending the request. + + A task representing the asyncronous get operation. The task returns a + of 's. + + + + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + This method is not supported in threads. + + + + + + + + + + + + + + Represents a REST-based voice channel in a guild. @@ -4271,6 +4460,35 @@ Unable to modify this object using a different token. + + + Represents a thread user recieved over the REST api. + + + + + Gets the this user is in. + + + + + Gets the timestamp for when this user joined this thread. + + + + + Gets the guild this user is in. + + + + + Gets the guild user for this thread user. + + + A task representing the asyncronous get operation. The task returns a + that represents the current thread user. + + Represents a REST-based user. diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index b312fd99f..51e98c1a3 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -415,6 +415,15 @@ namespace Discord.API } // Threads + public async Task ModifyThreadAsync(ulong channelId, ModifyThreadParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + var bucket = new BucketIds(channelId: channelId); + + return await SendJsonAsync("PATCH", () => $"channels/{channelId}", args, bucket, options: options); + } + public async Task StartThreadAsync(ulong channelId, ulong messageId, StartThreadParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -426,7 +435,7 @@ namespace Discord.API 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)); @@ -484,7 +493,7 @@ namespace Discord.API var bucket = new BucketIds(channelId: channelId); - return await SendAsync("GET", () => $"/channels/{channelId}", bucket, options: options); + return await SendAsync("GET", () => $"channels/{channelId}/thread-members", bucket, options: options); } public async Task GetActiveThreadsAsync(ulong channelId, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index f12977d62..6343aed03 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -41,14 +41,14 @@ namespace Discord.Rest { base.Update(model); CategoryId = model.CategoryId; - Topic = model.Topic.Value; + Topic = model.Topic.GetValueOrDefault(); if (model.SlowMode.IsSpecified) SlowModeInterval = model.SlowMode.Value; IsNsfw = model.Nsfw.GetValueOrDefault(); } /// - public async Task ModifyAsync(Action func, RequestOptions options = null) + public virtual async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); @@ -173,7 +173,7 @@ namespace Discord.Rest /// A task that represents the asynchronous creation operation. The task result contains the newly created /// webhook. /// - public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + public virtual Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); /// /// Gets a webhook available in this text channel. @@ -184,7 +184,7 @@ namespace Discord.Rest /// A task that represents the asynchronous get operation. The task result contains a webhook associated /// with the identifier; null if the webhook is not found. /// - public Task GetWebhookAsync(ulong id, RequestOptions options = null) + public virtual Task GetWebhookAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetWebhookAsync(this, Discord, id, options); /// /// Gets the webhooks available in this text channel. @@ -194,7 +194,7 @@ namespace Discord.Rest /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of webhooks that is available in this channel. /// - public Task> GetWebhooksAsync(RequestOptions options = null) + public virtual Task> GetWebhooksAsync(RequestOptions options = null) => ChannelHelper.GetWebhooksAsync(this, Discord, options); /// @@ -205,7 +205,7 @@ namespace Discord.Rest /// A task that represents the asynchronous get operation. The task result contains the category channel /// representing the parent of this channel; null if none is set. /// - public Task GetCategoryAsync(RequestOptions options = null) + public virtual Task GetCategoryAsync(RequestOptions options = null) => ChannelHelper.GetCategoryAsync(this, Discord, options); /// public Task SyncPermissionsAsync(RequestOptions options = null) @@ -213,18 +213,55 @@ namespace Discord.Rest //Invites /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - public Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + public virtual Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); - public Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); /// - public async Task> GetInvitesAsync(RequestOptions options = null) + public virtual async Task> GetInvitesAsync(RequestOptions options = null) => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); private string DebuggerDisplay => $"{Name} ({Id}, Text)"; + /// + /// Creates a thread within this . + /// + /// + /// When is the thread type will be based off of the + /// channel its created in. When called on a , it creates a . + /// When called on a , it creates a . The id of the created + /// thread will be the same as the id of the message, and as such a message can only have a + /// single thread created from it. + /// + /// The name of the thread. + /// + /// The type of the thread. + /// + /// Note: This parameter is not used if the parameter is not specified. + /// + /// + /// + /// The duration on which this thread archives after. + /// + /// Note: Options and + /// are only available for guilds that are boosted. You can check in the to see if the + /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. + /// + /// + /// The message which to start the thread from. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. The task result contains a + /// + public async Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, + ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, RequestOptions options = null) + { + var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, options); + return RestThreadChannel.Create(Discord, this.Guild, model); + } + //ITextChannel /// async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) @@ -236,6 +273,9 @@ namespace Discord.Rest async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); + async Task ITextChannel.CreateThreadAsync(string name, ThreadType type, ThreadArchiveDuration autoArchiveDuration, IMessage message, RequestOptions options) + => await CreateThreadAsync(name, type, autoArchiveDuration, message, options); + //IMessageChannel /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs new file mode 100644 index 000000000..640b7443b --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -0,0 +1,230 @@ +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.Rest +{ + /// + /// Represents a thread channel recieved over REST. + /// + public class RestThreadChannel : RestTextChannel, IThreadChannel + { + public ThreadType Type { get; private set; } + /// + public bool Joined { get; private set; } + + /// + public bool Archived { get; private set; } + + /// + public ThreadArchiveDuration AutoArchiveDuration { get; private set; } + + /// + public DateTimeOffset ArchiveTimestamp { get; private set; } + + /// + public bool Locked { get; private set; } + + /// + public int MemberCount { get; private set; } + + /// + public int MessageCount { get; private set; } + + /// + /// Gets the parent text channel id. + /// + public ulong ParentChannelId { get; private set; } + + internal RestThreadChannel(BaseDiscordClient discord, IGuild guild, ulong id) + : base(discord, guild, id) + { + + } + + internal new static RestThreadChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestThreadChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) + { + base.Update(model); + + this.Joined = model.ThreadMember.IsSpecified; + + if (model.ThreadMetadata.IsSpecified) + { + this.Archived = model.ThreadMetadata.Value.Archived; + this.AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration; + this.ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; + this.Locked = model.ThreadMetadata.Value.Locked.GetValueOrDefault(false); + + } + + this.MemberCount = model.MemberCount.GetValueOrDefault(0); + this.MessageCount = model.MessageCount.GetValueOrDefault(0); + this.Type = (ThreadType)model.Type; + this.ParentChannelId = model.CategoryId.Value; + } + + /// + /// Gets a user within this thread. + /// + /// The id of the user to fetch. + /// The options to be used when sending the request. + /// + /// A task representing the asyncronous get operation. The task returns a + /// if found, otherwise . + /// + public new Task GetUserAsync(ulong userId, RequestOptions options = null) + => ThreadHelper.GetUserAsync(userId, this, Discord, options); + + /// + /// Gets a collection of users within this thread. + /// + /// The options to be used when sending the request. + /// + /// A task representing the asyncronous get operation. The task returns a + /// of 's. + /// + public new async Task> GetUsersAsync(RequestOptions options = null) + => (await ThreadHelper.GetUsersAsync(this, Discord, options).ConfigureAwait(false)).ToImmutableArray(); + + /// + public override async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ThreadHelper.ModifyAsync(this, Discord, func, options); + Update(model); + } + + /// + /// + /// This method is not supported in threads. + /// + public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task GetCategoryAsync(RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task> GetInvitesAsync(RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override OverwritePermissions? GetPermissionOverwrite(IRole role) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override OverwritePermissions? GetPermissionOverwrite(IUser user) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task> GetWebhooksAsync(RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + => throw new NotImplementedException(); + + /// + /// + /// This method is not supported in threads. + /// + public override IReadOnlyCollection PermissionOverwrites + => throw new NotImplementedException(); + + + /// + public Task JoinAsync(RequestOptions options = null) + => Discord.ApiClient.JoinThreadAsync(this.Id, options); + + /// + public Task LeaveAsync(RequestOptions options = null) + => Discord.ApiClient.LeaveThreadAsync(this.Id, options); + + /// + public Task AddUserAsync(IGuildUser user, RequestOptions options = null) + => Discord.ApiClient.AddThreadMemberAsync(this.Id, user.Id, options); + + /// + public Task RemoveUserAsync(IGuildUser user, RequestOptions options = null) + => Discord.ApiClient.RemoveThreadMemberAsync(this.Id, user.Id, options); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs new file mode 100644 index 000000000..958210d85 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -0,0 +1,66 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + internal static class ThreadHelper + { + public static async Task CreateThreadAsync(BaseDiscordClient client, ITextChannel channel, string name, ThreadType type = ThreadType.PublicThread, + ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, RequestOptions options = null) + { + if (autoArchiveDuration == ThreadArchiveDuration.OneWeek && !channel.Guild.Features.Contains("SEVEN_DAY_THREAD_ARCHIVE")) + throw new ArgumentException($"The guild {channel.Guild.Name} does not have the SEVEN_DAY_THREAD_ARCHIVE feature!"); + + if (autoArchiveDuration == ThreadArchiveDuration.ThreeDays && !channel.Guild.Features.Contains("THREE_DAY_THREAD_ARCHIVE")) + throw new ArgumentException($"The guild {channel.Guild.Name} does not have the THREE_DAY_THREAD_ARCHIVE feature!"); + + var args = new StartThreadParams() + { + Name = name, + Duration = autoArchiveDuration, + Type = type + }; + + Model model = null; + + if (message != null) + model = await client.ApiClient.StartThreadAsync(channel.Id, message.Id, args, options).ConfigureAwait(false); + else + model = await client.ApiClient.StartThreadAsync(channel.Id, args, options).ConfigureAwait(false); + + return model; + } + + public static async Task ModifyAsync(IThreadChannel channel, BaseDiscordClient client, + Action func, + RequestOptions options) + { + var args = new TextChannelProperties(); + func(args); + var apiArgs = new API.Rest.ModifyThreadParams + { + Name = args.Name, + Archived = args.Archived, + AutoArchiveDuration = args.AutoArchiveDuration, + Locked = args.Locked, + Slowmode = args.SlowModeInterval + }; + return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); + } + + public static async Task GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) + { + var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, options); + + return users.Select(x => RestThreadUser.Create(client, channel.Guild, x, channel)).ToArray(); + } + + public static async Task GetUserAsync(ulong userdId, IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) + => (await GetUsersAsync(channel, client, options).ConfigureAwait(false)).FirstOrDefault(x => x.Id == userdId); + } +} diff --git a/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs b/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs new file mode 100644 index 000000000..d74591e75 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestThreadUser.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.ThreadMember; + +namespace Discord.Rest +{ + /// + /// Represents a thread user recieved over the REST api. + /// + public class RestThreadUser : RestEntity + { + /// + /// Gets the this user is in. + /// + public IThreadChannel Thread { get; } + + /// + /// Gets the timestamp for when this user joined this thread. + /// + public DateTimeOffset JoinedAt { get; private set; } + + /// + /// Gets the guild this user is in. + /// + public IGuild Guild { get; } + + internal RestThreadUser(BaseDiscordClient discord, IGuild guild, IThreadChannel channel, ulong id) + : base(discord, id) + { + this.Guild = guild; + this.Thread = channel; + } + + internal static RestThreadUser Create(BaseDiscordClient client, IGuild guild, Model model, IThreadChannel channel) + { + var entity = new RestThreadUser(client, guild, channel, model.UserId.Value); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + this.JoinedAt = model.JoinTimestamp; + } + + /// + /// Gets the guild user for this thread user. + /// + /// + /// A task representing the asyncronous get operation. The task returns a + /// that represents the current thread user. + /// + public Task GetGuildUser() + => Guild.GetUserAsync(this.Id); + } +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index f29f13cf6..d49afff25 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -572,5 +572,25 @@ namespace Discord.WebSocket } internal readonly AsyncEvent, Task>> _threadDeleted = new AsyncEvent, Task>>(); + /// + /// Fired when a user joins a thread + /// + public event Func ThreadMemberJoined + { + add { _threadMemberJoined.Add(value); } + remove { _threadMemberJoined.Remove(value); } + } + internal readonly AsyncEvent> _threadMemberJoined = new AsyncEvent>(); + + /// + /// Fired when a user leaves a thread + /// + public event Func ThreadMemberLeft + { + add { _threadMemberLeft.Add(value); } + remove { _threadMemberLeft.Remove(value); } + } + internal readonly AsyncEvent> _threadMemberLeft = new AsyncEvent>(); + } } diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml index 8db8a1340..8c6f031fa 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml @@ -771,6 +771,16 @@ Fired when a thread is deleted. + + + Fired when a user joins a thread + + + + + Fired when a user leaves a thread + + @@ -2380,6 +2390,16 @@ Represents a thread channel inside of a guild. + + + Gets the owner of the current thread. + + + + + Gets the current users within this thread. + + @@ -2411,182 +2431,152 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + Gets a collection of cached users within this thread. + - + - + - Gets a message from this message channel. + Gets all users inside this thread. - This method follows the same behavior as described in . - Please visit its documentation for more details on this method. + If all users are not downloaded then this method will call and return the result. - 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. - + A task representing the download operation. - + - Gets the last N messages from this message channel. + Downloads all users that have access to this thread. - - 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. - + A task representing the asyncronous download operation. + + + - + + + + - Gets a collection of messages in this channel. + Adds a user to this thread. - - 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 to add. The options to be used when sending the request. - Paged collection of messages. + A task that represents the asynchronous operation of adding a member to a thread. - + - Gets a collection of messages in this channel. + Removes a user from this thread. - - 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 to remove from this thread. The options to be used when sending the request. - Paged collection of messages. + A task that represents the asynchronous operation of removing a user from this thread. - - - - - - - - - - - - - - - Message content is too long, length must be less or equal to . - - - - - - - Message content is too long, length must be less or equal to . - - - - - - - - - - - - - - - - - - - - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + - - + + + + This method is not supported in threads. + @@ -4535,6 +4525,11 @@ + + + Represents a thread user received over the gateway. + + Gets the this user is in. @@ -4565,9 +4560,6 @@ - - - @@ -4580,27 +4572,6 @@ - - - - - - - - - - - - - - - - - - - - - @@ -4658,15 +4629,6 @@ - - - - - - - - - diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index c52675d66..0c55eeab6 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1975,63 +1975,58 @@ namespace Discord.WebSocket // Threads case "THREAD_CREATE": { - try - { - await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_CREATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_CREATE)").ConfigureAwait(false); - var data = (payload as JToken).ToObject(_serializer); + var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId.Value); + var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await UnknownGuildAsync(type, data.GuildId.Value); - return; - } + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value); + return; + } - var threadChannel = (SocketThreadChannel)guild.AddChannel(this.State, data); + SocketThreadChannel threadChannel = null; - await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); + if ((threadChannel = guild.ThreadChannels.FirstOrDefault(x => x.Id == data.Id)) != null) + { + threadChannel.Update(this.State, data); + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); } - catch(Exception x) + else { - + threadChannel = (SocketThreadChannel)guild.AddChannel(this.State, data); + threadChannel.AddOrUpdateThreadMember(data.ThreadMember.Value, guild.CurrentUser); + await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false); } } break; case "THREAD_UPDATE": { - try - { - 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; - } + await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false); - var channel = (SocketThreadChannel)guild.GetChannel(data.Id); + 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 before = channel.Clone(); - channel.Update(State, data); + var channel = (SocketThreadChannel)guild.GetChannel(data.Id); - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + var before = channel.Clone(); + channel.Update(State, data); - await TimedInvokeAsync(_threadUpdated, nameof(ThreadUpdated), before, channel).ConfigureAwait(false); - } - catch(Exception x) + 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": @@ -2075,19 +2070,30 @@ namespace Discord.WebSocket if(entity == null) { - guild.AddChannel(this.State, thread); + entity = (SocketThreadChannel)guild.AddChannel(this.State, thread); } else { entity.Update(this.State, thread); } + + foreach(var member in data.Members.Where(x => x.Id.Value == entity.Id)) + { + var guildMember = guild.GetUser(member.Id.Value); + + entity.AddOrUpdateThreadMember(member, guildMember); + } } } break; case "THREAD_MEMBER_UPDATE": { await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBER_UPDATE)").ConfigureAwait(false); - var p = payload; + + var data = (payload as JToken).ToObject(_serializer); + + //var guild = State.GetGuild(data.) + } break; @@ -2097,6 +2103,69 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); + var guild = State.GetGuild(data.GuildId); + + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); + return; + } + + var thread = (SocketThreadChannel)guild.GetChannel(data.Id); + + if(thread == null) + { + await UnknownChannelAsync(type, data.Id); + return; + } + + IReadOnlyCollection leftUsers = null; + IReadOnlyCollection joinUsers = null; + + + if (data.RemovedMemberIds.IsSpecified) + { + leftUsers = thread.RemoveUsers(data.RemovedMemberIds.Value); + } + + if (data.AddedMembers.IsSpecified) + { + List newThreadMembers = new List(); + foreach(var threadMember in data.AddedMembers.Value) + { + SocketGuildUser guildMember; + + if (threadMember.Member.IsSpecified) + { + guildMember = guild.AddOrUpdateUser(threadMember.Member.Value); + } + else + { + guildMember = guild.GetUser(threadMember.UserId.Value); + } + + newThreadMembers.Add(thread.AddOrUpdateThreadMember(threadMember, guildMember)); + } + + if (newThreadMembers.Any()) + joinUsers = newThreadMembers.ToImmutableArray(); + } + + if (leftUsers != null) + { + foreach(var threadUser in leftUsers) + { + await TimedInvokeAsync(_threadMemberLeft, nameof(ThreadMemberLeft), threadUser).ConfigureAwait(false); + } + } + + if(joinUsers != null) + { + foreach(var threadUser in joinUsers) + { + await TimedInvokeAsync(_threadMemberJoined, nameof(ThreadMemberJoined), threadUser).ConfigureAwait(false); + } + } } break; @@ -2138,6 +2207,7 @@ namespace Discord.WebSocket catch (Exception ex) { await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); + Console.WriteLine(ex); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs index 5cfbcc1a8..4a1dc45c7 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -70,6 +70,7 @@ namespace Discord.WebSocket { case SocketDMChannel dmChannel: dmChannel.AddMessage(msg); break; case SocketGroupChannel groupChannel: groupChannel.AddMessage(msg); break; + case SocketThreadChannel threadChannel: threadChannel.AddMessage(msg); break; case SocketTextChannel textChannel: textChannel.AddMessage(msg); break; default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type."); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 192d74328..ca9164a46 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -78,9 +78,46 @@ namespace Discord.WebSocket } /// - public Task ModifyAsync(Action func, RequestOptions options = null) + public virtual Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); + /// + /// Creates a thread within this . + /// + /// + /// When is the thread type will be based off of the + /// channel its created in. When called on a , it creates a . + /// When called on a , it creates a . The id of the created + /// thread will be the same as the id of the message, and as such a message can only have a + /// single thread created from it. + /// + /// The name of the thread. + /// + /// The type of the thread. + /// + /// Note: This parameter is not used if the parameter is not specified. + /// + /// + /// + /// The duration on which this thread archives after. + /// + /// Note: Options and + /// are only available for guilds that are boosted. You can check in the to see if the + /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. + /// + /// + /// The message which to start the thread from. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous create operation. The task result contains a + /// + public async Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, + ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, RequestOptions options = null) + { + var model = await ThreadHelper.CreateThreadAsync(Discord, this, name, type, autoArchiveDuration, message, options); + return SocketThreadChannel.Create(this.Guild, Discord.State, model); + } + //Messages /// public SocketMessage GetCachedMessage(ulong id) @@ -235,7 +272,7 @@ namespace Discord.WebSocket /// A task that represents the asynchronous creation operation. The task result contains the newly created /// webhook. /// - public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + public virtual Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); /// /// Gets a webhook available in this text channel. @@ -246,7 +283,7 @@ namespace Discord.WebSocket /// A task that represents the asynchronous get operation. The task result contains a webhook associated /// with the identifier; null if the webhook is not found. /// - public Task GetWebhookAsync(ulong id, RequestOptions options = null) + public virtual Task GetWebhookAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetWebhookAsync(this, Discord, id, options); /// /// Gets the webhooks available in this text channel. @@ -256,21 +293,21 @@ namespace Discord.WebSocket /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of webhooks that is available in this channel. /// - public Task> GetWebhooksAsync(RequestOptions options = null) + public virtual Task> GetWebhooksAsync(RequestOptions options = null) => ChannelHelper.GetWebhooksAsync(this, Discord, options); //Invites /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); /// - public async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options).ConfigureAwait(false); /// - public async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + public virtual async Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => await ChannelHelper.CreateInviteToStreamAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, user, options).ConfigureAwait(false); /// - public async Task> GetInvitesAsync(RequestOptions options = null) + public virtual async Task> GetInvitesAsync(RequestOptions options = null) => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); private string DebuggerDisplay => $"{Name} ({Id}, Text)"; @@ -286,6 +323,9 @@ namespace Discord.WebSocket /// async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); + /// + async Task ITextChannel.CreateThreadAsync(string name, ThreadType type, ThreadArchiveDuration autoArchiveDuration, IMessage message, RequestOptions options) + => await CreateThreadAsync(name, type, autoArchiveDuration, message, options); //IGuildChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 4072276cb..141d7535c 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -9,6 +9,8 @@ using System.Text; using System.Threading.Tasks; using Model = Discord.API.Channel; using ThreadMember = Discord.API.ThreadMember; +using MemberUpdates = Discord.API.Gateway.ThreadMembersUpdated; +using System.Collections.Concurrent; namespace Discord.WebSocket { @@ -16,8 +18,21 @@ namespace Discord.WebSocket /// Represents a thread channel inside of a guild. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketThreadChannel : SocketGuildChannel, IThreadChannel, ISocketMessageChannel + public class SocketThreadChannel : SocketTextChannel, IThreadChannel { + /// + public ThreadType Type { get; private set; } + + /// + /// Gets the owner of the current thread. + /// + public SocketThreadUser Owner { get; private set; } + + /// + /// Gets the current users within this thread. + /// + public SocketThreadUser CurrentUser + => Users.FirstOrDefault(x => x.Id == Discord.CurrentUser.Id); /// public bool Joined { get; private set; } @@ -25,7 +40,8 @@ namespace Discord.WebSocket /// /// if this thread is private, otherwise /// - public bool IsPrivateThread { get; private set; } + public bool IsPrivateThread + => this.Type == ThreadType.PrivateThread; /// /// Gets the parent channel this thread resides in. @@ -50,37 +66,29 @@ namespace Discord.WebSocket /// 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; } + /// + /// Gets a collection of cached users within this thread. + /// + public new IReadOnlyCollection Users => + _members.Values.ToImmutableArray(); - /// - public ulong? CategoryId { get; private set; } - /// - public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + private ConcurrentDictionary _members; - public new IReadOnlyCollection Users = ImmutableArray.Create(); + private string DebuggerDisplay => $"{Name} ({Id}, Thread)"; - private readonly MessageCache _messages; + private bool _usersDownloaded = false; - private string DebuggerDisplay => $"{Name} ({Id}, Thread)"; + private object _downloadLock = new object(); internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketTextChannel parent) : base(discord, id, guild) { this.ParentChannel = parent; + this._members = new ConcurrentDictionary(); } - internal static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) + internal new 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); @@ -92,10 +100,9 @@ namespace Discord.WebSocket { base.Update(state, model); + this.Type = (ThreadType)model.Type; this.MessageCount = model.MessageCount.GetValueOrDefault(-1); this.MemberCount = model.MemberCount.GetValueOrDefault(-1); - - this.IsPrivateThread = model.Type == ChannelType.PrivateThread; if (model.ThreadMetadata.IsSpecified) { @@ -104,259 +111,233 @@ namespace Discord.WebSocket this.AutoArchiveDuration = (ThreadArchiveDuration)model.ThreadMetadata.Value.AutoArchiveDuration; this.Locked = model.ThreadMetadata.Value.Locked.GetValueOrDefault(false); } + + if (model.OwnerId.IsSpecified) + { + this.Owner = GetUser(model.OwnerId.Value); + } + + this.Joined = model.ThreadMember.IsSpecified; } - internal void Update(ClientState state, ThreadMember self) + internal IReadOnlyCollection RemoveUsers(ulong[] users) { + List threadUsers = new(); - } + foreach (var userId in users) + { + if (_members.TryRemove(userId, out var user)) + threadUsers.Add(user); + } - /// - public virtual Task SyncPermissionsAsync(RequestOptions options = null) - => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + return threadUsers.ToImmutableArray(); + } - /// - public Task ModifyAsync(Action func, RequestOptions options = null) - => ChannelHelper.ModifyAsync(this, Discord, func, options); + internal SocketThreadUser AddOrUpdateThreadMember(ThreadMember model, SocketGuildUser guildMember) + { + if (_members.TryGetValue(model.UserId.Value, out SocketThreadUser member)) + member.Update(model); + else + { + member = SocketThreadUser.Create(this.Guild, this, model, guildMember); + member.GlobalUser.AddRef(); + _members[member.Id] = member; + } + return member; + } - //Messages + //Users /// - public SocketMessage GetCachedMessage(ulong id) - => _messages?.Get(id); + public new SocketThreadUser GetUser(ulong id) + { + var user = Users.FirstOrDefault(x => x.Id == id); + return user; + } /// - /// Gets a message from this message channel. + /// Gets all users inside this thread. /// /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. + /// If all users are not downloaded then this method will call and return the result. /// - /// 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) + /// A task representing the download operation. + public async Task> GetUsersAsync(RequestOptions options = null) { - IMessage msg = _messages?.Get(id); - if (msg == null) - msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); - return msg; + // download all users if we havent + if (!_usersDownloaded) + { + await DownloadUsersAsync(options); + this._usersDownloaded = true; + } + + return this.Users; } + /// - /// Gets the last N messages from this message channel. + /// Downloads all users that have access to this thread. /// - /// - /// 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); + /// A task representing the asyncronous download operation. + public async Task DownloadUsersAsync(RequestOptions options = null) + { + var users = await Discord.ApiClient.ListThreadMembersAsync(this.Id, options); + + lock (_downloadLock) + { + foreach (var threadMember in users) + { + var guildUser = this.Guild.GetUser(threadMember.UserId.Value); + + this.AddOrUpdateThreadMember(threadMember, guildUser); + } + } + } + + internal new SocketThreadChannel Clone() => MemberwiseClone() as SocketThreadChannel; + + /// + public Task JoinAsync(RequestOptions options = null) + => Discord.ApiClient.JoinThreadAsync(this.Id, options); + + /// + public Task LeaveAsync(RequestOptions options = null) + => Discord.ApiClient.LeaveThreadAsync(this.Id, options); /// - /// Gets a collection of messages in this channel. + /// Adds a user to this thread. /// - /// - /// 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 to add. /// The options to be used when sending the request. /// - /// Paged collection of messages. + /// A task that represents the asynchronous operation of adding a member to a thread. /// - 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); + public Task AddUserAsync(IGuildUser user, RequestOptions options = null) + => Discord.ApiClient.AddThreadMemberAsync(this.Id, user.Id, options); /// - /// Gets a collection of messages in this channel. + /// Removes a user from this thread. /// - /// - /// 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 to remove from this thread. /// The options to be used when sending the request. /// - /// Paged collection of messages. + /// A task that represents the asynchronous operation of removing a user from this thread. /// - 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 RemoveUserAsync(IGuildUser user, RequestOptions options = null) + => Discord.ApiClient.RemoveThreadMemberAsync(this.Id, user.Id, 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); + /// + /// + /// This method is not supported in threads. + /// + public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) + => throw new NotImplementedException(); - internal void AddMessage(SocketMessage msg) - => _messages?.Add(msg); + /// + /// + /// This method is not supported in threads. + /// + public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) + => throw new NotImplementedException(); - internal SocketMessage RemoveMessage(ulong id) - => _messages?.Remove(id); + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); - //Users - /// - public new SocketThreadUser GetUser(ulong id) - { - var user = Users.FirstOrDefault(x => x.Id == id); - return user; - } + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); - public Task GetUsersAsync() - { - return Task.CompletedTask; - } + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); - - internal new SocketThreadChannel Clone() => MemberwiseClone() as SocketThreadChannel; + /// + /// + /// This method is not supported in threads. + /// + public override Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) + => throw new NotImplementedException(); - //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"); + /// + /// + /// This method is not supported in threads. + /// + public override Task> GetInvitesAsync(RequestOptions options = null) + => throw new NotImplementedException(); - //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(); + /// + /// + /// This method is not supported in threads. + /// + public override OverwritePermissions? GetPermissionOverwrite(IRole role) + => throw new NotImplementedException(); - //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); + /// + /// + /// This method is not supported in threads. + /// + public override OverwritePermissions? GetPermissionOverwrite(IUser user) + => throw new NotImplementedException(); - /// - 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); + /// + /// + /// This method is not supported in threads. + /// + public override Task GetWebhookAsync(ulong id, RequestOptions options = null) + => throw new NotImplementedException(); - // 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() - { - return Task.CompletedTask; - } + /// + /// + /// This method is not supported in threads. + /// + public override Task> GetWebhooksAsync(RequestOptions options = null) + => throw new NotImplementedException(); - public Task LeaveAsync() - { - return Task.CompletedTask; - } + /// + /// + /// This method is not supported in threads. + /// + public override Task ModifyAsync(Action func, RequestOptions options = null) + => throw new NotImplementedException(); - public Task AddThreadMember(IGuildUser user) - { - return Task.CompletedTask; - } + /// + /// + /// This method is not supported in threads. + /// + public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) + => throw new NotImplementedException(); - public Task RemoveThreadMember(IGuildUser user) - { - return Task.CompletedTask; - } + /// + /// + /// This method is not supported in threads. + /// + public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) + => throw new NotImplementedException(); + /// + /// + /// This method is not supported in threads. + /// + public override IReadOnlyCollection PermissionOverwrites + => throw new NotImplementedException(); + /// + /// + /// This method is not supported in threads. + /// + public override Task SyncPermissionsAsync(RequestOptions options = null) + => throw new NotImplementedException(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 547dc6d7f..58e2c152f 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -387,7 +387,15 @@ namespace Discord.WebSocket state.AddChannel(channel); channels.TryAdd(channel.Id); } + + for(int i = 0; i < model.Threads.Length; i++) + { + var threadChannel = SocketThreadChannel.Create(this, state, model.Threads[i]); + state.AddChannel(threadChannel); + channels.TryAdd(threadChannel.Id); + } } + _channels = channels; var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index 66a5579c4..df9194d5b 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -10,7 +10,10 @@ using System.Collections.Immutable; namespace Discord.WebSocket { - public class SocketThreadUser : IGuildUser + /// + /// Represents a thread user received over the gateway. + /// + public class SocketThreadUser : SocketUser, IGuildUser { /// /// Gets the this user is in. @@ -44,56 +47,36 @@ namespace Discord.WebSocket => GuildUser.IsPending; /// - public string AvatarId - => GuildUser.AvatarId; - - /// - public string Discriminator - => GuildUser.Discriminator; + public override string AvatarId + { + get => GuildUser.AvatarId; + internal set => GuildUser.AvatarId = value; + } /// - public ushort DiscriminatorValue - => GuildUser.DiscriminatorValue; + public override ushort DiscriminatorValue + { + get => GuildUser.DiscriminatorValue; + internal set => GuildUser.DiscriminatorValue = value; + } /// - public bool IsBot - => GuildUser.IsBot; + public override bool IsBot + { + get => GuildUser.IsBot; + internal set => GuildUser.IsBot = value; + } /// - public bool IsWebhook + public override 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 override string Username + { + get => GuildUser.Username; + internal set => GuildUser.Username = value; + } /// public bool IsDeafened @@ -130,13 +113,14 @@ namespace Discord.WebSocket private SocketGuildUser GuildUser { get; set; } internal SocketThreadUser(SocketGuild guild, SocketThreadChannel thread, SocketGuildUser member) + : base(guild.Discord, member.Id) { this.Thread = thread; this.Guild = guild; this.GuildUser = member; } - internal SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) + internal static SocketThreadUser Create(SocketGuild guild, SocketThreadChannel thread, Model model, SocketGuildUser member) { var entity = new SocketThreadUser(guild, thread, member); entity.Update(model); @@ -146,6 +130,16 @@ namespace Discord.WebSocket internal void Update(Model model) { this.ThreadJoinedAt = model.JoinTimestamp; + + if (model.Presence.IsSpecified) + { + this.GuildUser.Update(Discord.State, model.Presence.Value, true); + } + + if (model.Member.IsSpecified) + { + this.GuildUser.Update(Discord.State, model.Member.Value); + } } @@ -182,15 +176,6 @@ namespace Discord.WebSocket /// 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; @@ -203,6 +188,10 @@ namespace Discord.WebSocket /// IReadOnlyCollection IGuildUser.RoleIds => this.GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); + internal override SocketGlobalUser GlobalUser => GuildUser.GlobalUser; + + internal override SocketPresence Presence { get => GuildUser.Presence; set => GuildUser.Presence = value; } + /// /// Gets the guild user of this thread user. /// diff --git a/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs b/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs index cd29b2606..137dc5575 100644 --- a/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs +++ b/test/Discord.Net.Tests.Unit/GuildPermissionsTests.cs @@ -161,7 +161,7 @@ namespace Discord AssertUtil(GuildPermission.ManageNicknames, x => x.ManageNicknames, (p, enable) => p.Modify(manageNicknames: enable)); AssertUtil(GuildPermission.ManageRoles, x => x.ManageRoles, (p, enable) => p.Modify(manageRoles: enable)); AssertUtil(GuildPermission.ManageWebhooks, x => x.ManageWebhooks, (p, enable) => p.Modify(manageWebhooks: enable)); - AssertUtil(GuildPermission.ManageEmojis, x => x.ManageEmojis, (p, enable) => p.Modify(manageEmojis: enable)); + AssertUtil(GuildPermission.ManageEmojis, x => x.ManageEmojisAndStickers, (p, enable) => p.Modify(manageEmojis: enable)); } } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index 0bde9d1e0..18ff3b3ed 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -210,5 +210,7 @@ namespace Discord { throw new NotImplementedException(); } + + public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, RequestOptions options = null) => throw new NotImplementedException(); } }