@@ -132,6 +132,16 @@ namespace Discord | |||||
/// </returns> | /// </returns> | ||||
public const int MaxAuditLogEntriesPerBatch = 100; | public const int MaxAuditLogEntriesPerBatch = 100; | ||||
/// <summary> | |||||
/// Returns the max number of stickers that can be sent on a message. | |||||
/// </summary> | |||||
public const int MaxStickersPerMessage = 3; | |||||
/// <summary> | |||||
/// Returns the max number of embeds that can be sent on a message. | |||||
/// </summary> | |||||
public const int MaxEmbedsPerMessage = 10; | |||||
/// <summary> | /// <summary> | ||||
/// Gets or sets how a request should act in the case of an error, by default. | /// Gets or sets how a request should act in the case of an error, by default. | ||||
/// </summary> | /// </summary> | ||||
@@ -26,6 +26,8 @@ namespace Discord | |||||
/// <summary> The channel is a stage voice channel. </summary> | /// <summary> The channel is a stage voice channel. </summary> | ||||
Stage = 13, | Stage = 13, | ||||
/// <summary> The channel is a guild directory used in hub servers. (Unreleased)</summary> | /// <summary> The channel is a guild directory used in hub servers. (Unreleased)</summary> | ||||
GuildDirectory = 14 | |||||
GuildDirectory = 14, | |||||
/// <summary> The channel is a forum channel containing multiple threads. </summary> | |||||
Forum = 15 | |||||
} | } | ||||
} | } |
@@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// Gets a value that indicates whether the channel is NSFW. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// <c>true</c> if the channel has the NSFW flag enabled; otherwise <c>false</c>. | |||||
/// </returns> | |||||
bool IsNsfw { get; } | |||||
/// <summary> | |||||
/// Gets the current topic for this text channel. | |||||
/// </summary> | |||||
/// <returns> | |||||
/// A string representing the topic set in the channel; <c>null</c> if none is set. | |||||
/// </returns> | |||||
string Topic { get; } | |||||
/// <summary> | |||||
/// Gets the default archive duration for a newly created post. | |||||
/// </summary> | |||||
ThreadArchiveDuration DefaultAutoArchiveDuration { get; } | |||||
/// <summary> | |||||
/// Gets a collection of tags inside of this forum channel. | |||||
/// </summary> | |||||
IReadOnlyCollection<ForumTag> Tags { get; } | |||||
/// <summary> | |||||
/// Creates a new post (thread) within the forum. | |||||
/// </summary> | |||||
/// <param name="title">The title of the post.</param> | |||||
/// <param name="archiveDuration">The archive duration of the post.</param> | |||||
/// <param name="message"> | |||||
/// The starting message of the post. The content of the message supports full markdown. | |||||
/// </param> | |||||
/// <param name="slowmode">The slowmode for the posts thread.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents the asynchronous creation operation. | |||||
/// </returns> | |||||
Task<IThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, Message message, int? slowmode = null, RequestOptions options = null); | |||||
/// <summary> | |||||
/// Gets a collection of active threads within this forum channel. | |||||
/// </summary> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents an asynchronous get operation for retrieving the threads. The task result contains | |||||
/// a collection of active threads. | |||||
/// </returns> | |||||
Task<IReadOnlyCollection<IThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null); | |||||
/// <summary> | |||||
/// Gets a collection of publicly archived threads within this forum channel. | |||||
/// </summary> | |||||
/// <param name="limit">The optional limit of how many to get.</param> | |||||
/// <param name="before">The optional date to return threads created before this timestamp.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents an asynchronous get operation for retrieving the threads. The task result contains | |||||
/// a collection of publicly archived threads. | |||||
/// </returns> | |||||
Task<IReadOnlyCollection<IThreadChannel>> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); | |||||
/// <summary> | |||||
/// Gets a collection of privatly archived threads within this forum channel. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// The bot requires the <see cref="GuildPermission.ManageThreads"/> permission in order to execute this request. | |||||
/// </remarks> | |||||
/// <param name="limit">The optional limit of how many to get.</param> | |||||
/// <param name="before">The optional date to return threads created before this timestamp.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents an asynchronous get operation for retrieving the threads. The task result contains | |||||
/// a collection of privatly archived threads. | |||||
/// </returns> | |||||
Task<IReadOnlyCollection<IThreadChannel>> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); | |||||
/// <summary> | |||||
/// Gets a collection of privatly archived threads that the current bot has joined within this forum channel. | |||||
/// </summary> | |||||
/// <param name="limit">The optional limit of how many to get.</param> | |||||
/// <param name="before">The optional date to return threads created before this timestamp.</param> | |||||
/// <param name="options">The options to be used when sending the request.</param> | |||||
/// <returns> | |||||
/// A task that represents an asynchronous get operation for retrieving the threads. The task result contains | |||||
/// a collection of privatly archived threads. | |||||
/// </returns> | |||||
Task<IReadOnlyCollection<IThreadChannel>> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null); | |||||
} | |||||
} |
@@ -0,0 +1,42 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord | |||||
{ | |||||
/// <summary> | |||||
/// A struct representing a forum channel tag. | |||||
/// </summary> | |||||
public struct ForumTag | |||||
{ | |||||
/// <summary> | |||||
/// Gets the Id of the tag. | |||||
/// </summary> | |||||
public ulong Id { get; } | |||||
/// <summary> | |||||
/// Gets the name of the tag. | |||||
/// </summary> | |||||
public string Name { get; } | |||||
/// <summary> | |||||
/// Gets the emoji of the tag or <see langword="null"/> if none is set. | |||||
/// </summary> | |||||
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; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,74 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord | |||||
{ | |||||
/// <summary> | |||||
/// Represents a message created by a <see cref="MessageBuilder"/> that can be sent to a channel. | |||||
/// </summary> | |||||
public sealed class Message | |||||
{ | |||||
/// <summary> | |||||
/// Gets the content of the message. | |||||
/// </summary> | |||||
public string Content { get; } | |||||
/// <summary> | |||||
/// Gets whether or not this message should be read by a text-to-speech engine. | |||||
/// </summary> | |||||
public bool IsTTS { get; } | |||||
/// <summary> | |||||
/// Gets a collection of embeds sent along with this message. | |||||
/// </summary> | |||||
public IReadOnlyCollection<Embed> Embeds { get; } | |||||
/// <summary> | |||||
/// Gets the allowed mentions for this message. | |||||
/// </summary> | |||||
public AllowedMentions AllowedMentions { get; } | |||||
/// <summary> | |||||
/// Gets the message reference (reply to) for this message. | |||||
/// </summary> | |||||
public MessageReference MessageReference { get; } | |||||
/// <summary> | |||||
/// Gets the components of this message. | |||||
/// </summary> | |||||
public MessageComponent Components { get; } | |||||
/// <summary> | |||||
/// Gets a collection of sticker ids that will be sent with this message. | |||||
/// </summary> | |||||
public IReadOnlyCollection<ulong> StickerIds { get; } | |||||
/// <summary> | |||||
/// Gets a collection of files sent with this message. | |||||
/// </summary> | |||||
public IReadOnlyCollection<FileAttachment> Attachments { get; } | |||||
/// <summary> | |||||
/// Gets the message flags for this message. | |||||
/// </summary> | |||||
public MessageFlags Flags { get; } | |||||
internal Message(string content, bool istts, IReadOnlyCollection<Embed> embeds, AllowedMentions allowedMentions, | |||||
MessageReference messagereference, MessageComponent components, IReadOnlyCollection<ulong> stickerIds, | |||||
IReadOnlyCollection<FileAttachment> attachments, MessageFlags flags) | |||||
{ | |||||
Content = content; | |||||
IsTTS = istts; | |||||
Embeds = embeds; | |||||
AllowedMentions = allowedMentions; | |||||
MessageReference = messagereference; | |||||
Components = components; | |||||
StickerIds = stickerIds; | |||||
Attachments = attachments; | |||||
Flags = flags; | |||||
} | |||||
} | |||||
} |
@@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// Represents a generic message builder that can build <see cref="Message"/>s. | |||||
/// </summary> | |||||
public class MessageBuilder | |||||
{ | |||||
private string _content; | |||||
private List<ISticker> _stickers; | |||||
private List<EmbedBuilder> _embeds; | |||||
/// <summary> | |||||
/// Gets or sets the content of this message | |||||
/// </summary> | |||||
/// <exception cref="ArgumentOutOfRangeException">The content is bigger than the <see cref="DiscordConfig.MaxMessageSize"/>.</exception> | |||||
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; | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Gets or sets whether or not this message is TTS. | |||||
/// </summary> | |||||
public bool IsTTS { get; set; } | |||||
/// <summary> | |||||
/// Gets or sets the embeds of this message. | |||||
/// </summary> | |||||
public List<EmbedBuilder> 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; | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Gets or sets the allowed mentions of this message. | |||||
/// </summary> | |||||
public AllowedMentions AllowedMentions { get; set; } | |||||
/// <summary> | |||||
/// Gets or sets the message reference (reply to) of this message. | |||||
/// </summary> | |||||
public MessageReference MessageReference { get; set; } | |||||
/// <summary> | |||||
/// Gets or sets the components of this message. | |||||
/// </summary> | |||||
public ComponentBuilder Components { get; set; } = new(); | |||||
/// <summary> | |||||
/// Gets or sets the stickers sent with this message. | |||||
/// </summary> | |||||
public List<ISticker> 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; | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Gets or sets the files sent with this message. | |||||
/// </summary> | |||||
public List<FileAttachment> Files { get; set; } = new(); | |||||
/// <summary> | |||||
/// Gets or sets the message flags. | |||||
/// </summary> | |||||
public MessageFlags Flags { get; set; } | |||||
/// <summary> | |||||
/// Sets the <see cref="Content"/> of this message. | |||||
/// </summary> | |||||
/// <param name="content">The content of the message.</param> | |||||
/// <returns>The current builder.</returns> | |||||
public MessageBuilder WithContent(string content) | |||||
{ | |||||
Content = content; | |||||
return this; | |||||
} | |||||
/// <summary> | |||||
/// Sets the <see cref="IsTTS"/> of this message. | |||||
/// </summary> | |||||
/// <param name="isTTS">whether or not this message is tts.</param> | |||||
/// <returns>The current builder.</returns> | |||||
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<SelectMenuOptionBuilder> 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<Embed>.Empty; | |||||
return new Message( | |||||
_content, | |||||
IsTTS, | |||||
embeds, | |||||
AllowedMentions, | |||||
MessageReference, | |||||
Components?.Build(), | |||||
_stickers != null && _stickers.Any() ? _stickers.Select(x => x.Id).ToImmutableArray() : ImmutableArray<ulong>.Empty, | |||||
Files?.ToImmutableArray() ?? ImmutableArray<FileAttachment>.Empty, | |||||
Flags | |||||
); | |||||
} | |||||
} | |||||
} |
@@ -66,5 +66,11 @@ namespace Discord.API | |||||
[JsonProperty("member_count")] | [JsonProperty("member_count")] | ||||
public Optional<int> MemberCount { get; set; } | public Optional<int> MemberCount { get; set; } | ||||
//ForumChannel | |||||
[JsonProperty("available_tags")] | |||||
public Optional<ForumTags[]> ForumTags { get; set; } | |||||
[JsonProperty("default_auto_archive_duration")] | |||||
public Optional<ThreadArchiveDuration> DefaultAutoArchiveDuration { get; set; } | |||||
} | } | ||||
} | } |
@@ -9,8 +9,5 @@ namespace Discord.API.Rest | |||||
[JsonProperty("members")] | [JsonProperty("members")] | ||||
public ThreadMember[] Members { get; set; } | public ThreadMember[] Members { get; set; } | ||||
[JsonProperty("has_more")] | |||||
public bool HasMore { get; set; } | |||||
} | } | ||||
} | } |
@@ -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<ulong?> EmojiId { get; set; } | |||||
[JsonProperty("emoji_name")] | |||||
public Optional<string> EmojiName { get; set; } | |||||
} | |||||
} |
@@ -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<int?> Slowmode { get; set; } | |||||
public Optional<string> Content { get; set; } | |||||
public Optional<bool> IsTTS { get; set; } | |||||
public Optional<Embed[]> Embeds { get; set; } | |||||
public Optional<AllowedMentions> AllowedMentions { get; set; } | |||||
public Optional<ActionRowComponent[]> MessageComponent { get; set; } | |||||
public Optional<MessageFlags?> Flags { get; set; } | |||||
public Optional<ulong[]> Stickers { get; set; } | |||||
public CreateMultipartPostAsync(params FileAttachment[] attachments) | |||||
{ | |||||
Files = attachments; | |||||
} | |||||
public IReadOnlyDictionary<string, object> ToDictionary() | |||||
{ | |||||
var d = new Dictionary<string, object>(); | |||||
var payload = new Dictionary<string, object>(); | |||||
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<object> 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<string>.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; | |||||
} | |||||
} | |||||
} |
@@ -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<int?> Slowmode { get; set; } | |||||
// message | |||||
[JsonProperty("content")] | |||||
public string Content { get; set; } | |||||
[JsonProperty("tts")] | |||||
public Optional<bool> IsTTS { get; set; } | |||||
[JsonProperty("embeds")] | |||||
public Optional<Embed[]> Embeds { get; set; } | |||||
[JsonProperty("allowed_mentions")] | |||||
public Optional<AllowedMentions> AllowedMentions { get; set; } | |||||
[JsonProperty("components")] | |||||
public Optional<API.ActionRowComponent[]> Components { get; set; } | |||||
[JsonProperty("sticker_ids")] | |||||
public Optional<ulong[]> Stickers { get; set; } | |||||
[JsonProperty("flags")] | |||||
public Optional<MessageFlags> Flags { get; set; } | |||||
} | |||||
} |
@@ -37,7 +37,7 @@ namespace Discord.API.Rest | |||||
if (Content.IsSpecified) | if (Content.IsSpecified) | ||||
payload["content"] = Content.Value; | payload["content"] = Content.Value; | ||||
if (IsTTS.IsSpecified) | if (IsTTS.IsSpecified) | ||||
payload["tts"] = IsTTS.Value.ToString(); | |||||
payload["tts"] = IsTTS.Value; | |||||
if (Nonce.IsSpecified) | if (Nonce.IsSpecified) | ||||
payload["nonce"] = Nonce.Value; | payload["nonce"] = Nonce.Value; | ||||
if (Embeds.IsSpecified) | if (Embeds.IsSpecified) | ||||
@@ -50,7 +50,7 @@ namespace Discord.API.Rest | |||||
if (Content.IsSpecified) | if (Content.IsSpecified) | ||||
data["content"] = Content.Value; | data["content"] = Content.Value; | ||||
if (IsTTS.IsSpecified) | if (IsTTS.IsSpecified) | ||||
data["tts"] = IsTTS.Value.ToString(); | |||||
data["tts"] = IsTTS.Value; | |||||
if (MessageComponents.IsSpecified) | if (MessageComponents.IsSpecified) | ||||
data["components"] = MessageComponents.Value; | data["components"] = MessageComponents.Value; | ||||
if (Embeds.IsSpecified) | if (Embeds.IsSpecified) | ||||
@@ -36,7 +36,7 @@ namespace Discord.API.Rest | |||||
if (Content.IsSpecified) | if (Content.IsSpecified) | ||||
payload["content"] = Content.Value; | payload["content"] = Content.Value; | ||||
if (IsTTS.IsSpecified) | if (IsTTS.IsSpecified) | ||||
payload["tts"] = IsTTS.Value.ToString(); | |||||
payload["tts"] = IsTTS.Value; | |||||
if (Nonce.IsSpecified) | if (Nonce.IsSpecified) | ||||
payload["nonce"] = Nonce.Value; | payload["nonce"] = Nonce.Value; | ||||
if (Username.IsSpecified) | if (Username.IsSpecified) | ||||
@@ -464,6 +464,24 @@ namespace Discord.API | |||||
#endregion | #endregion | ||||
#region Threads | #region Threads | ||||
public async Task<Channel> CreatePostAsync(ulong channelId, CreatePostParams args, RequestOptions options = null) | |||||
{ | |||||
Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||||
var bucket = new BucketIds(channelId: channelId); | |||||
return await SendJsonAsync<Channel>("POST", () => $"channels/{channelId}/threads", args, bucket, options: options); | |||||
} | |||||
public async Task<Channel> CreatePostAsync(ulong channelId, CreateMultipartPostAsync args, RequestOptions options = null) | |||||
{ | |||||
Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||||
var bucket = new BucketIds(channelId: channelId); | |||||
return await SendMultipartAsync<Channel>("POST", () => $"channels/{channelId}/threads", args.ToDictionary(), bucket, options: options); | |||||
} | |||||
public async Task<Channel> ModifyThreadAsync(ulong channelId, ModifyThreadParams args, RequestOptions options = null) | public async Task<Channel> ModifyThreadAsync(ulong channelId, ModifyThreadParams args, RequestOptions options = null) | ||||
{ | { | ||||
Preconditions.NotEqual(channelId, 0, nameof(channelId)); | Preconditions.NotEqual(channelId, 0, nameof(channelId)); | ||||
@@ -564,15 +582,15 @@ namespace Discord.API | |||||
return await SendAsync<ThreadMember>("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); | return await SendAsync<ThreadMember>("GET", () => $"channels/{channelId}/thread-members/{userId}", bucket, options: options).ConfigureAwait(false); | ||||
} | } | ||||
public async Task<ChannelThreads> GetActiveThreadsAsync(ulong channelId, RequestOptions options = null) | |||||
public async Task<ChannelThreads> GetActiveThreadsAsync(ulong guildId, RequestOptions options = null) | |||||
{ | { | ||||
Preconditions.NotEqual(channelId, 0, nameof(channelId)); | |||||
Preconditions.NotEqual(guildId, 0, nameof(guildId)); | |||||
options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
var bucket = new BucketIds(channelId: channelId); | |||||
var bucket = new BucketIds(guildId: guildId); | |||||
return await SendAsync<ChannelThreads>("GET", () => $"channels/{channelId}/threads/active", bucket, options: options); | |||||
return await SendAsync<ChannelThreads>("GET", () => $"guilds/{guildId}/threads/active", bucket, options: options); | |||||
} | } | ||||
public async Task<ChannelThreads> GetPublicArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, RequestOptions options = null) | public async Task<ChannelThreads> GetPublicArchivedThreadsAsync(ulong channelId, DateTimeOffset? before = null, int? limit = null, RequestOptions options = null) | ||||
@@ -1,5 +1,7 @@ | |||||
using Discord.API.Rest; | using Discord.API.Rest; | ||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Collections.Immutable; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Model = Discord.API.Channel; | using Model = Discord.API.Channel; | ||||
@@ -60,6 +62,33 @@ namespace Discord.Rest | |||||
return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); | return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false); | ||||
} | } | ||||
public static async Task<IReadOnlyCollection<RestThreadChannel>> 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<IReadOnlyCollection<RestThreadChannel>> 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<IReadOnlyCollection<RestThreadChannel>> 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<IReadOnlyCollection<RestThreadChannel>> 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<RestThreadUser[]> GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) | public static async Task<RestThreadUser[]> GetUsersAsync(IThreadChannel channel, BaseDiscordClient client, RequestOptions options = null) | ||||
{ | { | ||||
var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, options); | var users = await client.ApiClient.ListThreadMembersAsync(channel.Id, options); | ||||
@@ -73,5 +102,49 @@ namespace Discord.Rest | |||||
return RestThreadUser.Create(client, channel.Guild, model, channel); | return RestThreadUser.Create(client, channel.Guild, model, channel); | ||||
} | } | ||||
public static async Task<RestThreadChannel> 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<API.Embed[]>.Unspecified, | |||||
Flags = message.Flags, | |||||
IsTTS = message.IsTTS, | |||||
MessageComponent = message.Components?.Components?.Any() ?? false ? message.Components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional<API.ActionRowComponent[]>.Unspecified, | |||||
Slowmode = slowmode, | |||||
Stickers = message.StickerIds?.Any() ?? false ? message.StickerIds.ToArray() : Optional<ulong[]>.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<API.Embed[]>.Unspecified, | |||||
Flags = message.Flags, | |||||
IsTTS = message.IsTTS, | |||||
Components = message.Components?.Components?.Any() ?? false ? message.Components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional<API.ActionRowComponent[]>.Unspecified, | |||||
Slowmode = slowmode, | |||||
Stickers = message.StickerIds?.Any() ?? false ? message.StickerIds.ToArray() : Optional<ulong[]>.Unspecified, | |||||
Title = title | |||||
}; | |||||
model = await client.ApiClient.CreatePostAsync(channel.Id, args, options); | |||||
} | |||||
return RestThreadChannel.Create(client, channel.Guild, model); | |||||
} | |||||
} | } | ||||
} | } |
@@ -352,7 +352,7 @@ namespace Discord.Rest | |||||
#endregion | #endregion | ||||
#region Responses | #region Responses | ||||
public static async Task<Message> ModifyFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, Action<MessageProperties> func, | |||||
public static async Task<Discord.API.Message> ModifyFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, Action<MessageProperties> func, | |||||
RequestOptions options = null) | RequestOptions options = null) | ||||
{ | { | ||||
var args = new MessageProperties(); | var args = new MessageProperties(); | ||||
@@ -394,7 +394,7 @@ namespace Discord.Rest | |||||
} | } | ||||
public static async Task DeleteFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, RequestOptions options = null) | public static async Task DeleteFollowupMessageAsync(BaseDiscordClient client, RestFollowupMessage message, RequestOptions options = null) | ||||
=> await client.ApiClient.DeleteInteractionFollowupMessageAsync(message.Id, message.Token, options); | => await client.ApiClient.DeleteInteractionFollowupMessageAsync(message.Id, message.Token, options); | ||||
public static async Task<Message> ModifyInteractionResponseAsync(BaseDiscordClient client, string token, Action<MessageProperties> func, | |||||
public static async Task<API.Message> ModifyInteractionResponseAsync(BaseDiscordClient client, string token, Action<MessageProperties> func, | |||||
RequestOptions options = null) | RequestOptions options = null) | ||||
{ | { | ||||
var args = new MessageProperties(); | var args = new MessageProperties(); | ||||
@@ -1,4 +1,4 @@ | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json; | |||||
using System; | using System; | ||||
using System.Globalization; | using System.Globalization; | ||||
@@ -14,7 +14,7 @@ namespace Discord.Net.Converters | |||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | 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) | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | ||||
@@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// Represents a forum channel in a guild. | |||||
/// </summary> | |||||
public class SocketForumChannel : SocketGuildChannel, IForumChannel | |||||
{ | |||||
/// <inheritdoc/> | |||||
public bool IsNsfw { get; private set; } | |||||
/// <inheritdoc/> | |||||
public string Topic { get; private set; } | |||||
/// <inheritdoc/> | |||||
public ThreadArchiveDuration DefaultAutoArchiveDuration { get; private set; } | |||||
/// <inheritdoc/> | |||||
public IReadOnlyCollection<ForumTag> Tags { get; private set; } | |||||
/// <inheritdoc/> | |||||
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(); | |||||
} | |||||
/// <inheritdoc cref="IForumChannel.CreatePostAsync(string, ThreadArchiveDuration, Message, int?, RequestOptions)"/> | |||||
public Task<RestThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, Message message, int? slowmode = null, RequestOptions options = null) | |||||
=> ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, message, slowmode, options); | |||||
/// <inheritdoc cref="IForumChannel.GetActiveThreadsAsync(RequestOptions)"/> | |||||
public Task<IReadOnlyCollection<RestThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null) | |||||
=> ThreadHelper.GetActiveThreadsAsync(Guild, Discord, options); | |||||
/// <inheritdoc cref="IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int?, DateTimeOffset?, RequestOptions)"/> | |||||
public Task<IReadOnlyCollection<RestThreadChannel>> GetJoinedPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) | |||||
=> ThreadHelper.GetJoinedPrivateArchivedThreadsAsync(this, Discord, limit, before, options); | |||||
/// <inheritdoc cref="IForumChannel.GetPrivateArchivedThreadsAsync(int?, DateTimeOffset?, RequestOptions)"/> | |||||
public Task<IReadOnlyCollection<RestThreadChannel>> GetPrivateArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) | |||||
=> ThreadHelper.GetPrivateArchivedThreadsAsync(this, Discord, limit, before, options); | |||||
/// <inheritdoc cref="IForumChannel.GetPublicArchivedThreadsAsync(int?, DateTimeOffset?, RequestOptions)"/> | |||||
public Task<IReadOnlyCollection<RestThreadChannel>> GetPublicArchivedThreadsAsync(int? limit = null, DateTimeOffset? before = null, RequestOptions options = null) | |||||
=> ThreadHelper.GetPublicArchivedThreadsAsync(this, Discord, limit, before, options); | |||||
#region IForumChannel | |||||
async Task<IThreadChannel> IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, Message message, int? slowmode, RequestOptions options) | |||||
=> await CreatePostAsync(title, archiveDuration, message, slowmode, options).ConfigureAwait(false); | |||||
async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetActiveThreadsAsync(RequestOptions options) | |||||
=> await GetActiveThreadsAsync(options).ConfigureAwait(false); | |||||
async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetPublicArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) | |||||
=> await GetPublicArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); | |||||
async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) | |||||
=> await GetPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); | |||||
async Task<IReadOnlyCollection<IThreadChannel>> IForumChannel.GetJoinedPrivateArchivedThreadsAsync(int? limit, DateTimeOffset? before, RequestOptions options) | |||||
=> await GetJoinedPrivateArchivedThreadsAsync(limit, before, options).ConfigureAwait(false); | |||||
#endregion | |||||
} | |||||
} |
@@ -59,6 +59,7 @@ namespace Discord.WebSocket | |||||
ChannelType.Category => SocketCategoryChannel.Create(guild, state, model), | ChannelType.Category => SocketCategoryChannel.Create(guild, state, model), | ||||
ChannelType.PrivateThread or ChannelType.PublicThread or ChannelType.NewsThread => SocketThreadChannel.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.Stage => SocketStageChannel.Create(guild, state, model), | ||||
ChannelType.Forum => SocketForumChannel.Create(guild, state, model), | |||||
_ => new SocketGuildChannel(guild.Discord, model.Id, guild), | _ => new SocketGuildChannel(guild.Discord, model.Id, guild), | ||||
}; | }; | ||||
} | } | ||||