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); } diff --git a/docs/guides/v2_v3_guide/v2_to_v3_guide.md b/docs/guides/v2_v3_guide/v2_to_v3_guide.md index a837f44d2..91fc1b43d 100644 --- a/docs/guides/v2_v3_guide/v2_to_v3_guide.md +++ b/docs/guides/v2_v3_guide/v2_to_v3_guide.md @@ -37,6 +37,7 @@ _client = new DiscordSocketClient(config); - AllUnprivileged: This is a group of most common intents, that do NOT require any [developer portal] intents to be enabled. This includes intents that receive messages such as: `GatewayIntents.GuildMessages, GatewayIntents.DirectMessages` - GuildMembers: An intent disabled by default, as you need to enable it in the [developer portal]. +- MessageContent: An intent also disabled by default as you also need to enable it in the [developer portal]. - GuildPresences: Also disabled by default, this intent together with `GuildMembers` are the only intents not included in `AllUnprivileged`. - All: All intents, it is ill advised to use this without care, as it _can_ cause a memory leak from presence. The library will give responsive warnings if you specify unnecessary intents. diff --git a/samples/BasicBot/Program.cs b/samples/BasicBot/Program.cs index 179dfce05..a71de9fc8 100644 --- a/samples/BasicBot/Program.cs +++ b/samples/BasicBot/Program.cs @@ -34,9 +34,16 @@ namespace BasicBot public Program() { + // Config used by DiscordSocketClient + // Define intents for the client + var config = new DiscordSocketConfig + { + GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent + }; + // It is recommended to Dispose of a client when you are finished // using it, at the end of your app's lifetime. - _client = new DiscordSocketClient(); + _client = new DiscordSocketClient(config); // Subscribing to client events, so that we may receive them whenever they're invoked. _client.Log += LogAsync; diff --git a/samples/BasicBot/_BasicBot.csproj b/samples/BasicBot/_BasicBot.csproj index e6245d340..7d3004ad9 100644 --- a/samples/BasicBot/_BasicBot.csproj +++ b/samples/BasicBot/_BasicBot.csproj @@ -1,4 +1,4 @@ - + Exe @@ -6,7 +6,7 @@ - + diff --git a/samples/InteractionFramework/_InteractionFramework.csproj b/samples/InteractionFramework/_InteractionFramework.csproj index 8892a65b7..a0fa14d74 100644 --- a/samples/InteractionFramework/_InteractionFramework.csproj +++ b/samples/InteractionFramework/_InteractionFramework.csproj @@ -13,7 +13,7 @@ - + diff --git a/samples/ShardedClient/Program.cs b/samples/ShardedClient/Program.cs index 2b8f49edb..cb7b0dbb3 100644 --- a/samples/ShardedClient/Program.cs +++ b/samples/ShardedClient/Program.cs @@ -28,7 +28,8 @@ namespace ShardedClient // have 1 shard per 1500-2000 guilds your bot is in. var config = new DiscordSocketConfig { - TotalShards = 2 + TotalShards = 2, + GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent }; // You should dispose a service provider created using ASP.NET diff --git a/samples/ShardedClient/_ShardedClient.csproj b/samples/ShardedClient/_ShardedClient.csproj index 68a43c7cd..5c1c6a20c 100644 --- a/samples/ShardedClient/_ShardedClient.csproj +++ b/samples/ShardedClient/_ShardedClient.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/TextCommandFramework/Program.cs b/samples/TextCommandFramework/Program.cs index 8a18daf72..ccd23436e 100644 --- a/samples/TextCommandFramework/Program.cs +++ b/samples/TextCommandFramework/Program.cs @@ -60,6 +60,10 @@ namespace TextCommandFramework private ServiceProvider ConfigureServices() { return new ServiceCollection() + .AddSingleton(new DiscordSocketConfig + { + GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent + }) .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/samples/TextCommandFramework/_TextCommandFramework.csproj b/samples/TextCommandFramework/_TextCommandFramework.csproj index 6e00625e8..5307303ce 100644 --- a/samples/TextCommandFramework/_TextCommandFramework.csproj +++ b/samples/TextCommandFramework/_TextCommandFramework.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/samples/WebhookClient/_WebhookClient.csproj b/samples/WebhookClient/_WebhookClient.csproj index 515fcf3a4..acea75d2c 100644 --- a/samples/WebhookClient/_WebhookClient.csproj +++ b/samples/WebhookClient/_WebhookClient.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 60b7d20d8..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) @@ -116,24 +116,24 @@ 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, - GuildWigitDisabled = 50004, + GuildWidgetDisabled = 50004, CannotEditOtherUsersMessage = 50005, CannotSendEmptyMessage = 50006, CannotSendMessageToUser = 50007, CannotSendMessageToVoiceChannel = 50008, - ChannelVerificationTooHight = 50009, + ChannelVerificationTooHigh = 50009, OAuth2ApplicationDoesntHaveBot = 50010, OAuth2ApplicationLimitReached = 50011, InvalidOAuth2State = 50012, @@ -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, @@ -172,9 +174,8 @@ namespace Discord ServerRequiresMonetization = 50097, ServerRequiresBoosts = 50101, RequestBodyContainsInvalidJSON = 50109, - FailedToResizeAssetBelowTheMaximumSize = 50138, OwnershipCannotBeTransferredToABotUser = 50132, - AssetResizeBelowTheMaximumSize= 50138, + FailedToResizeAssetBelowTheMaximumSize = 50138, UploadedFileNotFound = 50146, MissingPermissionToSendThisSticker = 50600, #endregion @@ -213,8 +214,8 @@ namespace Discord LottieCantContainRasters = 170002, StickerMaximumFramerateExceeded = 170003, StickerMaximumFrameCountExceeded = 170004, - LottieMaximumDimentionsExceeded = 170005, - StickerFramerateBoundsExceeed = 170006, + LottieMaximumDimensionsExceeded = 170005, + StickerFramerateBoundsExceeded = 170006, StickerAnimationDurationTooLong = 170007, #endregion @@ -222,7 +223,7 @@ namespace Discord CannotUpdateFinishedEvent = 180000, FailedStageCreation = 180002, #endregion - + #region Forum & Automod MessageWasBlockedByAutomaticModeration = 200000, TitleWasBlockedByAutomaticModeration = 200001, 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/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/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/Entities/Messages/TimestampTag.cs b/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs index 3beffdbb6..1ca6dc41c 100644 --- a/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs +++ b/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs @@ -5,17 +5,28 @@ namespace Discord /// /// Represents a class used to make timestamps in messages. see . /// - public class TimestampTag + public readonly struct TimestampTag { /// - /// Gets or sets the style of the timestamp tag. + /// Gets the time for this timestamp tag. /// - public TimestampTagStyles Style { get; set; } = TimestampTagStyles.ShortDateTime; + public DateTimeOffset Time { get; } /// - /// Gets or sets the time for this timestamp tag. + /// Gets the style of this tag. if none was provided. /// - public DateTimeOffset Time { get; set; } + public TimestampTagStyles? Style { get; } + + /// + /// Creates a new from the provided time. + /// + /// The time for this timestamp tag. + /// The style for this timestamp tag. + public TimestampTag(DateTimeOffset time, TimestampTagStyles? style = null) + { + Time = time; + Style = style; + } /// /// Converts the current timestamp tag to the string representation supported by discord. @@ -23,11 +34,23 @@ namespace Discord /// If the is null then the default 0 will be used. /// /// + /// + /// Will use the provided if provided. If this value is null, it will default to . + /// /// A string that is compatible in a discord message, ex: <t:1625944201:f> public override string ToString() - { - return $""; - } + => ToString(Style ?? TimestampTagStyles.ShortDateTime); + + /// + /// Converts the current timestamp tag to the string representation supported by discord. + /// + /// If the is null then the default 0 will be used. + /// + /// + /// The formatting style for this tag. + /// A string that is compatible in a discord message, ex: <t:1625944201:f> + public string ToString(TimestampTagStyles style) + => $""; /// /// Creates a new timestamp tag with the specified object. @@ -35,14 +58,8 @@ namespace Discord /// The time of this timestamp tag. /// The style for this timestamp tag. /// The newly create timestamp tag. - public static TimestampTag FromDateTime(DateTime time, TimestampTagStyles style = TimestampTagStyles.ShortDateTime) - { - return new TimestampTag - { - Style = style, - Time = time - }; - } + public static TimestampTag FromDateTime(DateTime time, TimestampTagStyles? style = null) + => new(time, style); /// /// Creates a new timestamp tag with the specified object. @@ -50,13 +67,25 @@ namespace Discord /// The time of this timestamp tag. /// The style for this timestamp tag. /// The newly create timestamp tag. - public static TimestampTag FromDateTimeOffset(DateTimeOffset time, TimestampTagStyles style = TimestampTagStyles.ShortDateTime) - { - return new TimestampTag - { - Style = style, - Time = time - }; - } + public static TimestampTag FromDateTimeOffset(DateTimeOffset time, TimestampTagStyles? style = null) + => new(time, style); + + /// + /// Immediately formats the provided time and style into a timestamp string. + /// + /// The time of this timestamp tag. + /// The style for this timestamp tag. + /// The newly create timestamp string. + public static string FormatFromDateTime(DateTime time, TimestampTagStyles style) + => FormatFromDateTimeOffset(time, style); + + /// + /// Immediately formats the provided time and style into a timestamp string. + /// + /// The time of this timestamp tag. + /// The style for this timestamp tag. + /// The newly create timestamp string. + public static string FormatFromDateTimeOffset(DateTimeOffset time, TimestampTagStyles style) + => $""; } -} \ No newline at end of file +} 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.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.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); 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/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; } /// 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/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f0f99933e..f2239010f 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; } /// @@ -906,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++) 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/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 566d32067..31e973788 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 @@ + + + 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();