From c7ac59d89225adf42bb270c55d04aa6440be16f2 Mon Sep 17 00:00:00 2001 From: Quahu Date: Wed, 28 Sep 2022 15:35:24 -0700 Subject: [PATCH 01/24] Fixed an oversight clearing session data upon any disconnect. (#2485) --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f0f99933e..a90ffff7a 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -352,10 +352,6 @@ namespace Discord.WebSocket if (guild.IsAvailable) await GuildUnavailableAsync(guild).ConfigureAwait(false); } - - _sessionId = null; - _lastSeq = 0; - ApiClient.ResumeGatewayUrl = null; } /// From ed3863597a1ffbe309d590eba6a2dbeb645bdcb9 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Mon, 7 Nov 2022 18:49:03 +0300 Subject: [PATCH 02/24] [Feature] Add new discord stuff (#2501) * changesss - Added missing error code - Moved region a bit - Add new `guild feature` - Add new `NitroBasic` subscription plan * fix sending message in rest text-in-voice channels --- src/Discord.Net.Core/DiscordErrorCode.cs | 8 +++++--- src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs | 4 ++++ src/Discord.Net.Core/Entities/Users/PremiumType.cs | 7 ++++++- .../Entities/Channels/RestVoiceChannel.cs | 3 +-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 60b7d20d8..24ad8e67e 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -116,15 +116,15 @@ namespace Discord TargetUserNotInVoice = 40032, MessageAlreadyCrossposted = 40033, ApplicationNameAlreadyExists = 40041, - #endregion - - #region Action Preconditions/Checks (50XXX) ApplicationInteractionFailedToSend = 40043, CannotSendAMessageInAForumChannel = 40058, ThereAreNoTagsAvailableThatCanBeSetByNonModerators = 40066, ATagIsRequiredToCreateAForumPostInThisChannel = 40067, InteractionHasAlreadyBeenAcknowledged = 40060, TagNamesMustBeUnique = 40061, + #endregion + + #region Action Preconditions/Checks (50XXX) MissingPermissions = 50001, InvalidAccountType = 50002, CannotExecuteForDM = 50003, @@ -154,6 +154,7 @@ namespace Discord BulkDeleteMessageTooOld = 50034, InvalidFormBody = 50035, InviteAcceptedForGuildThatBotIsntIn = 50036, + InvalidActivityAction = 50039, InvalidAPIVersion = 50041, FileUploadTooBig = 50045, InvalidFileUpload = 50046, @@ -161,6 +162,7 @@ namespace Discord InvalidGuild = 50055, InvalidMessageType = 50068, PaymentSourceRequiredForGift = 50070, + CannotModifySystemWebhook = 50073, CannotDeleteRequiredCommunityChannel = 50074, CannotEditStickersWithinAMessage = 50080, InvalidSticker = 50081, diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs b/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs index 52a70a6f5..8ec3ee2ae 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs @@ -181,5 +181,9 @@ namespace Discord /// The guild has enabled the welcome screen. /// WelcomeScreenEnabled = 1L << 41, + /// + /// The guild has been set as a support server on the App Directory. + /// + DeveloperSupportServer = 1L << 42, } } diff --git a/src/Discord.Net.Core/Entities/Users/PremiumType.cs b/src/Discord.Net.Core/Entities/Users/PremiumType.cs index 2b41e0b6a..24165d4e9 100644 --- a/src/Discord.Net.Core/Entities/Users/PremiumType.cs +++ b/src/Discord.Net.Core/Entities/Users/PremiumType.cs @@ -16,6 +16,11 @@ namespace Discord /// /// Nitro subscription. Includes app perks as well as the games subscription service. /// - Nitro = 2 + Nitro = 2, + + /// + /// Nitro Basic subscription. Includes app perks like video backgrounds, sending bigger files. + /// + NitroBasic = 3 } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 31d313a48..3c9869c4c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -19,8 +19,7 @@ namespace Discord.Rest /// /// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel. /// - public virtual bool IsTextInVoice - => Guild.Features.HasTextInVoice; + public virtual bool IsTextInVoice => true; /// public int Bitrate { get; private set; } /// From 7077c4469a17f5124fb27af3342847d6177943ea Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Mon, 7 Nov 2022 07:49:46 -0800 Subject: [PATCH 03/24] Avoid throwing on missing Application (#2497) --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index a90ffff7a..f2239010f 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -902,7 +902,7 @@ namespace Discord.WebSocket var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; currentUser.Presence = new SocketPresence(Status, null, activities); ApiClient.CurrentUserId = currentUser.Id; - ApiClient.CurrentApplicationId = data.Application.Id; + ApiClient.CurrentApplicationId = data.Application?.Id; Rest.CurrentUser = RestSelfUser.Create(this, data.User); int unavailableGuilds = 0; for (int i = 0; i < data.Guilds.Length; i++) From 6712ef4573e9f01a0d46ccdbcc97798f95f9affe Mon Sep 17 00:00:00 2001 From: Ge Date: Tue, 8 Nov 2022 00:24:19 +0800 Subject: [PATCH 04/24] Fix duplicated members of DiscordErrorCode (#2500) Additionally, fix typo in DiscordErrorCode --- src/Discord.Net.Core/DiscordErrorCode.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 24ad8e67e..ccd547e92 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -94,10 +94,10 @@ namespace Discord MaxNumberOfDailyApplicationCommandCreatesHasBeenReached = 30034, MaximumBansForNonGuildMembersReached = 30035, MaximumBanFetchesReached = 30037, - MaximumUncompleteGuildScheduledEvents = 30038, + MaximumUncompletedGuildScheduledEvents = 30038, MaximumStickersReached = 30039, MaximumPruneRequestReached = 30040, - MaximumGuildWigitsReached = 30042, + MaximumGuildWidgetsReached = 30042, #endregion #region General Request Errors (40XXX) @@ -128,12 +128,12 @@ namespace Discord MissingPermissions = 50001, InvalidAccountType = 50002, CannotExecuteForDM = 50003, - GuildWigitDisabled = 50004, + GuildWidgetDisabled = 50004, CannotEditOtherUsersMessage = 50005, CannotSendEmptyMessage = 50006, CannotSendMessageToUser = 50007, CannotSendMessageToVoiceChannel = 50008, - ChannelVerificationTooHight = 50009, + ChannelVerificationTooHigh = 50009, OAuth2ApplicationDoesntHaveBot = 50010, OAuth2ApplicationLimitReached = 50011, InvalidOAuth2State = 50012, @@ -174,9 +174,8 @@ namespace Discord ServerRequiresMonetization = 50097, ServerRequiresBoosts = 50101, RequestBodyContainsInvalidJSON = 50109, - FailedToResizeAssetBelowTheMaximumSize = 50138, OwnershipCannotBeTransferredToABotUser = 50132, - AssetResizeBelowTheMaximumSize= 50138, + FailedToResizeAssetBelowTheMaximumSize = 50138, UploadedFileNotFound = 50146, MissingPermissionToSendThisSticker = 50600, #endregion @@ -215,8 +214,8 @@ namespace Discord LottieCantContainRasters = 170002, StickerMaximumFramerateExceeded = 170003, StickerMaximumFrameCountExceeded = 170004, - LottieMaximumDimentionsExceeded = 170005, - StickerFramerateBoundsExceeed = 170006, + LottieMaximumDimensionsExceeded = 170005, + StickerFramerateBoundsExceeded = 170006, StickerAnimationDurationTooLong = 170007, #endregion @@ -224,7 +223,7 @@ namespace Discord CannotUpdateFinishedEvent = 180000, FailedStageCreation = 180002, #endregion - + #region Forum & Automod MessageWasBlockedByAutomaticModeration = 200000, TitleWasBlockedByAutomaticModeration = 200001, From 01ae904fe1703454699a408ae6f0a17afb929040 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Mon, 7 Nov 2022 19:25:49 +0300 Subject: [PATCH 05/24] [Feature] Add missing properties in forum & thread channels (#2469) * add `AppliedTags` property * convert collections into immutable arrays * remove "not supported" remark * implement `ThreadChannelProperties` * Add `DefaultSlowModeInterval` and `DefaultSlowModeInterval` properties to forum channels * add `Moderated` property to `ForumTag`` * `ForumTag` inherits `ISnowflakeEntity` * Fix `DiscordRestClient.GetChannelAsync` not getting forum channel * a lot of changes added: - channel flags - `ForumTagBuilder` - imroved channel modification * fixed a bug in forum tag emoji parsing * inherit forum channel from `INesteeChannel` * implement `INestedChannel` in forum channels * Add `Flags` property to channels * add iteraface for forum tags & add equality operators * Add default reaction emoji property * add support for modifing default reaction & some renaming * add createForumChannelAsync to guild * *fix resharper being a d... and moving code to next line* * add a `ForumChannels` property * Some fixes & add support for `default_sort_order` * fix misleading comment * fix #2502 * support creating post with applied tags * fix xmldoc * set category id on model update * add limit checks for tag count --- .../Entities/Channels/ChannelFlags.cs | 22 ++ .../Channels/ForumChannelProperties.cs | 60 ++++++ .../Entities/Channels/ForumSortOrder.cs | 17 ++ .../Channels/GuildChannelProperties.cs | 5 + .../Entities/Channels/IForumChannel.cs | 68 ++++++- .../Entities/Channels/IGuildChannel.cs | 11 + .../Entities/Channels/IThreadChannel.cs | 20 ++ .../Channels/TextChannelProperties.cs | 13 +- .../Channels/ThreadChannelProperties.cs | 26 +++ src/Discord.Net.Core/Entities/ForumTag.cs | 42 ---- .../Entities/ForumTags/ForumTag.cs | 67 ++++++ .../Entities/ForumTags/ForumTagBuilder.cs | 191 ++++++++++++++++++ .../ForumTags/ForumTagBuilderExtensions.cs | 11 + .../Entities/ForumTags/ForumTagProperties.cs | 48 +++++ .../Entities/ForumTags/IForumTag.cs | 29 +++ .../Entities/Guilds/IGuild.cs | 12 ++ .../ApplicationCommandOptionType.cs | 2 +- .../Extensions/ChannelExtensions.cs | 3 + src/Discord.Net.Rest/API/Common/Channel.cs | 18 +- .../API/Common/ForumReactionEmoji.cs | 12 ++ src/Discord.Net.Rest/API/Common/ForumTags.cs | 3 + .../API/Rest/CreateGuildChannelParams.cs | 12 ++ .../API/Rest/CreateMultipartPostAsync.cs | 3 + .../API/Rest/CreatePostParams.cs | 3 + .../API/Rest/ModifyForumChannelParams.cs | 23 +++ .../Rest/ModifyForumReactionEmojiParams.cs | 15 ++ .../API/Rest/ModifyForumTagParams.cs | 23 +++ .../API/Rest/ModifyGuildChannelParams.cs | 2 + .../API/Rest/ModifyThreadParams.cs | 7 + .../Entities/Channels/ChannelHelper.cs | 1 + .../Entities/Channels/ForumHelper.cs | 63 ++++++ .../Entities/Channels/RestChannel.cs | 7 +- .../Entities/Channels/RestForumChannel.cs | 118 ++++++++--- .../Entities/Channels/RestGuildChannel.cs | 5 + .../Entities/Channels/RestThreadChannel.cs | 12 ++ .../Entities/Channels/ThreadHelper.cs | 35 +++- .../Entities/Guilds/GuildHelper.cs | 61 ++++++ .../Entities/Guilds/RestGuild.cs | 16 ++ .../Entities/Channels/SocketForumChannel.cs | 114 +++++++++-- .../Entities/Channels/SocketGuildChannel.cs | 5 + .../Entities/Channels/SocketThreadChannel.cs | 12 +- .../Entities/Guilds/SocketGuild.cs | 28 +++ .../MockedEntities/MockedCategoryChannel.cs | 2 + .../MockedEntities/MockedTextChannel.cs | 2 + .../MockedEntities/MockedVoiceChannel.cs | 2 + 45 files changed, 1132 insertions(+), 119 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs delete mode 100644 src/Discord.Net.Core/Entities/ForumTag.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs create mode 100644 src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs diff --git a/src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs b/src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs new file mode 100644 index 000000000..37f34a90e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs @@ -0,0 +1,22 @@ +namespace Discord; + +/// +/// Represents public flags for a channel. +/// +public enum ChannelFlags +{ + /// + /// Default value for flags, when none are given to a channel. + /// + None = 0, + + /// + /// Flag given to a thread channel pinned on top of parent forum channel. + /// + Pinned = 1 << 1, + + /// + /// Flag given to a forum channel that requires people to select tags when posting. + /// + RequireTag = 1 << 4 +} diff --git a/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs new file mode 100644 index 000000000..e1a123b37 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace Discord; + +public class ForumChannelProperties : TextChannelProperties +{ + + /// + /// Gets or sets the topic of the channel. + /// + /// + /// Not available in forum channels. + /// + public new Optional SlowModeInterval { get; } + + /// + /// Gets or sets rate limit on creating posts in this forum channel. + /// + /// + /// Setting this value to anything above zero will require each user to wait X seconds before + /// creating another thread; setting this value to 0 will disable rate limits for this channel. + /// + /// Users with or + /// will be exempt from rate limits. + /// + /// + /// Thrown if the value does not fall within [0, 21600]. + public Optional ThreadCreationInterval { get; set; } + + + /// + /// Gets or sets the default slow-mode for threads in 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 child threads. + /// + /// Users with or + /// will be exempt from slow-mode. + /// + /// + /// Thrown if the value does not fall within [0, 21600]. + public Optional DefaultSlowModeInterval { get; set; } + + /// + /// Gets or sets a collection of tags inside of this forum channel. + /// + public Optional> Tags { get; set; } + + /// + /// Gets or sets a new default reaction emoji in this forum channel. + /// + public Optional DefaultReactionEmoji { get; set; } + + /// + /// Gets or sets the rule used to order posts in forum channels. + /// + public Optional DefaultSortOrder { get; set; } +} diff --git a/src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs b/src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs new file mode 100644 index 000000000..2a576d978 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs @@ -0,0 +1,17 @@ +namespace Discord; + +/// +/// Defines the rule used to order posts in forum channels. +/// +public enum ForumSortOrder +{ + /// + /// Sort forum posts by activity. + /// + LatestActivity = 0, + + /// + /// Sort forum posts by creation time (from most recent to oldest). + /// + CreationDate = 1 +} diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index 339d6fffd..1e7d69c2d 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -36,5 +36,10 @@ namespace Discord /// Gets or sets the permission overwrites for this channel. /// public Optional> PermissionOverwrites { get; set; } + + /// + /// Gets or sets the flags of the channel. + /// + public Optional Flags { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs index f4c6da2e2..55521bade 100644 --- a/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Discord { - public interface IForumChannel : IGuildChannel, IMentionable + public interface IForumChannel : IGuildChannel, IMentionable, INestedChannel { /// /// Gets a value that indicates whether the channel is NSFW. @@ -35,6 +35,55 @@ namespace Discord /// IReadOnlyCollection Tags { get; } + /// + /// Gets the current rate limit on creating posts in this forum channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// + int ThreadCreationInterval { get; } + + /// + /// Gets the current default slow-mode delay for threads in this forum channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// + int DefaultSlowModeInterval { get; } + + /// + /// Gets the emoji to show in the add reaction button on a thread in a forum channel + /// + /// + /// If the emoji is only the will be populated. + /// Use to get the emoji. + /// + IEmote DefaultReactionEmoji { get; } + + /// + /// Gets or sets the rule used to order posts in forum channels. + /// + /// + /// Defaults to null, which indicates a preferred sort order hasn't been set + /// + ForumSortOrder? DefaultSortOrder { get; } + + /// + /// Modifies this forum channel. + /// + /// + /// This method modifies the current forum channel with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// The delegate containing the properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + /// /// Creates a new post (thread) within the forum. /// @@ -52,12 +101,13 @@ namespace Discord /// A collection of stickers to send with the message. /// A array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Creates a new post (thread) within the forum. @@ -78,13 +128,14 @@ namespace Discord /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// Task CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Creates a new post (thread) within the forum. @@ -106,13 +157,14 @@ namespace Discord /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// public Task CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None); + ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Creates a new post (thread) within the forum. @@ -132,12 +184,13 @@ namespace Discord /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// public Task CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Creates a new post (thread) within the forum. @@ -155,14 +208,15 @@ namespace Discord /// /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. - /// A array of s to send with this response. Max 10. + /// An array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// public Task CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Gets a collection of active threads within this forum channel. diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 992bd71fc..12874f2c2 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -21,6 +21,17 @@ namespace Discord /// int Position { get; } + /// + /// Gets the flags related to this channel. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// A channel's flags, if any is associated. + /// + ChannelFlags Flags { get; } + /// /// Gets the guild associated with this channel. /// diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs index f03edbbf9..52df07dcc 100644 --- a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord @@ -56,6 +57,14 @@ namespace Discord /// bool? IsInvitable { get; } + /// + /// Gets ids of tags applied to a forum thread + /// + /// + /// This property is only available on forum threads. + /// + IReadOnlyCollection AppliedTags { get; } + /// /// Gets when the thread was created. /// @@ -102,5 +111,16 @@ namespace Discord /// A task that represents the asynchronous operation of removing a user from this thread. /// Task RemoveUserAsync(IGuildUser user, RequestOptions options = null); + + /// + /// Modifies this thread channel. + /// + /// The delegate containing the properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// + Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs index 2dceb025c..acd69f480 100644 --- a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Discord { @@ -39,20 +40,10 @@ 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 new file mode 100644 index 000000000..af5c44129 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Discord; + + +/// +/// Provides properties that are used to modify an with the specified changes. +/// +/// +public class ThreadChannelProperties : TextChannelProperties +{ + /// + /// Gets or sets the tags applied to a forum thread + /// + public Optional> AppliedTags { get; set; } + + /// + /// Gets or sets whether or not the thread is locked. + /// + public Optional Locked { get; set; } + + /// + /// Gets or sets whether or not the thread is archived. + /// + public Optional Archived { get; set; } +} diff --git a/src/Discord.Net.Core/Entities/ForumTag.cs b/src/Discord.Net.Core/Entities/ForumTag.cs deleted file mode 100644 index 26ae4301e..000000000 --- a/src/Discord.Net.Core/Entities/ForumTag.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord -{ - /// - /// A struct representing a forum channel tag. - /// - public struct ForumTag - { - /// - /// Gets the Id of the tag. - /// - public ulong Id { get; } - - /// - /// Gets the name of the tag. - /// - public string Name { get; } - - /// - /// Gets the emoji of the tag or if none is set. - /// - public IEmote Emoji { get; } - - internal ForumTag(ulong id, string name, ulong? emojiId, string emojiName) - { - if (emojiId.HasValue && emojiId.Value != 0) - Emoji = new Emote(emojiId.Value, emojiName, false); - else if (emojiName != null) - Emoji = new Emoji(name); - else - Emoji = null; - - Id = id; - Name = name; - } - } -} diff --git a/src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs b/src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs new file mode 100644 index 000000000..afdb99bf7 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#nullable enable + +namespace Discord +{ + /// + /// A struct representing a forum channel tag. + /// + public struct ForumTag : ISnowflakeEntity, IForumTag + { + /// + /// Gets the Id of the tag. + /// + public ulong Id { get; } + + /// + public string Name { get; } + + /// + public IEmote? Emoji { get; } + + /// + public bool IsModerated { get; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal ForumTag(ulong id, string name, ulong? emojiId = null, string? emojiName = null, bool moderated = false) + { + if (emojiId.HasValue && emojiId.Value != 0) + Emoji = new Emote(emojiId.Value, null, false); + else if (emojiName != null) + Emoji = new Emoji(emojiName); + else + Emoji = null; + + Id = id; + Name = name; + IsModerated = moderated; + } + + public override int GetHashCode() => (Id, Name, Emoji, IsModerated).GetHashCode(); + + public override bool Equals(object? obj) + => obj is ForumTag tag && Equals(tag); + + /// + /// Gets whether supplied tag is equals to the current one. + /// + public bool Equals(ForumTag tag) + => Id == tag.Id && + Name == tag.Name && + (Emoji is Emoji emoji && tag.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) || + Emoji is Emote emote && tag.Emoji is Emote otherEmote && emote.Equals(otherEmote)) && + IsModerated == tag.IsModerated; + + public static bool operator ==(ForumTag? left, ForumTag? right) + => left?.Equals(right) ?? right is null; + + public static bool operator !=(ForumTag? left, ForumTag? right) => !(left == right); + } +} diff --git a/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs b/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs new file mode 100644 index 000000000..d8e881189 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs @@ -0,0 +1,191 @@ +#nullable enable +using System; + +namespace Discord; + +public class ForumTagBuilder +{ + private string? _name; + private IEmote? _emoji; + private bool _moderated; + private ulong? _id; + + /// + /// Returns the maximum length of name allowed by Discord. + /// + public const int MaxNameLength = 20; + + /// + /// Gets or sets the snowflake Id of the tag. + /// + /// + /// If set this will update existing tag or will create a new one otherwise. + /// + public ulong? Id + { + get { return _id; } + set { _id = value; } + } + + /// + /// Gets or sets the name of the tag. + /// + /// Name length must be less than or equal to . + public string? Name + { + get { return _name; } + set + { + if (value?.Length > MaxNameLength) + throw new ArgumentException(message: $"Name length must be less than or equal to {MaxNameLength}.", paramName: nameof(Name)); + _name = value; + } + } + + /// + /// Gets or sets the emoji of the tag. + /// + public IEmote? Emoji + { + get { return _emoji; } + set { _emoji = value; } + } + + /// + /// Gets or sets whether this tag can only be added to or removed from threads by a member + /// with the permission + /// + public bool IsModerated + { + get { return _moderated; } + set { _moderated = value; } + } + + /// + /// Initializes a new class. + /// + public ForumTagBuilder() + { + + } + + /// + /// Initializes a new class with values + /// + /// If set existing tag will be updated or a new one will be created otherwise. + /// Name of the tag. + /// Sets whether this tag can only be added to or removed from threads by a member + /// with the permission. + public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false) + { + Name = name; + IsModerated = isModerated; + Id = id; + } + + /// + /// Initializes a new class with values + /// + /// Name of the tag. + /// If set existing tag will be updated or a new one will be created otherwise. + /// Display emoji of the tag. + /// Sets whether this tag can only be added to or removed from threads by a member + /// with the permission. + public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false, IEmote? emoji = null) + { + Name = name; + Emoji = emoji; + IsModerated = isModerated; + Id = id; + } + + /// + /// Initializes a new class with values + /// + /// /// Name of the tag. + /// If set existing tag will be updated or a new one will be created otherwise. + /// The id of custom Display emoji of the tag. + /// Sets whether this tag can only be added to or removed from threads by a member + /// with the permission + public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false, ulong? emoteId = null) + { + Name = name; + if(emoteId is not null) + Emoji = new Emote(emoteId.Value, null, false); + IsModerated = isModerated; + Id = id; + } + + /// + /// Builds the Tag. + /// + /// An instance of + /// "Name must be set to build the tag" + public ForumTagProperties Build() + { + if (_name is null) + throw new ArgumentNullException(nameof(Name), "Name must be set to build the tag"); + return new ForumTagProperties(_name!, _emoji, _moderated); + } + + /// + /// Sets the name of the tag. + /// + /// Name length must be less than or equal to . + public ForumTagBuilder WithName(string name) + { + Name = name; + return this; + } + + /// + /// Sets the id of the tag. + /// + /// If set existing tag will be updated or a new one will be created otherwise. + /// Name length must be less than or equal to . + public ForumTagBuilder WithId(ulong? id) + { + Id = id; + return this; + } + + /// + /// Sets the emoji of the tag. + /// + public ForumTagBuilder WithEmoji(IEmote? emoji) + { + Emoji = emoji; + return this; + } + + /// + /// Sets whether this tag can only be added to or removed from threads by a member + /// with the permission + /// + public ForumTagBuilder WithModerated(bool moderated) + { + IsModerated = moderated; + return this; + } + + public override int GetHashCode() => base.GetHashCode(); + + public override bool Equals(object? obj) + => obj is ForumTagBuilder builder && Equals(builder); + + /// + /// Gets whether supplied tag builder is equals to the current one. + /// + public bool Equals(ForumTagBuilder? builder) + => builder is not null && + Id == builder.Id && + Name == builder.Name && + (Emoji is Emoji emoji && builder.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) || + Emoji is Emote emote && builder.Emoji is Emote otherEmote && emote.Equals(otherEmote)) && + IsModerated == builder.IsModerated; + + public static bool operator ==(ForumTagBuilder? left, ForumTagBuilder? right) + => left?.Equals(right) ?? right is null ; + + public static bool operator !=(ForumTagBuilder? left, ForumTagBuilder? right) => !(left == right); +} diff --git a/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs b/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs new file mode 100644 index 000000000..73a953fe6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs @@ -0,0 +1,11 @@ +namespace Discord; + +public static class ForumTagBuilderExtensions +{ + public static ForumTagBuilder ToForumTagBuilder(this ForumTag tag) + => new ForumTagBuilder(tag.Name, tag.Id, tag.IsModerated, tag.Emoji); + + public static ForumTagBuilder ToForumTagBuilder(this ForumTagProperties tag) + => new ForumTagBuilder(tag.Name, tag.Id, tag.IsModerated, tag.Emoji); + +} diff --git a/src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs b/src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs new file mode 100644 index 000000000..6ded49204 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs @@ -0,0 +1,48 @@ +namespace Discord; + +#nullable enable + +public class ForumTagProperties : IForumTag +{ + /// + /// Gets the Id of the tag. + /// + public ulong Id { get; } + + /// + public string Name { get; } + + /// + public IEmote? Emoji { get; } + + /// + public bool IsModerated { get; } + + internal ForumTagProperties(string name, IEmote? emoji = null, bool isMmoderated = false) + { + Name = name; + Emoji = emoji; + IsModerated = isMmoderated; + } + + public override int GetHashCode() => (Id, Name, Emoji, IsModerated).GetHashCode(); + + public override bool Equals(object? obj) + => obj is ForumTagProperties tag && Equals(tag); + + /// + /// Gets whether supplied tag is equals to the current one. + /// + public bool Equals(ForumTagProperties? tag) + => tag is not null && + Id == tag.Id && + Name == tag.Name && + (Emoji is Emoji emoji && tag.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) || + Emoji is Emote emote && tag.Emoji is Emote otherEmote && emote.Equals(otherEmote)) && + IsModerated == tag.IsModerated; + + public static bool operator ==(ForumTagProperties? left, ForumTagProperties? right) + => left?.Equals(right) ?? right is null; + + public static bool operator !=(ForumTagProperties? left, ForumTagProperties? right) => !(left == right); +} diff --git a/src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs b/src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs new file mode 100644 index 000000000..8b8b866b2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs @@ -0,0 +1,29 @@ +namespace Discord; + +#nullable enable + +/// +/// Represents a Discord forum tag +/// +public interface IForumTag +{ + /// + /// Gets the name of the tag. + /// + string Name { get; } + + /// + /// Gets the emoji of the tag or if none is set. + /// + /// + /// If the emoji is only the will be populated. + /// Use to get the emoji. + /// + IEmote? Emoji { get; } + + /// + /// Gets whether this tag can only be added to or removed from threads by a member + /// with the permission + /// + bool IsModerated { get; } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 34a08f1e7..d1ff7b99c 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -761,6 +761,18 @@ namespace Discord /// Task CreateCategoryAsync(string name, Action func = null, RequestOptions options = null); + /// + /// Creates a new channel forum in this guild. + /// + /// The new name for the forum. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// forum channel. + /// + Task CreateForumChannelAsync(string name, Action func = null, RequestOptions options = null); + /// /// Gets a collection of all the voice regions this guild can access. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs index 4506b66d9..2bad7fcb7 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs @@ -21,7 +21,7 @@ namespace Discord String = 3, /// - /// An . + /// An . /// Integer = 4, diff --git a/src/Discord.Net.Core/Extensions/ChannelExtensions.cs b/src/Discord.Net.Core/Extensions/ChannelExtensions.cs index b5ddae1cf..a24588792 100644 --- a/src/Discord.Net.Core/Extensions/ChannelExtensions.cs +++ b/src/Discord.Net.Core/Extensions/ChannelExtensions.cs @@ -46,6 +46,9 @@ namespace Discord case ITextChannel: return ChannelType.Text; + + case IForumChannel: + return ChannelType.Forum; } return null; diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index d9d7d469c..f9184cd1f 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -70,8 +70,24 @@ namespace Discord.API //ForumChannel [JsonProperty("available_tags")] public Optional ForumTags { get; set; } - + + [JsonProperty("applied_tags")] + public Optional AppliedTags { get; set; } + [JsonProperty("default_auto_archive_duration")] public Optional AutoArchiveDuration { get; set; } + + [JsonProperty("default_thread_rate_limit_per_user")] + public Optional ThreadRateLimitPerUser { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } + + [JsonProperty("default_sort_order")] + public Optional DefaultSortOrder { get; set; } + + [JsonProperty("default_reaction_emoji")] + public Optional DefaultReactionEmoji { get; set; } + } } diff --git a/src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs b/src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs new file mode 100644 index 000000000..ae2d2b546 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +public class ForumReactionEmoji +{ + [JsonProperty("emoji_id")] + public ulong? EmojiId { get; set; } + + [JsonProperty("emoji_name")] + public Optional EmojiName { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Common/ForumTags.cs b/src/Discord.Net.Rest/API/Common/ForumTags.cs index 18354e7b2..c4a1fa2a2 100644 --- a/src/Discord.Net.Rest/API/Common/ForumTags.cs +++ b/src/Discord.Net.Rest/API/Common/ForumTags.cs @@ -17,5 +17,8 @@ namespace Discord.API public Optional EmojiId { get; set; } [JsonProperty("emoji_name")] public Optional EmojiName { get; set; } + + [JsonProperty("moderated")] + public bool Moderated { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs index 57816e448..74590fb35 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs @@ -23,6 +23,8 @@ namespace Discord.API.Rest public Optional IsNsfw { get; set; } [JsonProperty("rate_limit_per_user")] public Optional SlowModeInterval { get; set; } + [JsonProperty("default_auto_archive_duration")] + public Optional DefaultAutoArchiveDuration { get; set; } //Voice channels [JsonProperty("bitrate")] @@ -30,6 +32,16 @@ namespace Discord.API.Rest [JsonProperty("user_limit")] public Optional UserLimit { get; set; } + //Forum channels + [JsonProperty("default_reaction_emoji")] + public Optional DefaultReactionEmoji { get; set; } + [JsonProperty("default_thread_rate_limit_per_user")] + public Optional ThreadRateLimitPerUser { get; set; } + [JsonProperty("available_tags")] + public Optional AvailableTags { get; set; } + [JsonProperty("default_sort_order")] + public Optional DefaultSortOrder { get; set; } + public CreateGuildChannelParams(string name, ChannelType type) { Name = name; diff --git a/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs index 0c8bc5494..bb10a4681 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs @@ -27,6 +27,7 @@ namespace Discord.API.Rest public Optional MessageComponent { get; set; } public Optional Flags { get; set; } public Optional Stickers { get; set; } + public Optional TagIds { get; set; } public CreateMultipartPostAsync(params FileAttachment[] attachments) { @@ -59,6 +60,8 @@ namespace Discord.API.Rest message["sticker_ids"] = Stickers.Value; if (Flags.IsSpecified) message["flags"] = Flags.Value; + if (TagIds.IsSpecified) + message["applied_tags"] = TagIds.Value; List attachments = new(); diff --git a/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs index 974e07c0a..d74678f63 100644 --- a/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs @@ -21,5 +21,8 @@ namespace Discord.API.Rest [JsonProperty("message")] public ForumThreadMessage Message { get; set; } + + [JsonProperty("applied_tags")] + public Optional Tags { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs new file mode 100644 index 000000000..d8733a2bd --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest; + + +[JsonObject(MemberSerialization = MemberSerialization.OptIn)] +internal class ModifyForumChannelParams : ModifyTextChannelParams +{ + [JsonProperty("available_tags")] + public Optional Tags { get; set; } + + [JsonProperty("default_thread_rate_limit_per_user")] + public Optional DefaultSlowModeInterval { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional ThreadCreationInterval { get; set; } + + [JsonProperty("default_reaction_emoji")] + public Optional DefaultReactionEmoji { get; set; } + + [JsonProperty("default_sort_order")] + public Optional DefaultSortOrder { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs new file mode 100644 index 000000000..d659f70cd --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +[JsonObject(MemberSerialization = MemberSerialization.OptIn)] +public class ModifyForumReactionEmojiParams +{ + [JsonProperty("emoji_id")] + public Optional EmojiId { get; set; } + + [JsonProperty("emoji_name")] + public Optional EmojiName { get; set; } +} + + diff --git a/src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs new file mode 100644 index 000000000..4d792d0af --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyForumTagParams + { + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("emoji_id")] + public Optional EmojiId { get; set; } + + [JsonProperty("emoji_name")] + public Optional EmojiName { get; set; } + + [JsonProperty("moderated")] + public bool Moderated { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs index dfe9cd980..dea0c037f 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -13,5 +13,7 @@ namespace Discord.API.Rest public Optional CategoryId { get; set; } [JsonProperty("permission_overwrites")] public Optional Overwrites { get; set; } + [JsonProperty("flags")] + public Optional Flags { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs index 8c9216c3f..bd651b22c 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API.Rest { @@ -18,5 +19,11 @@ namespace Discord.API.Rest [JsonProperty("rate_limit_per_user")] public Optional Slowmode { get; set; } + + [JsonProperty("applied_tags")] + public Optional> AppliedTags { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index d66fd5e51..4e353c39b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -38,6 +38,7 @@ namespace Discord.Rest Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), + Flags = args.Flags.GetValueOrDefault(), }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } diff --git a/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs new file mode 100644 index 000000000..3d087e77d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs @@ -0,0 +1,63 @@ +using Discord.API; +using System; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest; + +internal static class ForumHelper +{ + public static async Task ModifyAsync(IForumChannel channel, BaseDiscordClient client, + Action func, + RequestOptions options) + { + var args = new ForumChannelProperties(); + func(args); + + Preconditions.AtMost(args.Tags.IsSpecified ? args.Tags.Value.Count() : 0, 5, nameof(args.Tags), "Forum channel can have max 20 tags."); + + var apiArgs = new API.Rest.ModifyForumChannelParams() + { + Name = args.Name, + Position = args.Position, + CategoryId = args.CategoryId, + Overwrites = args.PermissionOverwrites.IsSpecified + ? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), + DefaultSlowModeInterval = args.DefaultSlowModeInterval, + ThreadCreationInterval = args.ThreadCreationInterval, + Tags = args.Tags.IsSpecified + ? args.Tags.Value.Select(tag => new API.ModifyForumTagParams + { + Name = tag.Name, + EmojiId = tag.Emoji is Emote emote + ? emote.Id + : Optional.Unspecified, + EmojiName = tag.Emoji is Emoji emoji + ? emoji.Name + : Optional.Unspecified + }).ToArray() + : Optional.Create(), + Flags = args.Flags.GetValueOrDefault(), + Topic = args.Topic, + DefaultReactionEmoji = args.DefaultReactionEmoji.IsSpecified + ? new API.ModifyForumReactionEmojiParams + { + EmojiId = args.DefaultReactionEmoji.Value is Emote emote ? + emote.Id : Optional.Unspecified, + EmojiName = args.DefaultReactionEmoji.Value is Emoji emoji ? + emoji.Name : Optional.Unspecified + } + : Optional.Unspecified, + DefaultSortOrder = args.DefaultSortOrder + }; + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index c730596c7..3f472c74e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -30,13 +30,15 @@ namespace Discord.Rest ChannelType.Stage or ChannelType.NewsThread or ChannelType.PrivateThread or - ChannelType.PublicThread + ChannelType.PublicThread or + ChannelType.Forum => RestGuildChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model), ChannelType.DM or ChannelType.Group => CreatePrivate(discord, model) as RestChannel, ChannelType.Category => RestCategoryChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model), _ => new RestChannel(discord, model.Id), }; } + internal static RestChannel Create(BaseDiscordClient discord, Model model, IGuild guild) { return model.Type switch @@ -47,7 +49,8 @@ namespace Discord.Rest ChannelType.Stage or ChannelType.NewsThread or ChannelType.PrivateThread or - ChannelType.PublicThread + ChannelType.PublicThread or + ChannelType.Forum => RestGuildChannel.Create(discord, guild, model), ChannelType.DM or ChannelType.Group => CreatePrivate(discord, model) as RestChannel, ChannelType.Category => RestCategoryChannel.Create(discord, guild, model), diff --git a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs index aff8400aa..5ec81a7fc 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs @@ -26,6 +26,21 @@ namespace Discord.Rest /// public IReadOnlyCollection Tags { get; private set; } + /// + public int ThreadCreationInterval { get; private set; } + + /// + public int DefaultSlowModeInterval { get; private set; } + + /// + public ulong? CategoryId { get; private set; } + + /// + public IEmote DefaultReactionEmoji { get; private set; } + + /// + public ForumSortOrder? DefaultSortOrder { get; private set; } + /// public string Mention => MentionUtils.MentionChannel(Id); @@ -35,9 +50,9 @@ namespace Discord.Rest } - internal new static RestStageChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + internal new static RestForumChannel Create(BaseDiscordClient discord, IGuild guild, Model model) { - var entity = new RestStageChannel(discord, guild, model.Id); + var entity = new RestForumChannel(discord, guild, model.Id); entity.Update(model); return entity; } @@ -49,46 +64,75 @@ namespace Discord.Rest Topic = model.Topic.GetValueOrDefault(); DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); + if (model.ThreadRateLimitPerUser.IsSpecified) + DefaultSlowModeInterval = model.ThreadRateLimitPerUser.Value; + + if(model.SlowMode.IsSpecified) + ThreadCreationInterval = model.SlowMode.Value; + + DefaultSortOrder = model.DefaultSortOrder.GetValueOrDefault(); + Tags = model.ForumTags.GetValueOrDefault(Array.Empty()).Select( - x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault(), x.Moderated) ).ToImmutableArray(); + + if (model.DefaultReactionEmoji.IsSpecified && model.DefaultReactionEmoji.Value is not null) + { + if (model.DefaultReactionEmoji.Value.EmojiId.HasValue && model.DefaultReactionEmoji.Value.EmojiId.Value != 0) + DefaultReactionEmoji = new Emote(model.DefaultReactionEmoji.Value.EmojiId.GetValueOrDefault(), null, false); + else if (model.DefaultReactionEmoji.Value.EmojiName.IsSpecified) + DefaultReactionEmoji = new Emoji(model.DefaultReactionEmoji.Value.EmojiName.Value); + else + DefaultReactionEmoji = null; + } + + CategoryId = model.CategoryId.GetValueOrDefault(); + } + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ForumHelper.ModifyAsync(this, Discord, func, options); + Update(model); } - /// - public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + /// + public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, + string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public async Task CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) { using var file = new FileAttachment(filePath, isSpoiler: isSpoiler); - return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false); } - /// + /// public async Task CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) { using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler); - return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false); } - /// + /// public Task CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public Task CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); /// public Task> GetActiveThreadsAsync(RequestOptions options = null) @@ -115,17 +159,45 @@ namespace Discord.Rest => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); - async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); #endregion + + #region INestedChannel + /// + 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 virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + 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 virtual async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + /// + async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) + { + if (CategoryId.HasValue && mode == CacheMode.AllowDownload) + return (await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false)) as ICategoryChannel; + return null; + } + + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 4f9af0335..a6c9c3674 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -26,6 +26,9 @@ namespace Discord.Rest /// public ulong GuildId => Guild.Id; + /// + public ChannelFlags Flags { get; private set; } + internal RestGuildChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, id) { @@ -62,6 +65,8 @@ namespace Discord.Rest newOverwrites.Add(overwrites[i].ToEntity()); _overwrites = newOverwrites.ToImmutable(); } + + Flags = model.Flags.GetValueOrDefault(ChannelFlags.None); } /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs index c763a6660..c1be5182e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -37,6 +37,9 @@ namespace Discord.Rest /// public bool? IsInvitable { get; private set; } + /// + public IReadOnlyCollection AppliedTags { get; private set; } + /// public override DateTimeOffset CreatedAt { get; } @@ -77,6 +80,8 @@ namespace Discord.Rest MessageCount = model.MessageCount.GetValueOrDefault(0); Type = (ThreadType)model.Type; ParentChannelId = model.CategoryId.Value; + + AppliedTags = model.AppliedTags.GetValueOrDefault(Array.Empty()).ToImmutableArray(); } /// @@ -109,6 +114,13 @@ namespace Discord.Rest Update(model); } + /// + public 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. diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index f5fce5a50..52cba0657 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -1,3 +1,4 @@ +using Discord.API; using Discord.API.Rest; using System; using System.Collections.Generic; @@ -46,18 +47,23 @@ namespace Discord.Rest } public static async Task ModifyAsync(IThreadChannel channel, BaseDiscordClient client, - Action func, + Action func, RequestOptions options) { - var args = new TextChannelProperties(); + var args = new ThreadChannelProperties(); func(args); + + Preconditions.AtMost(args.AppliedTags.IsSpecified ? args.AppliedTags.Value.Count() : 0, 5, nameof(args.AppliedTags), "Forum post can have max 5 applied tags."); + var apiArgs = new ModifyThreadParams { Name = args.Name, Archived = args.Archived, AutoArchiveDuration = args.AutoArchiveDuration, Locked = args.Locked, - Slowmode = args.SlowModeInterval + Slowmode = args.SlowModeInterval, + AppliedTags = args.AppliedTags, + Flags = args.Flags, }; return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } @@ -103,7 +109,10 @@ namespace Discord.Rest return RestThreadUser.Create(client, channel.Guild, model, channel); } - public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, + ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ulong[] tagIds = null) { embeds ??= Array.Empty(); if (embed != null) @@ -112,6 +121,7 @@ namespace Discord.Rest Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.AtMost(tagIds?.Length ?? 0, 5, nameof(tagIds), "Forum post can have max 5 applied tags."); // check that user flag and user Id list are exclusive, same with role flag and role Id list if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) @@ -134,10 +144,12 @@ namespace Discord.Rest Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + if (channel.Flags.HasFlag(ChannelFlags.RequireTag)) + throw new ArgumentException($"The channel {channel.Name} requires posts to have at least one tag."); + var args = new CreatePostParams() { Title = title, @@ -151,7 +163,8 @@ namespace Discord.Rest Flags = flags, Components = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, - } + }, + Tags = tagIds }; var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options).ConfigureAwait(false); @@ -159,7 +172,9 @@ namespace Discord.Rest return RestThreadChannel.Create(client, channel.Guild, model); } - public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable attachments, + ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags, ulong[] tagIds = null) { embeds ??= Array.Empty(); if (embed != null) @@ -168,6 +183,8 @@ namespace Discord.Rest Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.AtMost(tagIds?.Length ?? 0, 5, nameof(tagIds), "Forum post can have max 5 applied tags."); + // check that user flag and user Id list are exclusive, same with role flag and role Id list if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) @@ -190,9 +207,11 @@ namespace Discord.Rest Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + + if (channel.Flags.HasFlag(ChannelFlags.RequireTag)) + throw new ArgumentException($"The channel {channel.Name} requires posts to have at least one tag."); var args = new CreateMultipartPostAsync(attachments.ToArray()) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index c4e3764d1..74e797fd4 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -1,3 +1,4 @@ +using Discord.API; using Discord.API.Rest; using System; using System.Collections.Generic; @@ -252,6 +253,7 @@ namespace Discord.Rest Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), + DefaultAutoArchiveDuration = props.AutoArchiveDuration }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestTextChannel.Create(client, guild, model); @@ -338,6 +340,65 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestCategoryChannel.Create(client, guild, model); } + + /// is null. + public static async Task CreateForumChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options, Action func = null) + { + if (name == null) + throw new ArgumentNullException(paramName: nameof(name)); + + var props = new ForumChannelProperties(); + func?.Invoke(props); + + Preconditions.AtMost(props.Tags.IsSpecified ? props.Tags.Value.Count() : 0, 5, nameof(props.Tags), "Forum channel can have max 20 tags."); + + var args = new CreateGuildChannelParams(name, ChannelType.Forum) + { + Position = props.Position, + Overwrites = props.PermissionOverwrites.IsSpecified + ? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), + SlowModeInterval = props.ThreadCreationInterval, + AvailableTags = props.Tags.GetValueOrDefault(Array.Empty()).Select( + x => new ModifyForumTagParams + { + Id = x.Id, + Name = x.Name, + EmojiId = x.Emoji is Emote emote + ? emote.Id + : Optional.Unspecified, + EmojiName = x.Emoji is Emoji emoji + ? emoji.Name + : Optional.Unspecified, + Moderated = x.IsModerated + }).ToArray(), + DefaultReactionEmoji = props.DefaultReactionEmoji.IsSpecified + ? new API.ModifyForumReactionEmojiParams + { + EmojiId = props.DefaultReactionEmoji.Value is Emote emote ? + emote.Id : Optional.Unspecified, + EmojiName = props.DefaultReactionEmoji.Value is Emoji emoji ? + emoji.Name : Optional.Unspecified + } + : Optional.Unspecified, + ThreadRateLimitPerUser = props.DefaultSlowModeInterval, + CategoryId = props.CategoryId, + IsNsfw = props.IsNsfw, + Topic = props.Topic, + DefaultAutoArchiveDuration = props.AutoArchiveDuration, + DefaultSortOrder = props.DefaultSortOrder.GetValueOrDefault(ForumSortOrder.LatestActivity) + }; + + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestForumChannel.Create(client, guild, model); + } #endregion #region Voice Regions diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index eb3254619..24f6ae28d 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -710,6 +710,19 @@ namespace Discord.Rest public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); + /// + /// Creates a category channel with the provided name. + /// + /// The name of the new channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is . + /// + /// The created category channel. + /// + public Task CreateForumChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateForumChannelAsync(this, Discord, name, options, func); + /// /// Gets a collection of all the voice regions this guild can access. /// @@ -1370,6 +1383,9 @@ namespace Discord.Rest /// async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateForumChannelAsync(string name, Action func, RequestOptions options) + => await CreateForumChannelAsync(name, func, options).ConfigureAwait(false); /// async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs index ea58ecdb5..9d46bc2aa 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -27,9 +27,33 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Tags { get; private set; } + /// + public int ThreadCreationInterval { get; private set; } + + /// + public int DefaultSlowModeInterval { get; private set; } + /// public string Mention => MentionUtils.MentionChannel(Id); + /// + public ulong? CategoryId { get; private set; } + + /// + public IEmote DefaultReactionEmoji { get; private set; } + + /// + public ForumSortOrder? DefaultSortOrder { get; private set; } + + /// + /// Gets the parent (category) of this channel in the guild's channel list. + /// + /// + /// An representing the parent of this channel; null if none is set. + /// + public ICategoryChannel Category + => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + internal SocketForumChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { } internal new static SocketForumChannel Create(SocketGuild guild, ClientState state, Model model) @@ -46,46 +70,70 @@ namespace Discord.WebSocket Topic = model.Topic.GetValueOrDefault(); DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); + if (model.ThreadRateLimitPerUser.IsSpecified) + DefaultSlowModeInterval = model.ThreadRateLimitPerUser.Value; + + if (model.SlowMode.IsSpecified) + ThreadCreationInterval = model.SlowMode.Value; + + DefaultSortOrder = model.DefaultSortOrder.GetValueOrDefault(); + Tags = model.ForumTags.GetValueOrDefault(Array.Empty()).Select( - x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault(), x.Moderated) ).ToImmutableArray(); + + if (model.DefaultReactionEmoji.IsSpecified && model.DefaultReactionEmoji.Value is not null) + { + if (model.DefaultReactionEmoji.Value.EmojiId.HasValue && model.DefaultReactionEmoji.Value.EmojiId.Value != 0) + DefaultReactionEmoji = new Emote(model.DefaultReactionEmoji.Value.EmojiId.GetValueOrDefault(), null, false); + else if (model.DefaultReactionEmoji.Value.EmojiName.IsSpecified) + DefaultReactionEmoji = new Emoji(model.DefaultReactionEmoji.Value.EmojiName.Value); + else + DefaultReactionEmoji = null; + } + + CategoryId = model.CategoryId.GetValueOrDefault(); } - /// - public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + /// + public virtual Task ModifyAsync(Action func, RequestOptions options = null) + => ForumHelper.ModifyAsync(this, Discord, func, options); + + /// + public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public async Task CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) { using var file = new FileAttachment(filePath, isSpoiler: isSpoiler); - return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false); } - /// + /// public async Task CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) { using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler); - return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false); } - /// + /// public Task CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public Task CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); /// public Task> GetActiveThreadsAsync(RequestOptions options = null) @@ -112,17 +160,41 @@ namespace Discord.WebSocket => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); - async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); #endregion + + #region INestedChannel + /// + 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 virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + 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 virtual async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + /// + Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(Category); + + /// + public virtual Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 808982785..0811f2238 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -30,6 +30,9 @@ namespace Discord.WebSocket /// public int Position { get; private set; } + /// + public ChannelFlags Flags { get; private set; } + /// public virtual IReadOnlyCollection PermissionOverwrites => _overwrites; /// @@ -74,6 +77,8 @@ namespace Discord.WebSocket for (int i = 0; i < overwrites.Length; i++) newOverwrites.Add(overwrites[i].ToEntity()); _overwrites = newOverwrites.ToImmutable(); + + Flags = model.Flags.GetValueOrDefault(ChannelFlags.None); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 78462b062..d9d54f91e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -89,6 +89,9 @@ namespace Discord.WebSocket /// public bool? IsInvitable { get; private set; } + /// + public IReadOnlyCollection AppliedTags { get; private set; } + /// public override DateTimeOffset CreatedAt { get; } @@ -149,6 +152,8 @@ namespace Discord.WebSocket } HasJoined = model.ThreadMember.IsSpecified; + + AppliedTags = model.AppliedTags.GetValueOrDefault(Array.Empty()).ToImmutableArray(); } internal IReadOnlyCollection RemoveUsers(ulong[] users) @@ -334,12 +339,13 @@ namespace Discord.WebSocket => throw new NotSupportedException("This method is not supported in threads."); /// - /// - /// This method is not supported in threads. - /// public override Task ModifyAsync(Action func, RequestOptions options = null) => ThreadHelper.ModifyAsync(this, Discord, func, options); + /// + public Task ModifyAsync(Action func, RequestOptions options = null) + => ThreadHelper.ModifyAsync(this, Discord, func, options); + /// /// /// This method is not supported in threads. diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 55f098b2f..c2778ed1d 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -302,6 +302,16 @@ namespace Discord.WebSocket /// public IReadOnlyCollection ThreadChannels => Channels.OfType().ToImmutableArray(); + + /// + /// Gets a collection of all forum channels in this guild. + /// + /// + /// A read-only collection of forum channels found within this guild. + /// + public IReadOnlyCollection ForumChannels + => Channels.OfType().ToImmutableArray(); + /// /// Gets the current logged-in user. /// @@ -790,6 +800,7 @@ namespace Discord.WebSocket /// public Task CreateStageChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateStageChannelAsync(this, Discord, name, options, func); + /// /// Creates a new channel category in this guild. /// @@ -804,6 +815,20 @@ namespace Discord.WebSocket public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); + /// + /// Creates a new channel forum in this guild. + /// + /// The new name for the forum. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is . + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// forum channel. + /// + public Task CreateForumChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateForumChannelAsync(this, Discord, name, options, func); + internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { var channel = SocketGuildChannel.Create(this, state, model); @@ -1897,6 +1922,9 @@ namespace Discord.WebSocket /// async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateForumChannelAsync(string name, Action func, RequestOptions options) + => await CreateForumChannelAsync(name, func, options).ConfigureAwait(false); /// async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs index 712570467..08d3c740a 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs @@ -20,6 +20,8 @@ namespace Discord public DateTimeOffset CreatedAt => throw new NotImplementedException(); public ulong Id => throw new NotImplementedException(); + + public ChannelFlags Flags => throw new NotImplementedException(); public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) { diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index ab1d3e534..8913c127d 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -34,6 +34,8 @@ namespace Discord public ulong Id => throw new NotImplementedException(); + public ChannelFlags Flags => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) { throw new NotImplementedException(); diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs index 2ffc75a24..3f167ba5f 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -36,6 +36,8 @@ namespace Discord public string Mention => throw new NotImplementedException(); + public ChannelFlags Flags => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) => throw new NotImplementedException(); From ea039b848cd4e2a81bf00d657067ba76a29234b6 Mon Sep 17 00:00:00 2001 From: Payton <30909416+Trosclair@users.noreply.github.com> Date: Mon, 7 Nov 2022 10:26:18 -0600 Subject: [PATCH 06/24] Update events.cs (#2505) Fixed compiler error caused by missing semicolon --- docs/guides/concepts/samples/events.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/concepts/samples/events.cs b/docs/guides/concepts/samples/events.cs index dce625b33..29542ef2f 100644 --- a/docs/guides/concepts/samples/events.cs +++ b/docs/guides/concepts/samples/events.cs @@ -22,7 +22,7 @@ public class Program { Console.WriteLine("Bot is connected!"); return Task.CompletedTask; - } + }; await Task.Delay(-1); } From 68698171844cb9e49ccfc883d9f3045ba80b5956 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Tue, 22 Nov 2022 14:05:26 +0300 Subject: [PATCH 07/24] Command execution code rework and TypeConverters auto-scope fix (#2306) * command execution rework and sync service scopes for typeconverters * replace ValueTask with Task * fix implementation bugs Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- .../Info/Commands/AutocompleteCommandInfo.cs | 7 +- .../Info/Commands/CommandInfo.cs | 124 +++++++++--------- .../Info/Commands/ComponentCommandInfo.cs | 40 ++---- .../ContextCommands/ContextCommandInfo.cs | 2 +- .../ContextCommands/MessageCommandInfo.cs | 13 +- .../ContextCommands/UserCommandInfo.cs | 11 +- .../Info/Commands/ModalCommandInfo.cs | 37 +++--- .../Info/Commands/SlashCommandInfo.cs | 75 +++++------ .../Info/ModalInfo.cs | 6 +- .../InteractionService.cs | 4 +- .../Results/ParseResult.cs | 12 +- 11 files changed, 159 insertions(+), 172 deletions(-) diff --git a/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs index 9e30c55f4..b8940f42f 100644 --- a/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs @@ -23,7 +23,7 @@ namespace Discord.Interactions public string CommandName { get; } /// - public override IReadOnlyCollection Parameters { get; } + public override IReadOnlyList Parameters { get; } /// public override bool SupportsWildCards => false; @@ -41,9 +41,12 @@ namespace Discord.Interactions if (context.Interaction is not IAutocompleteInteraction) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction"); - return await RunAsync(context, Array.Empty(), services).ConfigureAwait(false); + return await base.ExecuteAsync(context, services).ConfigureAwait(false); } + protected override Task ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) + => Task.FromResult(ParseResult.FromSuccess(Array.Empty()) as IResult); + /// protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) => CommandService._autocompleteCommandExecutedEvent.InvokeAsync(this, context, result); diff --git a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs index ea5ded11c..99895d3ed 100644 --- a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs @@ -64,7 +64,7 @@ namespace Discord.Interactions public IReadOnlyCollection Preconditions { get; } /// - public abstract IReadOnlyCollection Parameters { get; } + public abstract IReadOnlyList Parameters { get; } internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService) { @@ -85,71 +85,16 @@ namespace Discord.Interactions } /// - public abstract Task ExecuteAsync(IInteractionContext context, IServiceProvider services); - protected abstract Task InvokeModuleEvent(IInteractionContext context, IResult result); - protected abstract string GetLogString(IInteractionContext context); - - /// - public async Task CheckPreconditionsAsync(IInteractionContext context, IServiceProvider services) - { - async Task CheckGroups(ILookup preconditions, string type) - { - foreach (IGrouping preconditionGroup in preconditions) - { - if (preconditionGroup.Key == null) - { - foreach (PreconditionAttribute precondition in preconditionGroup) - { - var result = await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false); - if (!result.IsSuccess) - return result; - } - } - else - { - var results = new List(); - foreach (PreconditionAttribute precondition in preconditionGroup) - results.Add(await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false)); - - if (!results.Any(p => p.IsSuccess)) - return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); - } - } - return PreconditionGroupResult.FromSuccess(); - } - - var moduleResult = await CheckGroups(Module.GroupedPreconditions, "Module").ConfigureAwait(false); - if (!moduleResult.IsSuccess) - return moduleResult; - - var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); - return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess(); - } - - protected async Task RunAsync(IInteractionContext context, object[] args, IServiceProvider services) + public virtual async Task ExecuteAsync(IInteractionContext context, IServiceProvider services) { switch (RunMode) { case RunMode.Sync: - { - if (CommandService._autoServiceScopes) - { - using var scope = services?.CreateScope(); - return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false); - } - - return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); - } + return await ExecuteInternalAsync(context, services).ConfigureAwait(false); case RunMode.Async: _ = Task.Run(async () => { - if (CommandService._autoServiceScopes) - { - using var scope = services?.CreateScope(); - await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false); - } - else - await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); + await ExecuteInternalAsync(context, services).ConfigureAwait(false); }); break; default: @@ -159,16 +104,33 @@ namespace Discord.Interactions return ExecuteResult.FromSuccess(); } - private async Task ExecuteInternalAsync(IInteractionContext context, object[] args, IServiceProvider services) + protected abstract Task ParseArgumentsAsync(IInteractionContext context, IServiceProvider services); + + private async Task ExecuteInternalAsync(IInteractionContext context, IServiceProvider services) { await CommandService._cmdLogger.DebugAsync($"Executing {GetLogString(context)}").ConfigureAwait(false); + using var scope = services?.CreateScope(); + + if (CommandService._autoServiceScopes) + services = scope?.ServiceProvider ?? EmptyServiceProvider.Instance; + try { var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false); if (!preconditionResult.IsSuccess) return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false); + var argsResult = await ParseArgumentsAsync(context, services).ConfigureAwait(false); + + if (!argsResult.IsSuccess) + return await InvokeEventAndReturn(context, argsResult).ConfigureAwait(false); + + if(argsResult is not ParseResult parseResult) + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); + + var args = parseResult.Args; + var index = 0; foreach (var parameter in Parameters) { @@ -221,7 +183,47 @@ namespace Discord.Interactions } } - protected async ValueTask InvokeEventAndReturn(IInteractionContext context, IResult result) + protected abstract Task InvokeModuleEvent(IInteractionContext context, IResult result); + protected abstract string GetLogString(IInteractionContext context); + + /// + public async Task CheckPreconditionsAsync(IInteractionContext context, IServiceProvider services) + { + async Task CheckGroups(ILookup preconditions, string type) + { + foreach (IGrouping preconditionGroup in preconditions) + { + if (preconditionGroup.Key == null) + { + foreach (PreconditionAttribute precondition in preconditionGroup) + { + var result = await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false); + if (!result.IsSuccess) + return result; + } + } + else + { + var results = new List(); + foreach (PreconditionAttribute precondition in preconditionGroup) + results.Add(await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false)); + + if (!results.Any(p => p.IsSuccess)) + return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); + } + } + return PreconditionGroupResult.FromSuccess(); + } + + var moduleResult = await CheckGroups(Module.GroupedPreconditions, "Module").ConfigureAwait(false); + if (!moduleResult.IsSuccess) + return moduleResult; + + var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false); + return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess(); + } + + protected async Task InvokeEventAndReturn(IInteractionContext context, T result) where T : IResult { await InvokeModuleEvent(context, result).ConfigureAwait(false); return result; diff --git a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs index 22d6aba6c..5877eaea9 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs @@ -13,7 +13,7 @@ namespace Discord.Interactions public class ComponentCommandInfo : CommandInfo { /// - public override IReadOnlyCollection Parameters { get; } + public override IReadOnlyList Parameters { get; } /// public override bool SupportsWildCards => true; @@ -25,48 +25,32 @@ namespace Discord.Interactions /// public override async Task ExecuteAsync(IInteractionContext context, IServiceProvider services) - => await ExecuteAsync(context, services, null).ConfigureAwait(false); - - /// - /// Execute this command using dependency injection. - /// - /// Context that will be injected to the . - /// Services that will be used while initializing the . - /// Provide additional string parameters to the method along with the auto generated parameters. - /// - /// A task representing the asynchronous command execution process. - /// - public async Task ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs) { - if (context.Interaction is not IComponentInteraction componentInteraction) + if (context.Interaction is not IComponentInteraction) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction"); - return await ExecuteAsync(context, Parameters, additionalArgs, componentInteraction.Data, services); + return await base.ExecuteAsync(context, services).ConfigureAwait(false); } - /// - public async Task ExecuteAsync(IInteractionContext context, IEnumerable paramList, IEnumerable wildcardCaptures, IComponentInteractionData data, - IServiceProvider services) + protected override async Task ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) { - var paramCount = paramList.Count(); - var captureCount = wildcardCaptures?.Count() ?? 0; - - if (context.Interaction is not IComponentInteraction messageComponent) - return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction"); + var captures = (context as IRouteMatchContainer)?.SegmentMatches?.ToList(); + var captureCount = captures?.Count() ?? 0; try { - var args = new object[paramCount]; + var data = (context.Interaction as IComponentInteraction).Data; + var args = new object[Parameters.Count]; - for (var i = 0; i < paramCount; i++) + for(var i = 0; i < Parameters.Count; i++) { - var parameter = Parameters.ElementAt(i); + var parameter = Parameters[i]; var isCapture = i < captureCount; if (isCapture ^ parameter.IsRouteSegmentParameter) return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Argument type and parameter type didn't match (Wild Card capture/Component value)")).ConfigureAwait(false); - var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, wildcardCaptures.ElementAt(i), services).ConfigureAwait(false) : + var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, captures[i].Value, services).ConfigureAwait(false) : await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false); if (!readResult.IsSuccess) @@ -75,7 +59,7 @@ namespace Discord.Interactions args[i] = readResult.Value; } - return await RunAsync(context, args, services).ConfigureAwait(false); + return ParseResult.FromSuccess(args); } catch (Exception ex) { diff --git a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs index 2d6d748d4..33b82b127 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs @@ -24,7 +24,7 @@ namespace Discord.Interactions public GuildPermission? DefaultMemberPermissions { get; } /// - public override IReadOnlyCollection Parameters { get; } + public override IReadOnlyList Parameters { get; } /// public override bool SupportsWildCards => false; diff --git a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/MessageCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/MessageCommandInfo.cs index e05955df8..76eda2cf6 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/MessageCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/MessageCommandInfo.cs @@ -14,18 +14,23 @@ namespace Discord.Interactions /// public override async Task ExecuteAsync(IInteractionContext context, IServiceProvider services) { - if (context.Interaction is not IMessageCommandInteraction messageCommand) + if (context.Interaction is not IMessageCommandInteraction) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation"); + return await base.ExecuteAsync(context, services).ConfigureAwait(false); + } + + protected override Task ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) + { try { - object[] args = new object[1] { messageCommand.Data.Message }; + object[] args = new object[1] { (context.Interaction as IMessageCommandInteraction).Data.Message }; - return await RunAsync(context, args, services).ConfigureAwait(false); + return Task.FromResult(ParseResult.FromSuccess(args) as IResult); } catch (Exception ex) { - return ExecuteResult.FromError(ex); + return Task.FromResult(ParseResult.FromError(ex) as IResult); } } diff --git a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/UserCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/UserCommandInfo.cs index 8862e1798..a26f3f560 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/UserCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/UserCommandInfo.cs @@ -17,15 +17,20 @@ namespace Discord.Interactions if (context.Interaction is not IUserCommandInteraction userCommand) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation"); + return await base.ExecuteAsync(context, services).ConfigureAwait(false); + } + + protected override Task ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) + { try { - object[] args = new object[1] { userCommand.Data.User }; + object[] args = new object[1] { (context.Interaction as IUserCommandInteraction).Data.User }; - return await RunAsync(context, args, services).ConfigureAwait(false); + return Task.FromResult(ParseResult.FromSuccess(args) as IResult); } catch (Exception ex) { - return ExecuteResult.FromError(ex); + return Task.FromResult(ParseResult.FromError(ex) as IResult); } } diff --git a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs index 4866bd1da..849b4de2e 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.Tracing; using System.Linq; using System.Threading.Tasks; namespace Discord.Interactions @@ -20,7 +19,7 @@ namespace Discord.Interactions public override bool SupportsWildCards => true; /// - public override IReadOnlyCollection Parameters { get; } + public override IReadOnlyList Parameters { get; } internal ModalCommandInfo(Builders.ModalCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) { @@ -30,34 +29,29 @@ namespace Discord.Interactions /// public override async Task ExecuteAsync(IInteractionContext context, IServiceProvider services) - => await ExecuteAsync(context, services, null).ConfigureAwait(false); - - /// - /// Execute this command using dependency injection. - /// - /// Context that will be injected to the . - /// Services that will be used while initializing the . - /// Provide additional string parameters to the method along with the auto generated parameters. - /// - /// A task representing the asynchronous command execution process. - /// - public async Task ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs) { if (context.Interaction is not IModalInteraction modalInteraction) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Modal Interaction."); + return await base.ExecuteAsync(context, services).ConfigureAwait(false); + } + + protected override async Task ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) + { + var captures = (context as IRouteMatchContainer)?.SegmentMatches?.ToList(); + var captureCount = captures?.Count() ?? 0; + try { var args = new object[Parameters.Count]; - var captureCount = additionalArgs?.Length ?? 0; - for(var i = 0; i < Parameters.Count; i++) + for (var i = 0; i < Parameters.Count; i++) { var parameter = Parameters.ElementAt(i); - if(i < captureCount) + if (i < captureCount) { - var readResult = await parameter.TypeReader.ReadAsync(context, additionalArgs[i], services).ConfigureAwait(false); + var readResult = await parameter.TypeReader.ReadAsync(context, captures[i].Value, services).ConfigureAwait(false); if (!readResult.IsSuccess) return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false); @@ -69,13 +63,14 @@ namespace Discord.Interactions if (!modalResult.IsSuccess) return await InvokeEventAndReturn(context, modalResult).ConfigureAwait(false); - if (modalResult is not ParseResult parseResult) + if (modalResult is not TypeConverterResult converterResult) return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason.")); - args[i] = parseResult.Value; + args[i] = converterResult.Value; } } - return await RunAsync(context, args, services); + + return ParseResult.FromSuccess(args); } catch (Exception ex) { diff --git a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs index e428144c7..634fd9643 100644 --- a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs @@ -33,7 +33,7 @@ namespace Discord.Interactions public GuildPermission? DefaultMemberPermissions { get; } /// - public override IReadOnlyCollection Parameters { get; } + public override IReadOnlyList Parameters { get; } /// public override bool SupportsWildCards => false; @@ -41,9 +41,9 @@ namespace Discord.Interactions /// /// Gets the flattened collection of command parameters and complex parameter fields. /// - public IReadOnlyCollection FlattenedParameters { get; } + public IReadOnlyList FlattenedParameters { get; } - internal SlashCommandInfo (Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) + internal SlashCommandInfo(Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) { Description = builder.Description; DefaultPermission = builder.DefaultPermission; @@ -60,49 +60,45 @@ namespace Discord.Interactions } /// - public override async Task ExecuteAsync (IInteractionContext context, IServiceProvider services) + public override async Task ExecuteAsync(IInteractionContext context, IServiceProvider services) { - if(context.Interaction is not ISlashCommandInteraction slashCommand) + if (context.Interaction is not ISlashCommandInteraction) return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Slash Command Interaction"); - var options = slashCommand.Data.Options; - - while (options != null && options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup)) - options = options.ElementAt(0)?.Options; - - return await ExecuteAsync(context, Parameters, options?.ToList(), services); + return await base.ExecuteAsync(context, services); } - private async Task ExecuteAsync (IInteractionContext context, IEnumerable paramList, - List argList, IServiceProvider services) + protected override async Task ParseArgumentsAsync(IInteractionContext context, IServiceProvider services) { - try + List GetOptions() { - var slashCommandParameterInfos = paramList.ToList(); - var args = new object[slashCommandParameterInfos.Count]; - - for (var i = 0; i < slashCommandParameterInfos.Count; i++) - { - var parameter = slashCommandParameterInfos[i]; - var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false); - - if (!result.IsSuccess) - return await InvokeEventAndReturn(context, result).ConfigureAwait(false); + var options = (context.Interaction as ISlashCommandInteraction).Data.Options; - if (result is not ParseResult parseResult) - return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); + while (options != null && options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup)) + options = options.ElementAt(0)?.Options; - args[i] = parseResult.Value; - } - return await RunAsync(context, args, services).ConfigureAwait(false); + return options.ToList(); } - catch(Exception ex) + + var options = GetOptions(); + var args = new object[Parameters.Count]; + for(var i = 0; i < Parameters.Count; i++) { - return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false); + var parameter = Parameters[i]; + var result = await ParseArgumentAsync(parameter, context, options, services).ConfigureAwait(false); + + if (!result.IsSuccess) + return await InvokeEventAndReturn(context, ParseResult.FromError(result)).ConfigureAwait(false); + + if (result is not TypeConverterResult converterResult) + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); + + args[i] = converterResult.Value; } + return ParseResult.FromSuccess(args); } - private async Task ParseArgument(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List argList, + private async ValueTask ParseArgumentAsync(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List argList, IServiceProvider services) { if (parameterInfo.IsComplexParameter) @@ -111,32 +107,29 @@ namespace Discord.Interactions for (var i = 0; i < ctorArgs.Length; i++) { - var result = await ParseArgument(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false); + var result = await ParseArgumentAsync(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false); if (!result.IsSuccess) return result; - if (result is not ParseResult parseResult) + if (result is not TypeConverterResult converterResult) return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); - ctorArgs[i] = parseResult.Value; + ctorArgs[i] = converterResult.Value; } - return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); + return TypeConverterResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); } var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); if (arg == default) return parameterInfo.IsRequired ? ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters") : - ParseResult.FromSuccess(parameterInfo.DefaultValue); + TypeConverterResult.FromSuccess(parameterInfo.DefaultValue); var typeConverter = parameterInfo.TypeConverter; var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); - if (!readResult.IsSuccess) - return readResult; - - return ParseResult.FromSuccess(readResult.Value); + return readResult; } protected override Task InvokeModuleEvent (IInteractionContext context, IResult result) diff --git a/src/Discord.Net.Interactions/Info/ModalInfo.cs b/src/Discord.Net.Interactions/Info/ModalInfo.cs index 5130c26a1..de6c0ddf2 100644 --- a/src/Discord.Net.Interactions/Info/ModalInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModalInfo.cs @@ -103,7 +103,7 @@ namespace Discord.Interactions public async Task CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false) { if (context.Interaction is not IModalInteraction modalInteraction) - return ParseResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); + return TypeConverterResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction."); services ??= EmptyServiceProvider.Instance; @@ -120,7 +120,7 @@ namespace Discord.Interactions if (!throwOnMissingField) args[i] = input.DefaultValue; else - return ParseResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); + return TypeConverterResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}"); } else { @@ -133,7 +133,7 @@ namespace Discord.Interactions } } - return ParseResult.FromSuccess(_initializer(args)); + return TypeConverterResult.FromSuccess(_initializer(args)); } } } diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index 50c1f5546..035c6bed1 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -822,7 +822,7 @@ namespace Discord.Interactions SetMatchesIfApplicable(context, result); - return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); } private async Task ExecuteAutocompleteAsync (IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services ) @@ -869,7 +869,7 @@ namespace Discord.Interactions SetMatchesIfApplicable(context, result); - return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); + return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); } private static void SetMatchesIfApplicable(IInteractionContext context, SearchResult searchResult) diff --git a/src/Discord.Net.Interactions/Results/ParseResult.cs b/src/Discord.Net.Interactions/Results/ParseResult.cs index dfc6a57fe..ac9943976 100644 --- a/src/Discord.Net.Interactions/Results/ParseResult.cs +++ b/src/Discord.Net.Interactions/Results/ParseResult.cs @@ -2,9 +2,9 @@ using System; namespace Discord.Interactions { - internal struct ParseResult : IResult + public struct ParseResult : IResult { - public object Value { get; } + public object[] Args { get; } public InteractionCommandError? Error { get; } @@ -12,15 +12,15 @@ namespace Discord.Interactions public bool IsSuccess => !Error.HasValue; - private ParseResult(object value, InteractionCommandError? error, string reason) + private ParseResult(object[] args, InteractionCommandError? error, string reason) { - Value = value; + Args = args; Error = error; ErrorReason = reason; } - public static ParseResult FromSuccess(object value) => - new ParseResult(value, null, null); + public static ParseResult FromSuccess(object[] args) => + new ParseResult(args, null, null); public static ParseResult FromError(Exception exception) => new ParseResult(null, InteractionCommandError.Exception, exception.Message); From 11ed0ff35199f6f2cb62956756283d0da53e84aa Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Tue, 22 Nov 2022 14:05:40 +0300 Subject: [PATCH 08/24] update license and icon nuspec props (#2467) --- src/Discord.Net/Discord.Net.nuspec | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 0ddd4af5e..5bb2e1e5b 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -9,9 +9,9 @@ An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp https://github.com/discord-net/Discord.Net - http://opensource.org/licenses/MIT + MIT false - https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png + PackageLogo.png @@ -55,4 +55,7 @@ + + + From 55280a569e976b327b41985f333c5f631f8d3af2 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Fri, 25 Nov 2022 10:47:43 +0100 Subject: [PATCH 09/24] meta 3.9.0 (#2517) * meta 3.9.0 * Update changelog --- CHANGELOG.md | 20 ++++++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9608ae4eb..aa67c9a0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [3.9.0] - 2022-11-13 +### Added + +- #2469 Add missing properties in forum & thread channels (01ae904) +- #2501 Add new discord stuff (ed38635) + +### Fixed + +- #2500 Fix duplicated members of DiscordErrorCode (6712ef4) +- #2468 Fix TimestampTag being sadge (bc89d3c) +- #2497 Avoid throwing on missing Application (7077c44) +- #2485 Fixed an oversight clearing session data upon any disconnect. (c7ac59d) + +### Misc + +- #2471 Update samples to use `MessageContent` intent & update `v2 => v3 guide` (a4d34f6) +- #2505 Update events.cs (ea039b8) +- #2467 Update license and icon nuspec props (11ed0ff) +- #2306 Command execution code rework & TypeConverters auto-scope fix (6869817) + ## [3.8.1] - 2022-09-12 ### Added diff --git a/Discord.Net.targets b/Discord.Net.targets index 991f7c495..4a0d75465 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.8.1 + 3.9.0 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 2fc0d53b1..8b9c6af34 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.8.1", + "_appFooter": "Discord.Net (c) 2015-2022 3.9.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 5bb2e1e5b..31e973788 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.8.1$suffix$ + 3.9.0$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From bd2f719774d601832675f03852cf0fbc53aaf21e Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Wed, 14 Dec 2022 09:56:06 +0300 Subject: [PATCH 10/24] Fix `GetActiveThreadsAsync` & add it to `ITextChannel` (#2526) * fix `GetActiveThreadsAsync` being sadge * ah, forgot about mocked channel --- src/Discord.Net.Core/Entities/Channels/ITextChannel.cs | 10 ++++++++++ .../Entities/Channels/RestForumChannel.cs | 4 ++-- .../Entities/Channels/RestTextChannel.cs | 8 ++++++++ .../Entities/Channels/RestThreadChannel.cs | 4 ++++ .../Entities/Channels/RestVoiceChannel.cs | 4 ++++ src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs | 4 ++-- .../Entities/Channels/SocketForumChannel.cs | 4 ++-- .../Entities/Channels/SocketTextChannel.cs | 10 +++++++++- .../Entities/Channels/SocketThreadChannel.cs | 4 ++++ .../Entities/Channels/SocketVoiceChannel.cs | 4 ++++ .../MockedEntities/MockedTextChannel.cs | 3 ++- 11 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index af4e5ec6a..f5605d84c 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -160,5 +160,15 @@ namespace Discord /// Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null); + + /// + /// Gets a collection of active threads within this channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of active threads. + /// + Task> GetActiveThreadsAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs index 5ec81a7fc..a2eef3d2d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs @@ -134,9 +134,9 @@ namespace Discord.Rest MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public Task> GetActiveThreadsAsync(RequestOptions options = null) - => ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); + => ThreadHelper.GetActiveThreadsAsync(Guild, Id, Discord, options); /// public Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 81f21bcd7..f0669534e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -288,6 +288,10 @@ namespace Discord.Rest /// public Task SyncPermissionsAsync(RequestOptions options = null) => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + + /// + public virtual Task> GetActiveThreadsAsync(RequestOptions options = null) + => ThreadHelper.GetActiveThreadsAsync(Guild, Id, Discord, options); #endregion #region Invites @@ -321,6 +325,10 @@ namespace Discord.Rest async Task ITextChannel.CreateThreadAsync(string name, ThreadType type, ThreadArchiveDuration autoArchiveDuration, IMessage message, bool? invitable, int? slowmode, RequestOptions options) => await CreateThreadAsync(name, type, autoArchiveDuration, message, invitable, slowmode, options); + + /// + async Task> ITextChannel.GetActiveThreadsAsync(RequestOptions options) + => await GetActiveThreadsAsync(options); #endregion #region IMessageChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs index c1be5182e..86e65d7b6 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -241,5 +241,9 @@ namespace Discord.Rest /// public Task RemoveUserAsync(IGuildUser user, RequestOptions options = null) => Discord.ApiClient.RemoveThreadMemberAsync(Id, user.Id, options); + + /// This method is not supported in threads. + public override Task> GetActiveThreadsAsync(RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 3c9869c4c..01db33ffb 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -234,6 +234,10 @@ namespace Discord.Rest return base.TriggerTypingAsync(options); } + /// Threads are not supported in voice channels + public override Task> GetActiveThreadsAsync(RequestOptions options = null) + => throw new NotSupportedException("Threads are not supported in voice channels"); + #endregion diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index 52cba0657..0cdc92bcb 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -68,10 +68,10 @@ namespace Discord.Rest return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task> GetActiveThreadsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + public static async Task> GetActiveThreadsAsync(IGuild guild, ulong channelId, BaseDiscordClient client, RequestOptions options) { var result = await client.ApiClient.GetActiveThreadsAsync(guild.Id, options).ConfigureAwait(false); - return result.Threads.Select(x => RestThreadChannel.Create(client, guild, x)).ToImmutableArray(); + return result.Threads.Where(x => x.CategoryId == channelId).Select(x => RestThreadChannel.Create(client, guild, x)).ToImmutableArray(); } public static async Task> GetPublicArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs index 9d46bc2aa..8929a46f9 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -135,9 +135,9 @@ namespace Discord.WebSocket MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public Task> GetActiveThreadsAsync(RequestOptions options = null) - => ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); + => ThreadHelper.GetActiveThreadsAsync(Guild, Id, Discord, options); /// public Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 2d8aeeae7..48da69e09 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -130,7 +130,12 @@ namespace Discord.WebSocket return thread; } -#endregion + + /// + public virtual Task> GetActiveThreadsAsync(RequestOptions options = null) + => ThreadHelper.GetActiveThreadsAsync(Guild, Id, Discord, options); + + #endregion #region Messages /// @@ -378,6 +383,9 @@ namespace Discord.WebSocket /// async Task ITextChannel.CreateThreadAsync(string name, ThreadType type, ThreadArchiveDuration autoArchiveDuration, IMessage message, bool? invitable, int? slowmode, RequestOptions options) => await CreateThreadAsync(name, type, autoArchiveDuration, message, invitable, slowmode, options); + /// + async Task> ITextChannel.GetActiveThreadsAsync(RequestOptions options) + => await GetActiveThreadsAsync(options); #endregion #region IGuildChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index d9d54f91e..423e5504f 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -374,6 +374,10 @@ namespace Discord.WebSocket public override Task SyncPermissionsAsync(RequestOptions options = null) => throw new NotSupportedException("This method is not supported in threads."); + /// This method is not supported in threads. + public override Task> GetActiveThreadsAsync(RequestOptions options = null) + => throw new NotSupportedException("This method is not supported in threads."); + string IChannel.Name => Name; } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 9036659fe..f2bb5d6d3 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -296,6 +296,10 @@ namespace Discord.WebSocket return base.TriggerTypingAsync(options); } + /// Threads are not supported in voice channels + public override Task> GetActiveThreadsAsync(RequestOptions options = null) + => throw new NotSupportedException("Threads are not supported in voice channels"); + #endregion private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index 8913c127d..11737fb81 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -214,11 +214,12 @@ namespace Discord { throw new NotImplementedException(); } - + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task> GetActiveThreadsAsync(RequestOptions options = null) => throw new NotImplementedException(); } } From 82b772ac032dcc869fed1c1b263bce2f0001781a Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Wed, 14 Dec 2022 09:57:06 +0300 Subject: [PATCH 11/24] [Feature] Add missing property & new stuff (#2521) * add active developer badge support * add `OwnerId` to threads * add default forum layout support * oops, forgot to update modifyasync * add missing application flags * Add `50155` error code --- src/Discord.Net.Core/DiscordErrorCode.cs | 1 + .../Entities/ApplicationFlags.cs | 41 ++++++++++++------- .../Channels/ForumChannelProperties.cs | 5 +++ .../Entities/Channels/ForumLayout.cs | 22 ++++++++++ .../Entities/Channels/IForumChannel.cs | 7 +++- .../Entities/Channels/IThreadChannel.cs | 5 +++ .../Entities/Users/UserProperties.cs | 5 +++ src/Discord.Net.Rest/API/Common/Channel.cs | 3 ++ .../API/Rest/ModifyForumChannelParams.cs | 3 ++ .../Entities/Channels/ForumHelper.cs | 3 +- .../Entities/Channels/RestForumChannel.cs | 4 ++ .../Entities/Channels/RestThreadChannel.cs | 5 +++ .../Entities/Channels/SocketForumChannel.cs | 5 +++ .../Entities/Channels/SocketThreadChannel.cs | 3 ++ 14 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Channels/ForumLayout.cs diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index ccd547e92..8301842d4 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -177,6 +177,7 @@ namespace Discord OwnershipCannotBeTransferredToABotUser = 50132, FailedToResizeAssetBelowTheMaximumSize = 50138, UploadedFileNotFound = 50146, + FeatureInProcessOfRollingOut = 50155, MissingPermissionToSendThisSticker = 50600, #endregion diff --git a/src/Discord.Net.Core/Entities/ApplicationFlags.cs b/src/Discord.Net.Core/Entities/ApplicationFlags.cs index 1ede4257d..0410f63b5 100644 --- a/src/Discord.Net.Core/Entities/ApplicationFlags.cs +++ b/src/Discord.Net.Core/Entities/ApplicationFlags.cs @@ -4,20 +4,31 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Discord +namespace Discord; + +/// +/// Represents public flags for an application. +/// +public enum ApplicationFlags { - /// - /// Represents public flags for an application. - /// - public enum ApplicationFlags - { - GatewayPresence = 1 << 12, - GatewayPresenceLimited = 1 << 13, - GatewayGuildMembers = 1 << 14, - GatewayGuildMembersLimited = 1 << 15, - VerificationPendingGuildLimit = 1 << 16, - Embedded = 1 << 17, - GatewayMessageContent = 1 << 18, - GatewayMessageContentLimited = 1 << 19 - } + GatewayPresence = 1 << 12, + + GatewayPresenceLimited = 1 << 13, + + GatewayGuildMembers = 1 << 14, + + GatewayGuildMembersLimited = 1 << 15, + + VerificationPendingGuildLimit = 1 << 16, + + Embedded = 1 << 17, + + GatewayMessageContent = 1 << 18, + + GatewayMessageContentLimited = 1 << 19, + + ApplicationCommandBadge = 1 << 23, + + ActiveApplication = 1 << 24 } + diff --git a/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs index e1a123b37..26b985b81 100644 --- a/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs @@ -57,4 +57,9 @@ public class ForumChannelProperties : TextChannelProperties /// Gets or sets the rule used to order posts in forum channels. /// public Optional DefaultSortOrder { get; set; } + + /// + /// Gets or sets the rule used to display posts in a forum channel. + /// + public Optional DefaultLayout { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Channels/ForumLayout.cs b/src/Discord.Net.Core/Entities/Channels/ForumLayout.cs new file mode 100644 index 000000000..d20a10596 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ForumLayout.cs @@ -0,0 +1,22 @@ +namespace Discord; + +/// +/// Represents the layout type used to display posts in a forum channel. +/// +public enum ForumLayout +{ + /// + /// A preferred forum layout hasn't been set by a server admin + /// + Default = 0, + + /// + /// List View: display forum posts in a text-focused list + /// + List = 1, + + /// + /// Gallery View: display forum posts in a media-focused gallery + /// + Grid = 2 +} diff --git a/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs index 55521bade..09fb0fb7e 100644 --- a/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs @@ -63,13 +63,18 @@ namespace Discord IEmote DefaultReactionEmoji { get; } /// - /// Gets or sets the rule used to order posts in forum channels. + /// Gets the rule used to order posts in forum channels. /// /// /// Defaults to null, which indicates a preferred sort order hasn't been set /// ForumSortOrder? DefaultSortOrder { get; } + /// + /// Gets the rule used to display posts in a forum channel. + /// + ForumLayout DefaultLayout { get; } + /// /// Modifies this forum channel. /// diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs index 52df07dcc..be4967265 100644 --- a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs @@ -74,6 +74,11 @@ namespace Discord /// new DateTimeOffset CreatedAt { get; } + /// + /// Gets the id of the creator of the thread. + /// + ulong OwnerId { get; } + /// /// Joins the current thread. /// diff --git a/src/Discord.Net.Core/Entities/Users/UserProperties.cs b/src/Discord.Net.Core/Entities/Users/UserProperties.cs index 4cf4162a9..aeba6a234 100644 --- a/src/Discord.Net.Core/Entities/Users/UserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/UserProperties.cs @@ -69,5 +69,10 @@ namespace Discord /// Flag given to bots that use only outgoing webhooks, exclusively. /// BotHTTPInteractions = 1 << 19, + + /// + /// Flag given to users that are active developers. + /// + ActiveDeveloper = 1 << 22 } } diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index f9184cd1f..b8bfe1ea5 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -89,5 +89,8 @@ namespace Discord.API [JsonProperty("default_reaction_emoji")] public Optional DefaultReactionEmoji { get; set; } + [JsonProperty("default_forum_layout")] + public Optional DefaultForumLayout { get; set; } + } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs index d8733a2bd..ba5da8a3b 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs @@ -20,4 +20,7 @@ internal class ModifyForumChannelParams : ModifyTextChannelParams [JsonProperty("default_sort_order")] public Optional DefaultSortOrder { get; set; } + + [JsonProperty("default_forum_layout")] + public Optional DefaultLayout { get; set; } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs index 3d087e77d..4ecafd87e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs @@ -56,7 +56,8 @@ internal static class ForumHelper emoji.Name : Optional.Unspecified } : Optional.Unspecified, - DefaultSortOrder = args.DefaultSortOrder + DefaultSortOrder = args.DefaultSortOrder, + DefaultLayout = args.DefaultLayout, }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs index a2eef3d2d..7d837cde5 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs @@ -41,6 +41,9 @@ namespace Discord.Rest /// public ForumSortOrder? DefaultSortOrder { get; private set; } + /// + public ForumLayout DefaultLayout { get; private set; } + /// public string Mention => MentionUtils.MentionChannel(Id); @@ -87,6 +90,7 @@ namespace Discord.Rest } CategoryId = model.CategoryId.GetValueOrDefault(); + DefaultLayout= model.DefaultForumLayout.GetValueOrDefault(); } /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs index 86e65d7b6..1555b2093 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -40,6 +40,9 @@ namespace Discord.Rest /// public IReadOnlyCollection AppliedTags { get; private set; } + /// + public ulong OwnerId { get; private set; } + /// public override DateTimeOffset CreatedAt { get; } @@ -76,6 +79,8 @@ namespace Discord.Rest IsLocked = model.ThreadMetadata.Value.Locked.GetValueOrDefault(false); } + OwnerId = model.OwnerId.GetValueOrDefault(0); + MemberCount = model.MemberCount.GetValueOrDefault(0); MessageCount = model.MessageCount.GetValueOrDefault(0); Type = (ThreadType)model.Type; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs index 8929a46f9..ed36f5d4b 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -45,6 +45,9 @@ namespace Discord.WebSocket /// public ForumSortOrder? DefaultSortOrder { get; private set; } + /// + public ForumLayout DefaultLayout { get; private set; } + /// /// Gets the parent (category) of this channel in the guild's channel list. /// @@ -93,6 +96,8 @@ namespace Discord.WebSocket } CategoryId = model.CategoryId.GetValueOrDefault(); + + DefaultLayout = model.DefaultForumLayout.GetValueOrDefault(); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 423e5504f..5c739c857 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -95,6 +95,9 @@ namespace Discord.WebSocket /// public override DateTimeOffset CreatedAt { get; } + /// + ulong IThreadChannel.OwnerId => _ownerId; + /// /// Gets a collection of cached users within this thread. /// From 4cad546d57ed75e9c63794569463068f0250b140 Mon Sep 17 00:00:00 2001 From: Frederik P <34724135+F0903@users.noreply.github.com> Date: Wed, 14 Dec 2022 07:57:54 +0100 Subject: [PATCH 12/24] Implemented ClientDisconnect event for audio client. (#2520) --- src/Discord.Net.Core/Audio/IAudioClient.cs | 1 + .../API/Voice/ClientDisconnectEvent.cs | 14 ++++++++++++++ .../Audio/AudioClient.Events.cs | 8 +++++++- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 11 ++++++++++- 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/Discord.Net.WebSocket/API/Voice/ClientDisconnectEvent.cs diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index 2fc52a529..1fc34b446 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -13,6 +13,7 @@ namespace Discord.Audio event Func StreamCreated; event Func StreamDestroyed; event Func SpeakingUpdated; + event Func ClientDisconnected; /// Gets the current connection state of this client. ConnectionState ConnectionState { get; } diff --git a/src/Discord.Net.WebSocket/API/Voice/ClientDisconnectEvent.cs b/src/Discord.Net.WebSocket/API/Voice/ClientDisconnectEvent.cs new file mode 100644 index 000000000..d7c1bb2d2 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Voice/ClientDisconnectEvent.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Voice; +internal class ClientDisconnectEvent +{ + [JsonProperty("user_id")] + public ulong UserId { get; set; } +} diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs index b3e438a01..ea8d2d2a5 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Audio @@ -47,5 +47,11 @@ namespace Discord.Audio remove { _speakingUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _speakingUpdatedEvent = new AsyncEvent>(); + public event Func ClientDisconnected + { + add { _clientDisconnectedEvent.Add(value); } + remove { _clientDisconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _clientDisconnectedEvent = new AsyncEvent>(); } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 3549fb106..78780577c 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -11,7 +11,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord.Audio { @@ -279,6 +279,15 @@ namespace Discord.Audio await _speakingUpdatedEvent.InvokeAsync(data.UserId, data.Speaking); } break; + case VoiceOpCode.ClientDisconnect: + { + await _audioLogger.DebugAsync("Received ClientDisconnect").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + + await _clientDisconnectedEvent.InvokeAsync(data.UserId); + } + break; default: await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); return; From 25cfb8822fe63adfff4cb493326ad8915e634dbc Mon Sep 17 00:00:00 2001 From: BokuNoPasya <49203428+1NieR@users.noreply.github.com> Date: Wed, 14 Dec 2022 12:00:59 +0500 Subject: [PATCH 13/24] Add SendFiles to UserExtensions (#2509) * Add SendFiles to UserExtensions * fix Build --- .../Extensions/UserExtensions.cs | 91 +++++++++++++++++++ .../Entities/Channels/IRestMessageChannel.cs | 6 ++ .../Channels/ISocketMessageChannel.cs | 13 +++ 3 files changed, 110 insertions(+) diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index ce914170d..469957c42 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using System.Collections.Generic; using System.IO; namespace Discord @@ -163,6 +164,96 @@ namespace Discord return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false); } + /// + /// Sends a file via DM with an optional caption. + /// + /// + /// This method attempts to send an attachment as a direct-message to the user. + /// + /// + /// Please note that this method will throw an + /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. + /// + /// + /// You may want to consider catching for + /// 50007 when using this method. + /// + /// + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The user to send the DM to. + /// The attachment containing the file and description. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// The message component to be included with this message. Used for interactions. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public static async Task SendFileAsync(this IUser user, + FileAttachment attachment, + string text = null, + bool isTTS = false, + Embed embed = null, + RequestOptions options = null, + MessageComponent components = null, + Embed[] embeds = null) + { + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(attachment, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false); + } + + /// + /// Sends a collection of files via DM. + /// + /// + /// This method attempts to send an attachments as a direct-message to the user. + /// + /// + /// Please note that this method will throw an + /// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked. + /// + /// + /// You may want to consider catching for + /// 50007 when using this method. + /// + /// + /// + /// If you wish to upload an image and have it embedded in a embed, + /// you may upload the file and refer to the file with "attachment://filename.ext" in the + /// . See the example section for its usage. + /// + /// + /// The user to send the DM to. + /// A collection of attachments to upload. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// The message component to be included with this message. Used for interactions. + /// A array of s to send with this response. Max 10. + /// + /// A task that represents an asynchronous send operation for delivering the message. The task result + /// contains the sent message. + /// + public static async Task SendFilesAsync(this IUser user, + IEnumerable attachments, + string text = null, + bool isTTS = false, + Embed embed = null, + RequestOptions options = null, + MessageComponent components = null, + Embed[] embeds = null) + { + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFilesAsync(attachments, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false); + } + /// /// Bans the user from the guild and optionally prunes their recent messages. /// diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 0cf92bb04..678db093f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -17,6 +17,12 @@ namespace Discord.Rest /// new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); /// /// Gets a message from this message channel. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index b632bcb60..ea4249cf0 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -35,6 +35,19 @@ namespace Discord.WebSocket MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + /// + new Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, + Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + + /// + new Task SendFilesAsync(IEnumerable attachments, string text = null, + bool isTTS = false, Embed embed = null, RequestOptions options = null, + AllowedMentions allowedMentions = null, MessageReference messageReference = null, + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, + MessageFlags flags = MessageFlags.None); + /// /// Gets a cached message from this channel. /// From 3b107c2d01be911d149ae8fa8ea296ead72817a6 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 14 Dec 2022 17:06:57 +0300 Subject: [PATCH 14/24] implement wildcard lenght quantifiers, TreatAsRegex property and solve catastrpohic backtracking (#2528) --- .../Commands/ComponentInteractionAttribute.cs | 6 ++++ .../Commands/ModalInteractionAttribute.cs | 5 +++ .../Builders/Commands/CommandBuilder.cs | 20 +++++++++++ .../Builders/Commands/ICommandBuilder.cs | 14 ++++++++ .../Builders/ModuleClassBuilder.cs | 4 ++- .../Info/Commands/CommandInfo.cs | 3 ++ .../Info/ICommandInfo.cs | 2 ++ .../Map/CommandMapNode.cs | 9 ++--- .../Utilities/RegexUtils.cs | 33 +++++++++++++++++++ 9 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs index 70bc285fc..823410cdf 100644 --- a/src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; namespace Discord.Interactions { @@ -28,6 +29,11 @@ namespace Discord.Interactions /// public RunMode RunMode { get; } + /// + /// Gets or sets whether the should be treated as a raw Regex pattern. + /// + public bool TreatAsRegex { get; set; } = false; + /// /// Create a command for component interaction handling. /// diff --git a/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs b/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs index a0ce91cda..f5df950bd 100644 --- a/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs +++ b/src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs @@ -28,6 +28,11 @@ namespace Discord.Interactions /// public RunMode RunMode { get; } + /// + /// Gets or sets whether the should be treated as a raw Regex pattern. + /// + public bool TreatAsRegex { get; set; } = false; + /// /// Create a command for modal interaction handling. /// diff --git a/src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs index 5c35e8871..d7f90678d 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs @@ -35,6 +35,9 @@ namespace Discord.Interactions.Builders /// public bool IgnoreGroupNames { get; set; } + /// + public bool TreatNameAsRegex { get; set; } + /// public RunMode RunMode { get; set; } @@ -117,6 +120,19 @@ namespace Discord.Interactions.Builders return Instance; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public TBuilder WithNameAsRegex (bool value) + { + TreatNameAsRegex = value; + return Instance; + } + /// /// Adds parameter builders to . /// @@ -163,6 +179,10 @@ namespace Discord.Interactions.Builders ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) => SetRunMode(runMode); + /// + ICommandBuilder ICommandBuilder.WithNameAsRegex(bool value) => + WithNameAsRegex(value); + /// ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) => AddParameters(parameters as TParamBuilder); diff --git a/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs index 95007296c..97bc1e8e9 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs @@ -34,6 +34,11 @@ namespace Discord.Interactions.Builders /// bool IgnoreGroupNames { get; set; } + /// + /// Gets or sets whether the should be directly used as a Regex pattern. + /// + bool TreatNameAsRegex { get; set; } + /// /// Gets or sets the run mode this command gets executed with. /// @@ -90,6 +95,15 @@ namespace Discord.Interactions.Builders /// ICommandBuilder SetRunMode (RunMode runMode); + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + ICommandBuilder WithNameAsRegex(bool value); + /// /// Adds parameter builders to . /// diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 35126a674..82acd800d 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -274,6 +274,7 @@ namespace Discord.Interactions.Builders builder.Name = interaction.CustomId; builder.RunMode = interaction.RunMode; builder.IgnoreGroupNames = interaction.IgnoreGroupNames; + builder.TreatNameAsRegex = interaction.TreatAsRegex; } break; case PreconditionAttribute precondition: @@ -287,7 +288,7 @@ namespace Discord.Interactions.Builders var parameters = methodInfo.GetParameters(); - var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count; + var wildCardCount = RegexUtils.GetWildCardCount(builder.Name, commandService._wildCardExp); foreach (var parameter in parameters) builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount)); @@ -355,6 +356,7 @@ namespace Discord.Interactions.Builders builder.Name = modal.CustomId; builder.RunMode = modal.RunMode; builder.IgnoreGroupNames = modal.IgnoreGroupNames; + builder.TreatNameAsRegex = modal.TreatAsRegex; } break; case PreconditionAttribute precondition: diff --git a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs index 99895d3ed..92e2f30bb 100644 --- a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs @@ -66,6 +66,8 @@ namespace Discord.Interactions /// public abstract IReadOnlyList Parameters { get; } + public bool TreatNameAsRegex { get; } + internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService) { CommandService = commandService; @@ -78,6 +80,7 @@ namespace Discord.Interactions RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode; Attributes = builder.Attributes.ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray(); + TreatNameAsRegex = builder.TreatNameAsRegex && SupportsWildCards; _action = builder.Callback; _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); diff --git a/src/Discord.Net.Interactions/Info/ICommandInfo.cs b/src/Discord.Net.Interactions/Info/ICommandInfo.cs index 843d5198b..1de6e0df7 100644 --- a/src/Discord.Net.Interactions/Info/ICommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/ICommandInfo.cs @@ -65,6 +65,8 @@ namespace Discord.Interactions /// IReadOnlyCollection Parameters { get; } + bool TreatNameAsRegex { get; } + /// /// Executes the command with the provided context. /// diff --git a/src/Discord.Net.Interactions/Map/CommandMapNode.cs b/src/Discord.Net.Interactions/Map/CommandMapNode.cs index c866fe00e..3dec30f4a 100644 --- a/src/Discord.Net.Interactions/Map/CommandMapNode.cs +++ b/src/Discord.Net.Interactions/Map/CommandMapNode.cs @@ -2,14 +2,13 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Text.RegularExpressions; namespace Discord.Interactions { internal class CommandMapNode where T : class, ICommandInfo - { - private const string RegexWildCardExp = "(\\S+)?"; - + { private readonly string _wildCardStr = "*"; private readonly ConcurrentDictionary> _nodes; private readonly ConcurrentDictionary _commands; @@ -35,10 +34,8 @@ namespace Discord.Interactions { if (keywords.Count == index + 1) { - if (commandInfo.SupportsWildCards && commandInfo.Name.Contains(_wildCardStr)) + if (commandInfo.SupportsWildCards && RegexUtils.TryBuildRegexPattern(commandInfo, _wildCardStr, out var patternStr)) { - var escapedStr = RegexUtils.EscapeExcluding(commandInfo.Name, _wildCardStr.ToArray()); - var patternStr = "\\A" + escapedStr.Replace(_wildCardStr, RegexWildCardExp) + "\\Z"; var regex = new Regex(patternStr, RegexOptions.Singleline | RegexOptions.Compiled); if (!_wildCardCommands.TryAdd(regex, commandInfo)) diff --git a/src/Discord.Net.Interactions/Utilities/RegexUtils.cs b/src/Discord.Net.Interactions/Utilities/RegexUtils.cs index 82ba944f8..b3316106c 100644 --- a/src/Discord.Net.Interactions/Utilities/RegexUtils.cs +++ b/src/Discord.Net.Interactions/Utilities/RegexUtils.cs @@ -1,3 +1,4 @@ +using Discord.Interactions; using System; using System.Linq; @@ -81,5 +82,37 @@ namespace System.Text.RegularExpressions { return (ch <= '|' && _category[ch] >= E); } + + internal static int GetWildCardCount(string input, string wildCardExpression) + { + var escapedWildCard = Regex.Escape(wildCardExpression); + var match = Regex.Matches(input, $@"(?(T commandInfo, string wildCardStr, out string pattern) where T: class, ICommandInfo + { + if (commandInfo.TreatNameAsRegex) + { + pattern = commandInfo.Name; + return true; + } + + if (GetWildCardCount(commandInfo.Name, wildCardStr) == 0) + { + pattern = null; + return false; + } + + var escapedWildCard = Regex.Escape(wildCardStr); + var unquantified = Regex.Replace(commandInfo.Name, $@"(?[^{escapedWildCard}]?)", + @"([^\n\t${delimiter}]+)${delimiter}"); + + var quantified = Regex.Replace(unquantified, $@"(?[0-9]+)(?,[0-9]*)?(?[^{escapedWildCard}]?)", + @"([^\n\t${delimiter}]{${start}${end}})${delimiter}"); + + pattern = "\\A" + quantified + "\\Z"; + return true; + } } } From 20d8fdf0bf4dd8d8915165909f9db997ced8600e Mon Sep 17 00:00:00 2001 From: Halbritter Date: Fri, 16 Dec 2022 19:08:42 +0100 Subject: [PATCH 15/24] Fully qualify SlashCommandBuilder namespace (#2534) Update to make sure its cleear which SlashCommandBuilder is used. --- .../application-commands/slash-commands/parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/int_basics/application-commands/slash-commands/parameters.md b/docs/guides/int_basics/application-commands/slash-commands/parameters.md index 4f3cd2e8c..a9371abeb 100644 --- a/docs/guides/int_basics/application-commands/slash-commands/parameters.md +++ b/docs/guides/int_basics/application-commands/slash-commands/parameters.md @@ -45,7 +45,7 @@ public async Task Client_Ready() { ulong guildId = 848176216011046962; - var guildCommand = new SlashCommandBuilder() + var guildCommand = new Discord.SlashCommandBuilder() .WithName("list-roles") .WithDescription("Lists all roles of a user.") .AddOption("user", ApplicationCommandOptionType.User, "The users whos roles you want to be listed", isRequired: true); From 60956c720b0785df6fd642b945d28a902743e4c9 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Sat, 17 Dec 2022 19:05:50 +0300 Subject: [PATCH 16/24] Update deploy.yml (#2535) --- azure/deploy.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/azure/deploy.yml b/azure/deploy.yml index d3460ad6c..53bbbd393 100644 --- a/azure/deploy.yml +++ b/azure/deploy.yml @@ -11,6 +11,11 @@ steps: dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag) displayName: Pack projects +- task: NuGetToolInstaller@1 + displayName: Download and Cache Nuget.exe + inputs: + versionSpec: 6.4.0 + - task: NuGetCommand@2 displayName: Pack metapackage (release mode) condition: eq(variables['buildTag'], True) From 56b1a930e7c62dc1497f4b3d6023a20e4c8d143c Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Tue, 20 Dec 2022 01:00:56 +0300 Subject: [PATCH 17/24] [Feature] Age restricted (NSFW) application commands support (#2531) * add `nsfw` to data model & internal methods; add missing property * add `nsfw` prop to command builders * add `NsfwCommandAttribute` to Interaction Framework * working state * docs? --- docs/guides/int_framework/permissions.md | 5 ++++ .../samples/permissions/nsfw-permissions.cs | 6 +++++ .../ApplicationCommandProperties.cs | 5 ++++ .../ContextMenus/MessageCommandBuilder.cs | 21 ++++++++++++++-- .../ContextMenus/UserCommandBuilder.cs | 21 ++++++++++++++-- .../Interactions/IApplicationCommand.cs | 5 ++++ .../SlashCommands/SlashCommandBuilder.cs | 21 ++++++++++++++-- .../Attributes/NsfwCommandAttribute.cs | 25 +++++++++++++++++++ .../Commands/ContextCommandBuilder.cs | 18 +++++++++++++ .../Builders/Commands/SlashCommandBuilder.cs | 18 +++++++++++++ .../Builders/ModuleBuilder.cs | 18 +++++++++++++ .../Builders/ModuleClassBuilder.cs | 9 +++++++ .../ContextCommands/ContextCommandInfo.cs | 4 +++ .../Info/Commands/SlashCommandInfo.cs | 4 +++ .../Info/IApplicationCommandInfo.cs | 5 ++++ .../Info/ModuleInfo.cs | 6 +++++ .../Utilities/ApplicationCommandRestUtil.cs | 9 ++++++- .../API/Common/ApplicationCommand.cs | 3 +++ .../Rest/CreateApplicationCommandParams.cs | 6 ++++- .../Rest/ModifyApplicationCommandParams.cs | 6 +++++ .../Interactions/InteractionHelper.cs | 22 ++++++++++------ .../Interactions/RestApplicationCommand.cs | 4 +++ .../SocketApplicationCommand.cs | 4 +++ 23 files changed, 230 insertions(+), 15 deletions(-) create mode 100644 docs/guides/int_framework/samples/permissions/nsfw-permissions.cs create mode 100644 src/Discord.Net.Interactions/Attributes/NsfwCommandAttribute.cs diff --git a/docs/guides/int_framework/permissions.md b/docs/guides/int_framework/permissions.md index e35bb162d..f02c50ebb 100644 --- a/docs/guides/int_framework/permissions.md +++ b/docs/guides/int_framework/permissions.md @@ -55,5 +55,10 @@ The amount of nesting you can do is realistically endless. > If the nested class is marked with `Group`, as required for setting up subcommands, this example will not work. > As mentioned before, subcommands cannot have seperate permissions from the top level command. +### NSFW Commands +Commands can be limited to only age restricted channels and DMs: + +[!code-csharp[Nsfw-Permissions](samples/permissions/nsfw-permissions.cs)] + [permissions]: xref:Discord.GuildPermission diff --git a/docs/guides/int_framework/samples/permissions/nsfw-permissions.cs b/docs/guides/int_framework/samples/permissions/nsfw-permissions.cs new file mode 100644 index 000000000..21f93b54d --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/nsfw-permissions.cs @@ -0,0 +1,6 @@ +[NsfwCommand(true)] +[SlashCommand("beautiful-code", "Get an image of perfect code")] +public async Task BeautifulCodeAsync(...) +{ + ... +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index 0c1c628cd..78182c404 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -83,6 +83,11 @@ namespace Discord /// public Optional IsDMEnabled { get; set; } + /// + /// Gets or sets whether or not this command is age restricted. + /// + public Optional IsNsfw { get; set; } + /// /// Gets or sets the default permissions required by a user to execute this application command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs index 613e30376..b7037cc63 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs @@ -46,6 +46,11 @@ namespace Discord /// public bool IsDMEnabled { get; set; } = true; + /// + /// Gets or sets whether or not this command is age restricted. + /// + public bool IsNsfw{ get; set; } = false; + /// /// Gets or sets the default permission required to use this slash command. /// @@ -68,7 +73,8 @@ namespace Discord IsDefaultPermission = IsDefaultPermission, IsDMEnabled = IsDMEnabled, DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified, - NameLocalizations = NameLocalizations + NameLocalizations = NameLocalizations, + IsNsfw = IsNsfw, }; return props; @@ -123,7 +129,7 @@ namespace Discord } /// - /// Sets whether or not this command can be used in dms + /// Sets whether or not this command can be used in dms. /// /// if the command is available in dms, otherwise . /// The current builder. @@ -133,6 +139,17 @@ namespace Discord return this; } + /// + /// Sets whether or not this command is age restricted. + /// + /// if the command is age restricted, otherwise . + /// The current builder. + public MessageCommandBuilder WithNsfw(bool permission) + { + IsNsfw = permission; + return this; + } + /// /// Adds a new entry to the collection. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs index 8ac524582..85efc4938 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs @@ -46,6 +46,11 @@ namespace Discord /// public bool IsDMEnabled { get; set; } = true; + /// + /// Gets or sets whether or not this command is age restricted. + /// + public bool IsNsfw { get; set; } = false; + /// /// Gets or sets the default permission required to use this slash command. /// @@ -66,7 +71,8 @@ namespace Discord IsDefaultPermission = IsDefaultPermission, IsDMEnabled = IsDMEnabled, DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified, - NameLocalizations = NameLocalizations + NameLocalizations = NameLocalizations, + IsNsfw = IsNsfw, }; return props; @@ -121,7 +127,7 @@ namespace Discord } /// - /// Sets whether or not this command can be used in dms + /// Sets whether or not this command can be used in dms. /// /// if the command is available in dms, otherwise . /// The current builder. @@ -131,6 +137,17 @@ namespace Discord return this; } + /// + /// Sets whether or not this command is age restricted. + /// + /// if the command is age restricted, otherwise . + /// The current builder. + public UserCommandBuilder WithNsfw(bool permission) + { + IsNsfw = permission; + return this; + } + /// /// Adds a new entry to the collection. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs index 6f9ce7a45..afab93500 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -42,6 +42,11 @@ namespace Discord /// bool IsEnabledInDm { get; } + /// + /// Indicates whether the command is age restricted. + /// + bool IsNsfw { get; } + /// /// Set of default required to invoke the command. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs index 03fb24c8b..b0b8e9600 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs @@ -84,6 +84,11 @@ namespace Discord /// Gets or sets whether or not this command can be used in DMs. /// public bool IsDMEnabled { get; set; } = true; + + /// + /// Gets or sets whether or not this command is age restricted. + /// + public bool IsNsfw { get; set; } = false; /// /// Gets or sets the default permission required to use this slash command. @@ -110,7 +115,8 @@ namespace Discord NameLocalizations = _nameLocalizations, DescriptionLocalizations = _descriptionLocalizations, IsDMEnabled = IsDMEnabled, - DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified + DefaultMemberPermissions = DefaultMemberPermissions ?? Optional.Unspecified, + IsNsfw = IsNsfw, }; if (Options != null && Options.Any()) @@ -161,7 +167,7 @@ namespace Discord } /// - /// Sets whether or not this command can be used in dms + /// Sets whether or not this command can be used in dms. /// /// if the command is available in dms, otherwise . /// The current builder. @@ -171,6 +177,17 @@ namespace Discord return this; } + /// + /// Sets whether or not this command is age restricted. + /// + /// if the command is age restricted, otherwise . + /// The current builder. + public SlashCommandBuilder WithNsfw(bool permission) + { + IsNsfw = permission; + return this; + } + /// /// Sets the default member permissions required to use this application command. /// diff --git a/src/Discord.Net.Interactions/Attributes/NsfwCommandAttribute.cs b/src/Discord.Net.Interactions/Attributes/NsfwCommandAttribute.cs new file mode 100644 index 000000000..b218edb9b --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/NsfwCommandAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Sets the property of an application command or module. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class NsfwCommandAttribute : Attribute + { + /// + /// Gets whether or not this command is age restricted. + /// + public bool IsNsfw { get; } + + /// + /// Sets the property of an application command or module. + /// + /// Whether or not this command is age restricted. + public NsfwCommandAttribute(bool isNsfw) + { + IsNsfw = isNsfw; + } + } +} diff --git a/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs index be0e5eb70..ce6d8b504 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs @@ -25,6 +25,11 @@ namespace Discord.Interactions.Builders /// public bool IsEnabledInDm { get; set; } = true; + /// + /// Gets whether this command is age restricted. + /// + public bool IsNsfw { get; set; } = false; + /// /// Gets the default permissions needed for executing this command. /// @@ -95,6 +100,19 @@ namespace Discord.Interactions.Builders return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ContextCommandBuilder SetNsfw(bool isNsfw) + { + IsNsfw = isNsfw; + return this; + } + /// /// Sets . /// diff --git a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs index c21fd5ae8..ead6db3cf 100644 --- a/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs @@ -25,6 +25,11 @@ namespace Discord.Interactions.Builders /// public bool IsEnabledInDm { get; set; } = true; + /// + /// Gets whether this command is age restricted. + /// + public bool IsNsfw { get; set; } = false; + /// /// Gets the default permissions needed for executing this command. /// @@ -95,6 +100,19 @@ namespace Discord.Interactions.Builders return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public SlashCommandBuilder SetNsfw(bool isNsfw) + { + IsNsfw = isNsfw; + return this; + } + /// /// Sets . /// diff --git a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs index 0eb91ee6a..7da7624cd 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleBuilder.cs @@ -59,6 +59,11 @@ namespace Discord.Interactions.Builders /// public bool IsEnabledInDm { get; set; } = true; + /// + /// Gets whether this command is age restricted. + /// + public bool IsNsfw { get; set; } = false; + /// /// Gets the default permissions needed for executing this command. /// @@ -190,6 +195,19 @@ namespace Discord.Interactions.Builders return this; } + /// + /// Sets . + /// + /// New value of the . + /// + /// The builder instance. + /// + public ModuleBuilder SetNsfw(bool isNsfw) + { + IsNsfw = isNsfw; + return this; + } + /// /// Sets . /// diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 82acd800d..3f0504e44 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -101,6 +101,9 @@ namespace Discord.Interactions.Builders case DontAutoRegisterAttribute dontAutoRegister: builder.DontAutoRegister = true; break; + case NsfwCommandAttribute nsfwCommand: + builder.SetNsfw(nsfwCommand.IsNsfw); + break; default: builder.AddAttributes(attribute); break; @@ -192,6 +195,9 @@ namespace Discord.Interactions.Builders case PreconditionAttribute precondition: builder.WithPreconditions(precondition); break; + case NsfwCommandAttribute nsfwCommand: + builder.SetNsfw(nsfwCommand.IsNsfw); + break; default: builder.WithAttributes(attribute); break; @@ -244,6 +250,9 @@ namespace Discord.Interactions.Builders case PreconditionAttribute precondition: builder.WithPreconditions(precondition); break; + case NsfwCommandAttribute nsfwCommand: + builder.SetNsfw(nsfwCommand.IsNsfw); + break; default: builder.WithAttributes(attribute); break; diff --git a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs index 33b82b127..61f79453c 100644 --- a/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs @@ -20,6 +20,9 @@ namespace Discord.Interactions /// public bool IsEnabledInDm { get; } + /// + public bool IsNsfw { get; } + /// public GuildPermission? DefaultMemberPermissions { get; } @@ -37,6 +40,7 @@ namespace Discord.Interactions { CommandType = builder.CommandType; DefaultPermission = builder.DefaultPermission; + IsNsfw = builder.IsNsfw; IsEnabledInDm = builder.IsEnabledInDm; DefaultMemberPermissions = builder.DefaultMemberPermissions; Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); diff --git a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs index 634fd9643..ee3939702 100644 --- a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs @@ -29,6 +29,9 @@ namespace Discord.Interactions /// public bool IsEnabledInDm { get; } + /// + public bool IsNsfw { get; } + /// public GuildPermission? DefaultMemberPermissions { get; } @@ -48,6 +51,7 @@ namespace Discord.Interactions Description = builder.Description; DefaultPermission = builder.DefaultPermission; IsEnabledInDm = builder.IsEnabledInDm; + IsNsfw = builder.IsNsfw; DefaultMemberPermissions = builder.DefaultMemberPermissions; Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray(); diff --git a/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs b/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs index dd1b97899..d5bcdd3a3 100644 --- a/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs @@ -28,6 +28,11 @@ namespace Discord.Interactions /// public bool IsEnabledInDm { get; } + /// + /// Gets whether this command can is age restricted. + /// + public bool IsNsfw { get; } + /// /// Gets the default permissions needed for executing this command. /// diff --git a/src/Discord.Net.Interactions/Info/ModuleInfo.cs b/src/Discord.Net.Interactions/Info/ModuleInfo.cs index 4f40f1607..5c4cac587 100644 --- a/src/Discord.Net.Interactions/Info/ModuleInfo.cs +++ b/src/Discord.Net.Interactions/Info/ModuleInfo.cs @@ -49,6 +49,11 @@ namespace Discord.Interactions /// public bool IsEnabledInDm { get; } + /// + /// Gets whether this command is age restricted. + /// + public bool IsNsfw { get; } + /// /// Gets the default permissions needed for executing this command. /// @@ -121,6 +126,7 @@ namespace Discord.Interactions Description = builder.Description; Parent = parent; DefaultPermission = builder.DefaultPermission; + IsNsfw = builder.IsNsfw; IsEnabledInDm = builder.IsEnabledInDm; DefaultMemberPermissions = BuildDefaultMemberPermissions(builder); SlashCommands = BuildSlashCommands(builder).ToImmutableArray(); diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 9b507f1bb..dc98d4e43 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -54,6 +54,7 @@ namespace Discord.Interactions Description = commandInfo.Description, IsDefaultPermission = commandInfo.DefaultPermission, IsDMEnabled = commandInfo.IsEnabledInDm, + IsNsfw = commandInfo.IsNsfw, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), }.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) .WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) @@ -97,7 +98,8 @@ namespace Discord.Interactions Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), - IsDMEnabled = commandInfo.IsEnabledInDm + IsDMEnabled = commandInfo.IsEnabledInDm, + IsNsfw = commandInfo.IsNsfw, } .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) .Build(), @@ -106,6 +108,7 @@ namespace Discord.Interactions Name = commandInfo.Name, IsDefaultPermission = commandInfo.DefaultPermission, DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(), + IsNsfw = commandInfo.IsNsfw, IsDMEnabled = commandInfo.IsEnabledInDm } .WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary.Empty) @@ -162,6 +165,7 @@ namespace Discord.Interactions Description = moduleInfo.Description, IsDefaultPermission = moduleInfo.DefaultPermission, IsDMEnabled = moduleInfo.IsEnabledInDm, + IsNsfw = moduleInfo.IsNsfw, DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions } .WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary.Empty) @@ -225,6 +229,7 @@ namespace Discord.Interactions IsDefaultPermission = command.IsDefaultPermission, DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, IsDMEnabled = command.IsEnabledInDm, + IsNsfw = command.IsNsfw, Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified, NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, @@ -234,6 +239,7 @@ namespace Discord.Interactions Name = command.Name, IsDefaultPermission = command.IsDefaultPermission, DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsNsfw = command.IsNsfw, IsDMEnabled = command.IsEnabledInDm, NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty @@ -243,6 +249,7 @@ namespace Discord.Interactions Name = command.Name, IsDefaultPermission = command.IsDefaultPermission, DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue, + IsNsfw = command.IsNsfw, IsDMEnabled = command.IsEnabledInDm, NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty, DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary.Empty diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs index e46369277..6e434d466 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -44,5 +44,8 @@ namespace Discord.API [JsonProperty("default_member_permissions")] public Optional DefaultMemberPermission { get; set; } + + [JsonProperty("nsfw")] + public Optional Nsfw { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs index 2257d4b97..36ea0270a 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs @@ -35,9 +35,12 @@ namespace Discord.API.Rest [JsonProperty("default_member_permissions")] public Optional DefaultMemberPermission { get; set; } + [JsonProperty("nsfw")] + public Optional Nsfw { get; set; } + public CreateApplicationCommandParams() { } public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null, - IDictionary nameLocalizations = null, IDictionary descriptionLocalizations = null) + IDictionary nameLocalizations = null, IDictionary descriptionLocalizations = null, bool nsfw = false) { Name = name; Description = description; @@ -45,6 +48,7 @@ namespace Discord.API.Rest Type = type; NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional>.Unspecified; DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional>.Unspecified; + Nsfw = nsfw; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs index f49a3f33d..222e16b84 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs @@ -17,6 +17,12 @@ namespace Discord.API.Rest [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + [JsonProperty("nsfw")] + public Optional Nsfw { get; set; } + + [JsonProperty("default_member_permissions")] + public Optional DefaultMemberPermission { get; set; } + [JsonProperty("name_localizations")] public Optional> NameLocalizations { get; set; } diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index deca00b72..a118ca8c3 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -107,7 +107,8 @@ namespace Discord.Rest // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), - DmPermission = arg.IsDMEnabled.ToNullable() + DmPermission = arg.IsDMEnabled.ToNullable(), + Nsfw = arg.IsNsfw.GetValueOrDefault(false), }; if (arg is SlashCommandProperties slashProps) @@ -147,8 +148,9 @@ namespace Discord.Rest // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), - DmPermission = arg.IsDMEnabled.ToNullable() - }; + DmPermission = arg.IsDMEnabled.ToNullable(), + Nsfw = arg.IsNsfw.GetValueOrDefault(false) + }; if (arg is SlashCommandProperties slashProps) { @@ -190,7 +192,8 @@ namespace Discord.Rest // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), - DmPermission = arg.IsDMEnabled.ToNullable() + DmPermission = arg.IsDMEnabled.ToNullable(), + Nsfw = arg.IsNsfw.GetValueOrDefault(false) }; if (arg is SlashCommandProperties slashProps) @@ -252,7 +255,9 @@ namespace Discord.Rest ? args.IsDefaultPermission.Value : Optional.Unspecified, NameLocalizations = args.NameLocalizations?.ToDictionary(), - DescriptionLocalizations = args.DescriptionLocalizations?.ToDictionary() + DescriptionLocalizations = args.DescriptionLocalizations?.ToDictionary(), + Nsfw = args.IsNsfw.GetValueOrDefault(false), + DefaultMemberPermission = args.DefaultMemberPermissions.ToNullable() }; if (args is SlashCommandProperties slashProps) @@ -312,7 +317,8 @@ namespace Discord.Rest // TODO: better conversion to nullable optionals DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable(), - DmPermission = arg.IsDMEnabled.ToNullable() + DmPermission = arg.IsDMEnabled.ToNullable(), + Nsfw = arg.IsNsfw.GetValueOrDefault(false) }; if (arg is SlashCommandProperties slashProps) @@ -347,7 +353,9 @@ namespace Discord.Rest ? arg.IsDefaultPermission.Value : Optional.Unspecified, NameLocalizations = arg.NameLocalizations?.ToDictionary(), - DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary() + DescriptionLocalizations = arg.DescriptionLocalizations?.ToDictionary(), + Nsfw = arg.IsNsfw.GetValueOrDefault(false), + DefaultMemberPermission = arg.DefaultMemberPermissions.ToNullable() }; if (arg is SlashCommandProperties slashProps) diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs index 468d10712..ed22712e7 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -30,6 +30,9 @@ namespace Discord.Rest /// public bool IsEnabledInDm { get; private set; } + /// + public bool IsNsfw { get; private set; } + /// public GuildPermissions DefaultMemberPermissions { get; private set; } @@ -101,6 +104,7 @@ namespace Discord.Rest IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); + IsNsfw = model.Nsfw.GetValueOrDefault(false).GetValueOrDefault(false); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs index b0ddd0012..cdefd3260 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -39,6 +39,9 @@ namespace Discord.WebSocket /// public bool IsEnabledInDm { get; private set; } + /// + public bool IsNsfw { get; private set; } + /// public GuildPermissions DefaultMemberPermissions { get; private set; } @@ -130,6 +133,7 @@ namespace Discord.WebSocket IsEnabledInDm = model.DmPermission.GetValueOrDefault(true).GetValueOrDefault(true); DefaultMemberPermissions = new GuildPermissions((ulong)model.DefaultMemberPermission.GetValueOrDefault(0).GetValueOrDefault(0)); + IsNsfw = model.Nsfw.GetValueOrDefault(false).GetValueOrDefault(false); } /// From a53f1dfa58f4c1fc7ff39ab417653dc895b6740e Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Fri, 23 Dec 2022 16:56:00 +0300 Subject: [PATCH 18/24] fix `CreatePost` methods for `IForumChannel` (#2541) --- .../Entities/Channels/RestForumChannel.cs | 10 +++++----- .../Entities/Channels/SocketForumChannel.cs | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs index 7d837cde5..3f17642f8 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs @@ -164,15 +164,15 @@ namespace Discord.Rest async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags).ConfigureAwait(false); async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags, tags).ConfigureAwait(false); async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags, tags).ConfigureAwait(false); async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags).ConfigureAwait(false); async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags); #endregion diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs index ed36f5d4b..5e67da68a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -165,16 +165,17 @@ namespace Discord.WebSocket => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags).ConfigureAwait(false); async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags, tags).ConfigureAwait(false); async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags, tags).ConfigureAwait(false); async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags).ConfigureAwait(false); async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) - => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags); #endregion From 5c9be0d03a3211a2de71b7d163a6e8cb34484de1 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Fri, 23 Dec 2022 11:20:42 -0400 Subject: [PATCH 19/24] meta: update changelog (#2544) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa67c9a0a..278325922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ - #2469 Add missing properties in forum & thread channels (01ae904) - #2501 Add new discord stuff (ed38635) +- #2521 Add missing property & new stuff (82b772a) +- #2520 Implemented ClientDisconnect event for audio client. (4cad546) +- #2509 Add SendFiles to UserExtensions ( 4cad546) +- #2528 Implement wildcard lenght quantifiers, TreatAsRegex property and solve catastrpohic backtracking (25cfb88) +- #2531 Add Age restricted (NSFW) application commands support (60956c7) ### Fixed @@ -12,6 +17,8 @@ - #2468 Fix TimestampTag being sadge (bc89d3c) - #2497 Avoid throwing on missing Application (7077c44) - #2485 Fixed an oversight clearing session data upon any disconnect. (c7ac59d) +- #2526 Fix `GetActiveThreadsAsync` & add it to `ITextChannel` (bd2f719) +- #2535 Fix deploy.yml (20d8fdf) ### Misc @@ -19,6 +26,7 @@ - #2505 Update events.cs (ea039b8) - #2467 Update license and icon nuspec props (11ed0ff) - #2306 Command execution code rework & TypeConverters auto-scope fix (6869817) +- #2534 Fully qualify SlashCommandBuilder namespace ( 3b107c2) ## [3.8.1] - 2022-09-12 ### Added From e9e687caf79a54a6b0f8573e7b0f6bb2b70c1d09 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Fri, 23 Dec 2022 17:28:48 -0400 Subject: [PATCH 20/24] fix changelog date (#2545) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 278325922..41811fb09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [3.9.0] - 2022-11-13 +## [3.9.0] - 2022-12-23 ### Added - #2469 Add missing properties in forum & thread channels (01ae904) From c67642acfa0972c25e870f9f008f16f136a4c6db Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Sun, 25 Dec 2022 15:36:33 +0300 Subject: [PATCH 21/24] fix `CreatePostError` (#2546) --- src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index 0cdc92bcb..8b5926867 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -148,7 +148,7 @@ namespace Discord.Rest throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); if (channel.Flags.HasFlag(ChannelFlags.RequireTag)) - throw new ArgumentException($"The channel {channel.Name} requires posts to have at least one tag."); + Preconditions.AtLeast(tagIds?.Length ?? 0, 1, nameof(tagIds), $"The channel {channel.Name} requires posts to have at least one tag."); var args = new CreatePostParams() { From 48fb1b5df484d06ee0947935101ddb83f70ad234 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Sun, 25 Dec 2022 15:41:15 +0300 Subject: [PATCH 22/24] [Feature] Selects v2 support (#2507) * Initial support for new select types * Merge branch 'dev' of https://github.com/discord-net/Discord.Net into dev * some component&action row builder additions * remove redundant code * changes1 * maybe working rest part? * working-ish state? * fix some xml docs & small rework * typos * fix `ActionRowBuilder` * update DefaultArrayComponentConverter to accomodate new select-v2 types * now supports dm channels in channel selects * add a note to IF docs * add notes about nullable properties * * update Modal.cs Co-authored-by: cat Co-authored-by: Cenngo --- docs/guides/int_framework/intro.md | 3 + .../MessageComponents/ComponentBuilder.cs | 114 +++++++++++++---- .../MessageComponents/ComponentType.cs | 19 ++- .../IComponentInteractionData.cs | 24 +++- .../MessageComponents/SelectMenuComponent.cs | 14 ++- .../Entities/Interactions/Modals/Modal.cs | 4 +- .../Utils/ChannelTypeUtils.cs | 14 +++ src/Discord.Net.Core/Utils/ComponentType.cs | 8 ++ .../DefaultArrayComponentConverter.cs | 51 ++++++-- .../API/Common/ActionRowComponent.cs | 4 + .../Common/MessageComponentInteractionData.cs | 4 + ...MessageComponentInteractionDataResolved.cs | 19 +++ .../API/Common/SelectMenuComponent.cs | 9 +- .../MessageComponents/RestMessageComponent.cs | 2 +- .../RestMessageComponentData.cs | 96 +++++++++++++- .../Entities/Interactions/Modals/RestModal.cs | 2 +- .../Interactions/Modals/RestModalData.cs | 17 +-- .../Entities/Messages/RestMessage.cs | 18 +-- .../Converters/MessageComponentConverter.cs | 4 + .../DiscordSocketClient.cs | 3 +- .../SocketMessageComponent.cs | 2 +- .../SocketMessageComponentData.cs | 118 +++++++++++++++--- .../Interaction/Modals/SocketModal.cs | 4 +- .../Interaction/Modals/SocketModalData.cs | 6 +- .../Entities/Messages/SocketMessage.cs | 6 +- 25 files changed, 470 insertions(+), 95 deletions(-) create mode 100644 src/Discord.Net.Core/Utils/ChannelTypeUtils.cs create mode 100644 src/Discord.Net.Core/Utils/ComponentType.cs create mode 100644 src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 21ea365de..4bee07e94 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -208,6 +208,9 @@ You may use as many wild card characters as you want. Unlike button interactions, select menu interactions also contain the values of the selected menu items. In this case, you should structure your method to accept a string array. +> [!NOTE] +> Use arrays of `IUser`, `IChannel`, `IRole`, `IMentionable` or their implementations to get data from a select menu with respective type. + [!code-csharp[Dropdown](samples/intro/dropdown.cs)] > [!NOTE] diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index fd8798ed3..29ff80cf2 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -1,7 +1,7 @@ +using Discord.Utils; using System; using System.Collections.Generic; using System.Linq; -using Discord.Utils; namespace Discord { @@ -92,9 +92,11 @@ namespace Discord /// The max values of the placeholder. /// Whether or not the menu is disabled. /// The row to add the menu to. + /// The type of the select menu. + /// Menus valid channel types (only for ) /// - public ComponentBuilder WithSelectMenu(string customId, List options, - string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0) + public ComponentBuilder WithSelectMenu(string customId, List options = null, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) { return WithSelectMenu(new SelectMenuBuilder() .WithCustomId(customId) @@ -102,7 +104,9 @@ namespace Discord .WithPlaceholder(placeholder) .WithMaxValues(maxValues) .WithMinValues(minValues) - .WithDisabled(disabled), + .WithDisabled(disabled) + .WithType(type) + .WithChannelTypes(channelTypes), row); } @@ -118,7 +122,7 @@ namespace Discord public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0) { Preconditions.LessThan(row, MaxActionRowCount, nameof(row)); - if (menu.Options.Distinct().Count() != menu.Options.Count) + if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) throw new InvalidOperationException("Please make sure that there is no duplicates values."); var builtMenu = menu.Build(); @@ -278,9 +282,7 @@ namespace Discord { if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false) throw new ArgumentException("TextInputComponents are not allowed in messages.", nameof(ActionRows)); - if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.ModalSubmit) ?? false) - throw new ArgumentException("ModalSubmit components are not allowed in messages.", nameof(ActionRows)); - + return _actionRows != null ? new MessageComponent(_actionRows.Select(x => x.Build()).ToList()) : MessageComponent.Empty; @@ -356,10 +358,13 @@ namespace Discord /// The placeholder of the menu. /// The min values of the placeholder. /// The max values of the placeholder. - /// Whether or not the menu is disabled. - /// The current builder. - public ActionRowBuilder WithSelectMenu(string customId, List options, - string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false) + /// Whether or not the menu is disabled. + /// The type of the select menu. + /// Menus valid channel types (only for ) + /// The current builder. + public ActionRowBuilder WithSelectMenu(string customId, List options = null, + string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, + ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null) { return WithSelectMenu(new SelectMenuBuilder() .WithCustomId(customId) @@ -367,7 +372,9 @@ namespace Discord .WithPlaceholder(placeholder) .WithMaxValues(maxValues) .WithMinValues(minValues) - .WithDisabled(disabled)); + .WithDisabled(disabled) + .WithType(type) + .WithChannelTypes(channelTypes)); } /// @@ -378,7 +385,7 @@ namespace Discord /// The current builder. public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu) { - if (menu.Options.Distinct().Count() != menu.Options.Count) + if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count) throw new InvalidOperationException("Please make sure that there is no duplicates values."); var builtMenu = menu.Build(); @@ -431,10 +438,10 @@ namespace Discord { var builtButton = button.Build(); - if(Components.Count >= 5) + if (Components.Count >= 5) throw new InvalidOperationException($"Components count reached {MaxChildCount}"); - if (Components.Any(x => x.Type == ComponentType.SelectMenu)) + if (Components.Any(x => x.Type.IsSelectType())) throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu"); AddComponent(builtButton); @@ -458,11 +465,15 @@ namespace Discord case ComponentType.ActionRow: return false; case ComponentType.Button: - if (Components.Any(x => x.Type == ComponentType.SelectMenu)) + if (Components.Any(x => x.Type.IsSelectType())) return false; else return Components.Count < 5; case ComponentType.SelectMenu: + case ComponentType.ChannelSelect: + case ComponentType.MentionableSelect: + case ComponentType.RoleSelect: + case ComponentType.UserSelect: return Components.Count == 0; default: return false; @@ -759,6 +770,18 @@ namespace Discord }; } + /// + /// Gets or sets the type of the current select menu. + /// + /// Type must be a select menu type. + public ComponentType Type + { + get => _type; + set => _type = value.IsSelectType() + ? value + : throw new ArgumentException("Type must be a select menu type.", nameof(value)); + } + /// /// Gets or sets the placeholder text of the current select menu. /// @@ -815,8 +838,6 @@ namespace Discord { if (value != null) Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options)); - else - throw new ArgumentNullException(nameof(value), $"{nameof(Options)} cannot be null."); _options = value; } @@ -827,11 +848,17 @@ namespace Discord /// public bool IsDisabled { get; set; } + /// + /// Gets or sets the menu's channel types (only valid on s). + /// + public List ChannelTypes { get; set; } + private List _options = new List(); private int _minValues = 1; private int _maxValues = 1; private string _placeholder; private string _customId; + private ComponentType _type = ComponentType.SelectMenu; /// /// Creates a new instance of a . @@ -862,7 +889,9 @@ namespace Discord /// The max values of this select menu. /// The min values of this select menu. /// Disabled this select menu or not. - public SelectMenuBuilder(string customId, List options, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false) + /// The of this select menu. + /// The types of channels this menu can select (only valid on s) + public SelectMenuBuilder(string customId, List options = null, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List channelTypes = null) { CustomId = customId; Options = options; @@ -870,6 +899,8 @@ namespace Discord IsDisabled = isDisabled; MaxValues = maxValues; MinValues = minValues; + Type = type; + ChannelTypes = channelTypes ?? new(); } /// @@ -990,6 +1021,47 @@ namespace Discord return this; } + /// + /// Sets the menu's current type. + /// + /// The type of the menu. + /// + /// The current builder. + /// + public SelectMenuBuilder WithType(ComponentType type) + { + Type = type; + return this; + } + + /// + /// Sets the menus valid channel types (only for s). + /// + /// The valid channel types of the menu. + /// + /// The current builder. + /// + public SelectMenuBuilder WithChannelTypes(List channelTypes) + { + ChannelTypes = channelTypes; + return this; + } + + /// + /// Sets the menus valid channel types (only for s). + /// + /// The valid channel types of the menu. + /// + /// The current builder. + /// + public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes) + { + ChannelTypes = channelTypes is null + ? ChannelTypeUtils.AllChannelTypes() + : channelTypes.ToList(); + return this; + } + /// /// Builds a /// @@ -998,7 +1070,7 @@ namespace Discord { var options = Options?.Select(x => x.Build()).ToList(); - return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled); + return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes); } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs index 1d63ee829..0ad3f741a 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs @@ -26,8 +26,23 @@ namespace Discord TextInput = 4, /// - /// An interaction sent when a model is submitted. + /// A select menu for picking from users. /// - ModalSubmit = 5, + UserSelect = 5, + + /// + /// A select menu for picking from roles. + /// + RoleSelect = 6, + + /// + /// A select menu for picking from roles and users. + /// + MentionableSelect = 7, + + /// + /// A select menu for picking from channels. + /// + ChannelSelect = 8, } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs index 039b6b41f..3a6526ee2 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs @@ -18,12 +18,32 @@ namespace Discord ComponentType Type { get; } /// - /// Gets the value(s) of a interaction response. + /// Gets the value(s) of a interaction response. if select type is different. /// IReadOnlyCollection Values { get; } /// - /// Gets the value of a interaction response. + /// Gets the channels(s) of a interaction response. if select type is different. + /// + IReadOnlyCollection Channels { get; } + + /// + /// Gets the user(s) of a or interaction response. if select type is different. + /// + IReadOnlyCollection Users { get; } + + /// + /// Gets the roles(s) of a or interaction response. if select type is different. + /// + IReadOnlyCollection Roles { get; } + + /// + /// Gets the guild member(s) of a or interaction response. if type select is different. + /// + IReadOnlyCollection Members { get; } + + /// + /// Gets the value of a interaction response. /// public string Value { get; } } diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs index 229c1e148..eccdd18c6 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +10,7 @@ namespace Discord public class SelectMenuComponent : IMessageComponent { /// - public ComponentType Type => ComponentType.SelectMenu; + public ComponentType Type { get; } /// public string CustomId { get; } @@ -39,6 +40,11 @@ namespace Discord /// public bool IsDisabled { get; } + /// + /// Gets the allowed channel types for this modal + /// + public IReadOnlyCollection ChannelTypes { get; } + /// /// Turns this select menu into a builder. /// @@ -52,9 +58,9 @@ namespace Discord Placeholder, MaxValues, MinValues, - IsDisabled); + IsDisabled, Type, ChannelTypes.ToList()); - internal SelectMenuComponent(string customId, List options, string placeholder, int minValues, int maxValues, bool disabled) + internal SelectMenuComponent(string customId, List options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable channelTypes = null) { CustomId = customId; Options = options; @@ -62,6 +68,8 @@ namespace Discord MinValues = minValues; MaxValues = maxValues; IsDisabled = disabled; + Type = type; + ChannelTypes = channelTypes?.ToArray() ?? Array.Empty(); } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs index a0fde5ea3..a435d33ef 100644 --- a/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs +++ b/src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs @@ -7,12 +7,12 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a modal interaction. + /// Represents a modal interaction. /// public class Modal : IMessageComponent { /// - public ComponentType Type => ComponentType.ModalSubmit; + public ComponentType Type => throw new NotSupportedException("Modals do not have a component type."); /// /// Gets the title of the modal. diff --git a/src/Discord.Net.Core/Utils/ChannelTypeUtils.cs b/src/Discord.Net.Core/Utils/ChannelTypeUtils.cs new file mode 100644 index 000000000..4dd764508 --- /dev/null +++ b/src/Discord.Net.Core/Utils/ChannelTypeUtils.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Discord.Utils; + +public static class ChannelTypeUtils +{ + public static List AllChannelTypes() + => new List() + { + ChannelType.Forum, ChannelType.Category, ChannelType.DM, ChannelType.Group, ChannelType.GuildDirectory, + ChannelType.News, ChannelType.NewsThread, ChannelType.PrivateThread, ChannelType.PublicThread, + ChannelType.Stage, ChannelType.Store, ChannelType.Text, ChannelType.Voice + }; +} diff --git a/src/Discord.Net.Core/Utils/ComponentType.cs b/src/Discord.Net.Core/Utils/ComponentType.cs new file mode 100644 index 000000000..c7d42c512 --- /dev/null +++ b/src/Discord.Net.Core/Utils/ComponentType.cs @@ -0,0 +1,8 @@ +namespace Discord.Utils; + +public static class ComponentTypeUtils +{ + public static bool IsSelectType(this ComponentType type) => type is ComponentType.ChannelSelect + or ComponentType.SelectMenu or ComponentType.RoleSelect or ComponentType.UserSelect + or ComponentType.MentionableSelect; +} diff --git a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs index 87fc431c5..5efdd537a 100644 --- a/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading.Tasks; namespace Discord.Interactions @@ -17,27 +19,56 @@ namespace Discord.Interactions throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter)} cannot be used to convert a non-array type."); _underlyingType = typeof(T).GetElementType(); - _typeReader = interactionService.GetTypeReader(_underlyingType); + + _typeReader = true switch + { + _ when typeof(IUser).IsAssignableFrom(_underlyingType) + || typeof(IChannel).IsAssignableFrom(_underlyingType) + || typeof(IMentionable).IsAssignableFrom(_underlyingType) + || typeof(IRole).IsAssignableFrom(_underlyingType) => null, + _ => interactionService.GetTypeReader(_underlyingType) + }; } public override async Task ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services) { - var results = new List(); + var objs = new List(); + + if(_typeReader is not null && option.Values.Count > 0) + foreach (var value in option.Values) + { + var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); + + if (!result.IsSuccess) + return result; - foreach (var value in option.Values) + objs.Add(result.Value); + } + else { - var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false); + var users = new Dictionary(); + + if (option.Users is not null) + foreach (var user in option.Users) + users[user.Id] = user; + + if(option.Members is not null) + foreach(var member in option.Members) + users[member.Id] = member; + + objs.AddRange(users.Values); - if (!result.IsSuccess) - return result; + if(option.Roles is not null) + objs.AddRange(option.Roles); - results.Add(result); + if (option.Channels is not null) + objs.AddRange(option.Channels); } - var destination = Array.CreateInstance(_underlyingType, results.Count); + var destination = Array.CreateInstance(_underlyingType, objs.Count); - for (var i = 0; i < results.Count; i++) - destination.SetValue(results[i].Value, i); + for (var i = 0; i < objs.Count; i++) + destination.SetValue(objs[i], i); return TypeConverterResult.FromSuccess(destination); } diff --git a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs index 9a7eb80dd..e97ca71d6 100644 --- a/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs +++ b/src/Discord.Net.Rest/API/Common/ActionRowComponent.cs @@ -21,6 +21,10 @@ namespace Discord.API { ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent), ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent), + ComponentType.ChannelSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), + ComponentType.UserSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), + ComponentType.RoleSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), + ComponentType.MentionableSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent), ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent), _ => null }; diff --git a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs index 4633fc25a..1bc45d21b 100644 --- a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs +++ b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API { @@ -15,5 +16,8 @@ namespace Discord.API [JsonProperty("value")] public Optional Value { get; set; } + + [JsonProperty("resolved")] + public Optional Resolved { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs new file mode 100644 index 000000000..04f97cdfd --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Discord.API; + +internal class MessageComponentInteractionDataResolved +{ + [JsonProperty("users")] + public Optional> Users { get; set; } + + [JsonProperty("members")] + public Optional> Members { get; set; } + + [JsonProperty("channels")] + public Optional> Channels { get; set; } + + [JsonProperty("roles")] + public Optional> Roles { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs index 25ac476c5..3975a8c1e 100644 --- a/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs +++ b/src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs @@ -26,6 +26,12 @@ namespace Discord.API [JsonProperty("disabled")] public bool Disabled { get; set; } + [JsonProperty("channel_types")] + public Optional ChannelTypes { get; set; } + + [JsonProperty("resolved")] + public Optional Resolved { get; set; } + [JsonProperty("values")] public Optional Values { get; set; } public SelectMenuComponent() { } @@ -34,11 +40,12 @@ namespace Discord.API { Type = component.Type; CustomId = component.CustomId; - Options = component.Options.Select(x => new SelectMenuOption(x)).ToArray(); + Options = component.Options?.Select(x => new SelectMenuOption(x)).ToArray(); Placeholder = component.Placeholder; MinValues = component.MinValues; MaxValues = component.MaxValues; Disabled = component.IsDisabled; + ChannelTypes = component.ChannelTypes.ToArray(); } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs index e0eab6051..b400852d6 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponent.cs @@ -34,7 +34,7 @@ namespace Discord.Rest ? (DataModel)model.Data.Value : null; - Data = new RestMessageComponentData(dataModel); + Data = new RestMessageComponentData(dataModel, client, Guild); } internal new static async Task CreateAsync(DiscordRestClient client, Model model, bool doApiCall) diff --git a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs index b0efe418c..bc44d0df5 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/MessageComponents/RestMessageComponentData.cs @@ -1,8 +1,12 @@ +using Discord.API; + using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using System.Threading.Tasks; + using Model = Discord.API.MessageComponentInteractionData; namespace Discord.Rest @@ -10,7 +14,7 @@ namespace Discord.Rest /// /// Represents data for a . /// - public class RestMessageComponentData : IComponentInteractionData, IDiscordInteractionData + public class RestMessageComponentData : IComponentInteractionData { /// public string CustomId { get; } @@ -21,17 +25,75 @@ namespace Discord.Rest /// public IReadOnlyCollection Values { get; } + /// + public IReadOnlyCollection Channels { get; } + + /// + public IReadOnlyCollection Users { get; } + + /// + public IReadOnlyCollection Roles { get; } + + /// + public IReadOnlyCollection Members { get; } + + #region IComponentInteractionData + + /// + IReadOnlyCollection IComponentInteractionData.Channels => Channels; + + /// + IReadOnlyCollection IComponentInteractionData.Users => Users; + + /// + IReadOnlyCollection IComponentInteractionData.Roles => Roles; + + /// + IReadOnlyCollection IComponentInteractionData.Members => Members; + + #endregion + /// public string Value { get; } - internal RestMessageComponentData(Model model) + internal RestMessageComponentData(Model model, BaseDiscordClient discord, IGuild guild) { CustomId = model.CustomId; Type = model.ComponentType; Values = model.Values.GetValueOrDefault(); + Value = model.Value.GetValueOrDefault(); + + if (model.Resolved.IsSpecified) + { + Users = model.Resolved.Value.Users.IsSpecified + ? model.Resolved.Value.Users.Value.Select(user => RestUser.Create(discord, user.Value)).ToImmutableArray() + : Array.Empty(); + + Members = model.Resolved.Value.Members.IsSpecified + ? model.Resolved.Value.Members.Value.Select(member => + { + member.Value.User = model.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value; + + return RestGuildUser.Create(discord, guild, member.Value); + }).ToImmutableArray() + : null; + + Channels = model.Resolved.Value.Channels.IsSpecified + ? model.Resolved.Value.Channels.Value.Select(channel => + { + if (channel.Value.Type is ChannelType.DM) + return RestDMChannel.Create(discord, channel.Value); + return RestChannel.Create(discord, channel.Value); + }).ToImmutableArray() + : Array.Empty(); + + Roles = model.Resolved.Value.Roles.IsSpecified + ? model.Resolved.Value.Roles.Value.Select(role => RestRole.Create(discord, guild, role.Value)).ToImmutableArray() + : Array.Empty(); + } } - internal RestMessageComponentData(IMessageComponent component) + internal RestMessageComponentData(IMessageComponent component, BaseDiscordClient discord, IGuild guild) { CustomId = component.CustomId; Type = component.Type; @@ -40,7 +102,33 @@ namespace Discord.Rest Value = textInput.Value.Value; if (component is API.SelectMenuComponent select) - Values = select.Values.Value; + { + Values = select.Values.GetValueOrDefault(null); + + if (select.Resolved.IsSpecified) + { + Users = select.Resolved.Value.Users.IsSpecified + ? select.Resolved.Value.Users.Value.Select(user => RestUser.Create(discord, user.Value)).ToImmutableArray() + : null; + + Members = select.Resolved.Value.Members.IsSpecified + ? select.Resolved.Value.Members.Value.Select(member => + { + member.Value.User = select.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value; + + return RestGuildUser.Create(discord, guild, member.Value); + }).ToImmutableArray() + : null; + + Channels = select.Resolved.Value.Channels.IsSpecified + ? select.Resolved.Value.Channels.Value.Select(channel => RestChannel.Create(discord, channel.Value)).ToImmutableArray() + : null; + + Roles = select.Resolved.Value.Roles.IsSpecified + ? select.Resolved.Value.Roles.Value.Select(role => RestRole.Create(discord, guild, role.Value)).ToImmutableArray() + : null; + } + } } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs index 9229b63b5..ef16d7b25 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModal.cs @@ -23,7 +23,7 @@ namespace Discord.Rest ? (DataModel)model.Data.Value : null; - Data = new RestModalData(dataModel); + Data = new RestModalData(dataModel, client, Guild); } internal new static async Task CreateAsync(DiscordRestClient client, ModelBase model, bool doApiCall) diff --git a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs index 22460ae51..7ecf804a1 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/Modals/RestModalData.cs @@ -10,7 +10,7 @@ namespace Discord.Rest /// /// Represents data sent from a Interaction. /// - public class RestModalData : IComponentInteractionData, IModalInteractionData + public class RestModalData : IModalInteractionData { /// public string CustomId { get; } @@ -20,25 +20,14 @@ namespace Discord.Rest /// public IReadOnlyCollection Components { get; } - /// - public ComponentType Type => ComponentType.ModalSubmit; - - /// - public IReadOnlyCollection Values - => throw new NotSupportedException("Modal interactions do not have values!"); - - /// - public string Value - => throw new NotSupportedException("Modal interactions do not have value!"); - IReadOnlyCollection IModalInteractionData.Components => Components; - internal RestModalData(Model model) + internal RestModalData(Model model, BaseDiscordClient discord, IGuild guild) { CustomId = model.CustomId; Components = model.Components .SelectMany(x => x.Components) - .Select(x => new RestMessageComponentData(x)) + .Select(x => new RestMessageComponentData(x, discord, guild)) .ToArray(); } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 69e038fd2..8b6b44e39 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -170,26 +170,28 @@ namespace Discord.Rest parsed.Url.GetValueOrDefault(), parsed.Disabled.GetValueOrDefault()); } - case ComponentType.SelectMenu: + case ComponentType.SelectMenu or ComponentType.ChannelSelect or ComponentType.RoleSelect or ComponentType.MentionableSelect or ComponentType.UserSelect: { var parsed = (API.SelectMenuComponent)y; return new SelectMenuComponent( parsed.CustomId, - parsed.Options.Select(z => new SelectMenuOption( + parsed.Options?.Select(z => new SelectMenuOption( z.Label, z.Value, z.Description.GetValueOrDefault(), z.Emoji.IsSpecified - ? z.Emoji.Value.Id.HasValue - ? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault()) - : new Emoji(z.Emoji.Value.Name) - : null, + ? z.Emoji.Value.Id.HasValue + ? new Emote(z.Emoji.Value.Id.Value, z.Emoji.Value.Name, z.Emoji.Value.Animated.GetValueOrDefault()) + : new Emoji(z.Emoji.Value.Name) + : null, z.Default.ToNullable())).ToList(), parsed.Placeholder.GetValueOrDefault(), parsed.MinValues, parsed.MaxValues, - parsed.Disabled - ); + parsed.Disabled, + parsed.Type, + parsed.ChannelTypes.GetValueOrDefault() + ); } default: return null; diff --git a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs index 36542d83b..7888219bc 100644 --- a/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/MessageComponentConverter.cs @@ -30,6 +30,10 @@ namespace Discord.Net.Converters messageComponent = new API.ButtonComponent(); break; case ComponentType.SelectMenu: + case ComponentType.ChannelSelect: + case ComponentType.MentionableSelect: + case ComponentType.RoleSelect: + case ComponentType.UserSelect: messageComponent = new API.SelectMenuComponent(); break; case ComponentType.TextInput: diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f2239010f..cb982889c 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -5,6 +5,7 @@ using Discord.Net.Converters; using Discord.Net.Udp; using Discord.Net.WebSockets; using Discord.Rest; +using Discord.Utils; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -2394,7 +2395,7 @@ namespace Discord.WebSocket await TimedInvokeAsync(_slashCommandExecuted, nameof(SlashCommandExecuted), slashCommand).ConfigureAwait(false); break; case SocketMessageComponent messageComponent: - if (messageComponent.Data.Type == ComponentType.SelectMenu) + if (messageComponent.Data.Type.IsSelectType()) await TimedInvokeAsync(_selectMenuExecuted, nameof(SelectMenuExecuted), messageComponent).ConfigureAwait(false); if (messageComponent.Data.Type == ComponentType.Button) await TimedInvokeAsync(_buttonExecuted, nameof(ButtonExecuted), messageComponent).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs index 2a1a67d04..286629ceb 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponent.cs @@ -35,7 +35,7 @@ namespace Discord.WebSocket ? (DataModel)model.Data.Value : null; - Data = new SocketMessageComponentData(dataModel); + Data = new SocketMessageComponentData(dataModel, client, client.State, client.Guilds.FirstOrDefault(x => x.Id == model.GuildId.GetValueOrDefault()), model.User.GetValueOrDefault()); } internal new static SocketMessageComponent Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel, SocketUser user) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs index c7f6c5106..0099ec77c 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/MessageComponents/SocketMessageComponentData.cs @@ -1,4 +1,9 @@ +using Discord.Rest; +using Discord.Utils; +using System; +using System.Linq; using System.Collections.Generic; +using System.Collections.Immutable; using Model = Discord.API.MessageComponentInteractionData; namespace Discord.WebSocket @@ -8,35 +13,84 @@ namespace Discord.WebSocket /// public class SocketMessageComponentData : IComponentInteractionData { - /// - /// Gets the components Custom Id that was clicked. - /// + /// public string CustomId { get; } - /// - /// Gets the type of the component clicked. - /// + /// public ComponentType Type { get; } - /// - /// Gets the value(s) of a interaction response. - /// + /// public IReadOnlyCollection Values { get; } - /// - /// Gets the value of a interaction response. - /// + /// + public IReadOnlyCollection Channels { get; } + + /// + /// Returns if user is cached, otherwise. + public IReadOnlyCollection Users { get; } + + /// + public IReadOnlyCollection Roles { get; } + + /// + public IReadOnlyCollection Members { get; } + + #region IComponentInteractionData + + /// + IReadOnlyCollection IComponentInteractionData.Channels => Channels; + + /// + IReadOnlyCollection IComponentInteractionData.Users => Users; + + /// + IReadOnlyCollection IComponentInteractionData.Roles => Roles; + + /// + IReadOnlyCollection IComponentInteractionData.Members => Members; + + #endregion + /// public string Value { get; } - internal SocketMessageComponentData(Model model) + internal SocketMessageComponentData(Model model, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) { CustomId = model.CustomId; Type = model.ComponentType; Values = model.Values.GetValueOrDefault(); Value = model.Value.GetValueOrDefault(); + + if (model.Resolved.IsSpecified) + { + Users = model.Resolved.Value.Users.IsSpecified + ? model.Resolved.Value.Users.Value.Select(user => (IUser)state.GetUser(user.Value.Id) ?? RestUser.Create(discord, user.Value)).ToImmutableArray() + : null; + + Members = model.Resolved.Value.Members.IsSpecified + ? model.Resolved.Value.Members.Value.Select(member => + { + member.Value.User = model.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value; + return SocketGuildUser.Create(guild, state, member.Value); + }).ToImmutableArray() + : null; + + Channels = model.Resolved.Value.Channels.IsSpecified + ? model.Resolved.Value.Channels.Value.Select( + channel => + { + if (channel.Value.Type is ChannelType.DM) + return SocketDMChannel.Create(discord, state, channel.Value.Id, dmUser); + return (SocketChannel)SocketGuildChannel.Create(guild, state, channel.Value); + }).ToImmutableArray() + : null; + + Roles = model.Resolved.Value.Roles.IsSpecified + ? model.Resolved.Value.Roles.Value.Select(role => SocketRole.Create(guild, state, role.Value)).ToImmutableArray() + : null; + } } - internal SocketMessageComponentData(IMessageComponent component) + internal SocketMessageComponentData(IMessageComponent component, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) { CustomId = component.CustomId; Type = component.Type; @@ -45,9 +99,39 @@ namespace Discord.WebSocket ? (component as API.TextInputComponent).Value.Value : null; - Values = component.Type == ComponentType.SelectMenu - ? (component as API.SelectMenuComponent).Values.Value - : null; + if (component is API.SelectMenuComponent select) + { + Values = select.Values.GetValueOrDefault(null); + + if (select.Resolved.IsSpecified) + { + Users = select.Resolved.Value.Users.IsSpecified + ? select.Resolved.Value.Users.Value.Select(user => (IUser)state.GetUser(user.Value.Id) ?? RestUser.Create(discord, user.Value)).ToImmutableArray() + : null; + + Members = select.Resolved.Value.Members.IsSpecified + ? select.Resolved.Value.Members.Value.Select(member => + { + member.Value.User = select.Resolved.Value.Users.Value.First(u => u.Key == member.Key).Value; + return SocketGuildUser.Create(guild, state, member.Value); + }).ToImmutableArray() + : null; + + Channels = select.Resolved.Value.Channels.IsSpecified + ? select.Resolved.Value.Channels.Value.Select( + channel => + { + if (channel.Value.Type is ChannelType.DM) + return SocketDMChannel.Create(discord, state, channel.Value.Id, dmUser); + return (SocketChannel)SocketGuildChannel.Create(guild, state, channel.Value); + }).ToImmutableArray() + : null; + + Roles = select.Resolved.Value.Roles.IsSpecified + ? select.Resolved.Value.Roles.Value.Select(role => SocketRole.Create(guild, state, role.Value)).ToImmutableArray() + : null; + } + } } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs index 647544b48..7b4466af3 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModal.cs @@ -27,8 +27,8 @@ namespace Discord.WebSocket var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value : null; - - Data = new SocketModalData(dataModel); + + Data = new SocketModalData(dataModel, client, client.State, client.State.GetGuild(model.GuildId.GetValueOrDefault()), model.User.GetValueOrDefault()); } internal new static SocketModal Create(DiscordSocketClient client, ModelBase model, ISocketMessageChannel channel, SocketUser user) diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs index df8be2fe8..ec1fe9622 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Modals/SocketModalData.cs @@ -10,7 +10,7 @@ namespace Discord.WebSocket /// /// Represents data sent from a . /// - public class SocketModalData : IDiscordInteractionData, IModalInteractionData + public class SocketModalData : IModalInteractionData { /// /// Gets the 's Custom Id. @@ -22,12 +22,12 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Components { get; } - internal SocketModalData(Model model) + internal SocketModalData(Model model, DiscordSocketClient discord, ClientState state, SocketGuild guild, API.User dmUser) { CustomId = model.CustomId; Components = model.Components .SelectMany(x => x.Components) - .Select(x => new SocketMessageComponentData(x)) + .Select(x => new SocketMessageComponentData(x, discord, state, guild, dmUser)) .ToArray(); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 3cd67beb5..40a645afb 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -118,7 +118,7 @@ namespace Discord.WebSocket /// /// Collection of WebSocket-based users. /// - public IReadOnlyCollection MentionedUsers => _userMentions; + public IReadOnlyCollection MentionedUsers => _userMentions; /// public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); @@ -226,7 +226,9 @@ namespace Discord.WebSocket parsed.Placeholder.GetValueOrDefault(), parsed.MinValues, parsed.MaxValues, - parsed.Disabled + parsed.Disabled, + parsed.Type, + parsed.ChannelTypes.GetValueOrDefault() ); } default: From 7c535b952a00166e3c22e3b39a68176fe52241c6 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Sun, 25 Dec 2022 17:40:05 +0300 Subject: [PATCH 23/24] [Feature] add missing invite guild properties & welcome screen support (#2510) * added models * working getter for welcome screen * * more changes * modify welcome screen support * fix some typos & remove `using` added by VS * Working-ish state * Resolve some reviews * change access modifier * forgot to add docs * revert to InviteGuild & extend it * resolve some reviews * Apply suggestions from code review Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- .../Entities/Guilds/IGuild.cs | 16 ++ .../Entities/Guilds/WelcomeScreen.cs | 24 +++ .../Entities/Guilds/WelcomeScreenChannel.cs | 41 +++++ .../Guilds/WelcomeScreenChannelProperties.cs | 54 ++++++ .../Entities/Invites/IInvite.cs | 1 + .../Entities/Invites/InviteGuild.cs | 156 ++++++++++++++++++ src/Discord.Net.Rest/API/Common/Guild.cs | 3 + .../API/Common/InviteGuild.cs | 36 +++- .../API/Common/WelcomeScreen.cs | 12 ++ .../API/Common/WelcomeScreenChannel.cs | 18 ++ .../Rest/ModifyGuildWelcomeScreenParams.cs | 15 ++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 29 ++++ .../Entities/Guilds/GuildHelper.cs | 80 +++++++-- .../Entities/Guilds/RestGuild.cs | 9 + .../Entities/Invites/RestInvite.cs | 38 +++++ .../Entities/Guilds/SocketGuild.cs | 8 + 16 files changed, 526 insertions(+), 14 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Guilds/WelcomeScreen.cs create mode 100644 src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannel.cs create mode 100644 src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannelProperties.cs create mode 100644 src/Discord.Net.Core/Entities/Invites/InviteGuild.cs create mode 100644 src/Discord.Net.Rest/API/Common/WelcomeScreen.cs create mode 100644 src/Discord.Net.Rest/API/Common/WelcomeScreenChannel.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyGuildWelcomeScreenParams.cs diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index d1ff7b99c..63f4a2280 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1251,5 +1251,21 @@ namespace Discord /// Task> BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, RequestOptions options = null); + + /// + /// Gets the welcome screen of the guild. Returns if the welcome channel is not set. + /// + /// + /// A task that represents the asynchronous creation operation. The task result contains a . + /// + Task GetWelcomeScreenAsync(RequestOptions options = null); + + /// + /// Modifies the welcome screen of the guild. Returns if welcome screen is removed. + /// + /// + /// A task that represents the asynchronous creation operation. The task result contains a . + /// + Task ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Guilds/WelcomeScreen.cs b/src/Discord.Net.Core/Entities/Guilds/WelcomeScreen.cs new file mode 100644 index 000000000..489256010 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/WelcomeScreen.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Discord; + +public class WelcomeScreen +{ + /// + /// Gets the server description shown in the welcome screen. if not set. + /// + public string Description { get; } + + /// + /// Gets the channels shown in the welcome screen, up to 5 channels. + /// + public IReadOnlyCollection Channels { get; } + + internal WelcomeScreen(string description, IReadOnlyCollection channels) + { + Description = description; + + Channels = channels.ToImmutableArray(); + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannel.cs b/src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannel.cs new file mode 100644 index 000000000..431831f6e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannel.cs @@ -0,0 +1,41 @@ +using System; + +namespace Discord; + +public class WelcomeScreenChannel : ISnowflakeEntity +{ + /// + /// Gets the channel's id. + /// + public ulong Id { get; } + + /// + /// Gets the description shown for the channel. + /// + public string Description { get; } + + /// + /// Gets the emoji for this channel. if it is unicode emoji, if it is a custom one and if none is set. + /// + /// + /// If the emoji is only the will be populated. + /// Use to get the emoji. + /// + public IEmote Emoji { get; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal WelcomeScreenChannel(ulong id, string description, string emojiName = null, ulong? emoteId = null) + { + Id = id; + Description = description; + + if (emoteId.HasValue && emoteId.Value != 0) + Emoji = new Emote(emoteId.Value, emojiName, false); + else if (emojiName != null) + Emoji = new Emoji(emojiName); + else + Emoji = null; + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannelProperties.cs b/src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannelProperties.cs new file mode 100644 index 000000000..c35470ef3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannelProperties.cs @@ -0,0 +1,54 @@ +using System; +using System.Xml.Linq; + +namespace Discord; + +public class WelcomeScreenChannelProperties : ISnowflakeEntity +{ + /// + /// Gets or sets the channel's id. + /// + public ulong Id { get; set; } + + /// + /// Gets or sets the description shown for the channel. + /// + public string Description { get; set; } + + /// + /// Gets or sets the emoji for this channel. if it is unicode emoji, if it is a custom one and if none is set. + /// + /// + /// If the emoji is only the will be populated. + /// Use to get the emoji. + /// + public IEmote Emoji { get; set; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + /// + /// Initializes a new instance of . + /// + /// Id if a channel. + /// Description for the channel in the welcome screen. + /// The emoji for the channel in the welcome screen. + public WelcomeScreenChannelProperties(ulong id, string description, IEmote emoji = null) + { + Id = id; + Description = description; + Emoji = emoji; + } + + /// + /// Initializes a new instance of . + /// + public WelcomeScreenChannelProperties() { } + /// + /// Initializes a new instance of . + /// + /// A welcome screen channel to modify. + /// A new instance of . + public static WelcomeScreenChannelProperties FromWelcomeScreenChannel(WelcomeScreenChannel channel) + => new (channel.Id, channel.Description, channel.Emoji); +} diff --git a/src/Discord.Net.Core/Entities/Invites/IInvite.cs b/src/Discord.Net.Core/Entities/Invites/IInvite.cs index 47ffffacb..eb1f6a243 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInvite.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -60,6 +60,7 @@ namespace Discord /// A guild object representing the guild that the invite points to. /// IGuild Guild { get; } + /// /// Gets the ID of the guild this invite is linked to. /// diff --git a/src/Discord.Net.Core/Entities/Invites/InviteGuild.cs b/src/Discord.Net.Core/Entities/Invites/InviteGuild.cs new file mode 100644 index 000000000..b290716ec --- /dev/null +++ b/src/Discord.Net.Core/Entities/Invites/InviteGuild.cs @@ -0,0 +1,156 @@ +using System; + +namespace Discord; + +public class InviteGuild : ISnowflakeEntity +{ + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + /// + public ulong Id { get; private set; } + + /// + /// Gets the name of this guild. + /// + /// + /// A string containing the name of this guild. + /// + public string Name { get; private set; } + + /// + /// Gets the description for the guild. + /// + /// + /// The description for the guild; if none is set. + /// + public string Description { get; private set; } + + /// + /// Gets the ID of this guild's splash image. + /// + /// + /// An identifier for the splash image; if none is set. + /// + public string SplashId { get; private set; } + + /// + /// Gets the URL of this guild's splash image. + /// + /// + /// A URL pointing to the guild's splash image; if none is set. + /// + public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); + + /// + /// Gets the identifier for this guilds banner image. + /// + /// + /// An identifier for the banner image; if none is set. + /// + public string BannerId { get; private set; } + + /// + /// Gets the URL of this guild's banner image. + /// + /// + /// A URL pointing to the guild's banner image; if none is set. + /// + public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId, ImageFormat.Auto); + + /// + /// Gets the features for this guild. + /// + /// + /// A flags enum containing all the features for the guild. + /// + public GuildFeatures Features { get; private set; } + + /// + /// Gets the ID of this guild's icon. + /// + /// + /// An identifier for the splash image; if none is set. + /// + public string IconId { get; private set; } + + /// + /// Gets the URL of this guild's icon. + /// + /// + /// A URL pointing to the guild's icon; if none is set. + /// + public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); + + /// + /// + /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + /// + /// + /// The level of requirements. + /// + public VerificationLevel VerificationLevel { get; private set; } + + /// + /// Gets the code for this guild's vanity invite URL. + /// + /// + /// A string containing the vanity invite code for this guild; if none is set. + /// + public string VanityURLCode { get; private set; } + + /// + /// Gets the number of premium subscribers of this guild. + /// + /// + /// This is the number of users who have boosted this guild. + /// + /// + /// The number of premium subscribers of this guild; + /// + public int PremiumSubscriptionCount { get; private set; } + + /// + /// Gets the NSFW level of this guild. + /// + /// + /// The NSFW level of this guild. + /// + public NsfwLevel NsfwLevel { get; private set; } + + /// + /// Gets the Welcome Screen of this guild + /// + /// + /// The welcome screen of this guild. if none is set. + /// + public WelcomeScreen WelcomeScreen { get; private set; } + + internal InviteGuild( + ulong id, + string name, + string description, + string splashId, + string bannerId, + GuildFeatures features, + string iconId, + VerificationLevel verificationLevel, + string vanityURLCode, + int premiumSubscriptionCount, + NsfwLevel nsfwLevel, + WelcomeScreen welcomeScreen) + { + Id = id; + Name = name; + Description = description; + SplashId = splashId; + BannerId = bannerId; + Features = features; + IconId = iconId; + VerificationLevel = verificationLevel; + VanityURLCode = vanityURLCode; + PremiumSubscriptionCount = premiumSubscriptionCount; + NsfwLevel = nsfwLevel; + WelcomeScreen = welcomeScreen; + } +} diff --git a/src/Discord.Net.Rest/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs index d550c54a0..d091c26a7 100644 --- a/src/Discord.Net.Rest/API/Common/Guild.cs +++ b/src/Discord.Net.Rest/API/Common/Guild.cs @@ -83,5 +83,8 @@ namespace Discord.API public Sticker[] Stickers { get; set; } [JsonProperty("premium_progress_bar_enabled")] public Optional IsBoostProgressBarEnabled { get; set; } + + [JsonProperty("welcome_screen")] + public Optional WelcomeScreen { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/InviteGuild.cs b/src/Discord.Net.Rest/API/Common/InviteGuild.cs index f5c634e4e..82a17d68a 100644 --- a/src/Discord.Net.Rest/API/Common/InviteGuild.cs +++ b/src/Discord.Net.Rest/API/Common/InviteGuild.cs @@ -6,9 +6,41 @@ namespace Discord.API { [JsonProperty("id")] public ulong Id { get; set; } + [JsonProperty("name")] public string Name { get; set; } - [JsonProperty("splash_hash")] - public string SplashHash { get; set; } + + [JsonProperty("splash")] + public Optional Splash { get; set; } + + [JsonProperty("banner")] + public Optional BannerHash { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("icon")] + public Optional IconHash { get; set; } + + [JsonProperty("features")] + public GuildFeatures Features { get; set; } + + [JsonProperty("verification_level")] + public VerificationLevel VerificationLevel { get; set; } + + [JsonProperty("vanity_url_code")] + public Optional VanityUrlCode { get; set; } + + [JsonProperty("premium_subscription_count")] + public Optional PremiumSubscriptionCount { get; set; } + + [JsonProperty("nsfw")] + public Optional Nsfw { get; set; } + + [JsonProperty("nsfw_level")] + public NsfwLevel NsfwLevel { get; set; } + + [JsonProperty("welcome_screen")] + public Optional WelcomeScreen { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/WelcomeScreen.cs b/src/Discord.Net.Rest/API/Common/WelcomeScreen.cs new file mode 100644 index 000000000..a462eaec1 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/WelcomeScreen.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +internal class WelcomeScreen +{ + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("welcome_channels")] + public WelcomeScreenChannel[] WelcomeChannels { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Common/WelcomeScreenChannel.cs b/src/Discord.Net.Rest/API/Common/WelcomeScreenChannel.cs new file mode 100644 index 000000000..426883dc5 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/WelcomeScreenChannel.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +internal class WelcomeScreenChannel +{ + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("emoji_id")] + public Optional EmojiId { get; set; } + + [JsonProperty("emoji_name")] + public Optional EmojiName{ get; set; } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildWelcomeScreenParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildWelcomeScreenParams.cs new file mode 100644 index 000000000..cf22bd927 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildWelcomeScreenParams.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest; + +internal class ModifyGuildWelcomeScreenParams +{ + [JsonProperty("enabled")] + public Optional Enabled { get; set; } + + [JsonProperty("welcome_channels")] + public Optional WelcomeChannels { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } +} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 615e5ac12..cefffadd4 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -2097,6 +2097,35 @@ namespace Discord.API #endregion + #region Guild Welcome Screen + + public async Task GetGuildWelcomeScreenAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + try + { + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/welcome-screen", ids, options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } + } + + public async Task ModifyGuildWelcomeScreenAsync(ModifyGuildWelcomeScreenParams args, ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/welcome-screen", args, ids, options: options).ConfigureAwait(false); + } + + #endregion + #region Users public async Task GetUserAsync(ulong userId, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 74e797fd4..530f7ce5a 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -20,7 +20,8 @@ namespace Discord.Rest public static async Task ModifyAsync(IGuild guild, BaseDiscordClient client, Action func, RequestOptions options) { - if (func == null) throw new ArgumentNullException(nameof(func)); + if (func == null) + throw new ArgumentNullException(nameof(func)); var args = new GuildProperties(); func(args); @@ -141,7 +142,7 @@ namespace Discord.Rest }; var mebibyte = Math.Pow(2, 20); - return (ulong) (tierFactor * mebibyte); + return (ulong)(tierFactor * mebibyte); } #endregion @@ -232,7 +233,8 @@ namespace Discord.Rest public static async Task CreateTextChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options, Action func = null) { - if (name == null) throw new ArgumentNullException(paramName: nameof(name)); + if (name == null) + throw new ArgumentNullException(paramName: nameof(name)); var props = new TextChannelProperties(); func?.Invoke(props); @@ -262,7 +264,8 @@ namespace Discord.Rest public static async Task CreateVoiceChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options, Action func = null) { - if (name == null) throw new ArgumentNullException(paramName: nameof(name)); + if (name == null) + throw new ArgumentNullException(paramName: nameof(name)); var props = new VoiceChannelProperties(); func?.Invoke(props); @@ -318,7 +321,8 @@ namespace Discord.Rest public static async Task CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options, Action func = null) { - if (name == null) throw new ArgumentNullException(paramName: nameof(name)); + if (name == null) + throw new ArgumentNullException(paramName: nameof(name)); var props = new GuildChannelProperties(); func?.Invoke(props); @@ -448,11 +452,13 @@ namespace Discord.Rest RequestOptions options) { var vanityModel = await client.ApiClient.GetVanityInviteAsync(guild.Id, options).ConfigureAwait(false); - if (vanityModel == null) throw new InvalidOperationException("This guild does not have a vanity URL."); + if (vanityModel == null) + throw new InvalidOperationException("This guild does not have a vanity URL."); var inviteModel = await client.ApiClient.GetInviteAsync(vanityModel.Code, options).ConfigureAwait(false); inviteModel.Uses = vanityModel.Uses; return RestInviteMetadata.Create(client, guild, null, inviteModel); } + #endregion #region Roles @@ -460,7 +466,8 @@ namespace Discord.Rest public static async Task CreateRoleAsync(IGuild guild, BaseDiscordClient client, string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) { - if (name == null) throw new ArgumentNullException(paramName: nameof(name)); + if (name == null) + throw new ArgumentNullException(paramName: nameof(name)); var createGuildRoleParams = new API.Rest.ModifyGuildRoleParams { @@ -676,7 +683,8 @@ namespace Discord.Rest public static async Task ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action func, RequestOptions options) { - if (func == null) throw new ArgumentNullException(paramName: nameof(func)); + if (func == null) + throw new ArgumentNullException(paramName: nameof(func)); var props = new EmoteProperties(); func(props); @@ -867,7 +875,7 @@ namespace Discord.Rest { switch (args.Status.Value) { - case GuildScheduledEventStatus.Active when guildEvent.Status != GuildScheduledEventStatus.Scheduled: + case GuildScheduledEventStatus.Active when guildEvent.Status != GuildScheduledEventStatus.Scheduled: case GuildScheduledEventStatus.Completed when guildEvent.Status != GuildScheduledEventStatus.Active: case GuildScheduledEventStatus.Cancelled when guildEvent.Status != GuildScheduledEventStatus.Scheduled: throw new ArgumentException($"Cannot set event to {args.Status.Value} when events status is {guildEvent.Status}"); @@ -909,7 +917,7 @@ namespace Discord.Rest : Optional.Unspecified }; - if(args.Location.IsSpecified) + if (args.Location.IsSpecified) { apiArgs.EntityMetadata = new API.GuildScheduledEventEntityMetadata() { @@ -949,7 +957,7 @@ namespace Discord.Rest Image? bannerImage = null, RequestOptions options = null) { - if(location != null) + if (location != null) { Preconditions.AtMost(location.Length, 100, nameof(location)); } @@ -985,7 +993,7 @@ namespace Discord.Rest Image = bannerImage.HasValue ? bannerImage.Value.ToModel() : Optional.Unspecified }; - if(location != null) + if (location != null) { apiArgs.EntityMetadata = new API.GuildScheduledEventEntityMetadata() { @@ -1004,5 +1012,53 @@ namespace Discord.Rest } #endregion + + #region Welcome Screen + + public static async Task GetWelcomeScreenAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + { + var model = await client.ApiClient.GetGuildWelcomeScreenAsync(guild.Id, options); + + if (model.WelcomeChannels.Length == 0) + return null; + + return new WelcomeScreen(model.Description.GetValueOrDefault(null), model.WelcomeChannels.Select( + x => new WelcomeScreenChannel( + x.ChannelId, x.Description, + x.EmojiName.GetValueOrDefault(null), + x.EmojiId.GetValueOrDefault(0))).ToList()); + } + + public static async Task ModifyWelcomeScreenAsync(bool enabled, string description, WelcomeScreenChannelProperties[] channels, IGuild guild, BaseDiscordClient client, RequestOptions options) + { + if (!guild.Features.HasFeature(GuildFeature.Community)) + throw new InvalidOperationException("Cannot update welcome screen in a non-community guild."); + + var args = new ModifyGuildWelcomeScreenParams + { + Enabled = enabled, + Description = description, + WelcomeChannels = channels?.Select(ch => new API.WelcomeScreenChannel + { + ChannelId = ch.Id, + Description = ch.Description, + EmojiName = ch.Emoji is Emoji emoj ? emoj.Name : Optional.Unspecified, + EmojiId = ch.Emoji is Emote emote ? emote.Id : Optional.Unspecified + }).ToArray() + }; + + var model = await client.ApiClient.ModifyGuildWelcomeScreenAsync(args, guild.Id, options); + + if(model.WelcomeChannels.Length == 0) + return null; + + return new WelcomeScreen(model.Description.GetValueOrDefault(null), model.WelcomeChannels.Select( + x => new WelcomeScreenChannel( + x.ChannelId, x.Description, + x.EmojiName.GetValueOrDefault(null), + x.EmojiId.GetValueOrDefault(0))).ToList()); + } + + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 24f6ae28d..79ec77c21 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1534,6 +1534,15 @@ namespace Discord.Rest else return null; } + + /// + public Task GetWelcomeScreenAsync(RequestOptions options = null) + => GuildHelper.GetWelcomeScreenAsync(this, Discord, options); + + /// + public Task ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null) + => GuildHelper.ModifyWelcomeScreenAsync(enabled, description, channels, this, Discord, options); + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs index 95b454c20..93f79ad29 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Invite; @@ -27,7 +29,17 @@ namespace Discord.Rest public IUser TargetUser { get; private set; } /// public TargetUserType TargetUserType { get; private set; } + + /// + /// Gets the guild this invite is linked to. + /// + /// + /// A partial guild object representing the guild that the invite points to. + /// + public InviteGuild InviteGuild { get; private set; } + internal IChannel Channel { get; } + internal IGuild Guild { get; } /// @@ -59,6 +71,32 @@ namespace Discord.Rest Inviter = model.Inviter.IsSpecified ? RestUser.Create(Discord, model.Inviter.Value) : null; TargetUser = model.TargetUser.IsSpecified ? RestUser.Create(Discord, model.TargetUser.Value) : null; TargetUserType = model.TargetUserType.IsSpecified ? model.TargetUserType.Value : TargetUserType.Undefined; + + if (model.Guild.IsSpecified) + { + InviteGuild = new InviteGuild + (model.Guild.Value.Id, + model.Guild.Value.Name, + model.Guild.Value.Description.IsSpecified ? model.Guild.Value.Description.Value : null, + model.Guild.Value.Splash.IsSpecified ? model.Guild.Value.Splash.Value : null, + model.Guild.Value.BannerHash.IsSpecified ? model.Guild.Value.BannerHash.Value : null, + model.Guild.Value.Features, + model.Guild.Value.IconHash.IsSpecified ? model.Guild.Value.IconHash.Value : null, + model.Guild.Value.VerificationLevel, + model.Guild.Value.VanityUrlCode.IsSpecified ? model.Guild.Value.VanityUrlCode.Value : null, + model.Guild.Value.PremiumSubscriptionCount.GetValueOrDefault(0), + model.Guild.Value.NsfwLevel, + model.Guild.Value.WelcomeScreen.IsSpecified + ? new WelcomeScreen( + model.Guild.Value.WelcomeScreen.Value.Description.IsSpecified ? model.Guild.Value.WelcomeScreen.Value.Description.Value : null, + model.Guild.Value.WelcomeScreen.Value.WelcomeChannels.Select(ch => + new WelcomeScreenChannel( + ch.ChannelId, + ch.Description, + ch.EmojiName.IsSpecified ? ch.EmojiName.Value : null, + ch.EmojiId.IsSpecified ? ch.EmojiId.Value : null)).ToImmutableArray()) + : null); + } } /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index c2778ed1d..f23804fb6 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -2039,6 +2039,14 @@ namespace Discord.WebSocket RequestOptions options) => await BulkOverwriteApplicationCommandAsync(properties, options); + /// + public Task GetWelcomeScreenAsync(RequestOptions options = null) + => GuildHelper.GetWelcomeScreenAsync(this, Discord, options); + + /// + public Task ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null) + => GuildHelper.ModifyWelcomeScreenAsync(enabled, description, channels, this, Discord, options); + void IDisposable.Dispose() { DisconnectAudioAsync().GetAwaiter().GetResult(); From 1d53ea7679dba182148a3256379a01cc63ef741a Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Mon, 26 Dec 2022 05:53:00 +0900 Subject: [PATCH 24/24] [Docs] Fix typo in autocompletion.md (#2548) Interations -> Interactions --- docs/guides/int_framework/autocompletion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/int_framework/autocompletion.md b/docs/guides/int_framework/autocompletion.md index 27da54e36..c09899015 100644 --- a/docs/guides/int_framework/autocompletion.md +++ b/docs/guides/int_framework/autocompletion.md @@ -7,7 +7,7 @@ title: Command Autocompletion [Autocompleters] provide a similar pattern to TypeConverters. [Autocompleters] are cached, singleton services and they are used by the -Interaction Service to handle Autocomplete Interations targeted to a specific Slash Command parameter. +Interaction Service to handle Autocomplete Interactions targeted to a specific Slash Command parameter. To start using AutocompleteHandlers, use the `[AutocompleteAttribute(Type type)]` overload of the [AutocompleteAttribute]. This will dynamically link the parameter to the [AutocompleteHandler] type.