diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs
index 067c55225..89614a96d 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 on a message.
+ ///
+ public const int MaxStickersPerMessage = 3;
+
+ ///
+ /// Returns the max number of embeds that can be sent on 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..06ff0e6a4
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Channels/IForumChannel.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+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 starting message of the post. The content of the message supports full markdown.
+ ///
+ /// The slowmode for the posts thread.
+ /// The options to be used when sending the request.
+ ///
+ /// A task that represents the asynchronous creation operation.
+ ///
+ Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, Message message, int? slowmode = null, RequestOptions options = null);
+
+ ///
+ /// 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 privatly 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 privatly archived threads.
+ ///
+ Task> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null);
+
+ ///
+ /// Gets a collection of privatly 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 privatly 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.Core/Entities/Messages/Message.cs b/src/Discord.Net.Core/Entities/Messages/Message.cs
new file mode 100644
index 000000000..581228ba9
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Messages/Message.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents a message created by a that can be sent to a channel.
+ ///
+ public sealed class Message
+ {
+ ///
+ /// Gets the content of the message.
+ ///
+ public string Content { get; }
+
+ ///
+ /// Gets whether or not this message should be read by a text-to-speech engine.
+ ///
+ public bool IsTTS { get; }
+
+ ///
+ /// Gets a collection of embeds sent along with this message.
+ ///
+ public IReadOnlyCollection Embeds { get; }
+
+ ///
+ /// Gets the allowed mentions for this message.
+ ///
+ public AllowedMentions AllowedMentions { get; }
+
+ ///
+ /// Gets the message reference (reply to) for this message.
+ ///
+ public MessageReference MessageReference { get; }
+
+ ///
+ /// Gets the components of this message.
+ ///
+ public MessageComponent Components { get; }
+
+ ///
+ /// Gets a collection of sticker ids that will be sent with this message.
+ ///
+ public IReadOnlyCollection StickerIds { get; }
+
+ ///
+ /// Gets a collection of files sent with this message.
+ ///
+ public IReadOnlyCollection Attachments { get; }
+
+ ///
+ /// Gets the message flags for this message.
+ ///
+ public MessageFlags Flags { get; }
+
+ internal Message(string content, bool istts, IReadOnlyCollection embeds, AllowedMentions allowedMentions,
+ MessageReference messagereference, MessageComponent components, IReadOnlyCollection stickerIds,
+ IReadOnlyCollection attachments, MessageFlags flags)
+ {
+ Content = content;
+ IsTTS = istts;
+ Embeds = embeds;
+ AllowedMentions = allowedMentions;
+ MessageReference = messagereference;
+ Components = components;
+ StickerIds = stickerIds;
+ Attachments = attachments;
+ Flags = flags;
+ }
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/Messages/MessageBuilder.cs b/src/Discord.Net.Core/Entities/Messages/MessageBuilder.cs
new file mode 100644
index 000000000..738c4102b
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/Messages/MessageBuilder.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents a generic message builder that can build s.
+ ///
+ public class MessageBuilder
+ {
+ private string _content;
+ private List _stickers;
+ private List _embeds;
+
+ ///
+ /// Gets or sets the content of this message
+ ///
+ /// The content is bigger than the .
+ public string Content
+ {
+ get => _content;
+ set
+ {
+ if (_content?.Length > DiscordConfig.MaxMessageSize)
+ throw new ArgumentOutOfRangeException(nameof(value), $"Message size must be less than or equal to {DiscordConfig.MaxMessageSize} characters");
+
+ _content = value;
+ }
+ }
+
+ ///
+ /// Gets or sets whether or not this message is TTS.
+ ///
+ public bool IsTTS { get; set; }
+
+ ///
+ /// Gets or sets the embeds of this message.
+ ///
+ public List Embeds
+ {
+ get
+ {
+ if (_embeds == null)
+ _embeds = new();
+ return _embeds;
+ }
+ set
+ {
+ if (value?.Count > DiscordConfig.MaxEmbedsPerMessage)
+ throw new ArgumentOutOfRangeException(nameof(value), $"Embed count must be less than or equal to {DiscordConfig.MaxEmbedsPerMessage}");
+
+ _embeds = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the allowed mentions of this message.
+ ///
+ public AllowedMentions AllowedMentions { get; set; }
+
+ ///
+ /// Gets or sets the message reference (reply to) of this message.
+ ///
+ public MessageReference MessageReference { get; set; }
+
+ ///
+ /// Gets or sets the components of this message.
+ ///
+ public ComponentBuilder Components { get; set; } = new();
+
+ ///
+ /// Gets or sets the stickers sent with this message.
+ ///
+ public List Stickers
+ {
+ get => _stickers;
+ set
+ {
+ if (value?.Count > DiscordConfig.MaxStickersPerMessage)
+ throw new ArgumentOutOfRangeException(nameof(value), $"Sticker count must be less than or equal to {DiscordConfig.MaxStickersPerMessage}");
+
+ _stickers = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the files sent with this message.
+ ///
+ public List Files { get; set; } = new();
+
+ ///
+ /// Gets or sets the message flags.
+ ///
+ public MessageFlags Flags { get; set; }
+
+ ///
+ /// Sets the of this message.
+ ///
+ /// The content of the message.
+ /// The current builder.
+ public MessageBuilder WithContent(string content)
+ {
+ Content = content;
+ return this;
+ }
+
+ ///
+ /// Sets the of this message.
+ ///
+ /// whether or not this message is tts.
+ /// The current builder.
+ public MessageBuilder WithTTS(bool isTTS)
+ {
+ IsTTS = isTTS;
+ return this;
+ }
+
+ public MessageBuilder WithEmbeds(params EmbedBuilder[] embeds)
+ {
+ Embeds = new(embeds);
+ return this;
+ }
+
+ public MessageBuilder AddEmbed(EmbedBuilder embed)
+ {
+ if (_embeds?.Count >= DiscordConfig.MaxEmbedsPerMessage)
+ throw new ArgumentOutOfRangeException(nameof(embed.Length), $"A message can only contain a maximum of {DiscordConfig.MaxEmbedsPerMessage} embeds");
+
+ _embeds ??= new();
+
+ _embeds.Add(embed);
+
+ return this;
+ }
+
+ public MessageBuilder WithAllowedMentions(AllowedMentions allowedMentions)
+ {
+ AllowedMentions = allowedMentions;
+ return this;
+ }
+
+ public MessageBuilder WithMessageReference(MessageReference reference)
+ {
+ MessageReference = reference;
+ return this;
+ }
+
+ public MessageBuilder WithMessageReference(IMessage message)
+ {
+ if (message != null)
+ MessageReference = new MessageReference(message.Id, message.Channel?.Id, ((IGuildChannel)message.Channel)?.GuildId);
+ return this;
+ }
+
+ public MessageBuilder WithComponentBuilder(ComponentBuilder builder)
+ {
+ Components = builder;
+ return this;
+ }
+
+ public MessageBuilder WithButton(ButtonBuilder button, int row = 0)
+ {
+ Components ??= new();
+ Components.WithButton(button, row);
+ return this;
+ }
+
+ public MessageBuilder WithButton(
+ string label = null,
+ string customId = null,
+ ButtonStyle style = ButtonStyle.Primary,
+ IEmote emote = null,
+ string url = null,
+ bool disabled = false,
+ int row = 0)
+ {
+ Components ??= new();
+ Components.WithButton(label, customId, style, emote, url, disabled, row);
+ return this;
+ }
+
+ public MessageBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0)
+ {
+ Components ??= new();
+ Components.WithSelectMenu(menu, row);
+ return this;
+ }
+
+ public MessageBuilder WithSelectMenu(string customId, List options,
+ string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0)
+ {
+ Components ??= new();
+ Components.WithSelectMenu(customId, options, placeholder, minValues, maxValues, disabled, row);
+ return this;
+ }
+
+ public Message Build()
+ {
+ var embeds = _embeds != null && _embeds.Count > 0
+ ? _embeds.Select(x => x.Build()).ToImmutableArray()
+ : ImmutableArray.Empty;
+
+ return new Message(
+ _content,
+ IsTTS,
+ embeds,
+ AllowedMentions,
+ MessageReference,
+ Components?.Build(),
+ _stickers != null && _stickers.Any() ? _stickers.Select(x => x.Id).ToImmutableArray() : ImmutableArray.Empty,
+ Files?.ToImmutableArray() ?? ImmutableArray.Empty,
+ Flags
+ );
+ }
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Common/Channel.cs b/src/Discord.Net.Rest/API/Common/Channel.cs
index d565b269a..f5d57a942 100644
--- a/src/Discord.Net.Rest/API/Common/Channel.cs
+++ b/src/Discord.Net.Rest/API/Common/Channel.cs
@@ -66,5 +66,11 @@ 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 DefaultAutoArchiveDuration { 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/Rest/CreateMultipartPostAsync.cs b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs
new file mode 100644
index 000000000..0a5c258ad
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs
@@ -0,0 +1,94 @@
+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 IsTTS { 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();
+
+ payload["title"] = Title;
+ payload["auto_archive_duration"] = ArchiveDuration;
+
+ if (Slowmode.IsSpecified)
+ payload["rate_limit_per_user"] = Slowmode.Value;
+ if (Content.IsSpecified)
+ payload["content"] = Content.Value;
+ if (IsTTS.IsSpecified)
+ payload["tts"] = IsTTS.Value;
+ if (Embeds.IsSpecified)
+ payload["embeds"] = Embeds.Value;
+ if (AllowedMentions.IsSpecified)
+ payload["allowed_mentions"] = AllowedMentions.Value;
+ if (MessageComponent.IsSpecified)
+ payload["components"] = MessageComponent.Value;
+ if (Stickers.IsSpecified)
+ payload["sticker_ids"] = Stickers.Value;
+ if (Flags.IsSpecified)
+ payload["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
+ });
+ }
+
+ payload["attachments"] = attachments;
+
+ 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..468610062
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Rest/CreatePostParams.cs
@@ -0,0 +1,44 @@
+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; }
+
+ // message
+ [JsonProperty("content")]
+ public string Content { get; set; }
+
+ [JsonProperty("tts")]
+ public Optional IsTTS { 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/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 3b829ee17..a6fdfc399 100644
--- a/src/Discord.Net.Rest/DiscordRestApiClient.cs
+++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs
@@ -464,6 +464,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));
@@ -564,15 +582,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/ThreadHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs
index e0074ecff..95c0496bf 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,49 @@ namespace Discord.Rest
return RestThreadUser.Create(client, channel.Guild, model, channel);
}
+
+ public static async Task CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, ThreadArchiveDuration archiveDuration, Message message, int? slowmode = null, RequestOptions options = null)
+ {
+ Model model;
+
+ if (message.Attachments?.Any() ?? false)
+ {
+ var args = new CreateMultipartPostAsync(message.Attachments.ToArray())
+ {
+ AllowedMentions = message.AllowedMentions.ToModel(),
+ ArchiveDuration = archiveDuration,
+ Content = message.Content,
+ Embeds = message.Embeds.Any() ? message.Embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified,
+ Flags = message.Flags,
+ IsTTS = message.IsTTS,
+ MessageComponent = message.Components?.Components?.Any() ?? false ? message.Components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified,
+ Slowmode = slowmode,
+ Stickers = message.StickerIds?.Any() ?? false ? message.StickerIds.ToArray() : Optional.Unspecified,
+ Title = title
+ };
+
+ model = await client.ApiClient.CreatePostAsync(channel.Id, args, options).ConfigureAwait(false);
+ }
+ else
+ {
+ var args = new CreatePostParams()
+ {
+ AllowedMentions = message.AllowedMentions.ToModel(),
+ ArchiveDuration = archiveDuration,
+ Content = message.Content,
+ Embeds = message.Embeds.Any() ? message.Embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified,
+ Flags = message.Flags,
+ IsTTS = message.IsTTS,
+ Components = message.Components?.Components?.Any() ?? false ? message.Components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified,
+ Slowmode = slowmode,
+ Stickers = message.StickerIds?.Any() ?? false ? message.StickerIds.ToArray() : Optional.Unspecified,
+ Title = title
+ };
+
+ 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 e345bfa94..fb44b101a 100644
--- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
+++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
@@ -352,7 +352,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();
@@ -394,7 +394,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..259335a9c
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs
@@ -0,0 +1,86 @@
+using Discord.Rest;
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+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.DefaultAutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay);
+
+ Tags = model.ForumTags.GetValueOrDefault(new API.ForumTags[0]).Select(
+ x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault())
+ ).ToImmutableArray();
+ }
+
+ ///
+ public Task CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, Message message, int? slowmode = null, RequestOptions options = null)
+ => ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, message, slowmode, options);
+
+ ///
+ 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.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, Message message, int? slowmode, RequestOptions options)
+ => await CreatePostAsync(title, archiveDuration, message, slowmode, options).ConfigureAwait(false);
+ 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);
+ #endregion
+ }
+}
diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
index 79f02fe1c..c538bc7fe 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),
};
}