Browse Source

initial implementation

pull/2316/head
Quin Lynch 3 years ago
parent
commit
2560a406e0
20 changed files with 804 additions and 15 deletions
  1. +10
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  2. +3
    -1
      src/Discord.Net.Core/Entities/Channels/ChannelType.cs
  3. +101
    -0
      src/Discord.Net.Core/Entities/Channels/IForumChannel.cs
  4. +42
    -0
      src/Discord.Net.Core/Entities/ForumTag.cs
  5. +74
    -0
      src/Discord.Net.Core/Entities/Messages/Message.cs
  6. +220
    -0
      src/Discord.Net.Core/Entities/Messages/MessageBuilder.cs
  7. +6
    -0
      src/Discord.Net.Rest/API/Common/Channel.cs
  8. +0
    -3
      src/Discord.Net.Rest/API/Common/ChannelThreads.cs
  9. +21
    -0
      src/Discord.Net.Rest/API/Common/ForumTags.cs
  10. +94
    -0
      src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs
  11. +44
    -0
      src/Discord.Net.Rest/API/Rest/CreatePostParams.cs
  12. +1
    -1
      src/Discord.Net.Rest/API/Rest/UploadFileParams.cs
  13. +1
    -1
      src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs
  14. +1
    -1
      src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs
  15. +22
    -4
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  16. +73
    -0
      src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs
  17. +2
    -2
      src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs
  18. +2
    -2
      src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs
  19. +86
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs
  20. +1
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs

+ 10
- 0
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -132,6 +132,16 @@ namespace Discord
/// </returns>
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>
/// Gets or sets how a request should act in the case of an error, by default.
/// </summary>


+ 3
- 1
src/Discord.Net.Core/Entities/Channels/ChannelType.cs View File

@@ -26,6 +26,8 @@ namespace Discord
/// <summary> The channel is a stage voice channel. </summary>
Stage = 13,
/// <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
}
}

+ 101
- 0
src/Discord.Net.Core/Entities/Channels/IForumChannel.cs View File

@@ -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);
}
}

+ 42
- 0
src/Discord.Net.Core/Entities/ForumTag.cs View File

@@ -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;
}
}
}

+ 74
- 0
src/Discord.Net.Core/Entities/Messages/Message.cs View File

@@ -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;
}
}
}

+ 220
- 0
src/Discord.Net.Core/Entities/Messages/MessageBuilder.cs View File

@@ -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
);
}
}
}

+ 6
- 0
src/Discord.Net.Rest/API/Common/Channel.cs View File

@@ -66,5 +66,11 @@ namespace Discord.API

[JsonProperty("member_count")]
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; }
}
}

+ 0
- 3
src/Discord.Net.Rest/API/Common/ChannelThreads.cs View File

@@ -9,8 +9,5 @@ namespace Discord.API.Rest

[JsonProperty("members")]
public ThreadMember[] Members { get; set; }

[JsonProperty("has_more")]
public bool HasMore { get; set; }
}
}

+ 21
- 0
src/Discord.Net.Rest/API/Common/ForumTags.cs View File

@@ -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; }
}
}

+ 94
- 0
src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs View File

@@ -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;
}
}
}

+ 44
- 0
src/Discord.Net.Rest/API/Rest/CreatePostParams.cs View File

@@ -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; }
}
}

+ 1
- 1
src/Discord.Net.Rest/API/Rest/UploadFileParams.cs View File

@@ -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)


+ 1
- 1
src/Discord.Net.Rest/API/Rest/UploadInteractionFileParams.cs View File

@@ -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)


+ 1
- 1
src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs View File

@@ -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)


+ 22
- 4
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -464,6 +464,24 @@ namespace Discord.API
#endregion

#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)
{
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);
}

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);

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)


+ 73
- 0
src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs View File

@@ -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<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)
{
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<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);
}
}
}

+ 2
- 2
src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs View File

@@ -352,7 +352,7 @@ namespace Discord.Rest
#endregion

#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)
{
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<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)
{
var args = new MessageProperties();


+ 2
- 2
src/Discord.Net.Rest/Net/Converters/UInt64Converter.cs View File

@@ -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)


+ 86
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs View File

@@ -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
}
}

+ 1
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs View File

@@ -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),
};
}


Loading…
Cancel
Save