From 01ae904fe1703454699a408ae6f0a17afb929040 Mon Sep 17 00:00:00 2001 From: Misha133 <61027276+Misha-133@users.noreply.github.com> Date: Mon, 7 Nov 2022 19:25:49 +0300 Subject: [PATCH] [Feature] Add missing properties in forum & thread channels (#2469) * add `AppliedTags` property * convert collections into immutable arrays * remove "not supported" remark * implement `ThreadChannelProperties` * Add `DefaultSlowModeInterval` and `DefaultSlowModeInterval` properties to forum channels * add `Moderated` property to `ForumTag`` * `ForumTag` inherits `ISnowflakeEntity` * Fix `DiscordRestClient.GetChannelAsync` not getting forum channel * a lot of changes added: - channel flags - `ForumTagBuilder` - imroved channel modification * fixed a bug in forum tag emoji parsing * inherit forum channel from `INesteeChannel` * implement `INestedChannel` in forum channels * Add `Flags` property to channels * add iteraface for forum tags & add equality operators * Add default reaction emoji property * add support for modifing default reaction & some renaming * add createForumChannelAsync to guild * *fix resharper being a d... and moving code to next line* * add a `ForumChannels` property * Some fixes & add support for `default_sort_order` * fix misleading comment * fix #2502 * support creating post with applied tags * fix xmldoc * set category id on model update * add limit checks for tag count --- .../Entities/Channels/ChannelFlags.cs | 22 ++ .../Channels/ForumChannelProperties.cs | 60 ++++++ .../Entities/Channels/ForumSortOrder.cs | 17 ++ .../Channels/GuildChannelProperties.cs | 5 + .../Entities/Channels/IForumChannel.cs | 68 ++++++- .../Entities/Channels/IGuildChannel.cs | 11 + .../Entities/Channels/IThreadChannel.cs | 20 ++ .../Channels/TextChannelProperties.cs | 13 +- .../Channels/ThreadChannelProperties.cs | 26 +++ src/Discord.Net.Core/Entities/ForumTag.cs | 42 ---- .../Entities/ForumTags/ForumTag.cs | 67 ++++++ .../Entities/ForumTags/ForumTagBuilder.cs | 191 ++++++++++++++++++ .../ForumTags/ForumTagBuilderExtensions.cs | 11 + .../Entities/ForumTags/ForumTagProperties.cs | 48 +++++ .../Entities/ForumTags/IForumTag.cs | 29 +++ .../Entities/Guilds/IGuild.cs | 12 ++ .../ApplicationCommandOptionType.cs | 2 +- .../Extensions/ChannelExtensions.cs | 3 + src/Discord.Net.Rest/API/Common/Channel.cs | 18 +- .../API/Common/ForumReactionEmoji.cs | 12 ++ src/Discord.Net.Rest/API/Common/ForumTags.cs | 3 + .../API/Rest/CreateGuildChannelParams.cs | 12 ++ .../API/Rest/CreateMultipartPostAsync.cs | 3 + .../API/Rest/CreatePostParams.cs | 3 + .../API/Rest/ModifyForumChannelParams.cs | 23 +++ .../Rest/ModifyForumReactionEmojiParams.cs | 15 ++ .../API/Rest/ModifyForumTagParams.cs | 23 +++ .../API/Rest/ModifyGuildChannelParams.cs | 2 + .../API/Rest/ModifyThreadParams.cs | 7 + .../Entities/Channels/ChannelHelper.cs | 1 + .../Entities/Channels/ForumHelper.cs | 63 ++++++ .../Entities/Channels/RestChannel.cs | 7 +- .../Entities/Channels/RestForumChannel.cs | 118 ++++++++--- .../Entities/Channels/RestGuildChannel.cs | 5 + .../Entities/Channels/RestThreadChannel.cs | 12 ++ .../Entities/Channels/ThreadHelper.cs | 35 +++- .../Entities/Guilds/GuildHelper.cs | 61 ++++++ .../Entities/Guilds/RestGuild.cs | 16 ++ .../Entities/Channels/SocketForumChannel.cs | 114 +++++++++-- .../Entities/Channels/SocketGuildChannel.cs | 5 + .../Entities/Channels/SocketThreadChannel.cs | 12 +- .../Entities/Guilds/SocketGuild.cs | 28 +++ .../MockedEntities/MockedCategoryChannel.cs | 2 + .../MockedEntities/MockedTextChannel.cs | 2 + .../MockedEntities/MockedVoiceChannel.cs | 2 + 45 files changed, 1132 insertions(+), 119 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs delete mode 100644 src/Discord.Net.Core/Entities/ForumTag.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs create mode 100644 src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs create mode 100644 src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs create mode 100644 src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs create mode 100644 src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs diff --git a/src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs b/src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs new file mode 100644 index 000000000..37f34a90e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs @@ -0,0 +1,22 @@ +namespace Discord; + +/// +/// Represents public flags for a channel. +/// +public enum ChannelFlags +{ + /// + /// Default value for flags, when none are given to a channel. + /// + None = 0, + + /// + /// Flag given to a thread channel pinned on top of parent forum channel. + /// + Pinned = 1 << 1, + + /// + /// Flag given to a forum channel that requires people to select tags when posting. + /// + RequireTag = 1 << 4 +} diff --git a/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs new file mode 100644 index 000000000..e1a123b37 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; + +namespace Discord; + +public class ForumChannelProperties : TextChannelProperties +{ + + /// + /// Gets or sets the topic of the channel. + /// + /// + /// Not available in forum channels. + /// + public new Optional SlowModeInterval { get; } + + /// + /// Gets or sets rate limit on creating posts in this forum channel. + /// + /// + /// Setting this value to anything above zero will require each user to wait X seconds before + /// creating another thread; setting this value to 0 will disable rate limits for this channel. + /// + /// Users with or + /// will be exempt from rate limits. + /// + /// + /// Thrown if the value does not fall within [0, 21600]. + public Optional ThreadCreationInterval { get; set; } + + + /// + /// Gets or sets the default slow-mode for threads in this channel. + /// + /// + /// Setting this value to anything above zero will require each user to wait X seconds before + /// sending another message; setting this value to 0 will disable slow-mode for child threads. + /// + /// Users with or + /// will be exempt from slow-mode. + /// + /// + /// Thrown if the value does not fall within [0, 21600]. + public Optional DefaultSlowModeInterval { get; set; } + + /// + /// Gets or sets a collection of tags inside of this forum channel. + /// + public Optional> Tags { get; set; } + + /// + /// Gets or sets a new default reaction emoji in this forum channel. + /// + public Optional DefaultReactionEmoji { get; set; } + + /// + /// Gets or sets the rule used to order posts in forum channels. + /// + public Optional DefaultSortOrder { get; set; } +} diff --git a/src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs b/src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs new file mode 100644 index 000000000..2a576d978 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs @@ -0,0 +1,17 @@ +namespace Discord; + +/// +/// Defines the rule used to order posts in forum channels. +/// +public enum ForumSortOrder +{ + /// + /// Sort forum posts by activity. + /// + LatestActivity = 0, + + /// + /// Sort forum posts by creation time (from most recent to oldest). + /// + CreationDate = 1 +} diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index 339d6fffd..1e7d69c2d 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -36,5 +36,10 @@ namespace Discord /// Gets or sets the permission overwrites for this channel. /// public Optional> PermissionOverwrites { get; set; } + + /// + /// Gets or sets the flags of the channel. + /// + public Optional Flags { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs index f4c6da2e2..55521bade 100644 --- a/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Discord { - public interface IForumChannel : IGuildChannel, IMentionable + public interface IForumChannel : IGuildChannel, IMentionable, INestedChannel { /// /// Gets a value that indicates whether the channel is NSFW. @@ -35,6 +35,55 @@ namespace Discord /// IReadOnlyCollection Tags { get; } + /// + /// Gets the current rate limit on creating posts in this forum channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// + int ThreadCreationInterval { get; } + + /// + /// Gets the current default slow-mode delay for threads in this forum channel. + /// + /// + /// An representing the time in seconds required before the user can send another + /// message; 0 if disabled. + /// + int DefaultSlowModeInterval { get; } + + /// + /// Gets the emoji to show in the add reaction button on a thread in a forum channel + /// + /// + /// If the emoji is only the will be populated. + /// Use to get the emoji. + /// + IEmote DefaultReactionEmoji { get; } + + /// + /// Gets or sets the rule used to order posts in forum channels. + /// + /// + /// Defaults to null, which indicates a preferred sort order hasn't been set + /// + ForumSortOrder? DefaultSortOrder { get; } + + /// + /// Modifies this forum channel. + /// + /// + /// This method modifies the current forum channel with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// The delegate containing the properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); + /// /// Creates a new post (thread) within the forum. /// @@ -52,12 +101,13 @@ namespace Discord /// A collection of stickers to send with the message. /// A array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Creates a new post (thread) within the forum. @@ -78,13 +128,14 @@ namespace Discord /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// Task CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Creates a new post (thread) within the forum. @@ -106,13 +157,14 @@ namespace Discord /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// public Task CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None); + ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Creates a new post (thread) within the forum. @@ -132,12 +184,13 @@ namespace Discord /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// public Task CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Creates a new post (thread) within the forum. @@ -155,14 +208,15 @@ namespace Discord /// /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. - /// A array of s to send with this response. Max 10. + /// An array of s to send with this response. Max 10. /// A message flag to be applied to the sent message, only is permitted. + /// An array of to be applied to the post. /// /// A task that represents the asynchronous creation operation. /// public Task CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null); /// /// Gets a collection of active threads within this forum channel. diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 992bd71fc..12874f2c2 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -21,6 +21,17 @@ namespace Discord /// int Position { get; } + /// + /// Gets the flags related to this channel. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// A channel's flags, if any is associated. + /// + ChannelFlags Flags { get; } + /// /// Gets the guild associated with this channel. /// diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs index f03edbbf9..52df07dcc 100644 --- a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord @@ -56,6 +57,14 @@ namespace Discord /// bool? IsInvitable { get; } + /// + /// Gets ids of tags applied to a forum thread + /// + /// + /// This property is only available on forum threads. + /// + IReadOnlyCollection AppliedTags { get; } + /// /// Gets when the thread was created. /// @@ -102,5 +111,16 @@ namespace Discord /// A task that represents the asynchronous operation of removing a user from this thread. /// Task RemoveUserAsync(IGuildUser user, RequestOptions options = null); + + /// + /// Modifies this thread channel. + /// + /// The delegate containing the properties to modify the channel with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// + Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs index 2dceb025c..acd69f480 100644 --- a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Discord { @@ -39,20 +40,10 @@ namespace Discord /// Thrown if the value does not fall within [0, 21600]. public Optional SlowModeInterval { get; set; } - /// - /// Gets or sets whether or not the thread is archived. - /// - public Optional Archived { get; set; } - - /// - /// Gets or sets whether or not the thread is locked. - /// - public Optional Locked { get; set; } - /// /// Gets or sets the auto archive duration. /// public Optional AutoArchiveDuration { get; set; } - + } } diff --git a/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs new file mode 100644 index 000000000..af5c44129 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Discord; + + +/// +/// Provides properties that are used to modify an with the specified changes. +/// +/// +public class ThreadChannelProperties : TextChannelProperties +{ + /// + /// Gets or sets the tags applied to a forum thread + /// + public Optional> AppliedTags { get; set; } + + /// + /// Gets or sets whether or not the thread is locked. + /// + public Optional Locked { get; set; } + + /// + /// Gets or sets whether or not the thread is archived. + /// + public Optional Archived { get; set; } +} diff --git a/src/Discord.Net.Core/Entities/ForumTag.cs b/src/Discord.Net.Core/Entities/ForumTag.cs deleted file mode 100644 index 26ae4301e..000000000 --- a/src/Discord.Net.Core/Entities/ForumTag.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord -{ - /// - /// A struct representing a forum channel tag. - /// - public struct ForumTag - { - /// - /// Gets the Id of the tag. - /// - public ulong Id { get; } - - /// - /// Gets the name of the tag. - /// - public string Name { get; } - - /// - /// Gets the emoji of the tag or if none is set. - /// - public IEmote Emoji { get; } - - internal ForumTag(ulong id, string name, ulong? emojiId, string emojiName) - { - if (emojiId.HasValue && emojiId.Value != 0) - Emoji = new Emote(emojiId.Value, emojiName, false); - else if (emojiName != null) - Emoji = new Emoji(name); - else - Emoji = null; - - Id = id; - Name = name; - } - } -} diff --git a/src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs b/src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs new file mode 100644 index 000000000..afdb99bf7 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +#nullable enable + +namespace Discord +{ + /// + /// A struct representing a forum channel tag. + /// + public struct ForumTag : ISnowflakeEntity, IForumTag + { + /// + /// Gets the Id of the tag. + /// + public ulong Id { get; } + + /// + public string Name { get; } + + /// + public IEmote? Emoji { get; } + + /// + public bool IsModerated { get; } + + /// + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + + internal ForumTag(ulong id, string name, ulong? emojiId = null, string? emojiName = null, bool moderated = false) + { + if (emojiId.HasValue && emojiId.Value != 0) + Emoji = new Emote(emojiId.Value, null, false); + else if (emojiName != null) + Emoji = new Emoji(emojiName); + else + Emoji = null; + + Id = id; + Name = name; + IsModerated = moderated; + } + + public override int GetHashCode() => (Id, Name, Emoji, IsModerated).GetHashCode(); + + public override bool Equals(object? obj) + => obj is ForumTag tag && Equals(tag); + + /// + /// Gets whether supplied tag is equals to the current one. + /// + public bool Equals(ForumTag tag) + => Id == tag.Id && + Name == tag.Name && + (Emoji is Emoji emoji && tag.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) || + Emoji is Emote emote && tag.Emoji is Emote otherEmote && emote.Equals(otherEmote)) && + IsModerated == tag.IsModerated; + + public static bool operator ==(ForumTag? left, ForumTag? right) + => left?.Equals(right) ?? right is null; + + public static bool operator !=(ForumTag? left, ForumTag? right) => !(left == right); + } +} diff --git a/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs b/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs new file mode 100644 index 000000000..d8e881189 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs @@ -0,0 +1,191 @@ +#nullable enable +using System; + +namespace Discord; + +public class ForumTagBuilder +{ + private string? _name; + private IEmote? _emoji; + private bool _moderated; + private ulong? _id; + + /// + /// Returns the maximum length of name allowed by Discord. + /// + public const int MaxNameLength = 20; + + /// + /// Gets or sets the snowflake Id of the tag. + /// + /// + /// If set this will update existing tag or will create a new one otherwise. + /// + public ulong? Id + { + get { return _id; } + set { _id = value; } + } + + /// + /// Gets or sets the name of the tag. + /// + /// Name length must be less than or equal to . + public string? Name + { + get { return _name; } + set + { + if (value?.Length > MaxNameLength) + throw new ArgumentException(message: $"Name length must be less than or equal to {MaxNameLength}.", paramName: nameof(Name)); + _name = value; + } + } + + /// + /// Gets or sets the emoji of the tag. + /// + public IEmote? Emoji + { + get { return _emoji; } + set { _emoji = value; } + } + + /// + /// Gets or sets whether this tag can only be added to or removed from threads by a member + /// with the permission + /// + public bool IsModerated + { + get { return _moderated; } + set { _moderated = value; } + } + + /// + /// Initializes a new class. + /// + public ForumTagBuilder() + { + + } + + /// + /// Initializes a new class with values + /// + /// If set existing tag will be updated or a new one will be created otherwise. + /// Name of the tag. + /// Sets whether this tag can only be added to or removed from threads by a member + /// with the permission. + public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false) + { + Name = name; + IsModerated = isModerated; + Id = id; + } + + /// + /// Initializes a new class with values + /// + /// Name of the tag. + /// If set existing tag will be updated or a new one will be created otherwise. + /// Display emoji of the tag. + /// Sets whether this tag can only be added to or removed from threads by a member + /// with the permission. + public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false, IEmote? emoji = null) + { + Name = name; + Emoji = emoji; + IsModerated = isModerated; + Id = id; + } + + /// + /// Initializes a new class with values + /// + /// /// Name of the tag. + /// If set existing tag will be updated or a new one will be created otherwise. + /// The id of custom Display emoji of the tag. + /// Sets whether this tag can only be added to or removed from threads by a member + /// with the permission + public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false, ulong? emoteId = null) + { + Name = name; + if(emoteId is not null) + Emoji = new Emote(emoteId.Value, null, false); + IsModerated = isModerated; + Id = id; + } + + /// + /// Builds the Tag. + /// + /// An instance of + /// "Name must be set to build the tag" + public ForumTagProperties Build() + { + if (_name is null) + throw new ArgumentNullException(nameof(Name), "Name must be set to build the tag"); + return new ForumTagProperties(_name!, _emoji, _moderated); + } + + /// + /// Sets the name of the tag. + /// + /// Name length must be less than or equal to . + public ForumTagBuilder WithName(string name) + { + Name = name; + return this; + } + + /// + /// Sets the id of the tag. + /// + /// If set existing tag will be updated or a new one will be created otherwise. + /// Name length must be less than or equal to . + public ForumTagBuilder WithId(ulong? id) + { + Id = id; + return this; + } + + /// + /// Sets the emoji of the tag. + /// + public ForumTagBuilder WithEmoji(IEmote? emoji) + { + Emoji = emoji; + return this; + } + + /// + /// Sets whether this tag can only be added to or removed from threads by a member + /// with the permission + /// + public ForumTagBuilder WithModerated(bool moderated) + { + IsModerated = moderated; + return this; + } + + public override int GetHashCode() => base.GetHashCode(); + + public override bool Equals(object? obj) + => obj is ForumTagBuilder builder && Equals(builder); + + /// + /// Gets whether supplied tag builder is equals to the current one. + /// + public bool Equals(ForumTagBuilder? builder) + => builder is not null && + Id == builder.Id && + Name == builder.Name && + (Emoji is Emoji emoji && builder.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) || + Emoji is Emote emote && builder.Emoji is Emote otherEmote && emote.Equals(otherEmote)) && + IsModerated == builder.IsModerated; + + public static bool operator ==(ForumTagBuilder? left, ForumTagBuilder? right) + => left?.Equals(right) ?? right is null ; + + public static bool operator !=(ForumTagBuilder? left, ForumTagBuilder? right) => !(left == right); +} diff --git a/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs b/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs new file mode 100644 index 000000000..73a953fe6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs @@ -0,0 +1,11 @@ +namespace Discord; + +public static class ForumTagBuilderExtensions +{ + public static ForumTagBuilder ToForumTagBuilder(this ForumTag tag) + => new ForumTagBuilder(tag.Name, tag.Id, tag.IsModerated, tag.Emoji); + + public static ForumTagBuilder ToForumTagBuilder(this ForumTagProperties tag) + => new ForumTagBuilder(tag.Name, tag.Id, tag.IsModerated, tag.Emoji); + +} diff --git a/src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs b/src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs new file mode 100644 index 000000000..6ded49204 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs @@ -0,0 +1,48 @@ +namespace Discord; + +#nullable enable + +public class ForumTagProperties : IForumTag +{ + /// + /// Gets the Id of the tag. + /// + public ulong Id { get; } + + /// + public string Name { get; } + + /// + public IEmote? Emoji { get; } + + /// + public bool IsModerated { get; } + + internal ForumTagProperties(string name, IEmote? emoji = null, bool isMmoderated = false) + { + Name = name; + Emoji = emoji; + IsModerated = isMmoderated; + } + + public override int GetHashCode() => (Id, Name, Emoji, IsModerated).GetHashCode(); + + public override bool Equals(object? obj) + => obj is ForumTagProperties tag && Equals(tag); + + /// + /// Gets whether supplied tag is equals to the current one. + /// + public bool Equals(ForumTagProperties? tag) + => tag is not null && + Id == tag.Id && + Name == tag.Name && + (Emoji is Emoji emoji && tag.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) || + Emoji is Emote emote && tag.Emoji is Emote otherEmote && emote.Equals(otherEmote)) && + IsModerated == tag.IsModerated; + + public static bool operator ==(ForumTagProperties? left, ForumTagProperties? right) + => left?.Equals(right) ?? right is null; + + public static bool operator !=(ForumTagProperties? left, ForumTagProperties? right) => !(left == right); +} diff --git a/src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs b/src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs new file mode 100644 index 000000000..8b8b866b2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs @@ -0,0 +1,29 @@ +namespace Discord; + +#nullable enable + +/// +/// Represents a Discord forum tag +/// +public interface IForumTag +{ + /// + /// Gets the name of the tag. + /// + string Name { get; } + + /// + /// Gets the emoji of the tag or if none is set. + /// + /// + /// If the emoji is only the will be populated. + /// Use to get the emoji. + /// + IEmote? Emoji { get; } + + /// + /// Gets whether this tag can only be added to or removed from threads by a member + /// with the permission + /// + bool IsModerated { get; } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 34a08f1e7..d1ff7b99c 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -761,6 +761,18 @@ namespace Discord /// Task CreateCategoryAsync(string name, Action func = null, RequestOptions options = null); + /// + /// Creates a new channel forum in this guild. + /// + /// The new name for the forum. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// forum channel. + /// + Task CreateForumChannelAsync(string name, Action func = null, RequestOptions options = null); + /// /// Gets a collection of all the voice regions this guild can access. /// diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs index 4506b66d9..2bad7fcb7 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs @@ -21,7 +21,7 @@ namespace Discord String = 3, /// - /// An . + /// An . /// Integer = 4, diff --git a/src/Discord.Net.Core/Extensions/ChannelExtensions.cs b/src/Discord.Net.Core/Extensions/ChannelExtensions.cs index b5ddae1cf..a24588792 100644 --- a/src/Discord.Net.Core/Extensions/ChannelExtensions.cs +++ b/src/Discord.Net.Core/Extensions/ChannelExtensions.cs @@ -46,6 +46,9 @@ namespace Discord case ITextChannel: return ChannelType.Text; + + case IForumChannel: + return ChannelType.Forum; } return null; diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index d9d7d469c..f9184cd1f 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -70,8 +70,24 @@ namespace Discord.API //ForumChannel [JsonProperty("available_tags")] public Optional ForumTags { get; set; } - + + [JsonProperty("applied_tags")] + public Optional AppliedTags { get; set; } + [JsonProperty("default_auto_archive_duration")] public Optional AutoArchiveDuration { get; set; } + + [JsonProperty("default_thread_rate_limit_per_user")] + public Optional ThreadRateLimitPerUser { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } + + [JsonProperty("default_sort_order")] + public Optional DefaultSortOrder { get; set; } + + [JsonProperty("default_reaction_emoji")] + public Optional DefaultReactionEmoji { get; set; } + } } diff --git a/src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs b/src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs new file mode 100644 index 000000000..ae2d2b546 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +public class ForumReactionEmoji +{ + [JsonProperty("emoji_id")] + public ulong? EmojiId { get; set; } + + [JsonProperty("emoji_name")] + public Optional EmojiName { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Common/ForumTags.cs b/src/Discord.Net.Rest/API/Common/ForumTags.cs index 18354e7b2..c4a1fa2a2 100644 --- a/src/Discord.Net.Rest/API/Common/ForumTags.cs +++ b/src/Discord.Net.Rest/API/Common/ForumTags.cs @@ -17,5 +17,8 @@ namespace Discord.API public Optional EmojiId { get; set; } [JsonProperty("emoji_name")] public Optional EmojiName { get; set; } + + [JsonProperty("moderated")] + public bool Moderated { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs index 57816e448..74590fb35 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs @@ -23,6 +23,8 @@ namespace Discord.API.Rest public Optional IsNsfw { get; set; } [JsonProperty("rate_limit_per_user")] public Optional SlowModeInterval { get; set; } + [JsonProperty("default_auto_archive_duration")] + public Optional DefaultAutoArchiveDuration { get; set; } //Voice channels [JsonProperty("bitrate")] @@ -30,6 +32,16 @@ namespace Discord.API.Rest [JsonProperty("user_limit")] public Optional UserLimit { get; set; } + //Forum channels + [JsonProperty("default_reaction_emoji")] + public Optional DefaultReactionEmoji { get; set; } + [JsonProperty("default_thread_rate_limit_per_user")] + public Optional ThreadRateLimitPerUser { get; set; } + [JsonProperty("available_tags")] + public Optional AvailableTags { get; set; } + [JsonProperty("default_sort_order")] + public Optional DefaultSortOrder { get; set; } + public CreateGuildChannelParams(string name, ChannelType type) { Name = name; diff --git a/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs index 0c8bc5494..bb10a4681 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs @@ -27,6 +27,7 @@ namespace Discord.API.Rest public Optional MessageComponent { get; set; } public Optional Flags { get; set; } public Optional Stickers { get; set; } + public Optional TagIds { get; set; } public CreateMultipartPostAsync(params FileAttachment[] attachments) { @@ -59,6 +60,8 @@ namespace Discord.API.Rest message["sticker_ids"] = Stickers.Value; if (Flags.IsSpecified) message["flags"] = Flags.Value; + if (TagIds.IsSpecified) + message["applied_tags"] = TagIds.Value; List attachments = new(); diff --git a/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs index 974e07c0a..d74678f63 100644 --- a/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs @@ -21,5 +21,8 @@ namespace Discord.API.Rest [JsonProperty("message")] public ForumThreadMessage Message { get; set; } + + [JsonProperty("applied_tags")] + public Optional Tags { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs new file mode 100644 index 000000000..d8733a2bd --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest; + + +[JsonObject(MemberSerialization = MemberSerialization.OptIn)] +internal class ModifyForumChannelParams : ModifyTextChannelParams +{ + [JsonProperty("available_tags")] + public Optional Tags { get; set; } + + [JsonProperty("default_thread_rate_limit_per_user")] + public Optional DefaultSlowModeInterval { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional ThreadCreationInterval { get; set; } + + [JsonProperty("default_reaction_emoji")] + public Optional DefaultReactionEmoji { get; set; } + + [JsonProperty("default_sort_order")] + public Optional DefaultSortOrder { get; set; } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs new file mode 100644 index 000000000..d659f70cd --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Discord.API; + +[JsonObject(MemberSerialization = MemberSerialization.OptIn)] +public class ModifyForumReactionEmojiParams +{ + [JsonProperty("emoji_id")] + public Optional EmojiId { get; set; } + + [JsonProperty("emoji_name")] + public Optional EmojiName { get; set; } +} + + diff --git a/src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs new file mode 100644 index 000000000..4d792d0af --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyForumTagParams + { + [JsonProperty("id")] + public Optional Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("emoji_id")] + public Optional EmojiId { get; set; } + + [JsonProperty("emoji_name")] + public Optional EmojiName { get; set; } + + [JsonProperty("moderated")] + public bool Moderated { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs index dfe9cd980..dea0c037f 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs @@ -13,5 +13,7 @@ namespace Discord.API.Rest public Optional CategoryId { get; set; } [JsonProperty("permission_overwrites")] public Optional Overwrites { get; set; } + [JsonProperty("flags")] + public Optional Flags { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs index 8c9216c3f..bd651b22c 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System.Collections.Generic; namespace Discord.API.Rest { @@ -18,5 +19,11 @@ namespace Discord.API.Rest [JsonProperty("rate_limit_per_user")] public Optional Slowmode { get; set; } + + [JsonProperty("applied_tags")] + public Optional> AppliedTags { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index d66fd5e51..4e353c39b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -38,6 +38,7 @@ namespace Discord.Rest Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), + Flags = args.Flags.GetValueOrDefault(), }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } diff --git a/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs new file mode 100644 index 000000000..3d087e77d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs @@ -0,0 +1,63 @@ +using Discord.API; +using System; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest; + +internal static class ForumHelper +{ + public static async Task ModifyAsync(IForumChannel channel, BaseDiscordClient client, + Action func, + RequestOptions options) + { + var args = new ForumChannelProperties(); + func(args); + + Preconditions.AtMost(args.Tags.IsSpecified ? args.Tags.Value.Count() : 0, 5, nameof(args.Tags), "Forum channel can have max 20 tags."); + + var apiArgs = new API.Rest.ModifyForumChannelParams() + { + Name = args.Name, + Position = args.Position, + CategoryId = args.CategoryId, + Overwrites = args.PermissionOverwrites.IsSpecified + ? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), + DefaultSlowModeInterval = args.DefaultSlowModeInterval, + ThreadCreationInterval = args.ThreadCreationInterval, + Tags = args.Tags.IsSpecified + ? args.Tags.Value.Select(tag => new API.ModifyForumTagParams + { + Name = tag.Name, + EmojiId = tag.Emoji is Emote emote + ? emote.Id + : Optional.Unspecified, + EmojiName = tag.Emoji is Emoji emoji + ? emoji.Name + : Optional.Unspecified + }).ToArray() + : Optional.Create(), + Flags = args.Flags.GetValueOrDefault(), + Topic = args.Topic, + DefaultReactionEmoji = args.DefaultReactionEmoji.IsSpecified + ? new API.ModifyForumReactionEmojiParams + { + EmojiId = args.DefaultReactionEmoji.Value is Emote emote ? + emote.Id : Optional.Unspecified, + EmojiName = args.DefaultReactionEmoji.Value is Emoji emoji ? + emoji.Name : Optional.Unspecified + } + : Optional.Unspecified, + DefaultSortOrder = args.DefaultSortOrder + }; + return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index c730596c7..3f472c74e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -30,13 +30,15 @@ namespace Discord.Rest ChannelType.Stage or ChannelType.NewsThread or ChannelType.PrivateThread or - ChannelType.PublicThread + ChannelType.PublicThread or + ChannelType.Forum => RestGuildChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model), ChannelType.DM or ChannelType.Group => CreatePrivate(discord, model) as RestChannel, ChannelType.Category => RestCategoryChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model), _ => new RestChannel(discord, model.Id), }; } + internal static RestChannel Create(BaseDiscordClient discord, Model model, IGuild guild) { return model.Type switch @@ -47,7 +49,8 @@ namespace Discord.Rest ChannelType.Stage or ChannelType.NewsThread or ChannelType.PrivateThread or - ChannelType.PublicThread + ChannelType.PublicThread or + ChannelType.Forum => RestGuildChannel.Create(discord, guild, model), ChannelType.DM or ChannelType.Group => CreatePrivate(discord, model) as RestChannel, ChannelType.Category => RestCategoryChannel.Create(discord, guild, model), diff --git a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs index aff8400aa..5ec81a7fc 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs @@ -26,6 +26,21 @@ namespace Discord.Rest /// public IReadOnlyCollection Tags { get; private set; } + /// + public int ThreadCreationInterval { get; private set; } + + /// + public int DefaultSlowModeInterval { get; private set; } + + /// + public ulong? CategoryId { get; private set; } + + /// + public IEmote DefaultReactionEmoji { get; private set; } + + /// + public ForumSortOrder? DefaultSortOrder { get; private set; } + /// public string Mention => MentionUtils.MentionChannel(Id); @@ -35,9 +50,9 @@ namespace Discord.Rest } - internal new static RestStageChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + internal new static RestForumChannel Create(BaseDiscordClient discord, IGuild guild, Model model) { - var entity = new RestStageChannel(discord, guild, model.Id); + var entity = new RestForumChannel(discord, guild, model.Id); entity.Update(model); return entity; } @@ -49,46 +64,75 @@ namespace Discord.Rest Topic = model.Topic.GetValueOrDefault(); DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); + if (model.ThreadRateLimitPerUser.IsSpecified) + DefaultSlowModeInterval = model.ThreadRateLimitPerUser.Value; + + if(model.SlowMode.IsSpecified) + ThreadCreationInterval = model.SlowMode.Value; + + DefaultSortOrder = model.DefaultSortOrder.GetValueOrDefault(); + Tags = model.ForumTags.GetValueOrDefault(Array.Empty()).Select( - x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault(), x.Moderated) ).ToImmutableArray(); + + if (model.DefaultReactionEmoji.IsSpecified && model.DefaultReactionEmoji.Value is not null) + { + if (model.DefaultReactionEmoji.Value.EmojiId.HasValue && model.DefaultReactionEmoji.Value.EmojiId.Value != 0) + DefaultReactionEmoji = new Emote(model.DefaultReactionEmoji.Value.EmojiId.GetValueOrDefault(), null, false); + else if (model.DefaultReactionEmoji.Value.EmojiName.IsSpecified) + DefaultReactionEmoji = new Emoji(model.DefaultReactionEmoji.Value.EmojiName.Value); + else + DefaultReactionEmoji = null; + } + + CategoryId = model.CategoryId.GetValueOrDefault(); + } + + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ForumHelper.ModifyAsync(this, Discord, func, options); + Update(model); } - /// - public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + /// + public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, + string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public async Task CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) { using var file = new FileAttachment(filePath, isSpoiler: isSpoiler); - return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false); } - /// + /// public async Task CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) { using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler); - return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false); } - /// + /// public Task CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public Task CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); /// public Task> GetActiveThreadsAsync(RequestOptions options = null) @@ -115,17 +159,45 @@ namespace Discord.Rest => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); - async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); #endregion + + #region INestedChannel + /// + public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + /// + public virtual async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + /// + async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) + { + if (CategoryId.HasValue && mode == CacheMode.AllowDownload) + return (await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false)) as ICategoryChannel; + return null; + } + + /// + public Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + #endregion } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 4f9af0335..a6c9c3674 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -26,6 +26,9 @@ namespace Discord.Rest /// public ulong GuildId => Guild.Id; + /// + public ChannelFlags Flags { get; private set; } + internal RestGuildChannel(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, id) { @@ -62,6 +65,8 @@ namespace Discord.Rest newOverwrites.Add(overwrites[i].ToEntity()); _overwrites = newOverwrites.ToImmutable(); } + + Flags = model.Flags.GetValueOrDefault(ChannelFlags.None); } /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs index c763a6660..c1be5182e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -37,6 +37,9 @@ namespace Discord.Rest /// public bool? IsInvitable { get; private set; } + /// + public IReadOnlyCollection AppliedTags { get; private set; } + /// public override DateTimeOffset CreatedAt { get; } @@ -77,6 +80,8 @@ namespace Discord.Rest MessageCount = model.MessageCount.GetValueOrDefault(0); Type = (ThreadType)model.Type; ParentChannelId = model.CategoryId.Value; + + AppliedTags = model.AppliedTags.GetValueOrDefault(Array.Empty()).ToImmutableArray(); } /// @@ -109,6 +114,13 @@ namespace Discord.Rest Update(model); } + /// + public async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await ThreadHelper.ModifyAsync(this, Discord, func, options); + Update(model); + } + /// /// /// This method is not supported in threads. diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index f5fce5a50..52cba0657 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -1,3 +1,4 @@ +using Discord.API; using Discord.API.Rest; using System; using System.Collections.Generic; @@ -46,18 +47,23 @@ namespace Discord.Rest } public static async Task ModifyAsync(IThreadChannel channel, BaseDiscordClient client, - Action func, + Action func, RequestOptions options) { - var args = new TextChannelProperties(); + var args = new ThreadChannelProperties(); func(args); + + Preconditions.AtMost(args.AppliedTags.IsSpecified ? args.AppliedTags.Value.Count() : 0, 5, nameof(args.AppliedTags), "Forum post can have max 5 applied tags."); + var apiArgs = new ModifyThreadParams { Name = args.Name, Archived = args.Archived, AutoArchiveDuration = args.AutoArchiveDuration, Locked = args.Locked, - Slowmode = args.SlowModeInterval + Slowmode = args.SlowModeInterval, + AppliedTags = args.AppliedTags, + Flags = args.Flags, }; return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } @@ -103,7 +109,10 @@ namespace Discord.Rest return RestThreadUser.Create(client, channel.Guild, model, channel); } - public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, + ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, + Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ulong[] tagIds = null) { embeds ??= Array.Empty(); if (embed != null) @@ -112,6 +121,7 @@ namespace Discord.Rest Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.AtMost(tagIds?.Length ?? 0, 5, nameof(tagIds), "Forum post can have max 5 applied tags."); // check that user flag and user Id list are exclusive, same with role flag and role Id list if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) @@ -134,10 +144,12 @@ namespace Discord.Rest Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + if (channel.Flags.HasFlag(ChannelFlags.RequireTag)) + throw new ArgumentException($"The channel {channel.Name} requires posts to have at least one tag."); + var args = new CreatePostParams() { Title = title, @@ -151,7 +163,8 @@ namespace Discord.Rest Flags = flags, Components = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, - } + }, + Tags = tagIds }; var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options).ConfigureAwait(false); @@ -159,7 +172,9 @@ namespace Discord.Rest return RestThreadChannel.Create(client, channel.Guild, model); } - public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable attachments, + ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags, ulong[] tagIds = null) { embeds ??= Array.Empty(); if (embed != null) @@ -168,6 +183,8 @@ namespace Discord.Rest Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.AtMost(tagIds?.Length ?? 0, 5, nameof(tagIds), "Forum post can have max 5 applied tags."); + // check that user flag and user Id list are exclusive, same with role flag and role Id list if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) @@ -190,9 +207,11 @@ namespace Discord.Rest Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + + if (channel.Flags.HasFlag(ChannelFlags.RequireTag)) + throw new ArgumentException($"The channel {channel.Name} requires posts to have at least one tag."); var args = new CreateMultipartPostAsync(attachments.ToArray()) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index c4e3764d1..74e797fd4 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -1,3 +1,4 @@ +using Discord.API; using Discord.API.Rest; using System; using System.Collections.Generic; @@ -252,6 +253,7 @@ namespace Discord.Rest Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), + DefaultAutoArchiveDuration = props.AutoArchiveDuration }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestTextChannel.Create(client, guild, model); @@ -338,6 +340,65 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestCategoryChannel.Create(client, guild, model); } + + /// is null. + public static async Task CreateForumChannelAsync(IGuild guild, BaseDiscordClient client, + string name, RequestOptions options, Action func = null) + { + if (name == null) + throw new ArgumentNullException(paramName: nameof(name)); + + var props = new ForumChannelProperties(); + func?.Invoke(props); + + Preconditions.AtMost(props.Tags.IsSpecified ? props.Tags.Value.Count() : 0, 5, nameof(props.Tags), "Forum channel can have max 20 tags."); + + var args = new CreateGuildChannelParams(name, ChannelType.Forum) + { + Position = props.Position, + Overwrites = props.PermissionOverwrites.IsSpecified + ? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() + }).ToArray() + : Optional.Create(), + SlowModeInterval = props.ThreadCreationInterval, + AvailableTags = props.Tags.GetValueOrDefault(Array.Empty()).Select( + x => new ModifyForumTagParams + { + Id = x.Id, + Name = x.Name, + EmojiId = x.Emoji is Emote emote + ? emote.Id + : Optional.Unspecified, + EmojiName = x.Emoji is Emoji emoji + ? emoji.Name + : Optional.Unspecified, + Moderated = x.IsModerated + }).ToArray(), + DefaultReactionEmoji = props.DefaultReactionEmoji.IsSpecified + ? new API.ModifyForumReactionEmojiParams + { + EmojiId = props.DefaultReactionEmoji.Value is Emote emote ? + emote.Id : Optional.Unspecified, + EmojiName = props.DefaultReactionEmoji.Value is Emoji emoji ? + emoji.Name : Optional.Unspecified + } + : Optional.Unspecified, + ThreadRateLimitPerUser = props.DefaultSlowModeInterval, + CategoryId = props.CategoryId, + IsNsfw = props.IsNsfw, + Topic = props.Topic, + DefaultAutoArchiveDuration = props.AutoArchiveDuration, + DefaultSortOrder = props.DefaultSortOrder.GetValueOrDefault(ForumSortOrder.LatestActivity) + }; + + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); + return RestForumChannel.Create(client, guild, model); + } #endregion #region Voice Regions diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index eb3254619..24f6ae28d 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -710,6 +710,19 @@ namespace Discord.Rest public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); + /// + /// Creates a category channel with the provided name. + /// + /// The name of the new channel. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is . + /// + /// The created category channel. + /// + public Task CreateForumChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateForumChannelAsync(this, Discord, name, options, func); + /// /// Gets a collection of all the voice regions this guild can access. /// @@ -1370,6 +1383,9 @@ namespace Discord.Rest /// async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateForumChannelAsync(string name, Action func, RequestOptions options) + => await CreateForumChannelAsync(name, func, options).ConfigureAwait(false); /// async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs index ea58ecdb5..9d46bc2aa 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -27,9 +27,33 @@ namespace Discord.WebSocket /// public IReadOnlyCollection Tags { get; private set; } + /// + public int ThreadCreationInterval { get; private set; } + + /// + public int DefaultSlowModeInterval { get; private set; } + /// public string Mention => MentionUtils.MentionChannel(Id); + /// + public ulong? CategoryId { get; private set; } + + /// + public IEmote DefaultReactionEmoji { get; private set; } + + /// + public ForumSortOrder? DefaultSortOrder { get; private set; } + + /// + /// Gets the parent (category) of this channel in the guild's channel list. + /// + /// + /// An representing the parent of this channel; null if none is set. + /// + public ICategoryChannel Category + => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + internal SocketForumChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { } internal new static SocketForumChannel Create(SocketGuild guild, ClientState state, Model model) @@ -46,46 +70,70 @@ namespace Discord.WebSocket Topic = model.Topic.GetValueOrDefault(); DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); + if (model.ThreadRateLimitPerUser.IsSpecified) + DefaultSlowModeInterval = model.ThreadRateLimitPerUser.Value; + + if (model.SlowMode.IsSpecified) + ThreadCreationInterval = model.SlowMode.Value; + + DefaultSortOrder = model.DefaultSortOrder.GetValueOrDefault(); + Tags = model.ForumTags.GetValueOrDefault(Array.Empty()).Select( - x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault(), x.Moderated) ).ToImmutableArray(); + + if (model.DefaultReactionEmoji.IsSpecified && model.DefaultReactionEmoji.Value is not null) + { + if (model.DefaultReactionEmoji.Value.EmojiId.HasValue && model.DefaultReactionEmoji.Value.EmojiId.Value != 0) + DefaultReactionEmoji = new Emote(model.DefaultReactionEmoji.Value.EmojiId.GetValueOrDefault(), null, false); + else if (model.DefaultReactionEmoji.Value.EmojiName.IsSpecified) + DefaultReactionEmoji = new Emoji(model.DefaultReactionEmoji.Value.EmojiName.Value); + else + DefaultReactionEmoji = null; + } + + CategoryId = model.CategoryId.GetValueOrDefault(); } - /// - public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + /// + public virtual Task ModifyAsync(Action func, RequestOptions options = null) + => ForumHelper.ModifyAsync(this, Discord, func, options); + + /// + public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public async Task CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) { using var file = new FileAttachment(filePath, isSpoiler: isSpoiler); - return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false); } - /// + /// public async Task CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageComponent components = null, - ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) + ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) { using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler); - return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); + return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false); } - /// + /// public Task CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); - /// + /// public Task CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) - => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null) + => ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()); /// public Task> GetActiveThreadsAsync(RequestOptions options = null) @@ -112,17 +160,41 @@ namespace Discord.WebSocket => await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); async Task> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) => await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); - async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false); - async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + async Task IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags) => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); #endregion + + #region INestedChannel + /// + public virtual async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + public virtual async Task CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options); + /// + public virtual async Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options); + public virtual Task CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => throw new NotImplementedException(); + /// + public virtual async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + + /// + Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(Category); + + /// + public virtual Task SyncPermissionsAsync(RequestOptions options = null) + => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 808982785..0811f2238 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -30,6 +30,9 @@ namespace Discord.WebSocket /// public int Position { get; private set; } + /// + public ChannelFlags Flags { get; private set; } + /// public virtual IReadOnlyCollection PermissionOverwrites => _overwrites; /// @@ -74,6 +77,8 @@ namespace Discord.WebSocket for (int i = 0; i < overwrites.Length; i++) newOverwrites.Add(overwrites[i].ToEntity()); _overwrites = newOverwrites.ToImmutable(); + + Flags = model.Flags.GetValueOrDefault(ChannelFlags.None); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 78462b062..d9d54f91e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -89,6 +89,9 @@ namespace Discord.WebSocket /// public bool? IsInvitable { get; private set; } + /// + public IReadOnlyCollection AppliedTags { get; private set; } + /// public override DateTimeOffset CreatedAt { get; } @@ -149,6 +152,8 @@ namespace Discord.WebSocket } HasJoined = model.ThreadMember.IsSpecified; + + AppliedTags = model.AppliedTags.GetValueOrDefault(Array.Empty()).ToImmutableArray(); } internal IReadOnlyCollection RemoveUsers(ulong[] users) @@ -334,12 +339,13 @@ namespace Discord.WebSocket => throw new NotSupportedException("This method is not supported in threads."); /// - /// - /// This method is not supported in threads. - /// public override Task ModifyAsync(Action func, RequestOptions options = null) => ThreadHelper.ModifyAsync(this, Discord, func, options); + /// + public Task ModifyAsync(Action func, RequestOptions options = null) + => ThreadHelper.ModifyAsync(this, Discord, func, options); + /// /// /// This method is not supported in threads. diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 55f098b2f..c2778ed1d 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -302,6 +302,16 @@ namespace Discord.WebSocket /// public IReadOnlyCollection ThreadChannels => Channels.OfType().ToImmutableArray(); + + /// + /// Gets a collection of all forum channels in this guild. + /// + /// + /// A read-only collection of forum channels found within this guild. + /// + public IReadOnlyCollection ForumChannels + => Channels.OfType().ToImmutableArray(); + /// /// Gets the current logged-in user. /// @@ -790,6 +800,7 @@ namespace Discord.WebSocket /// public Task CreateStageChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateStageChannelAsync(this, Discord, name, options, func); + /// /// Creates a new channel category in this guild. /// @@ -804,6 +815,20 @@ namespace Discord.WebSocket public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); + /// + /// Creates a new channel forum in this guild. + /// + /// The new name for the forum. + /// The delegate containing the properties to be applied to the channel upon its creation. + /// The options to be used when sending the request. + /// is . + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// forum channel. + /// + public Task CreateForumChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateForumChannelAsync(this, Discord, name, options, func); + internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { var channel = SocketGuildChannel.Create(this, state, model); @@ -1897,6 +1922,9 @@ namespace Discord.WebSocket /// async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); + /// + async Task IGuild.CreateForumChannelAsync(string name, Action func, RequestOptions options) + => await CreateForumChannelAsync(name, func, options).ConfigureAwait(false); /// async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs index 712570467..08d3c740a 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs @@ -20,6 +20,8 @@ namespace Discord public DateTimeOffset CreatedAt => throw new NotImplementedException(); public ulong Id => throw new NotImplementedException(); + + public ChannelFlags Flags => throw new NotImplementedException(); public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) { diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index ab1d3e534..8913c127d 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -34,6 +34,8 @@ namespace Discord public ulong Id => throw new NotImplementedException(); + public ChannelFlags Flags => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) { throw new NotImplementedException(); diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs index 2ffc75a24..3f167ba5f 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs @@ -36,6 +36,8 @@ namespace Discord public string Mention => throw new NotImplementedException(); + public ChannelFlags Flags => throw new NotImplementedException(); + public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException(); public Task ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) => throw new NotImplementedException();