Browse Source

Merge branch 'discord-net:dev' into meta/3.9

pull/2517/head
Armano den Boef GitHub 2 years ago
parent
commit
e1681cfe80
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1398 additions and 348 deletions
  1. +1
    -1
      docs/guides/concepts/samples/events.cs
  2. +1
    -0
      docs/guides/v2_v3_guide/v2_to_v3_guide.md
  3. +8
    -1
      samples/BasicBot/Program.cs
  4. +2
    -2
      samples/BasicBot/_BasicBot.csproj
  5. +1
    -1
      samples/InteractionFramework/_InteractionFramework.csproj
  6. +2
    -1
      samples/ShardedClient/Program.cs
  7. +1
    -1
      samples/ShardedClient/_ShardedClient.csproj
  8. +4
    -0
      samples/TextCommandFramework/Program.cs
  9. +2
    -2
      samples/TextCommandFramework/_TextCommandFramework.csproj
  10. +1
    -1
      samples/WebhookClient/_WebhookClient.csproj
  11. +13
    -12
      src/Discord.Net.Core/DiscordErrorCode.cs
  12. +22
    -0
      src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs
  13. +60
    -0
      src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs
  14. +17
    -0
      src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs
  15. +5
    -0
      src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs
  16. +61
    -7
      src/Discord.Net.Core/Entities/Channels/IForumChannel.cs
  17. +11
    -0
      src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs
  18. +20
    -0
      src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs
  19. +2
    -11
      src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs
  20. +26
    -0
      src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs
  21. +0
    -42
      src/Discord.Net.Core/Entities/ForumTag.cs
  22. +67
    -0
      src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs
  23. +191
    -0
      src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs
  24. +11
    -0
      src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs
  25. +48
    -0
      src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs
  26. +29
    -0
      src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs
  27. +4
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs
  28. +12
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  29. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs
  30. +54
    -25
      src/Discord.Net.Core/Entities/Messages/TimestampTag.cs
  31. +6
    -1
      src/Discord.Net.Core/Entities/Users/PremiumType.cs
  32. +3
    -0
      src/Discord.Net.Core/Extensions/ChannelExtensions.cs
  33. +5
    -2
      src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs
  34. +63
    -61
      src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
  35. +12
    -28
      src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
  36. +1
    -1
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs
  37. +9
    -4
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/MessageCommandInfo.cs
  38. +8
    -3
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/UserCommandInfo.cs
  39. +16
    -21
      src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs
  40. +34
    -41
      src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
  41. +3
    -3
      src/Discord.Net.Interactions/Info/ModalInfo.cs
  42. +2
    -2
      src/Discord.Net.Interactions/InteractionService.cs
  43. +6
    -6
      src/Discord.Net.Interactions/Results/ParseResult.cs
  44. +17
    -1
      src/Discord.Net.Rest/API/Common/Channel.cs
  45. +12
    -0
      src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs
  46. +3
    -0
      src/Discord.Net.Rest/API/Common/ForumTags.cs
  47. +12
    -0
      src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs
  48. +3
    -0
      src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs
  49. +3
    -0
      src/Discord.Net.Rest/API/Rest/CreatePostParams.cs
  50. +23
    -0
      src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs
  51. +15
    -0
      src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs
  52. +23
    -0
      src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs
  53. +2
    -0
      src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs
  54. +7
    -0
      src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs
  55. +1
    -0
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  56. +63
    -0
      src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs
  57. +5
    -2
      src/Discord.Net.Rest/Entities/Channels/RestChannel.cs
  58. +95
    -23
      src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs
  59. +5
    -0
      src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs
  60. +12
    -0
      src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs
  61. +1
    -2
      src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs
  62. +27
    -8
      src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs
  63. +61
    -0
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  64. +16
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  65. +1
    -5
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  66. +93
    -21
      src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs
  67. +5
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
  68. +9
    -3
      src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs
  69. +28
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  70. +5
    -2
      src/Discord.Net/Discord.Net.nuspec
  71. +2
    -0
      test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs
  72. +2
    -0
      test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs
  73. +2
    -0
      test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs

+ 1
- 1
docs/guides/concepts/samples/events.cs View File

@@ -22,7 +22,7 @@ public class Program
{
Console.WriteLine("Bot is connected!");
return Task.CompletedTask;
}
};
await Task.Delay(-1);
}


+ 1
- 0
docs/guides/v2_v3_guide/v2_to_v3_guide.md View File

@@ -37,6 +37,7 @@ _client = new DiscordSocketClient(config);
- AllUnprivileged: This is a group of most common intents, that do NOT require any [developer portal] intents to be enabled.
This includes intents that receive messages such as: `GatewayIntents.GuildMessages, GatewayIntents.DirectMessages`
- GuildMembers: An intent disabled by default, as you need to enable it in the [developer portal].
- MessageContent: An intent also disabled by default as you also need to enable it in the [developer portal].
- GuildPresences: Also disabled by default, this intent together with `GuildMembers` are the only intents not included in `AllUnprivileged`.
- All: All intents, it is ill advised to use this without care, as it _can_ cause a memory leak from presence.
The library will give responsive warnings if you specify unnecessary intents.


+ 8
- 1
samples/BasicBot/Program.cs View File

@@ -34,9 +34,16 @@ namespace BasicBot

public Program()
{
// Config used by DiscordSocketClient
// Define intents for the client
var config = new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
};

// It is recommended to Dispose of a client when you are finished
// using it, at the end of your app's lifetime.
_client = new DiscordSocketClient();
_client = new DiscordSocketClient(config);

// Subscribing to client events, so that we may receive them whenever they're invoked.
_client.Log += LogAsync;


+ 2
- 2
samples/BasicBot/_BasicBot.csproj View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
@@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Discord.Net.WebSocket" Version="3.6.1"/>
<PackageReference Include="Discord.Net.WebSocket" Version="3.8.1"/>
</ItemGroup>

</Project>

+ 1
- 1
samples/InteractionFramework/_InteractionFramework.csproj View File

@@ -13,7 +13,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Discord.Net.Interactions" Version="3.6.1" />
<PackageReference Include="Discord.Net.Interactions" Version="3.8.1" />
</ItemGroup>

</Project>

+ 2
- 1
samples/ShardedClient/Program.cs View File

@@ -28,7 +28,8 @@ namespace ShardedClient
// have 1 shard per 1500-2000 guilds your bot is in.
var config = new DiscordSocketConfig
{
TotalShards = 2
TotalShards = 2,
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
};

// You should dispose a service provider created using ASP.NET


+ 1
- 1
samples/ShardedClient/_ShardedClient.csproj View File

@@ -8,7 +8,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Discord.Net" Version="3.6.1" />
<PackageReference Include="Discord.Net" Version="3.8.1" />
</ItemGroup>

</Project>

+ 4
- 0
samples/TextCommandFramework/Program.cs View File

@@ -60,6 +60,10 @@ namespace TextCommandFramework
private ServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddSingleton(new DiscordSocketConfig
{
GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
})
.AddSingleton<DiscordSocketClient>()
.AddSingleton<CommandService>()
.AddSingleton<CommandHandlingService>()


+ 2
- 2
samples/TextCommandFramework/_TextCommandFramework.csproj View File

@@ -8,8 +8,8 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Discord.Net.Commands" Version="3.6.1" />
<PackageReference Include="Discord.Net.Websocket" Version="3.6.1" />
<PackageReference Include="Discord.Net.Commands" Version="3.8.1" />
<PackageReference Include="Discord.Net.Websocket" Version="3.8.1" />
</ItemGroup>

</Project>

+ 1
- 1
samples/WebhookClient/_WebhookClient.csproj View File

@@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Discord.Net.Webhook" Version="3.6.1" />
<PackageReference Include="Discord.Net.Webhook" Version="3.8.1" />
</ItemGroup>

</Project>

+ 13
- 12
src/Discord.Net.Core/DiscordErrorCode.cs View File

@@ -94,10 +94,10 @@ namespace Discord
MaxNumberOfDailyApplicationCommandCreatesHasBeenReached = 30034,
MaximumBansForNonGuildMembersReached = 30035,
MaximumBanFetchesReached = 30037,
MaximumUncompleteGuildScheduledEvents = 30038,
MaximumUncompletedGuildScheduledEvents = 30038,
MaximumStickersReached = 30039,
MaximumPruneRequestReached = 30040,
MaximumGuildWigitsReached = 30042,
MaximumGuildWidgetsReached = 30042,
#endregion

#region General Request Errors (40XXX)
@@ -116,24 +116,24 @@ namespace Discord
TargetUserNotInVoice = 40032,
MessageAlreadyCrossposted = 40033,
ApplicationNameAlreadyExists = 40041,
#endregion

#region Action Preconditions/Checks (50XXX)
ApplicationInteractionFailedToSend = 40043,
CannotSendAMessageInAForumChannel = 40058,
ThereAreNoTagsAvailableThatCanBeSetByNonModerators = 40066,
ATagIsRequiredToCreateAForumPostInThisChannel = 40067,
InteractionHasAlreadyBeenAcknowledged = 40060,
TagNamesMustBeUnique = 40061,
#endregion

#region Action Preconditions/Checks (50XXX)
MissingPermissions = 50001,
InvalidAccountType = 50002,
CannotExecuteForDM = 50003,
GuildWigitDisabled = 50004,
GuildWidgetDisabled = 50004,
CannotEditOtherUsersMessage = 50005,
CannotSendEmptyMessage = 50006,
CannotSendMessageToUser = 50007,
CannotSendMessageToVoiceChannel = 50008,
ChannelVerificationTooHight = 50009,
ChannelVerificationTooHigh = 50009,
OAuth2ApplicationDoesntHaveBot = 50010,
OAuth2ApplicationLimitReached = 50011,
InvalidOAuth2State = 50012,
@@ -154,6 +154,7 @@ namespace Discord
BulkDeleteMessageTooOld = 50034,
InvalidFormBody = 50035,
InviteAcceptedForGuildThatBotIsntIn = 50036,
InvalidActivityAction = 50039,
InvalidAPIVersion = 50041,
FileUploadTooBig = 50045,
InvalidFileUpload = 50046,
@@ -161,6 +162,7 @@ namespace Discord
InvalidGuild = 50055,
InvalidMessageType = 50068,
PaymentSourceRequiredForGift = 50070,
CannotModifySystemWebhook = 50073,
CannotDeleteRequiredCommunityChannel = 50074,
CannotEditStickersWithinAMessage = 50080,
InvalidSticker = 50081,
@@ -172,9 +174,8 @@ namespace Discord
ServerRequiresMonetization = 50097,
ServerRequiresBoosts = 50101,
RequestBodyContainsInvalidJSON = 50109,
FailedToResizeAssetBelowTheMaximumSize = 50138,
OwnershipCannotBeTransferredToABotUser = 50132,
AssetResizeBelowTheMaximumSize= 50138,
FailedToResizeAssetBelowTheMaximumSize = 50138,
UploadedFileNotFound = 50146,
MissingPermissionToSendThisSticker = 50600,
#endregion
@@ -213,8 +214,8 @@ namespace Discord
LottieCantContainRasters = 170002,
StickerMaximumFramerateExceeded = 170003,
StickerMaximumFrameCountExceeded = 170004,
LottieMaximumDimentionsExceeded = 170005,
StickerFramerateBoundsExceeed = 170006,
LottieMaximumDimensionsExceeded = 170005,
StickerFramerateBoundsExceeded = 170006,
StickerAnimationDurationTooLong = 170007,
#endregion

@@ -222,7 +223,7 @@ namespace Discord
CannotUpdateFinishedEvent = 180000,
FailedStageCreation = 180002,
#endregion
#region Forum & Automod
MessageWasBlockedByAutomaticModeration = 200000,
TitleWasBlockedByAutomaticModeration = 200001,


+ 22
- 0
src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs View File

@@ -0,0 +1,22 @@
namespace Discord;

/// <summary>
/// Represents public flags for a channel.
/// </summary>
public enum ChannelFlags
{
/// <summary>
/// Default value for flags, when none are given to a channel.
/// </summary>
None = 0,

/// <summary>
/// Flag given to a thread channel pinned on top of parent forum channel.
/// </summary>
Pinned = 1 << 1,

/// <summary>
/// Flag given to a forum channel that requires people to select tags when posting.
/// </summary>
RequireTag = 1 << 4
}

+ 60
- 0
src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;

namespace Discord;

public class ForumChannelProperties : TextChannelProperties
{

/// <summary>
/// Gets or sets the topic of the channel.
/// </summary>
/// <remarks>
/// Not available in forum channels.
/// </remarks>
public new Optional<int> SlowModeInterval { get; }

/// <summary>
/// Gets or sets rate limit on creating posts in this forum channel.
/// </summary>
/// <remarks>
/// Setting this value to anything above zero will require each user to wait X seconds before
/// creating another thread; setting this value to <c>0</c> will disable rate limits for this channel.
/// <note>
/// Users with <see cref="Discord.ChannelPermission.ManageMessages"/> or
/// <see cref="ChannelPermission.ManageChannels"/> will be exempt from rate limits.
/// </note>
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the value does not fall within [0, 21600].</exception>
public Optional<int> ThreadCreationInterval { get; set; }


/// <summary>
/// Gets or sets the default slow-mode for threads in this channel.
/// </summary>
/// <remarks>
/// Setting this value to anything above zero will require each user to wait X seconds before
/// sending another message; setting this value to <c>0</c> will disable slow-mode for child threads.
/// <note>
/// Users with <see cref="Discord.ChannelPermission.ManageMessages"/> or
/// <see cref="ChannelPermission.ManageChannels"/> will be exempt from slow-mode.
/// </note>
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the value does not fall within [0, 21600].</exception>
public Optional<int> DefaultSlowModeInterval { get; set; }

/// <summary>
/// Gets or sets a collection of tags inside of this forum channel.
/// </summary>
public Optional<IEnumerable<ForumTagProperties>> Tags { get; set; }

/// <summary>
/// Gets or sets a new default reaction emoji in this forum channel.
/// </summary>
public Optional<IEmote> DefaultReactionEmoji { get; set; }

/// <summary>
/// Gets or sets the rule used to order posts in forum channels.
/// </summary>
public Optional<ForumSortOrder> DefaultSortOrder { get; set; }
}

+ 17
- 0
src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs View File

@@ -0,0 +1,17 @@
namespace Discord;

/// <summary>
/// Defines the rule used to order posts in forum channels.
/// </summary>
public enum ForumSortOrder
{
/// <summary>
/// Sort forum posts by activity.
/// </summary>
LatestActivity = 0,

/// <summary>
/// Sort forum posts by creation time (from most recent to oldest).
/// </summary>
CreationDate = 1
}

+ 5
- 0
src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs View File

@@ -36,5 +36,10 @@ namespace Discord
/// Gets or sets the permission overwrites for this channel.
/// </summary>
public Optional<IEnumerable<Overwrite>> PermissionOverwrites { get; set; }

/// <summary>
/// Gets or sets the flags of the channel.
/// </summary>
public Optional<ChannelFlags> Flags { get; set; }
}
}

+ 61
- 7
src/Discord.Net.Core/Entities/Channels/IForumChannel.cs View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;

namespace Discord
{
public interface IForumChannel : IGuildChannel, IMentionable
public interface IForumChannel : IGuildChannel, IMentionable, INestedChannel
{
/// <summary>
/// Gets a value that indicates whether the channel is NSFW.
@@ -35,6 +35,55 @@ namespace Discord
/// </summary>
IReadOnlyCollection<ForumTag> Tags { get; }

/// <summary>
/// Gets the current rate limit on creating posts in this forum channel.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled.
/// </returns>
int ThreadCreationInterval { get; }

/// <summary>
/// Gets the current default slow-mode delay for threads in this forum channel.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled.
/// </returns>
int DefaultSlowModeInterval { get; }

/// <summary>
/// Gets the emoji to show in the add reaction button on a thread in a forum channel
/// </summary>
/// <remarks>
/// If the emoji is <see cref="Emote"/> only the <see cref="Emote.Id"/> will be populated.
/// Use <see cref="IGuild.GetEmoteAsync"/> to get the emoji.
/// </remarks>
IEmote DefaultReactionEmoji { get; }

/// <summary>
/// Gets or sets the rule used to order posts in forum channels.
/// </summary>
/// <remarks>
/// Defaults to null, which indicates a preferred sort order hasn't been set
/// </remarks>
ForumSortOrder? DefaultSortOrder { get; }

/// <summary>
/// Modifies this forum channel.
/// </summary>
/// <remarks>
/// This method modifies the current forum channel with the specified properties. To see an example of this
/// method and what properties are available, please refer to <see cref="ForumChannelProperties"/>.
/// </remarks>
/// <param name="func">The delegate containing the properties to modify the channel with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
Task ModifyAsync(Action<ForumChannelProperties> func, RequestOptions options = null);

/// <summary>
/// Creates a new post (thread) within the forum.
/// </summary>
@@ -52,12 +101,13 @@ namespace Discord
/// <param name="stickers">A collection of stickers to send with the message.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
Task<IThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null,
string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null);

/// <summary>
/// Creates a new post (thread) within the forum.
@@ -78,13 +128,14 @@ namespace Discord
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
Task<IThreadChannel> CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null);

/// <summary>
/// Creates a new post (thread) within the forum.
@@ -106,13 +157,14 @@ namespace Discord
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
public Task<IThreadChannel> CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None);
ISticker[] stickers = null, Embed[] embeds = null,MessageFlags flags = MessageFlags.None, ForumTag[] tags = null);

/// <summary>
/// Creates a new post (thread) within the forum.
@@ -132,12 +184,13 @@ namespace Discord
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
public Task<IThreadChannel> CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null);

/// <summary>
/// Creates a new post (thread) within the forum.
@@ -155,14 +208,15 @@ namespace Discord
/// </param>
/// <param name="components">The message components to be included with this message. Used for interactions.</param>
/// <param name="stickers">A collection of stickers to send with the file.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="embeds">An array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <param name="flags">A message flag to be applied to the sent message, only <see cref="MessageFlags.SuppressEmbeds"/> is permitted.</param>
/// <param name="tags">An array of <see cref="ForumTag"/> to be applied to the post.</param>
/// <returns>
/// A task that represents the asynchronous creation operation.
/// </returns>
public Task<IThreadChannel> CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None);
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null);

/// <summary>
/// Gets a collection of active threads within this forum channel.


+ 11
- 0
src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs View File

@@ -21,6 +21,17 @@ namespace Discord
/// </returns>
int Position { get; }

/// <summary>
/// Gets the flags related to this channel.
/// </summary>
/// <remarks>
/// This value is determined by bitwise OR-ing <see cref="ChannelFlags"/> values together.
/// </remarks>
/// <returns>
/// A channel's flags, if any is associated.
/// </returns>
ChannelFlags Flags { get; }

/// <summary>
/// Gets the guild associated with this channel.
/// </summary>


+ 20
- 0
src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord
@@ -56,6 +57,14 @@ namespace Discord
/// </remarks>
bool? IsInvitable { get; }

/// <summary>
/// Gets ids of tags applied to a forum thread
/// </summary>
/// <remarks>
/// This property is only available on forum threads.
/// </remarks>
IReadOnlyCollection<ulong> AppliedTags { get; }

/// <summary>
/// Gets when the thread was created.
/// </summary>
@@ -102,5 +111,16 @@ namespace Discord
/// A task that represents the asynchronous operation of removing a user from this thread.
/// </returns>
Task RemoveUserAsync(IGuildUser user, RequestOptions options = null);

/// <summary>
/// Modifies this thread channel.
/// </summary>
/// <param name="func">The delegate containing the properties to modify the channel with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
/// <seealso cref="ThreadChannelProperties"/>
Task ModifyAsync(Action<ThreadChannelProperties> func, RequestOptions options = null);
}
}

+ 2
- 11
src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace Discord
{
@@ -39,20 +40,10 @@ namespace Discord
/// <exception cref="ArgumentOutOfRangeException">Thrown if the value does not fall within [0, 21600].</exception>
public Optional<int> SlowModeInterval { get; set; }

/// <summary>
/// Gets or sets whether or not the thread is archived.
/// </summary>
public Optional<bool> Archived { get; set; }

/// <summary>
/// Gets or sets whether or not the thread is locked.
/// </summary>
public Optional<bool> Locked { get; set; }

/// <summary>
/// Gets or sets the auto archive duration.
/// </summary>
public Optional<ThreadArchiveDuration> AutoArchiveDuration { get; set; }

}
}

+ 26
- 0
src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;

namespace Discord;


/// <summary>
/// Provides properties that are used to modify an <see cref="IThreadChannel"/> with the specified changes.
/// </summary>
/// <seealso cref="IThreadChannel.ModifyAsync(System.Action{ThreadChannelProperties}, RequestOptions)"/>
public class ThreadChannelProperties : TextChannelProperties
{
/// <summary>
/// Gets or sets the tags applied to a forum thread
/// </summary>
public Optional<IEnumerable<ulong>> AppliedTags { get; set; }

/// <summary>
/// Gets or sets whether or not the thread is locked.
/// </summary>
public Optional<bool> Locked { get; set; }

/// <summary>
/// Gets or sets whether or not the thread is archived.
/// </summary>
public Optional<bool> Archived { get; set; }
}

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

@@ -1,42 +0,0 @@
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;
}
}
}

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

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

#nullable enable

namespace Discord
{
/// <summary>
/// A struct representing a forum channel tag.
/// </summary>
public struct ForumTag : ISnowflakeEntity, IForumTag
{
/// <summary>
/// Gets the Id of the tag.
/// </summary>
public ulong Id { get; }

/// <inheritdoc/>
public string Name { get; }

/// <inheritdoc/>
public IEmote? Emoji { get; }

/// <inheritdoc/>
public bool IsModerated { get; }

/// <inheritdoc/>
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);

internal ForumTag(ulong id, string name, ulong? emojiId = null, string? emojiName = null, bool moderated = false)
{
if (emojiId.HasValue && emojiId.Value != 0)
Emoji = new Emote(emojiId.Value, null, false);
else if (emojiName != null)
Emoji = new Emoji(emojiName);
else
Emoji = null;

Id = id;
Name = name;
IsModerated = moderated;
}

public override int GetHashCode() => (Id, Name, Emoji, IsModerated).GetHashCode();
public override bool Equals(object? obj)
=> obj is ForumTag tag && Equals(tag);

/// <summary>
/// Gets whether supplied tag is equals to the current one.
/// </summary>
public bool Equals(ForumTag tag)
=> Id == tag.Id &&
Name == tag.Name &&
(Emoji is Emoji emoji && tag.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) ||
Emoji is Emote emote && tag.Emoji is Emote otherEmote && emote.Equals(otherEmote)) &&
IsModerated == tag.IsModerated;

public static bool operator ==(ForumTag? left, ForumTag? right)
=> left?.Equals(right) ?? right is null;

public static bool operator !=(ForumTag? left, ForumTag? right) => !(left == right);
}
}

+ 191
- 0
src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs View File

@@ -0,0 +1,191 @@
#nullable enable
using System;

namespace Discord;

public class ForumTagBuilder
{
private string? _name;
private IEmote? _emoji;
private bool _moderated;
private ulong? _id;

/// <summary>
/// Returns the maximum length of name allowed by Discord.
/// </summary>
public const int MaxNameLength = 20;

/// <summary>
/// Gets or sets the snowflake Id of the tag.
/// </summary>
/// <remarks>
/// If set this will update existing tag or will create a new one otherwise.
/// </remarks>
public ulong? Id
{
get { return _id; }
set { _id = value; }
}

/// <summary>
/// Gets or sets the name of the tag.
/// </summary>
/// <exception cref="ArgumentException">Name length must be less than or equal to <see cref="MaxNameLength"/>.</exception>
public string? Name
{
get { return _name; }
set
{
if (value?.Length > MaxNameLength)
throw new ArgumentException(message: $"Name length must be less than or equal to {MaxNameLength}.", paramName: nameof(Name));
_name = value;
}
}

/// <summary>
/// Gets or sets the emoji of the tag.
/// </summary>
public IEmote? Emoji
{
get { return _emoji; }
set { _emoji = value; }
}

/// <summary>
/// Gets or sets whether this tag can only be added to or removed from threads by a member
/// with the <see cref="GuildPermissions.ManageThreads"/> permission
/// </summary>
public bool IsModerated
{
get { return _moderated; }
set { _moderated = value; }
}

/// <summary>
/// Initializes a new <see cref="ForumTagBuilder"/> class.
/// </summary>
public ForumTagBuilder()
{

}

/// <summary>
/// Initializes a new <see cref="ForumTagBuilder"/> class with values
/// </summary>
/// <param name="id"> If set existing tag will be updated or a new one will be created otherwise.</param>
/// <param name="name"> Name of the tag.</param>
/// <param name="isModerated"> Sets whether this tag can only be added to or removed from threads by a member
/// with the <see cref="GuildPermissions.ManageThreads"/> permission. </param>
public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false)
{
Name = name;
IsModerated = isModerated;
Id = id;
}

/// <summary>
/// Initializes a new <see cref="ForumTagBuilder"/> class with values
/// </summary>
/// <param name="name"> Name of the tag.</param>
/// <param name="id"> If set existing tag will be updated or a new one will be created otherwise.</param>
/// <param name="emoji"> Display emoji of the tag.</param>
/// <param name="isModerated"> Sets whether this tag can only be added to or removed from threads by a member
/// with the <see cref="GuildPermissions.ManageThreads"/> permission. </param>
public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false, IEmote? emoji = null)
{
Name = name;
Emoji = emoji;
IsModerated = isModerated;
Id = id;
}

/// <summary>
/// Initializes a new <see cref="ForumTagBuilder"/> class with values
/// </summary>
/// /// <param name="name"> Name of the tag.</param>
/// <param name="id"> If set existing tag will be updated or a new one will be created otherwise.</param>
/// <param name="emoteId"> The id of custom Display emoji of the tag.</param>
/// <param name="isModerated"> Sets whether this tag can only be added to or removed from threads by a member
/// with the <see cref="GuildPermissions.ManageThreads"/> permission </param>
public ForumTagBuilder(string name, ulong? id = null, bool isModerated = false, ulong? emoteId = null)
{
Name = name;
if(emoteId is not null)
Emoji = new Emote(emoteId.Value, null, false);
IsModerated = isModerated;
Id = id;
}

/// <summary>
/// Builds the Tag.
/// </summary>
/// <returns>An instance of <see cref="ForumTagProperties"/></returns>
/// <exception cref="ArgumentNullException">"Name must be set to build the tag"</exception>
public ForumTagProperties Build()
{
if (_name is null)
throw new ArgumentNullException(nameof(Name), "Name must be set to build the tag");
return new ForumTagProperties(_name!, _emoji, _moderated);
}

/// <summary>
/// Sets the name of the tag.
/// </summary>
/// <exception cref="ArgumentException">Name length must be less than or equal to <see cref="MaxNameLength"/>.</exception>
public ForumTagBuilder WithName(string name)
{
Name = name;
return this;
}

/// <summary>
/// Sets the id of the tag.
/// </summary>
/// <param name="id"> If set existing tag will be updated or a new one will be created otherwise.</param>
/// <exception cref="ArgumentException">Name length must be less than or equal to <see cref="MaxNameLength"/>.</exception>
public ForumTagBuilder WithId(ulong? id)
{
Id = id;
return this;
}

/// <summary>
/// Sets the emoji of the tag.
/// </summary>
public ForumTagBuilder WithEmoji(IEmote? emoji)
{
Emoji = emoji;
return this;
}

/// <summary>
/// Sets whether this tag can only be added to or removed from threads by a member
/// with the <see cref="GuildPermissions.ManageThreads"/> permission
/// </summary>
public ForumTagBuilder WithModerated(bool moderated)
{
IsModerated = moderated;
return this;
}

public override int GetHashCode() => base.GetHashCode();

public override bool Equals(object? obj)
=> obj is ForumTagBuilder builder && Equals(builder);

/// <summary>
/// Gets whether supplied tag builder is equals to the current one.
/// </summary>
public bool Equals(ForumTagBuilder? builder)
=> builder is not null &&
Id == builder.Id &&
Name == builder.Name &&
(Emoji is Emoji emoji && builder.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) ||
Emoji is Emote emote && builder.Emoji is Emote otherEmote && emote.Equals(otherEmote)) &&
IsModerated == builder.IsModerated;

public static bool operator ==(ForumTagBuilder? left, ForumTagBuilder? right)
=> left?.Equals(right) ?? right is null ;

public static bool operator !=(ForumTagBuilder? left, ForumTagBuilder? right) => !(left == right);
}

+ 11
- 0
src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs View File

@@ -0,0 +1,11 @@
namespace Discord;

public static class ForumTagBuilderExtensions
{
public static ForumTagBuilder ToForumTagBuilder(this ForumTag tag)
=> new ForumTagBuilder(tag.Name, tag.Id, tag.IsModerated, tag.Emoji);

public static ForumTagBuilder ToForumTagBuilder(this ForumTagProperties tag)
=> new ForumTagBuilder(tag.Name, tag.Id, tag.IsModerated, tag.Emoji);

}

+ 48
- 0
src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs View File

@@ -0,0 +1,48 @@
namespace Discord;

#nullable enable

public class ForumTagProperties : IForumTag
{
/// <summary>
/// Gets the Id of the tag.
/// </summary>
public ulong Id { get; }

/// <inheritdoc/>
public string Name { get; }

/// <inheritdoc/>
public IEmote? Emoji { get; }

/// <inheritdoc/>
public bool IsModerated { get; }

internal ForumTagProperties(string name, IEmote? emoji = null, bool isMmoderated = false)
{
Name = name;
Emoji = emoji;
IsModerated = isMmoderated;
}

public override int GetHashCode() => (Id, Name, Emoji, IsModerated).GetHashCode();

public override bool Equals(object? obj)
=> obj is ForumTagProperties tag && Equals(tag);

/// <summary>
/// Gets whether supplied tag is equals to the current one.
/// </summary>
public bool Equals(ForumTagProperties? tag)
=> tag is not null &&
Id == tag.Id &&
Name == tag.Name &&
(Emoji is Emoji emoji && tag.Emoji is Emoji otherEmoji && emoji.Equals(otherEmoji) ||
Emoji is Emote emote && tag.Emoji is Emote otherEmote && emote.Equals(otherEmote)) &&
IsModerated == tag.IsModerated;

public static bool operator ==(ForumTagProperties? left, ForumTagProperties? right)
=> left?.Equals(right) ?? right is null;

public static bool operator !=(ForumTagProperties? left, ForumTagProperties? right) => !(left == right);
}

+ 29
- 0
src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs View File

@@ -0,0 +1,29 @@
namespace Discord;

#nullable enable

/// <summary>
/// Represents a Discord forum tag
/// </summary>
public interface IForumTag
{
/// <summary>
/// Gets the name of the tag.
/// </summary>
string Name { get; }

/// <summary>
/// Gets the emoji of the tag or <see langword="null"/> if none is set.
/// </summary>
/// <remarks>
/// If the emoji is <see cref="Emote"/> only the <see cref="Emote.Id"/> will be populated.
/// Use <see cref="IGuild.GetEmoteAsync"/> to get the emoji.
/// </remarks>
IEmote? Emoji { get; }

/// <summary>
/// Gets whether this tag can only be added to or removed from threads by a member
/// with the <see cref="GuildPermissions.ManageThreads"/> permission
/// </summary>
bool IsModerated { get; }
}

+ 4
- 0
src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs View File

@@ -181,5 +181,9 @@ namespace Discord
/// The guild has enabled the welcome screen.
/// </summary>
WelcomeScreenEnabled = 1L << 41,
/// <summary>
/// The guild has been set as a support server on the App Directory.
/// </summary>
DeveloperSupportServer = 1L << 42,
}
}

+ 12
- 0
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -761,6 +761,18 @@ namespace Discord
/// </returns>
Task<ICategoryChannel> CreateCategoryAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null);

/// <summary>
/// Creates a new channel forum in this guild.
/// </summary>
/// <param name="name">The new name for the forum.</param>
/// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// forum channel.
/// </returns>
Task<IForumChannel> CreateForumChannelAsync(string name, Action<ForumChannelProperties> func = null, RequestOptions options = null);

/// <summary>
/// Gets a collection of all the voice regions this guild can access.
/// </summary>


+ 1
- 1
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs View File

@@ -21,7 +21,7 @@ namespace Discord
String = 3,

/// <summary>
/// An <see langword="int"/>.
/// An <see langword="long"/>.
/// </summary>
Integer = 4,



+ 54
- 25
src/Discord.Net.Core/Entities/Messages/TimestampTag.cs View File

@@ -5,17 +5,28 @@ namespace Discord
/// <summary>
/// Represents a class used to make timestamps in messages. see <see href="https://discord.com/developers/docs/reference#message-formatting-timestamp-styles"/>.
/// </summary>
public class TimestampTag
public readonly struct TimestampTag
{
/// <summary>
/// Gets or sets the style of the timestamp tag.
/// Gets the time for this timestamp tag.
/// </summary>
public TimestampTagStyles Style { get; set; } = TimestampTagStyles.ShortDateTime;
public DateTimeOffset Time { get; }

/// <summary>
/// Gets or sets the time for this timestamp tag.
/// Gets the style of this tag. <see langword="null"/> if none was provided.
/// </summary>
public DateTimeOffset Time { get; set; }
public TimestampTagStyles? Style { get; }

/// <summary>
/// Creates a new <see cref="TimestampTag"/> from the provided time.
/// </summary>
/// <param name="time">The time for this timestamp tag.</param>
/// <param name="style">The style for this timestamp tag.</param>
public TimestampTag(DateTimeOffset time, TimestampTagStyles? style = null)
{
Time = time;
Style = style;
}

/// <summary>
/// Converts the current timestamp tag to the string representation supported by discord.
@@ -23,11 +34,23 @@ namespace Discord
/// If the <see cref="Time"/> is null then the default 0 will be used.
/// </para>
/// </summary>
/// <remarks>
/// Will use the provided <see cref="Style"/> if provided. If this value is null, it will default to <see cref="TimestampTagStyles.ShortDateTime"/>.
/// </remarks>
/// <returns>A string that is compatible in a discord message, ex: <code>&lt;t:1625944201:f&gt;</code></returns>
public override string ToString()
{
return $"<t:{Time.ToUnixTimeSeconds()}:{(char)Style}>";
}
=> ToString(Style ?? TimestampTagStyles.ShortDateTime);

/// <summary>
/// Converts the current timestamp tag to the string representation supported by discord.
/// <para>
/// If the <see cref="Time"/> is null then the default 0 will be used.
/// </para>
/// </summary>
/// <param name="style">The formatting style for this tag.</param>
/// <returns>A string that is compatible in a discord message, ex: <code>&lt;t:1625944201:f&gt;</code></returns>
public string ToString(TimestampTagStyles style)
=> $"<t:{Time.ToUnixTimeSeconds()}:{(char)style}>";

/// <summary>
/// Creates a new timestamp tag with the specified <see cref="DateTime"/> object.
@@ -35,14 +58,8 @@ namespace Discord
/// <param name="time">The time of this timestamp tag.</param>
/// <param name="style">The style for this timestamp tag.</param>
/// <returns>The newly create timestamp tag.</returns>
public static TimestampTag FromDateTime(DateTime time, TimestampTagStyles style = TimestampTagStyles.ShortDateTime)
{
return new TimestampTag
{
Style = style,
Time = time
};
}
public static TimestampTag FromDateTime(DateTime time, TimestampTagStyles? style = null)
=> new(time, style);

/// <summary>
/// Creates a new timestamp tag with the specified <see cref="DateTimeOffset"/> object.
@@ -50,13 +67,25 @@ namespace Discord
/// <param name="time">The time of this timestamp tag.</param>
/// <param name="style">The style for this timestamp tag.</param>
/// <returns>The newly create timestamp tag.</returns>
public static TimestampTag FromDateTimeOffset(DateTimeOffset time, TimestampTagStyles style = TimestampTagStyles.ShortDateTime)
{
return new TimestampTag
{
Style = style,
Time = time
};
}
public static TimestampTag FromDateTimeOffset(DateTimeOffset time, TimestampTagStyles? style = null)
=> new(time, style);

/// <summary>
/// Immediately formats the provided time and style into a timestamp string.
/// </summary>
/// <param name="time">The time of this timestamp tag.</param>
/// <param name="style">The style for this timestamp tag.</param>
/// <returns>The newly create timestamp string.</returns>
public static string FormatFromDateTime(DateTime time, TimestampTagStyles style)
=> FormatFromDateTimeOffset(time, style);

/// <summary>
/// Immediately formats the provided time and style into a timestamp string.
/// </summary>
/// <param name="time">The time of this timestamp tag.</param>
/// <param name="style">The style for this timestamp tag.</param>
/// <returns>The newly create timestamp string.</returns>
public static string FormatFromDateTimeOffset(DateTimeOffset time, TimestampTagStyles style)
=> $"<t:{time.ToUnixTimeSeconds()}:{(char)style}>";
}
}
}

+ 6
- 1
src/Discord.Net.Core/Entities/Users/PremiumType.cs View File

@@ -16,6 +16,11 @@ namespace Discord
/// <summary>
/// Nitro subscription. Includes app perks as well as the games subscription service.
/// </summary>
Nitro = 2
Nitro = 2,

/// <summary>
/// Nitro Basic subscription. Includes app perks like video backgrounds, sending bigger files.
/// </summary>
NitroBasic = 3
}
}

+ 3
- 0
src/Discord.Net.Core/Extensions/ChannelExtensions.cs View File

@@ -46,6 +46,9 @@ namespace Discord

case ITextChannel:
return ChannelType.Text;

case IForumChannel:
return ChannelType.Forum;
}

return null;


+ 5
- 2
src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs View File

@@ -23,7 +23,7 @@ namespace Discord.Interactions
public string CommandName { get; }

/// <inheritdoc/>
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; }
public override IReadOnlyList<CommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => false;
@@ -41,9 +41,12 @@ namespace Discord.Interactions
if (context.Interaction is not IAutocompleteInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Autocomplete Interaction");

return await RunAsync(context, Array.Empty<object>(), services).ConfigureAwait(false);
return await base.ExecuteAsync(context, services).ConfigureAwait(false);
}

protected override Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services)
=> Task.FromResult(ParseResult.FromSuccess(Array.Empty<object>()) as IResult);

/// <inheritdoc/>
protected override Task InvokeModuleEvent(IInteractionContext context, IResult result) =>
CommandService._autocompleteCommandExecutedEvent.InvokeAsync(this, context, result);


+ 63
- 61
src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs View File

@@ -64,7 +64,7 @@ namespace Discord.Interactions
public IReadOnlyCollection<PreconditionAttribute> Preconditions { get; }

/// <inheritdoc cref="ICommandInfo.Parameters"/>
public abstract IReadOnlyCollection<TParameter> Parameters { get; }
public abstract IReadOnlyList<TParameter> Parameters { get; }

internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService)
{
@@ -85,71 +85,16 @@ namespace Discord.Interactions
}

/// <inheritdoc/>
public abstract Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services);
protected abstract Task InvokeModuleEvent(IInteractionContext context, IResult result);
protected abstract string GetLogString(IInteractionContext context);

/// <inheritdoc/>
public async Task<PreconditionResult> CheckPreconditionsAsync(IInteractionContext context, IServiceProvider services)
{
async Task<PreconditionResult> CheckGroups(ILookup<string, PreconditionAttribute> preconditions, string type)
{
foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions)
{
if (preconditionGroup.Key == null)
{
foreach (PreconditionAttribute precondition in preconditionGroup)
{
var result = await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false);
if (!result.IsSuccess)
return result;
}
}
else
{
var results = new List<PreconditionResult>();
foreach (PreconditionAttribute precondition in preconditionGroup)
results.Add(await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false));

if (!results.Any(p => p.IsSuccess))
return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results);
}
}
return PreconditionGroupResult.FromSuccess();
}

var moduleResult = await CheckGroups(Module.GroupedPreconditions, "Module").ConfigureAwait(false);
if (!moduleResult.IsSuccess)
return moduleResult;

var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false);
return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess();
}

protected async Task<IResult> RunAsync(IInteractionContext context, object[] args, IServiceProvider services)
public virtual async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
{
switch (RunMode)
{
case RunMode.Sync:
{
if (CommandService._autoServiceScopes)
{
using var scope = services?.CreateScope();
return await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false);
}

return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
}
return await ExecuteInternalAsync(context, services).ConfigureAwait(false);
case RunMode.Async:
_ = Task.Run(async () =>
{
if (CommandService._autoServiceScopes)
{
using var scope = services?.CreateScope();
await ExecuteInternalAsync(context, args, scope?.ServiceProvider ?? EmptyServiceProvider.Instance).ConfigureAwait(false);
}
else
await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
await ExecuteInternalAsync(context, services).ConfigureAwait(false);
});
break;
default:
@@ -159,16 +104,33 @@ namespace Discord.Interactions
return ExecuteResult.FromSuccess();
}

private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, object[] args, IServiceProvider services)
protected abstract Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services);

private async Task<IResult> ExecuteInternalAsync(IInteractionContext context, IServiceProvider services)
{
await CommandService._cmdLogger.DebugAsync($"Executing {GetLogString(context)}").ConfigureAwait(false);

using var scope = services?.CreateScope();
if (CommandService._autoServiceScopes)
services = scope?.ServiceProvider ?? EmptyServiceProvider.Instance;

try
{
var preconditionResult = await CheckPreconditionsAsync(context, services).ConfigureAwait(false);
if (!preconditionResult.IsSuccess)
return await InvokeEventAndReturn(context, preconditionResult).ConfigureAwait(false);

var argsResult = await ParseArgumentsAsync(context, services).ConfigureAwait(false);

if (!argsResult.IsSuccess)
return await InvokeEventAndReturn(context, argsResult).ConfigureAwait(false);

if(argsResult is not ParseResult parseResult)
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason.");

var args = parseResult.Args;

var index = 0;
foreach (var parameter in Parameters)
{
@@ -221,7 +183,47 @@ namespace Discord.Interactions
}
}

protected async ValueTask<IResult> InvokeEventAndReturn(IInteractionContext context, IResult result)
protected abstract Task InvokeModuleEvent(IInteractionContext context, IResult result);
protected abstract string GetLogString(IInteractionContext context);

/// <inheritdoc/>
public async Task<PreconditionResult> CheckPreconditionsAsync(IInteractionContext context, IServiceProvider services)
{
async Task<PreconditionResult> CheckGroups(ILookup<string, PreconditionAttribute> preconditions, string type)
{
foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions)
{
if (preconditionGroup.Key == null)
{
foreach (PreconditionAttribute precondition in preconditionGroup)
{
var result = await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false);
if (!result.IsSuccess)
return result;
}
}
else
{
var results = new List<PreconditionResult>();
foreach (PreconditionAttribute precondition in preconditionGroup)
results.Add(await precondition.CheckRequirementsAsync(context, this, services).ConfigureAwait(false));

if (!results.Any(p => p.IsSuccess))
return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results);
}
}
return PreconditionGroupResult.FromSuccess();
}

var moduleResult = await CheckGroups(Module.GroupedPreconditions, "Module").ConfigureAwait(false);
if (!moduleResult.IsSuccess)
return moduleResult;

var commandResult = await CheckGroups(_groupedPreconditions, "Command").ConfigureAwait(false);
return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess();
}

protected async Task<T> InvokeEventAndReturn<T>(IInteractionContext context, T result) where T : IResult
{
await InvokeModuleEvent(context, result).ConfigureAwait(false);
return result;


+ 12
- 28
src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs View File

@@ -13,7 +13,7 @@ namespace Discord.Interactions
public class ComponentCommandInfo : CommandInfo<ComponentCommandParameterInfo>
{
/// <inheritdoc/>
public override IReadOnlyCollection<ComponentCommandParameterInfo> Parameters { get; }
public override IReadOnlyList<ComponentCommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => true;
@@ -25,48 +25,32 @@ namespace Discord.Interactions

/// <inheritdoc/>
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
=> await ExecuteAsync(context, services, null).ConfigureAwait(false);

/// <summary>
/// Execute this command using dependency injection.
/// </summary>
/// <param name="context">Context that will be injected to the <see cref="InteractionModuleBase{T}"/>.</param>
/// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param>
/// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param>
/// <returns>
/// A task representing the asynchronous command execution process.
/// </returns>
public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs)
{
if (context.Interaction is not IComponentInteraction componentInteraction)
if (context.Interaction is not IComponentInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Component Interaction");

return await ExecuteAsync(context, Parameters, additionalArgs, componentInteraction.Data, services);
return await base.ExecuteAsync(context, services).ConfigureAwait(false);
}

/// <inheritdoc/>
public async Task<IResult> ExecuteAsync(IInteractionContext context, IEnumerable<CommandParameterInfo> paramList, IEnumerable<string> wildcardCaptures, IComponentInteractionData data,
IServiceProvider services)
protected override async Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services)
{
var paramCount = paramList.Count();
var captureCount = wildcardCaptures?.Count() ?? 0;

if (context.Interaction is not IComponentInteraction messageComponent)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Component Command Interaction");
var captures = (context as IRouteMatchContainer)?.SegmentMatches?.ToList();
var captureCount = captures?.Count() ?? 0;

try
{
var args = new object[paramCount];
var data = (context.Interaction as IComponentInteraction).Data;
var args = new object[Parameters.Count];

for (var i = 0; i < paramCount; i++)
for(var i = 0; i < Parameters.Count; i++)
{
var parameter = Parameters.ElementAt(i);
var parameter = Parameters[i];
var isCapture = i < captureCount;

if (isCapture ^ parameter.IsRouteSegmentParameter)
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Argument type and parameter type didn't match (Wild Card capture/Component value)")).ConfigureAwait(false);

var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, wildcardCaptures.ElementAt(i), services).ConfigureAwait(false) :
var readResult = isCapture ? await parameter.TypeReader.ReadAsync(context, captures[i].Value, services).ConfigureAwait(false) :
await parameter.TypeConverter.ReadAsync(context, data, services).ConfigureAwait(false);

if (!readResult.IsSuccess)
@@ -75,7 +59,7 @@ namespace Discord.Interactions
args[i] = readResult.Value;
}

return await RunAsync(context, args, services).ConfigureAwait(false);
return ParseResult.FromSuccess(args);
}
catch (Exception ex)
{


+ 1
- 1
src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs View File

@@ -24,7 +24,7 @@ namespace Discord.Interactions
public GuildPermission? DefaultMemberPermissions { get; }

/// <inheritdoc/>
public override IReadOnlyCollection<CommandParameterInfo> Parameters { get; }
public override IReadOnlyList<CommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => false;


+ 9
- 4
src/Discord.Net.Interactions/Info/Commands/ContextCommands/MessageCommandInfo.cs View File

@@ -14,18 +14,23 @@ namespace Discord.Interactions
/// <inheritdoc/>
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
{
if (context.Interaction is not IMessageCommandInteraction messageCommand)
if (context.Interaction is not IMessageCommandInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation");

return await base.ExecuteAsync(context, services).ConfigureAwait(false);
}

protected override Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services)
{
try
{
object[] args = new object[1] { messageCommand.Data.Message };
object[] args = new object[1] { (context.Interaction as IMessageCommandInteraction).Data.Message };

return await RunAsync(context, args, services).ConfigureAwait(false);
return Task.FromResult(ParseResult.FromSuccess(args) as IResult);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
return Task.FromResult(ParseResult.FromError(ex) as IResult);
}
}



+ 8
- 3
src/Discord.Net.Interactions/Info/Commands/ContextCommands/UserCommandInfo.cs View File

@@ -17,15 +17,20 @@ namespace Discord.Interactions
if (context.Interaction is not IUserCommandInteraction userCommand)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Message Command Interation");

return await base.ExecuteAsync(context, services).ConfigureAwait(false);
}

protected override Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services)
{
try
{
object[] args = new object[1] { userCommand.Data.User };
object[] args = new object[1] { (context.Interaction as IUserCommandInteraction).Data.User };

return await RunAsync(context, args, services).ConfigureAwait(false);
return Task.FromResult(ParseResult.FromSuccess(args) as IResult);
}
catch (Exception ex)
{
return ExecuteResult.FromError(ex);
return Task.FromResult(ParseResult.FromError(ex) as IResult);
}
}



+ 16
- 21
src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.Tracing;
using System.Linq;
using System.Threading.Tasks;
namespace Discord.Interactions
@@ -20,7 +19,7 @@ namespace Discord.Interactions
public override bool SupportsWildCards => true;

/// <inheritdoc/>
public override IReadOnlyCollection<ModalCommandParameterInfo> Parameters { get; }
public override IReadOnlyList<ModalCommandParameterInfo> Parameters { get; }

internal ModalCommandInfo(Builders.ModalCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService)
{
@@ -30,34 +29,29 @@ namespace Discord.Interactions

/// <inheritdoc/>
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
=> await ExecuteAsync(context, services, null).ConfigureAwait(false);

/// <summary>
/// Execute this command using dependency injection.
/// </summary>
/// <param name="context">Context that will be injected to the <see cref="InteractionModuleBase{T}"/>.</param>
/// <param name="services">Services that will be used while initializing the <see cref="InteractionModuleBase{T}"/>.</param>
/// <param name="additionalArgs">Provide additional string parameters to the method along with the auto generated parameters.</param>
/// <returns>
/// A task representing the asynchronous command execution process.
/// </returns>
public async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services, params string[] additionalArgs)
{
if (context.Interaction is not IModalInteraction modalInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Modal Interaction.");

return await base.ExecuteAsync(context, services).ConfigureAwait(false);
}

protected override async Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services)
{
var captures = (context as IRouteMatchContainer)?.SegmentMatches?.ToList();
var captureCount = captures?.Count() ?? 0;

try
{
var args = new object[Parameters.Count];
var captureCount = additionalArgs?.Length ?? 0;

for(var i = 0; i < Parameters.Count; i++)
for (var i = 0; i < Parameters.Count; i++)
{
var parameter = Parameters.ElementAt(i);

if(i < captureCount)
if (i < captureCount)
{
var readResult = await parameter.TypeReader.ReadAsync(context, additionalArgs[i], services).ConfigureAwait(false);
var readResult = await parameter.TypeReader.ReadAsync(context, captures[i].Value, services).ConfigureAwait(false);
if (!readResult.IsSuccess)
return await InvokeEventAndReturn(context, readResult).ConfigureAwait(false);

@@ -69,13 +63,14 @@ namespace Discord.Interactions
if (!modalResult.IsSuccess)
return await InvokeEventAndReturn(context, modalResult).ConfigureAwait(false);

if (modalResult is not ParseResult parseResult)
if (modalResult is not TypeConverterResult converterResult)
return await InvokeEventAndReturn(context, ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."));

args[i] = parseResult.Value;
args[i] = converterResult.Value;
}
}
return await RunAsync(context, args, services);

return ParseResult.FromSuccess(args);
}
catch (Exception ex)
{


+ 34
- 41
src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs View File

@@ -33,7 +33,7 @@ namespace Discord.Interactions
public GuildPermission? DefaultMemberPermissions { get; }

/// <inheritdoc/>
public override IReadOnlyCollection<SlashCommandParameterInfo> Parameters { get; }
public override IReadOnlyList<SlashCommandParameterInfo> Parameters { get; }

/// <inheritdoc/>
public override bool SupportsWildCards => false;
@@ -41,9 +41,9 @@ namespace Discord.Interactions
/// <summary>
/// Gets the flattened collection of command parameters and complex parameter fields.
/// </summary>
public IReadOnlyCollection<SlashCommandParameterInfo> FlattenedParameters { get; }
public IReadOnlyList<SlashCommandParameterInfo> FlattenedParameters { get; }

internal SlashCommandInfo (Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService)
internal SlashCommandInfo(Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService)
{
Description = builder.Description;
DefaultPermission = builder.DefaultPermission;
@@ -60,49 +60,45 @@ namespace Discord.Interactions
}

/// <inheritdoc/>
public override async Task<IResult> ExecuteAsync (IInteractionContext context, IServiceProvider services)
public override async Task<IResult> ExecuteAsync(IInteractionContext context, IServiceProvider services)
{
if(context.Interaction is not ISlashCommandInteraction slashCommand)
if (context.Interaction is not ISlashCommandInteraction)
return ExecuteResult.FromError(InteractionCommandError.ParseFailed, $"Provided {nameof(IInteractionContext)} doesn't belong to a Slash Command Interaction");

var options = slashCommand.Data.Options;

while (options != null && options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup))
options = options.ElementAt(0)?.Options;

return await ExecuteAsync(context, Parameters, options?.ToList(), services);
return await base.ExecuteAsync(context, services);
}

private async Task<IResult> ExecuteAsync (IInteractionContext context, IEnumerable<SlashCommandParameterInfo> paramList,
List<IApplicationCommandInteractionDataOption> argList, IServiceProvider services)
protected override async Task<IResult> ParseArgumentsAsync(IInteractionContext context, IServiceProvider services)
{
try
List<IApplicationCommandInteractionDataOption> GetOptions()
{
var slashCommandParameterInfos = paramList.ToList();
var args = new object[slashCommandParameterInfos.Count];

for (var i = 0; i < slashCommandParameterInfos.Count; i++)
{
var parameter = slashCommandParameterInfos[i];
var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false);

if (!result.IsSuccess)
return await InvokeEventAndReturn(context, result).ConfigureAwait(false);
var options = (context.Interaction as ISlashCommandInteraction).Data.Options;

if (result is not ParseResult parseResult)
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason.");
while (options != null && options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup))
options = options.ElementAt(0)?.Options;

args[i] = parseResult.Value;
}
return await RunAsync(context, args, services).ConfigureAwait(false);
return options.ToList();
}
catch(Exception ex)

var options = GetOptions();
var args = new object[Parameters.Count];
for(var i = 0; i < Parameters.Count; i++)
{
return await InvokeEventAndReturn(context, ExecuteResult.FromError(ex)).ConfigureAwait(false);
var parameter = Parameters[i];
var result = await ParseArgumentAsync(parameter, context, options, services).ConfigureAwait(false);

if (!result.IsSuccess)
return await InvokeEventAndReturn(context, ParseResult.FromError(result)).ConfigureAwait(false);

if (result is not TypeConverterResult converterResult)
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason.");

args[i] = converterResult.Value;
}
return ParseResult.FromSuccess(args);
}

private async Task<IResult> ParseArgument(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List<IApplicationCommandInteractionDataOption> argList,
private async ValueTask<IResult> ParseArgumentAsync(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List<IApplicationCommandInteractionDataOption> argList,
IServiceProvider services)
{
if (parameterInfo.IsComplexParameter)
@@ -111,32 +107,29 @@ namespace Discord.Interactions

for (var i = 0; i < ctorArgs.Length; i++)
{
var result = await ParseArgument(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false);
var result = await ParseArgumentAsync(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false);

if (!result.IsSuccess)
return result;

if (result is not ParseResult parseResult)
if (result is not TypeConverterResult converterResult)
return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason.");

ctorArgs[i] = parseResult.Value;
ctorArgs[i] = converterResult.Value;
}

return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs));
return TypeConverterResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs));
}

var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase));

if (arg == default)
return parameterInfo.IsRequired ? ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters") :
ParseResult.FromSuccess(parameterInfo.DefaultValue);
TypeConverterResult.FromSuccess(parameterInfo.DefaultValue);

var typeConverter = parameterInfo.TypeConverter;
var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false);
if (!readResult.IsSuccess)
return readResult;

return ParseResult.FromSuccess(readResult.Value);
return readResult;
}

protected override Task InvokeModuleEvent (IInteractionContext context, IResult result)


+ 3
- 3
src/Discord.Net.Interactions/Info/ModalInfo.cs View File

@@ -103,7 +103,7 @@ namespace Discord.Interactions
public async Task<IResult> CreateModalAsync(IInteractionContext context, IServiceProvider services = null, bool throwOnMissingField = false)
{
if (context.Interaction is not IModalInteraction modalInteraction)
return ParseResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction.");
return TypeConverterResult.FromError(InteractionCommandError.Unsuccessful, "Provided context doesn't belong to a Modal Interaction.");

services ??= EmptyServiceProvider.Instance;

@@ -120,7 +120,7 @@ namespace Discord.Interactions
if (!throwOnMissingField)
args[i] = input.DefaultValue;
else
return ParseResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}");
return TypeConverterResult.FromError(InteractionCommandError.BadArgs, $"Modal interaction is missing the required field: {input.CustomId}");
}
else
{
@@ -133,7 +133,7 @@ namespace Discord.Interactions
}
}

return ParseResult.FromSuccess(_initializer(args));
return TypeConverterResult.FromSuccess(_initializer(args));
}
}
}

+ 2
- 2
src/Discord.Net.Interactions/InteractionService.cs View File

@@ -822,7 +822,7 @@ namespace Discord.Interactions

SetMatchesIfApplicable(context, result);

return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false);
}

private async Task<IResult> ExecuteAutocompleteAsync (IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services )
@@ -869,7 +869,7 @@ namespace Discord.Interactions

SetMatchesIfApplicable(context, result);

return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false);
return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false);
}

private static void SetMatchesIfApplicable<T>(IInteractionContext context, SearchResult<T> searchResult)


+ 6
- 6
src/Discord.Net.Interactions/Results/ParseResult.cs View File

@@ -2,9 +2,9 @@ using System;

namespace Discord.Interactions
{
internal struct ParseResult : IResult
public struct ParseResult : IResult
{
public object Value { get; }
public object[] Args { get; }

public InteractionCommandError? Error { get; }

@@ -12,15 +12,15 @@ namespace Discord.Interactions

public bool IsSuccess => !Error.HasValue;

private ParseResult(object value, InteractionCommandError? error, string reason)
private ParseResult(object[] args, InteractionCommandError? error, string reason)
{
Value = value;
Args = args;
Error = error;
ErrorReason = reason;
}

public static ParseResult FromSuccess(object value) =>
new ParseResult(value, null, null);
public static ParseResult FromSuccess(object[] args) =>
new ParseResult(args, null, null);

public static ParseResult FromError(Exception exception) =>
new ParseResult(null, InteractionCommandError.Exception, exception.Message);


+ 17
- 1
src/Discord.Net.Rest/API/Common/Channel.cs View File

@@ -70,8 +70,24 @@ namespace Discord.API
//ForumChannel
[JsonProperty("available_tags")]
public Optional<ForumTags[]> ForumTags { get; set; }

[JsonProperty("applied_tags")]
public Optional<ulong[]> AppliedTags { get; set; }

[JsonProperty("default_auto_archive_duration")]
public Optional<ThreadArchiveDuration> AutoArchiveDuration { get; set; }

[JsonProperty("default_thread_rate_limit_per_user")]
public Optional<int> ThreadRateLimitPerUser { get; set; }

[JsonProperty("flags")]
public Optional<ChannelFlags> Flags { get; set; }

[JsonProperty("default_sort_order")]
public Optional<ForumSortOrder?> DefaultSortOrder { get; set; }

[JsonProperty("default_reaction_emoji")]
public Optional<ForumReactionEmoji> DefaultReactionEmoji { get; set; }

}
}

+ 12
- 0
src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs View File

@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace Discord.API;

public class ForumReactionEmoji
{
[JsonProperty("emoji_id")]
public ulong? EmojiId { get; set; }

[JsonProperty("emoji_name")]
public Optional<string> EmojiName { get; set; }
}

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

@@ -17,5 +17,8 @@ namespace Discord.API
public Optional<ulong?> EmojiId { get; set; }
[JsonProperty("emoji_name")]
public Optional<string> EmojiName { get; set; }

[JsonProperty("moderated")]
public bool Moderated { get; set; }
}
}

+ 12
- 0
src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs View File

@@ -23,6 +23,8 @@ namespace Discord.API.Rest
public Optional<bool> IsNsfw { get; set; }
[JsonProperty("rate_limit_per_user")]
public Optional<int> SlowModeInterval { get; set; }
[JsonProperty("default_auto_archive_duration")]
public Optional<ThreadArchiveDuration> DefaultAutoArchiveDuration { get; set; }

//Voice channels
[JsonProperty("bitrate")]
@@ -30,6 +32,16 @@ namespace Discord.API.Rest
[JsonProperty("user_limit")]
public Optional<int?> UserLimit { get; set; }

//Forum channels
[JsonProperty("default_reaction_emoji")]
public Optional<ModifyForumReactionEmojiParams> DefaultReactionEmoji { get; set; }
[JsonProperty("default_thread_rate_limit_per_user")]
public Optional<int> ThreadRateLimitPerUser { get; set; }
[JsonProperty("available_tags")]
public Optional<ModifyForumTagParams[]> AvailableTags { get; set; }
[JsonProperty("default_sort_order")]
public Optional<ForumSortOrder?> DefaultSortOrder { get; set; }

public CreateGuildChannelParams(string name, ChannelType type)
{
Name = name;


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

@@ -27,6 +27,7 @@ namespace Discord.API.Rest
public Optional<ActionRowComponent[]> MessageComponent { get; set; }
public Optional<MessageFlags?> Flags { get; set; }
public Optional<ulong[]> Stickers { get; set; }
public Optional<ulong[]> TagIds { get; set; }

public CreateMultipartPostAsync(params FileAttachment[] attachments)
{
@@ -59,6 +60,8 @@ namespace Discord.API.Rest
message["sticker_ids"] = Stickers.Value;
if (Flags.IsSpecified)
message["flags"] = Flags.Value;
if (TagIds.IsSpecified)
message["applied_tags"] = TagIds.Value;

List<object> attachments = new();



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

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

[JsonProperty("message")]
public ForumThreadMessage Message { get; set; }

[JsonProperty("applied_tags")]
public Optional<ulong[]> Tags { get; set; }
}
}

+ 23
- 0
src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs View File

@@ -0,0 +1,23 @@
using Newtonsoft.Json;

namespace Discord.API.Rest;


[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
internal class ModifyForumChannelParams : ModifyTextChannelParams
{
[JsonProperty("available_tags")]
public Optional<ModifyForumTagParams[]> Tags { get; set; }

[JsonProperty("default_thread_rate_limit_per_user")]
public Optional<int> DefaultSlowModeInterval { get; set; }

[JsonProperty("rate_limit_per_user")]
public Optional<int> ThreadCreationInterval { get; set; }

[JsonProperty("default_reaction_emoji")]
public Optional<ModifyForumReactionEmojiParams> DefaultReactionEmoji { get; set; }

[JsonProperty("default_sort_order")]
public Optional<ForumSortOrder> DefaultSortOrder { get; set; }
}

+ 15
- 0
src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs View File

@@ -0,0 +1,15 @@
using Newtonsoft.Json;

namespace Discord.API;

[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class ModifyForumReactionEmojiParams
{
[JsonProperty("emoji_id")]
public Optional<ulong?> EmojiId { get; set; }

[JsonProperty("emoji_name")]
public Optional<string> EmojiName { get; set; }
}



+ 23
- 0
src/Discord.Net.Rest/API/Rest/ModifyForumTagParams.cs View File

@@ -0,0 +1,23 @@
using Newtonsoft.Json;

namespace Discord.API
{
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
internal class ModifyForumTagParams
{
[JsonProperty("id")]
public Optional<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; }

[JsonProperty("moderated")]
public bool Moderated { get; set; }
}
}

+ 2
- 0
src/Discord.Net.Rest/API/Rest/ModifyGuildChannelParams.cs View File

@@ -13,5 +13,7 @@ namespace Discord.API.Rest
public Optional<ulong?> CategoryId { get; set; }
[JsonProperty("permission_overwrites")]
public Optional<Overwrite[]> Overwrites { get; set; }
[JsonProperty("flags")]
public Optional<ChannelFlags?> Flags { get; set; }
}
}

+ 7
- 0
src/Discord.Net.Rest/API/Rest/ModifyThreadParams.cs View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System.Collections.Generic;

namespace Discord.API.Rest
{
@@ -18,5 +19,11 @@ namespace Discord.API.Rest

[JsonProperty("rate_limit_per_user")]
public Optional<int> Slowmode { get; set; }

[JsonProperty("applied_tags")]
public Optional<IEnumerable<ulong>> AppliedTags { get; set; }

[JsonProperty("flags")]
public Optional<ChannelFlags> Flags { get; set; }
}
}

+ 1
- 0
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -38,6 +38,7 @@ namespace Discord.Rest
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
Flags = args.Flags.GetValueOrDefault(),
};
return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
}


+ 63
- 0
src/Discord.Net.Rest/Entities/Channels/ForumHelper.cs View File

@@ -0,0 +1,63 @@
using Discord.API;
using System;
using System.Linq;
using System.Threading.Tasks;
using Model = Discord.API.Channel;

namespace Discord.Rest;

internal static class ForumHelper
{
public static async Task<Model> ModifyAsync(IForumChannel channel, BaseDiscordClient client,
Action<ForumChannelProperties> func,
RequestOptions options)
{
var args = new ForumChannelProperties();
func(args);

Preconditions.AtMost(args.Tags.IsSpecified ? args.Tags.Value.Count() : 0, 5, nameof(args.Tags), "Forum channel can have max 20 tags.");
var apiArgs = new API.Rest.ModifyForumChannelParams()
{
Name = args.Name,
Position = args.Position,
CategoryId = args.CategoryId,
Overwrites = args.PermissionOverwrites.IsSpecified
? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue.ToString(),
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
DefaultSlowModeInterval = args.DefaultSlowModeInterval,
ThreadCreationInterval = args.ThreadCreationInterval,
Tags = args.Tags.IsSpecified
? args.Tags.Value.Select(tag => new API.ModifyForumTagParams
{
Name = tag.Name,
EmojiId = tag.Emoji is Emote emote
? emote.Id
: Optional<ulong?>.Unspecified,
EmojiName = tag.Emoji is Emoji emoji
? emoji.Name
: Optional<string>.Unspecified
}).ToArray()
: Optional.Create<API.ModifyForumTagParams[]>(),
Flags = args.Flags.GetValueOrDefault(),
Topic = args.Topic,
DefaultReactionEmoji = args.DefaultReactionEmoji.IsSpecified
? new API.ModifyForumReactionEmojiParams
{
EmojiId = args.DefaultReactionEmoji.Value is Emote emote ?
emote.Id : Optional<ulong?>.Unspecified,
EmojiName = args.DefaultReactionEmoji.Value is Emoji emoji ?
emoji.Name : Optional<string>.Unspecified
}
: Optional<ModifyForumReactionEmojiParams>.Unspecified,
DefaultSortOrder = args.DefaultSortOrder
};
return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
}
}

+ 5
- 2
src/Discord.Net.Rest/Entities/Channels/RestChannel.cs View File

@@ -30,13 +30,15 @@ namespace Discord.Rest
ChannelType.Stage or
ChannelType.NewsThread or
ChannelType.PrivateThread or
ChannelType.PublicThread
ChannelType.PublicThread or
ChannelType.Forum
=> RestGuildChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model),
ChannelType.DM or ChannelType.Group => CreatePrivate(discord, model) as RestChannel,
ChannelType.Category => RestCategoryChannel.Create(discord, new RestGuild(discord, model.GuildId.Value), model),
_ => new RestChannel(discord, model.Id),
};
}

internal static RestChannel Create(BaseDiscordClient discord, Model model, IGuild guild)
{
return model.Type switch
@@ -47,7 +49,8 @@ namespace Discord.Rest
ChannelType.Stage or
ChannelType.NewsThread or
ChannelType.PrivateThread or
ChannelType.PublicThread
ChannelType.PublicThread or
ChannelType.Forum
=> RestGuildChannel.Create(discord, guild, model),
ChannelType.DM or ChannelType.Group => CreatePrivate(discord, model) as RestChannel,
ChannelType.Category => RestCategoryChannel.Create(discord, guild, model),


+ 95
- 23
src/Discord.Net.Rest/Entities/Channels/RestForumChannel.cs View File

@@ -26,6 +26,21 @@ namespace Discord.Rest
/// <inheritdoc/>
public IReadOnlyCollection<ForumTag> Tags { get; private set; }

/// <inheritdoc/>
public int ThreadCreationInterval { get; private set; }

/// <inheritdoc/>
public int DefaultSlowModeInterval { get; private set; }

/// <inheritdoc/>
public ulong? CategoryId { get; private set; }

/// <inheritdoc/>
public IEmote DefaultReactionEmoji { get; private set; }

/// <inheritdoc/>
public ForumSortOrder? DefaultSortOrder { get; private set; }

/// <inheritdoc/>
public string Mention => MentionUtils.MentionChannel(Id);

@@ -35,9 +50,9 @@ namespace Discord.Rest

}

internal new static RestStageChannel Create(BaseDiscordClient discord, IGuild guild, Model model)
internal new static RestForumChannel Create(BaseDiscordClient discord, IGuild guild, Model model)
{
var entity = new RestStageChannel(discord, guild, model.Id);
var entity = new RestForumChannel(discord, guild, model.Id);
entity.Update(model);
return entity;
}
@@ -49,46 +64,75 @@ namespace Discord.Rest
Topic = model.Topic.GetValueOrDefault();
DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay);

if (model.ThreadRateLimitPerUser.IsSpecified)
DefaultSlowModeInterval = model.ThreadRateLimitPerUser.Value;

if(model.SlowMode.IsSpecified)
ThreadCreationInterval = model.SlowMode.Value;

DefaultSortOrder = model.DefaultSortOrder.GetValueOrDefault();

Tags = model.ForumTags.GetValueOrDefault(Array.Empty<API.ForumTags>()).Select(
x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault())
x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault(), x.Moderated)
).ToImmutableArray();

if (model.DefaultReactionEmoji.IsSpecified && model.DefaultReactionEmoji.Value is not null)
{
if (model.DefaultReactionEmoji.Value.EmojiId.HasValue && model.DefaultReactionEmoji.Value.EmojiId.Value != 0)
DefaultReactionEmoji = new Emote(model.DefaultReactionEmoji.Value.EmojiId.GetValueOrDefault(), null, false);
else if (model.DefaultReactionEmoji.Value.EmojiName.IsSpecified)
DefaultReactionEmoji = new Emoji(model.DefaultReactionEmoji.Value.EmojiName.Value);
else
DefaultReactionEmoji = null;
}

CategoryId = model.CategoryId.GetValueOrDefault();
}

/// <inheritdoc/>
public async Task ModifyAsync(Action<ForumChannelProperties> func, RequestOptions options = null)
{
var model = await ForumHelper.ModifyAsync(this, Discord, func, options);
Update(model);
}

/// <inheritdoc cref="IForumChannel.CreatePostAsync(string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
public Task<RestThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);
/// <inheritdoc cref="IForumChannel.CreatePostAsync(string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public Task<RestThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null,
string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null,
Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
=> ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray());

/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public async Task<RestThreadChannel> CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
{
using var file = new FileAttachment(filePath, isSpoiler: isSpoiler);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false);
}

/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, Stream, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, Stream, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public async Task<RestThreadChannel> CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
{
using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false);
}

/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, FileAttachment, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, FileAttachment, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public Task<RestThreadChannel> CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
=> ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray());

/// <inheritdoc cref="IForumChannel.CreatePostWithFilesAsync(string, IEnumerable{FileAttachment}, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
/// <inheritdoc cref="IForumChannel.CreatePostWithFilesAsync(string, IEnumerable{FileAttachment}, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public Task<RestThreadChannel> CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
=> ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray());

/// <inheritdoc cref="IForumChannel.GetActiveThreadsAsync(RequestOptions)"/>
public Task<IReadOnlyCollection<RestThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null)
@@ -115,17 +159,45 @@ namespace Discord.Rest
=> 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);
async Task<IThreadChannel> IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);

#endregion

#region INestedChannel
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false);
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options);
public virtual Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotImplementedException();
/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
=> await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<ICategoryChannel> INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options)
{
if (CategoryId.HasValue && mode == CacheMode.AllowDownload)
return (await Guild.GetChannelAsync(CategoryId.Value, mode, options).ConfigureAwait(false)) as ICategoryChannel;
return null;
}

/// <inheritdoc />
public Task SyncPermissionsAsync(RequestOptions options = null)
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options);
#endregion
}
}

+ 5
- 0
src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs View File

@@ -26,6 +26,9 @@ namespace Discord.Rest
/// <inheritdoc />
public ulong GuildId => Guild.Id;

/// <inheritdoc />
public ChannelFlags Flags { get; private set; }

internal RestGuildChannel(BaseDiscordClient discord, IGuild guild, ulong id)
: base(discord, id)
{
@@ -62,6 +65,8 @@ namespace Discord.Rest
newOverwrites.Add(overwrites[i].ToEntity());
_overwrites = newOverwrites.ToImmutable();
}

Flags = model.Flags.GetValueOrDefault(ChannelFlags.None);
}

/// <inheritdoc />


+ 12
- 0
src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs View File

@@ -37,6 +37,9 @@ namespace Discord.Rest
/// <inheritdoc/>
public bool? IsInvitable { get; private set; }

/// <inheritdoc/>
public IReadOnlyCollection<ulong> AppliedTags { get; private set; }

/// <inheritdoc cref="IThreadChannel.CreatedAt"/>
public override DateTimeOffset CreatedAt { get; }

@@ -77,6 +80,8 @@ namespace Discord.Rest
MessageCount = model.MessageCount.GetValueOrDefault(0);
Type = (ThreadType)model.Type;
ParentChannelId = model.CategoryId.Value;

AppliedTags = model.AppliedTags.GetValueOrDefault(Array.Empty<ulong>()).ToImmutableArray();
}

/// <summary>
@@ -109,6 +114,13 @@ namespace Discord.Rest
Update(model);
}

/// <inheritdoc/>
public async Task ModifyAsync(Action<ThreadChannelProperties> func, RequestOptions options = null)
{
var model = await ThreadHelper.ModifyAsync(this, Discord, func, options);
Update(model);
}

/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>


+ 1
- 2
src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs View File

@@ -19,8 +19,7 @@ namespace Discord.Rest
/// <summary>
/// Gets whether or not the guild has Text-In-Voice enabled and the voice channel is a TiV channel.
/// </summary>
public virtual bool IsTextInVoice
=> Guild.Features.HasTextInVoice;
public virtual bool IsTextInVoice => true;
/// <inheritdoc />
public int Bitrate { get; private set; }
/// <inheritdoc />


+ 27
- 8
src/Discord.Net.Rest/Entities/Channels/ThreadHelper.cs View File

@@ -1,3 +1,4 @@
using Discord.API;
using Discord.API.Rest;
using System;
using System.Collections.Generic;
@@ -46,18 +47,23 @@ namespace Discord.Rest
}

public static async Task<Model> ModifyAsync(IThreadChannel channel, BaseDiscordClient client,
Action<TextChannelProperties> func,
Action<ThreadChannelProperties> func,
RequestOptions options)
{
var args = new TextChannelProperties();
var args = new ThreadChannelProperties();
func(args);

Preconditions.AtMost(args.AppliedTags.IsSpecified ? args.AppliedTags.Value.Count() : 0, 5, nameof(args.AppliedTags), "Forum post can have max 5 applied tags.");

var apiArgs = new ModifyThreadParams
{
Name = args.Name,
Archived = args.Archived,
AutoArchiveDuration = args.AutoArchiveDuration,
Locked = args.Locked,
Slowmode = args.SlowModeInterval
Slowmode = args.SlowModeInterval,
AppliedTags = args.AppliedTags,
Flags = args.Flags,
};
return await client.ApiClient.ModifyThreadAsync(channel.Id, apiArgs, options).ConfigureAwait(false);
}
@@ -103,7 +109,10 @@ namespace Discord.Rest
return RestThreadUser.Create(client, channel.Guild, model, channel);
}

public static async Task<RestThreadChannel> CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
public static async Task<RestThreadChannel> CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title,
ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null,
RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null,
Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ulong[] tagIds = null)
{
embeds ??= Array.Empty<Embed>();
if (embed != null)
@@ -112,6 +121,7 @@ namespace Discord.Rest
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
Preconditions.AtMost(tagIds?.Length ?? 0, 5, nameof(tagIds), "Forum post can have max 5 applied tags.");

// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)
@@ -134,10 +144,12 @@ namespace Discord.Rest
Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed.");
}


if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds)
throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags));

if (channel.Flags.HasFlag(ChannelFlags.RequireTag))
throw new ArgumentException($"The channel {channel.Name} requires posts to have at least one tag.");

var args = new CreatePostParams()
{
Title = title,
@@ -151,7 +163,8 @@ namespace Discord.Rest
Flags = flags,
Components = components?.Components?.Any() ?? false ? components.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional<API.ActionRowComponent[]>.Unspecified,
Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional<ulong[]>.Unspecified,
}
},
Tags = tagIds
};

var model = await client.ApiClient.CreatePostAsync(channel.Id, args, options).ConfigureAwait(false);
@@ -159,7 +172,9 @@ namespace Discord.Rest
return RestThreadChannel.Create(client, channel.Guild, model);
}

public static async Task<RestThreadChannel> CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
public static async Task<RestThreadChannel> CreatePostAsync(IForumChannel channel, BaseDiscordClient client, string title, IEnumerable<FileAttachment> attachments,
ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components,
ISticker[] stickers, Embed[] embeds, MessageFlags flags, ulong[] tagIds = null)
{
embeds ??= Array.Empty<Embed>();
if (embed != null)
@@ -168,6 +183,8 @@ namespace Discord.Rest
Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed.");
Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed.");
Preconditions.AtMost(embeds.Length, 10, nameof(embeds), "A max of 10 embeds are allowed.");
Preconditions.AtMost(tagIds?.Length ?? 0, 5, nameof(tagIds), "Forum post can have max 5 applied tags.");


// check that user flag and user Id list are exclusive, same with role flag and role Id list
if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue)
@@ -190,9 +207,11 @@ namespace Discord.Rest
Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed.");
}


if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds)
throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags));
if (channel.Flags.HasFlag(ChannelFlags.RequireTag))
throw new ArgumentException($"The channel {channel.Name} requires posts to have at least one tag.");

var args = new CreateMultipartPostAsync(attachments.ToArray())
{


+ 61
- 0
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -1,3 +1,4 @@
using Discord.API;
using Discord.API.Rest;
using System;
using System.Collections.Generic;
@@ -252,6 +253,7 @@ namespace Discord.Rest
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
DefaultAutoArchiveDuration = props.AutoArchiveDuration
};
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
return RestTextChannel.Create(client, guild, model);
@@ -338,6 +340,65 @@ namespace Discord.Rest
var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
return RestCategoryChannel.Create(client, guild, model);
}

/// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception>
public static async Task<RestForumChannel> CreateForumChannelAsync(IGuild guild, BaseDiscordClient client,
string name, RequestOptions options, Action<ForumChannelProperties> func = null)
{
if (name == null)
throw new ArgumentNullException(paramName: nameof(name));

var props = new ForumChannelProperties();
func?.Invoke(props);

Preconditions.AtMost(props.Tags.IsSpecified ? props.Tags.Value.Count() : 0, 5, nameof(props.Tags), "Forum channel can have max 20 tags.");

var args = new CreateGuildChannelParams(name, ChannelType.Forum)
{
Position = props.Position,
Overwrites = props.PermissionOverwrites.IsSpecified
? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite
{
TargetId = overwrite.TargetId,
TargetType = overwrite.TargetType,
Allow = overwrite.Permissions.AllowValue.ToString(),
Deny = overwrite.Permissions.DenyValue.ToString()
}).ToArray()
: Optional.Create<API.Overwrite[]>(),
SlowModeInterval = props.ThreadCreationInterval,
AvailableTags = props.Tags.GetValueOrDefault(Array.Empty<ForumTagProperties>()).Select(
x => new ModifyForumTagParams
{
Id = x.Id,
Name = x.Name,
EmojiId = x.Emoji is Emote emote
? emote.Id
: Optional<ulong?>.Unspecified,
EmojiName = x.Emoji is Emoji emoji
? emoji.Name
: Optional<string>.Unspecified,
Moderated = x.IsModerated
}).ToArray(),
DefaultReactionEmoji = props.DefaultReactionEmoji.IsSpecified
? new API.ModifyForumReactionEmojiParams
{
EmojiId = props.DefaultReactionEmoji.Value is Emote emote ?
emote.Id : Optional<ulong?>.Unspecified,
EmojiName = props.DefaultReactionEmoji.Value is Emoji emoji ?
emoji.Name : Optional<string>.Unspecified
}
: Optional<ModifyForumReactionEmojiParams>.Unspecified,
ThreadRateLimitPerUser = props.DefaultSlowModeInterval,
CategoryId = props.CategoryId,
IsNsfw = props.IsNsfw,
Topic = props.Topic,
DefaultAutoArchiveDuration = props.AutoArchiveDuration,
DefaultSortOrder = props.DefaultSortOrder.GetValueOrDefault(ForumSortOrder.LatestActivity)
};

var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false);
return RestForumChannel.Create(client, guild, model);
}
#endregion

#region Voice Regions


+ 16
- 0
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -710,6 +710,19 @@ namespace Discord.Rest
public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null)
=> GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func);

/// <summary>
/// Creates a category channel with the provided name.
/// </summary>
/// <param name="name">The name of the new channel.</param>
/// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name" /> is <see langword="null"/>.</exception>
/// <returns>
/// The created category channel.
/// </returns>
public Task<RestForumChannel> CreateForumChannelAsync(string name, Action<ForumChannelProperties> func = null, RequestOptions options = null)
=> GuildHelper.CreateForumChannelAsync(this, Discord, name, options, func);

/// <summary>
/// Gets a collection of all the voice regions this guild can access.
/// </summary>
@@ -1370,6 +1383,9 @@ namespace Discord.Rest
/// <inheritdoc />
async Task<ICategoryChannel> IGuild.CreateCategoryAsync(string name, Action<GuildChannelProperties> func, RequestOptions options)
=> await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IForumChannel> IGuild.CreateForumChannelAsync(string name, Action<ForumChannelProperties> func, RequestOptions options)
=> await CreateForumChannelAsync(name, func, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IReadOnlyCollection<IVoiceRegion>> IGuild.GetVoiceRegionsAsync(RequestOptions options)


+ 1
- 5
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -352,10 +352,6 @@ namespace Discord.WebSocket
if (guild.IsAvailable)
await GuildUnavailableAsync(guild).ConfigureAwait(false);
}

_sessionId = null;
_lastSeq = 0;
ApiClient.ResumeGatewayUrl = null;
}

/// <inheritdoc />
@@ -906,7 +902,7 @@ namespace Discord.WebSocket
var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null;
currentUser.Presence = new SocketPresence(Status, null, activities);
ApiClient.CurrentUserId = currentUser.Id;
ApiClient.CurrentApplicationId = data.Application.Id;
ApiClient.CurrentApplicationId = data.Application?.Id;
Rest.CurrentUser = RestSelfUser.Create(this, data.User);
int unavailableGuilds = 0;
for (int i = 0; i < data.Guilds.Length; i++)


+ 93
- 21
src/Discord.Net.WebSocket/Entities/Channels/SocketForumChannel.cs View File

@@ -27,9 +27,33 @@ namespace Discord.WebSocket
/// <inheritdoc/>
public IReadOnlyCollection<ForumTag> Tags { get; private set; }

/// <inheritdoc/>
public int ThreadCreationInterval { get; private set; }

/// <inheritdoc/>
public int DefaultSlowModeInterval { get; private set; }

/// <inheritdoc/>
public string Mention => MentionUtils.MentionChannel(Id);

/// <inheritdoc/>
public ulong? CategoryId { get; private set; }

/// <inheritdoc/>
public IEmote DefaultReactionEmoji { get; private set; }

/// <inheritdoc/>
public ForumSortOrder? DefaultSortOrder { get; private set; }

/// <summary>
/// Gets the parent (category) of this channel in the guild's channel list.
/// </summary>
/// <returns>
/// An <see cref="ICategoryChannel"/> representing the parent of this channel; <c>null</c> if none is set.
/// </returns>
public ICategoryChannel Category
=> CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null;

internal SocketForumChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { }

internal new static SocketForumChannel Create(SocketGuild guild, ClientState state, Model model)
@@ -46,46 +70,70 @@ namespace Discord.WebSocket
Topic = model.Topic.GetValueOrDefault();
DefaultAutoArchiveDuration = model.AutoArchiveDuration.GetValueOrDefault(ThreadArchiveDuration.OneDay);

if (model.ThreadRateLimitPerUser.IsSpecified)
DefaultSlowModeInterval = model.ThreadRateLimitPerUser.Value;

if (model.SlowMode.IsSpecified)
ThreadCreationInterval = model.SlowMode.Value;

DefaultSortOrder = model.DefaultSortOrder.GetValueOrDefault();

Tags = model.ForumTags.GetValueOrDefault(Array.Empty<API.ForumTags>()).Select(
x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault())
x => new ForumTag(x.Id, x.Name, x.EmojiId.GetValueOrDefault(null), x.EmojiName.GetValueOrDefault(), x.Moderated)
).ToImmutableArray();

if (model.DefaultReactionEmoji.IsSpecified && model.DefaultReactionEmoji.Value is not null)
{
if (model.DefaultReactionEmoji.Value.EmojiId.HasValue && model.DefaultReactionEmoji.Value.EmojiId.Value != 0)
DefaultReactionEmoji = new Emote(model.DefaultReactionEmoji.Value.EmojiId.GetValueOrDefault(), null, false);
else if (model.DefaultReactionEmoji.Value.EmojiName.IsSpecified)
DefaultReactionEmoji = new Emoji(model.DefaultReactionEmoji.Value.EmojiName.Value);
else
DefaultReactionEmoji = null;
}

CategoryId = model.CategoryId.GetValueOrDefault();
}

/// <inheritdoc cref="IForumChannel.CreatePostAsync(string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
public Task<RestThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);
/// <inheritdoc />
public virtual Task ModifyAsync(Action<ForumChannelProperties> func, RequestOptions options = null)
=> ForumHelper.ModifyAsync(this, Discord, func, options);

/// <inheritdoc cref="IForumChannel.CreatePostAsync(string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public Task<RestThreadChannel> CreatePostAsync(string title, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay, int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
=> ThreadHelper.CreatePostAsync(this, Discord, title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray());

/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public async Task<RestThreadChannel> CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
{
using var file = new FileAttachment(filePath, isSpoiler: isSpoiler);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false);
}

/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, Stream, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, Stream, string, ThreadArchiveDuration, int?, string, Embed, RequestOptions, bool, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public async Task<RestThreadChannel> CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, bool isSpoiler = false,
AllowedMentions allowedMentions = null, MessageComponent components = null,
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
{
using var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
return await ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { file }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray()).ConfigureAwait(false);
}

/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, FileAttachment, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
/// <inheritdoc cref="IForumChannel.CreatePostWithFileAsync(string, FileAttachment, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public Task<RestThreadChannel> CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
=> ThreadHelper.CreatePostAsync(this, Discord, title, new FileAttachment[] { attachment }, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray());

/// <inheritdoc cref="IForumChannel.CreatePostWithFilesAsync(string, IEnumerable{FileAttachment}, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags)"/>
/// <inheritdoc cref="IForumChannel.CreatePostWithFilesAsync(string, IEnumerable{FileAttachment}, ThreadArchiveDuration, int?, string, Embed, RequestOptions, AllowedMentions, MessageComponent, ISticker[], Embed[], MessageFlags, ForumTag[])"/>
public Task<RestThreadChannel> CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration = ThreadArchiveDuration.OneDay,
int? slowmode = null, string text = null, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null,
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None)
=> ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);
MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None, ForumTag[] tags = null)
=> ThreadHelper.CreatePostAsync(this, Discord, title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags, tags?.Select(tag => tag.Id).ToArray());

/// <inheritdoc cref="IForumChannel.GetActiveThreadsAsync(RequestOptions)"/>
public Task<IReadOnlyCollection<RestThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null)
@@ -112,17 +160,41 @@ namespace Discord.WebSocket
=> 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);
async Task<IThreadChannel> IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostAsync(string title, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostAsync(title, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, string filePath, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostWithFileAsync(title, filePath, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, Stream stream, string filename, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostWithFileAsync(title, stream, filename, archiveDuration, slowmode, text, embed, options, isSpoiler, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostWithFileAsync(string title, FileAttachment attachment, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostWithFileAsync(title, attachment, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags).ConfigureAwait(false);
async Task<IThreadChannel> IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags)
async Task<IThreadChannel> IForumChannel.CreatePostWithFilesAsync(string title, IEnumerable<FileAttachment> attachments, ThreadArchiveDuration archiveDuration, int? slowmode, string text, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags, ForumTag[] tags)
=> await CreatePostWithFilesAsync(title, attachments, archiveDuration, slowmode, text, embed, options, allowedMentions, components, stickers, embeds, flags);

#endregion

#region INestedChannel
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false);
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(ulong applicationId, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, applicationId, options);
/// <inheritdoc />
public virtual async Task<IInviteMetadata> CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> await ChannelHelper.CreateInviteToApplicationAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, (ulong)application, options);
public virtual Task<IInviteMetadata> CreateInviteToStreamAsync(IUser user, int? maxAge, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null)
=> throw new NotImplementedException();
/// <inheritdoc />
public virtual async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null)
=> await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false);

/// <inheritdoc />
Task<ICategoryChannel> INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult(Category);

/// <inheritdoc />
public virtual Task SyncPermissionsAsync(RequestOptions options = null)
=> ChannelHelper.SyncPermissionsAsync(this, Discord, options);
#endregion
}
}

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

@@ -30,6 +30,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
public int Position { get; private set; }

/// <inheritdoc />
public ChannelFlags Flags { get; private set; }

/// <inheritdoc />
public virtual IReadOnlyCollection<Overwrite> PermissionOverwrites => _overwrites;
/// <summary>
@@ -74,6 +77,8 @@ namespace Discord.WebSocket
for (int i = 0; i < overwrites.Length; i++)
newOverwrites.Add(overwrites[i].ToEntity());
_overwrites = newOverwrites.ToImmutable();

Flags = model.Flags.GetValueOrDefault(ChannelFlags.None);
}

/// <inheritdoc />


+ 9
- 3
src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs View File

@@ -89,6 +89,9 @@ namespace Discord.WebSocket
/// <inheritdoc/>
public bool? IsInvitable { get; private set; }

/// <inheritdoc/>
public IReadOnlyCollection<ulong> AppliedTags { get; private set; }

/// <inheritdoc cref="IThreadChannel.CreatedAt"/>
public override DateTimeOffset CreatedAt { get; }

@@ -149,6 +152,8 @@ namespace Discord.WebSocket
}

HasJoined = model.ThreadMember.IsSpecified;

AppliedTags = model.AppliedTags.GetValueOrDefault(Array.Empty<ulong>()).ToImmutableArray();
}

internal IReadOnlyCollection<SocketThreadUser> RemoveUsers(ulong[] users)
@@ -334,12 +339,13 @@ namespace Discord.WebSocket
=> throw new NotSupportedException("This method is not supported in threads.");

/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>
/// </remarks>
public override Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null)
=> ThreadHelper.ModifyAsync(this, Discord, func, options);

/// <inheritdoc/>
public Task ModifyAsync(Action<ThreadChannelProperties> func, RequestOptions options = null)
=> ThreadHelper.ModifyAsync(this, Discord, func, options);

/// <inheritdoc/>
/// <remarks>
/// <b>This method is not supported in threads.</b>


+ 28
- 0
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -302,6 +302,16 @@ namespace Discord.WebSocket
/// </returns>
public IReadOnlyCollection<SocketThreadChannel> ThreadChannels
=> Channels.OfType<SocketThreadChannel>().ToImmutableArray();

/// <summary>
/// Gets a collection of all forum channels in this guild.
/// </summary>
/// <returns>
/// A read-only collection of forum channels found within this guild.
/// </returns>
public IReadOnlyCollection<SocketForumChannel> ForumChannels
=> Channels.OfType<SocketForumChannel>().ToImmutableArray();

/// <summary>
/// Gets the current logged-in user.
/// </summary>
@@ -790,6 +800,7 @@ namespace Discord.WebSocket
/// </returns>
public Task<RestStageChannel> CreateStageChannelAsync(string name, Action<VoiceChannelProperties> func = null, RequestOptions options = null)
=> GuildHelper.CreateStageChannelAsync(this, Discord, name, options, func);

/// <summary>
/// Creates a new channel category in this guild.
/// </summary>
@@ -804,6 +815,20 @@ namespace Discord.WebSocket
public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null)
=> GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func);

/// <summary>
/// Creates a new channel forum in this guild.
/// </summary>
/// <param name="name">The new name for the forum.</param>
/// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// forum channel.
/// </returns>
public Task<RestForumChannel> CreateForumChannelAsync(string name, Action<ForumChannelProperties> func = null, RequestOptions options = null)
=> GuildHelper.CreateForumChannelAsync(this, Discord, name, options, func);

internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model)
{
var channel = SocketGuildChannel.Create(this, state, model);
@@ -1897,6 +1922,9 @@ namespace Discord.WebSocket
/// <inheritdoc />
async Task<ICategoryChannel> IGuild.CreateCategoryAsync(string name, Action<GuildChannelProperties> func, RequestOptions options)
=> await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false);
/// <inheritdoc />
async Task<IForumChannel> IGuild.CreateForumChannelAsync(string name, Action<ForumChannelProperties> func, RequestOptions options)
=> await CreateForumChannelAsync(name, func, options).ConfigureAwait(false);

/// <inheritdoc />
async Task<IReadOnlyCollection<IVoiceRegion>> IGuild.GetVoiceRegionsAsync(RequestOptions options)


+ 5
- 2
src/Discord.Net/Discord.Net.nuspec View File

@@ -9,9 +9,9 @@
<description>An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components.</description>
<tags>discord;discordapp</tags>
<projectUrl>https://github.com/discord-net/Discord.Net</projectUrl>
<licenseUrl>http://opensource.org/licenses/MIT</licenseUrl>
<license type="expression">MIT</license>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<iconUrl>https://github.com/discord-net/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png</iconUrl>
<icon>PackageLogo.png</icon>
<dependencies>
<group targetFramework="net6.0">
<dependency id="Discord.Net.Core" version="3.9.0$suffix$" />
@@ -55,4 +55,7 @@
</group>
</dependencies>
</metadata>
<files>
<file src="../../docs/marketing/logo/PackageLogo.png" />
</files>
</package>

+ 2
- 0
test/Discord.Net.Tests.Unit/MockedEntities/MockedCategoryChannel.cs View File

@@ -20,6 +20,8 @@ namespace Discord
public DateTimeOffset CreatedAt => throw new NotImplementedException();

public ulong Id => throw new NotImplementedException();
public ChannelFlags Flags => throw new NotImplementedException();

public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
{


+ 2
- 0
test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs View File

@@ -34,6 +34,8 @@ namespace Discord

public ulong Id => throw new NotImplementedException();

public ChannelFlags Flags => throw new NotImplementedException();

public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null)
{
throw new NotImplementedException();


+ 2
- 0
test/Discord.Net.Tests.Unit/MockedEntities/MockedVoiceChannel.cs View File

@@ -36,6 +36,8 @@ namespace Discord

public string Mention => throw new NotImplementedException();

public ChannelFlags Flags => throw new NotImplementedException();

public Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException();
public Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) => throw new NotImplementedException();
public Task<IAudioClient> ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false) => throw new NotImplementedException();


Loading…
Cancel
Save