diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 067c55225..2db802f1e 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -132,6 +132,16 @@ namespace Discord /// public const int MaxAuditLogEntriesPerBatch = 100; + /// + /// Returns the max number of stickers that can be sent with a message. + /// + public const int MaxStickersPerMessage = 3; + + /// + /// Returns the max number of embeds that can be sent with a message. + /// + public const int MaxEmbedsPerMessage = 10; + /// /// Gets or sets how a request should act in the case of an error, by default. /// diff --git a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs index e60bd5031..15965abc3 100644 --- a/src/Discord.Net.Core/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Core/Entities/Channels/ChannelType.cs @@ -26,6 +26,8 @@ namespace Discord /// The channel is a stage voice channel. Stage = 13, /// The channel is a guild directory used in hub servers. (Unreleased) - GuildDirectory = 14 + GuildDirectory = 14, + /// The channel is a forum channel containing multiple threads. + Forum = 15 } } diff --git a/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs new file mode 100644 index 000000000..f4c6da2e2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IForumChannel : IGuildChannel, IMentionable + { + /// + /// Gets a value that indicates whether the channel is NSFW. + /// + /// + /// true if the channel has the NSFW flag enabled; otherwise false. + /// + bool IsNsfw { get; } + + /// + /// Gets the current topic for this text channel. + /// + /// + /// A string representing the topic set in the channel; null if none is set. + /// + string Topic { get; } + + /// + /// Gets the default archive duration for a newly created post. + /// + ThreadArchiveDuration DefaultAutoArchiveDuration { get; } + + /// + /// Gets a collection of tags inside of this forum channel. + /// + IReadOnlyCollection Tags { get; } + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// The message components to be included with this message. Used for interactions. + /// 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. + /// + /// 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); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The file path of the file. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// 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. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// 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); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The of the file to be sent. + /// The name of the attachment. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// 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. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// 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); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// The attachment containing the file and description. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// 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. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// 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); + + /// + /// Creates a new post (thread) within the forum. + /// + /// The title of the post. + /// A collection of attachments to upload. + /// The archive duration of the post. + /// The slowmode for the posts thread. + /// The message to be sent. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// + /// 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. + /// A message flag to be applied to the sent message, only is permitted. + /// + /// 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); + + /// + /// Gets a collection of active threads within this forum channel. + /// + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of active threads. + /// + Task> GetActiveThreadsAsync(RequestOptions options = null); + + /// + /// Gets a collection of publicly archived threads within this forum channel. + /// + /// The optional limit of how many to get. + /// The optional date to return threads created before this timestamp. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of publicly archived threads. + /// + Task> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); + + /// + /// Gets a collection of privately archived threads within this forum channel. + /// + /// + /// The bot requires the permission in order to execute this request. + /// + /// The optional limit of how many to get. + /// The optional date to return threads created before this timestamp. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of privately archived threads. + /// + Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); + + /// + /// Gets a collection of privately archived threads that the current bot has joined within this forum channel. + /// + /// The optional limit of how many to get. + /// The optional date to return threads created before this timestamp. + /// The options to be used when sending the request. + /// + /// A task that represents an asynchronous get operation for retrieving the threads. The task result contains + /// a collection of privately archived threads. + /// + Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); + } +} diff --git a/src/Discord.Net.Core/Entities/ForumTag.cs b/src/Discord.Net.Core/Entities/ForumTag.cs new file mode 100644 index 000000000..26ae4301e --- /dev/null +++ b/src/Discord.Net.Core/Entities/ForumTag.cs @@ -0,0 +1,42 @@ +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.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs index 0eab65686..d9d7d469c 100644 --- a/src/Discord.Net.Rest/API/Common/Channel.cs +++ b/src/Discord.Net.Rest/API/Common/Channel.cs @@ -67,6 +67,10 @@ namespace Discord.API [JsonProperty("member_count")] public Optional MemberCount { get; set; } + //ForumChannel + [JsonProperty("available_tags")] + public Optional ForumTags { get; set; } + [JsonProperty("default_auto_archive_duration")] public Optional AutoArchiveDuration { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/ChannelThreads.cs b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs index 94b2396bf..9fa3e38ce 100644 --- a/src/Discord.Net.Rest/API/Common/ChannelThreads.cs +++ b/src/Discord.Net.Rest/API/Common/ChannelThreads.cs @@ -9,8 +9,5 @@ namespace Discord.API.Rest [JsonProperty("members")] public ThreadMember[] Members { get; set; } - - [JsonProperty("has_more")] - public bool HasMore { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/ForumTags.cs b/src/Discord.Net.Rest/API/Common/ForumTags.cs new file mode 100644 index 000000000..18354e7b2 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ForumTags.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ForumTags + { + [JsonProperty("id")] + public ulong 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; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs b/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs new file mode 100644 index 000000000..132e38e5f --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/ForumThreadMessage.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal class ForumThreadMessage + { + [JsonProperty("content")] + public Optional Content { get; set; } + + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + + [JsonProperty("components")] + public Optional Components { get; set; } + + [JsonProperty("sticker_ids")] + public Optional Stickers { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs new file mode 100644 index 000000000..0c8bc5494 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs @@ -0,0 +1,96 @@ +using Discord.Net.Converters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class CreateMultipartPostAsync + { + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + + public FileAttachment[] Files { get; } + + public string Title { get; set; } + public ThreadArchiveDuration ArchiveDuration { get; set; } + public Optional Slowmode { get; set; } + + + public Optional Content { get; set; } + public Optional Embeds { get; set; } + public Optional AllowedMentions { get; set; } + public Optional MessageComponent { get; set; } + public Optional Flags { get; set; } + public Optional Stickers { get; set; } + + public CreateMultipartPostAsync(params FileAttachment[] attachments) + { + Files = attachments; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + + var payload = new Dictionary(); + var message = new Dictionary(); + + payload["name"] = Title; + payload["auto_archive_duration"] = ArchiveDuration; + + if (Slowmode.IsSpecified) + payload["rate_limit_per_user"] = Slowmode.Value; + + // message + if (Content.IsSpecified) + message["content"] = Content.Value; + if (Embeds.IsSpecified) + message["embeds"] = Embeds.Value; + if (AllowedMentions.IsSpecified) + message["allowed_mentions"] = AllowedMentions.Value; + if (MessageComponent.IsSpecified) + message["components"] = MessageComponent.Value; + if (Stickers.IsSpecified) + message["sticker_ids"] = Stickers.Value; + if (Flags.IsSpecified) + message["flags"] = Flags.Value; + + List attachments = new(); + + for (int n = 0; n != Files.Length; n++) + { + var attachment = Files[n]; + + var filename = attachment.FileName ?? "unknown.dat"; + if (attachment.IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) + filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); + d[$"files[{n}]"] = new MultipartFile(attachment.Stream, filename); + + attachments.Add(new + { + id = (ulong)n, + filename = filename, + description = attachment.Description ?? Optional.Unspecified + }); + } + + message["attachments"] = attachments; + + payload["message"] = message; + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + + return d; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs new file mode 100644 index 000000000..974e07c0a --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Rest +{ + internal class CreatePostParams + { + // thread + [JsonProperty("name")] + public string Title { get; set; } + + [JsonProperty("auto_archive_duration")] + public ThreadArchiveDuration ArchiveDuration { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional Slowmode { get; set; } + + [JsonProperty("message")] + public ForumThreadMessage Message { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 67a690e4d..b85ff646e 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -37,7 +37,7 @@ namespace Discord.API.Rest if (Content.IsSpecified) payload["content"] = Content.Value; if (IsTTS.IsSpecified) - payload["tts"] = IsTTS.Value.ToString(); + payload["tts"] = IsTTS.Value; if (Nonce.IsSpecified) payload["nonce"] = Nonce.Value; if (Embeds.IsSpecified) diff --git a/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs index f004dec82..ca0f49ccb 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs @@ -50,7 +50,7 @@ namespace Discord.API.Rest if (Content.IsSpecified) data["content"] = Content.Value; if (IsTTS.IsSpecified) - data["tts"] = IsTTS.Value.ToString(); + data["tts"] = IsTTS.Value; if (MessageComponents.IsSpecified) data["components"] = MessageComponents.Value; if (Embeds.IsSpecified) diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index 1a25e4782..d945d149b 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -36,7 +36,7 @@ namespace Discord.API.Rest if (Content.IsSpecified) payload["content"] = Content.Value; if (IsTTS.IsSpecified) - payload["tts"] = IsTTS.Value.ToString(); + payload["tts"] = IsTTS.Value; if (Nonce.IsSpecified) payload["nonce"] = Nonce.Value; if (Username.IsSpecified) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index dcb13d9e3..e179675ba 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -466,6 +466,24 @@ namespace Discord.API #endregion #region Threads + public async Task CreatePostAsync(ulong channelId, CreatePostParams args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + var bucket = new BucketIds(channelId: channelId); + + return await SendJsonAsync("POST", () => $"channels/{channelId}/threads", args, bucket, options: options); + } + + public async Task CreatePostAsync(ulong channelId, CreateMultipartPostAsync args, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + + var bucket = new BucketIds(channelId: channelId); + + return await SendMultipartAsync("POST", () => $"channels/{channelId}/threads", args.ToDictionary(), bucket, options: options); + } + public async Task ModifyThreadAsync(ulong channelId, ModifyThreadParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -566,15 +584,15 @@ namespace Discord.API return await SendAsync("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); } - public async Task GetActiveThreadsAsync(ulong channelId, RequestOptions options = null) + public async Task GetActiveThreadsAsync(ulong guildId, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); - var bucket = new BucketIds(channelId: channelId); + var bucket = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"channels/{channelId}/threads/active", bucket, options: options); + return await SendAsync("GET", () => $"guilds/{guildId}/threads/active", bucket, options: options); } public async Task GetPublicArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs new file mode 100644 index 000000000..aff8400aa --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.Rest +{ + /// + /// Represents a REST-based forum channel in a guild. + /// + public class RestForumChannel : RestGuildChannel, IForumChannel + { + /// + public bool IsNsfw { get; private set; } + + /// + public string Topic { get; private set; } + + /// + public ThreadArchiveDuration DefaultAutoArchiveDuration { get; private set; } + + /// + public IReadOnlyCollection Tags { get; private set; } + + /// + public string Mention => MentionUtils.MentionChannel(Id); + + internal RestForumChannel(BaseDiscordClient client, IGuild guild, ulong id) + : base(client, guild, id) + { + + } + + internal new static RestStageChannel Create(BaseDiscordClient discord, IGuild guild, Model model) + { + var entity = new RestStageChannel(discord, guild, model.Id); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) + { + base.Update(model); + IsNsfw = model.Nsfw.GetValueOrDefault(false); + Topic = model.Topic.GetValueOrDefault(); + DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); + + Tags = model.ForumTags.GetValueOrDefault(Array.Empty()).Select( + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + ).ToImmutableArray(); + } + + /// + 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 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) + { + 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); + } + + /// + 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) + { + 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); + } + + /// + 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); + + /// + 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); + + /// + public Task> GetActiveThreadsAsync(RequestOptions options = null) + => ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); + + /// + public Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetJoinedPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPublicArchivedThreadsAsync(this, Discord, limit, before, options); + + #region IForumChannel + async Task> IForumChannel.GetActiveThreadsAsync(RequestOptions options) + => await GetActiveThreadsAsync(options).ConfigureAwait(false); + async Task> IForumChannel.GetPublicArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPublicArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => 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) + => 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) + => 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) + => 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) + => 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) + => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + #endregion + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index fa2362854..4f9af0335 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -39,6 +39,7 @@ namespace Discord.Rest ChannelType.Text => RestTextChannel.Create(discord, guild, model), ChannelType.Voice => RestVoiceChannel.Create(discord, guild, model), ChannelType.Stage => RestStageChannel.Create(discord, guild, model), + ChannelType.Forum => RestForumChannel.Create(discord, guild, model), ChannelType.Category => RestCategoryChannel.Create(discord, guild, model), ChannelType.PublicThread or ChannelType.PrivateThread or ChannelType.NewsThread => RestThreadChannel.Create(discord, guild, model), _ => new RestGuildChannel(discord, guild, model.Id), diff --git a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs index e0074ecff..f5fce5a50 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs @@ -1,5 +1,7 @@ using Discord.API.Rest; using System; +using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; @@ -60,6 +62,33 @@ namespace Discord.Rest return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } + public static async Task> GetActiveThreadsAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + { + var result = await client.ApiClient.GetActiveThreadsAsync(guild.Id, options).ConfigureAwait(false); + return result.Threads.Select(x => RestThreadChannel.Create(client, guild, x)).ToImmutableArray(); + } + + public static async Task> GetPublicArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, + DateTimeOffset? before = null, RequestOptions options = null) + { + var result = await client.ApiClient.GetPublicArchivedThreadsAsync(channel.Id, before, limit, options); + return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); + } + + public static async Task> GetPrivateArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, + DateTimeOffset? before = null, RequestOptions options = null) + { + var result = await client.ApiClient.GetPrivateArchivedThreadsAsync(channel.Id, before, limit, options); + return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); + } + + public static async Task> GetJoinedPrivateArchivedThreadsAsync(IGuildChannel channel, BaseDiscordClient client, int? limit = null, + DateTimeOffset? before = null, RequestOptions options = null) + { + var result = await client.ApiClient.GetJoinedPrivateArchivedThreadsAsync(channel.Id, before, limit, options); + return result.Threads.Select(x => RestThreadChannel.Create(client, channel.Guild, x)).ToImmutableArray(); + } + public static async Task GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) { var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, options); @@ -73,5 +102,114 @@ 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) + { + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + 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."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + if (stickers != null) + { + 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)); + + var args = new CreatePostParams() + { + Title = title, + ArchiveDuration = archiveDuration, + Slowmode = slowmode, + Message = new() + { + AllowedMentions = allowedMentions.ToModel(), + Content = text, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + 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, + } + }; + + var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options).ConfigureAwait(false); + + 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) + { + embeds ??= Array.Empty(); + if (embed != null) + embeds = new[] { embed }.Concat(embeds).ToArray(); + + 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."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + if (stickers != null) + { + 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)); + + var args = new CreateMultipartPostAsync(attachments.ToArray()) + { + AllowedMentions = allowedMentions.ToModel(), + ArchiveDuration = archiveDuration, + Content = text, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + Flags = flags, + MessageComponent = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + Slowmode = slowmode, + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + Title = title + }; + + var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options); + + return RestThreadChannel.Create(client, channel.Guild, model); + } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index 74d7953ad..522c098e6 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -369,7 +369,7 @@ namespace Discord.Rest #endregion #region Responses - public static async Task ModifyFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, Action func, + public static async Task ModifyFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, Action func, RequestOptions options = null) { var args = new MessageProperties(); @@ -411,7 +411,7 @@ namespace Discord.Rest } public static async Task DeleteFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, RequestOptions options = null) => await client.ApiClient.DeleteInteractionFollowupMessageAsync(message.Id, message.Token, options); - public static async Task ModifyInteractionResponseAsync(BaseDiscordClient client, string token, Action func, + public static async Task ModifyInteractionResponseAsync(BaseDiscordClient client, string token, Action func, RequestOptions options = null) { var args = new MessageProperties(); diff --git a/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs b/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs index 27cbe9290..d7655a30a 100644 --- a/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs +++ b/src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; using System.Globalization; @@ -14,7 +14,7 @@ namespace Discord.Net.Converters public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return ulong.Parse((string)reader.Value, NumberStyles.None, CultureInfo.InvariantCulture); + return ulong.Parse(reader.Value?.ToString(), NumberStyles.None, CultureInfo.InvariantCulture); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs new file mode 100644 index 000000000..bc6e28442 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs @@ -0,0 +1,128 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord.WebSocket +{ + /// + /// Represents a forum channel in a guild. + /// + public class SocketForumChannel : SocketGuildChannel, IForumChannel + { + /// + public bool IsNsfw { get; private set; } + + /// + public string Topic { get; private set; } + + /// + public ThreadArchiveDuration DefaultAutoArchiveDuration { get; private set; } + + /// + public IReadOnlyCollection Tags { get; private set; } + + /// + public string Mention => MentionUtils.MentionChannel(Id); + + internal SocketForumChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { } + + internal new static SocketForumChannel Create(SocketGuild guild, ClientState state, Model model) + { + var entity = new SocketForumChannel(guild.Discord, model.Id, guild); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, Model model) + { + base.Update(state, model); + IsNsfw = model.Nsfw.GetValueOrDefault(false); + Topic = model.Topic.GetValueOrDefault(); + DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay); + + Tags = model.ForumTags.GetValueOrDefault(Array.Empty()).Select( + x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault()) + ).ToImmutableArray(); + } + + /// + 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 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) + { + 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); + } + + /// + 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) + { + 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); + } + + /// + 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); + + /// + 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); + + /// + public Task> GetActiveThreadsAsync(RequestOptions options = null) + => ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); + + /// + public Task> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetJoinedPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPrivateArchivedThreadsAsync(this, Discord, limit, before, options); + + /// + public Task> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) + => ThreadHelper.GetPublicArchivedThreadsAsync(this, Discord, limit, before, options); + + #region IForumChannel + async Task> IForumChannel.GetActiveThreadsAsync(RequestOptions options) + => await GetActiveThreadsAsync(options).ConfigureAwait(false); + async Task> IForumChannel.GetPublicArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => await GetPublicArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); + async Task> IForumChannel.GetPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) + => 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) + => 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) + => 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) + => 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) + => 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) + => await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags); + + #endregion + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 6d9e759b4..16ed7b32d 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -59,6 +59,7 @@ namespace Discord.WebSocket ChannelType.Category => SocketCategoryChannel.Create(guild, state, model), ChannelType.PrivateThread or ChannelType.PublicThread or ChannelType.NewsThread => SocketThreadChannel.Create(guild, state, model), ChannelType.Stage => SocketStageChannel.Create(guild, state, model), + ChannelType.Forum => SocketForumChannel.Create(guild, state, model), _ => new SocketGuildChannel(guild.Discord, model.Id, guild), }; } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index e12f3d1ef..9ce2f507a 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -705,7 +705,15 @@ namespace Discord.WebSocket /// public SocketThreadChannel GetThreadChannel(ulong id) => GetChannel(id) as SocketThreadChannel; - + /// + /// Gets a forum channel in this guild. + /// + /// The snowflake identifier for the forum channel. + /// + /// A forum channel associated with the specified ; if none is found. + /// + public SocketForumChannel GetForumChannel(ulong id) + => GetChannel(id) as SocketForumChannel; /// /// Gets a voice channel in this guild. ///