Browse Source

Merge branch 'discord-net:dev' into fix-NRE-in-scheduled-event-update-audit-entry

pull/2483/head
SaculRennorb GitHub 2 years ago
parent
commit
4781d8d729
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 1972 additions and 325 deletions
  1. +28
    -0
      CHANGELOG.md
  2. +1
    -1
      Discord.Net.targets
  3. +5
    -0
      azure/deploy.yml
  4. +1
    -1
      docs/docfx.json
  5. +1
    -1
      docs/guides/concepts/samples/events.cs
  6. +1
    -1
      docs/guides/int_basics/application-commands/slash-commands/parameters.md
  7. +1
    -1
      docs/guides/int_framework/autocompletion.md
  8. +3
    -0
      docs/guides/int_framework/intro.md
  9. +5
    -0
      docs/guides/int_framework/permissions.md
  10. +6
    -0
      docs/guides/int_framework/samples/permissions/nsfw-permissions.cs
  11. +1
    -0
      src/Discord.Net.Core/Audio/IAudioClient.cs
  12. +14
    -12
      src/Discord.Net.Core/DiscordErrorCode.cs
  13. +26
    -15
      src/Discord.Net.Core/Entities/ApplicationFlags.cs
  14. +22
    -0
      src/Discord.Net.Core/Entities/Channels/ChannelFlags.cs
  15. +65
    -0
      src/Discord.Net.Core/Entities/Channels/ForumChannelProperties.cs
  16. +22
    -0
      src/Discord.Net.Core/Entities/Channels/ForumLayout.cs
  17. +17
    -0
      src/Discord.Net.Core/Entities/Channels/ForumSortOrder.cs
  18. +5
    -0
      src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs
  19. +66
    -7
      src/Discord.Net.Core/Entities/Channels/IForumChannel.cs
  20. +11
    -0
      src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs
  21. +10
    -0
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  22. +25
    -0
      src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs
  23. +2
    -11
      src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs
  24. +26
    -0
      src/Discord.Net.Core/Entities/Channels/ThreadChannelProperties.cs
  25. +0
    -42
      src/Discord.Net.Core/Entities/ForumTag.cs
  26. +67
    -0
      src/Discord.Net.Core/Entities/ForumTags/ForumTag.cs
  27. +191
    -0
      src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilder.cs
  28. +11
    -0
      src/Discord.Net.Core/Entities/ForumTags/ForumTagBuilderExtensions.cs
  29. +48
    -0
      src/Discord.Net.Core/Entities/ForumTags/ForumTagProperties.cs
  30. +29
    -0
      src/Discord.Net.Core/Entities/ForumTags/IForumTag.cs
  31. +4
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildFeature.cs
  32. +28
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  33. +24
    -0
      src/Discord.Net.Core/Entities/Guilds/WelcomeScreen.cs
  34. +41
    -0
      src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannel.cs
  35. +54
    -0
      src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannelProperties.cs
  36. +1
    -1
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandOptionType.cs
  37. +5
    -0
      src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs
  38. +19
    -2
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs
  39. +19
    -2
      src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs
  40. +5
    -0
      src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs
  41. +93
    -21
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs
  42. +17
    -2
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs
  43. +22
    -2
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs
  44. +11
    -3
      src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs
  45. +2
    -2
      src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs
  46. +19
    -2
      src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs
  47. +1
    -0
      src/Discord.Net.Core/Entities/Invites/IInvite.cs
  48. +156
    -0
      src/Discord.Net.Core/Entities/Invites/InviteGuild.cs
  49. +6
    -1
      src/Discord.Net.Core/Entities/Users/PremiumType.cs
  50. +5
    -0
      src/Discord.Net.Core/Entities/Users/UserProperties.cs
  51. +3
    -0
      src/Discord.Net.Core/Extensions/ChannelExtensions.cs
  52. +91
    -0
      src/Discord.Net.Core/Extensions/UserExtensions.cs
  53. +14
    -0
      src/Discord.Net.Core/Utils/ChannelTypeUtils.cs
  54. +8
    -0
      src/Discord.Net.Core/Utils/ComponentType.cs
  55. +6
    -0
      src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs
  56. +5
    -0
      src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs
  57. +25
    -0
      src/Discord.Net.Interactions/Attributes/NsfwCommandAttribute.cs
  58. +20
    -0
      src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs
  59. +18
    -0
      src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs
  60. +14
    -0
      src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs
  61. +18
    -0
      src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs
  62. +18
    -0
      src/Discord.Net.Interactions/Builders/ModuleBuilder.cs
  63. +12
    -1
      src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs
  64. +5
    -2
      src/Discord.Net.Interactions/Info/Commands/AutocompleteCommandInfo.cs
  65. +66
    -61
      src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs
  66. +12
    -28
      src/Discord.Net.Interactions/Info/Commands/ComponentCommandInfo.cs
  67. +5
    -1
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/ContextCommandInfo.cs
  68. +9
    -4
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/MessageCommandInfo.cs
  69. +8
    -3
      src/Discord.Net.Interactions/Info/Commands/ContextCommands/UserCommandInfo.cs
  70. +16
    -21
      src/Discord.Net.Interactions/Info/Commands/ModalCommandInfo.cs
  71. +38
    -41
      src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs
  72. +5
    -0
      src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs
  73. +2
    -0
      src/Discord.Net.Interactions/Info/ICommandInfo.cs
  74. +3
    -3
      src/Discord.Net.Interactions/Info/ModalInfo.cs
  75. +6
    -0
      src/Discord.Net.Interactions/Info/ModuleInfo.cs
  76. +2
    -2
      src/Discord.Net.Interactions/InteractionService.cs
  77. +3
    -6
      src/Discord.Net.Interactions/Map/CommandMapNode.cs
  78. +6
    -6
      src/Discord.Net.Interactions/Results/ParseResult.cs
  79. +41
    -10
      src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs
  80. +8
    -1
      src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs
  81. +33
    -0
      src/Discord.Net.Interactions/Utilities/RegexUtils.cs
  82. +4
    -0
      src/Discord.Net.Rest/API/Common/ActionRowComponent.cs
  83. +3
    -0
      src/Discord.Net.Rest/API/Common/ApplicationCommand.cs
  84. +20
    -1
      src/Discord.Net.Rest/API/Common/Channel.cs
  85. +12
    -0
      src/Discord.Net.Rest/API/Common/ForumReactionEmoji.cs
  86. +3
    -0
      src/Discord.Net.Rest/API/Common/ForumTags.cs
  87. +3
    -0
      src/Discord.Net.Rest/API/Common/Guild.cs
  88. +34
    -2
      src/Discord.Net.Rest/API/Common/InviteGuild.cs
  89. +4
    -0
      src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs
  90. +19
    -0
      src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs
  91. +8
    -1
      src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs
  92. +12
    -0
      src/Discord.Net.Rest/API/Common/WelcomeScreen.cs
  93. +18
    -0
      src/Discord.Net.Rest/API/Common/WelcomeScreenChannel.cs
  94. +5
    -1
      src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs
  95. +12
    -0
      src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs
  96. +3
    -0
      src/Discord.Net.Rest/API/Rest/CreateMultipartPostAsync.cs
  97. +3
    -0
      src/Discord.Net.Rest/API/Rest/CreatePostParams.cs
  98. +6
    -0
      src/Discord.Net.Rest/API/Rest/ModifyApplicationCommandParams.cs
  99. +26
    -0
      src/Discord.Net.Rest/API/Rest/ModifyForumChannelParams.cs
  100. +15
    -0
      src/Discord.Net.Rest/API/Rest/ModifyForumReactionEmojiParams.cs

+ 28
- 0
CHANGELOG.md View File

@@ -1,5 +1,33 @@
# Changelog

## [3.9.0] - 2022-12-23
### Added

- #2469 Add missing properties in forum & thread channels (01ae904)
- #2501 Add new discord stuff (ed38635)
- #2521 Add missing property & new stuff (82b772a)
- #2520 Implemented ClientDisconnect event for audio client. (4cad546)
- #2509 Add SendFiles to UserExtensions ( 4cad546)
- #2528 Implement wildcard lenght quantifiers, TreatAsRegex property and solve catastrpohic backtracking (25cfb88)
- #2531 Add Age restricted (NSFW) application commands support (60956c7)

### Fixed

- #2500 Fix duplicated members of DiscordErrorCode (6712ef4)
- #2468 Fix TimestampTag being sadge (bc89d3c)
- #2497 Avoid throwing on missing Application (7077c44)
- #2485 Fixed an oversight clearing session data upon any disconnect. (c7ac59d)
- #2526 Fix `GetActiveThreadsAsync` & add it to `ITextChannel` (bd2f719)
- #2535 Fix deploy.yml (20d8fdf)

### Misc

- #2471 Update samples to use `MessageContent` intent & update `v2 => v3 guide` (a4d34f6)
- #2505 Update events.cs (ea039b8)
- #2467 Update license and icon nuspec props (11ed0ff)
- #2306 Command execution code rework & TypeConverters auto-scope fix (6869817)
- #2534 Fully qualify SlashCommandBuilder namespace ( 3b107c2)

## [3.8.1] - 2022-09-12
### Added



+ 1
- 1
Discord.Net.targets View File

@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>3.8.1</VersionPrefix>
<VersionPrefix>3.9.0</VersionPrefix>
<LangVersion>latest</LangVersion>
<Authors>Discord.Net Contributors</Authors>
<PackageTags>discord;discordapp</PackageTags>


+ 5
- 0
azure/deploy.yml View File

@@ -11,6 +11,11 @@ steps:
dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c $(buildConfiguration) -o "$(Build.ArtifactStagingDirectory)" /p:BuildNumber=$(buildNumber) /p:IsTagBuild=$(buildTag)
displayName: Pack projects

- task: NuGetToolInstaller@1
displayName: Download and Cache Nuget.exe
inputs:
versionSpec: 6.4.0

- task: NuGetCommand@2
displayName: Pack metapackage (release mode)
condition: eq(variables['buildTag'], True)


+ 1
- 1
docs/docfx.json View File

@@ -60,7 +60,7 @@
"overwrite": "_overwrites/**/**.md",
"globalMetadata": {
"_appTitle": "Discord.Net Documentation",
"_appFooter": "Discord.Net (c) 2015-2022 3.8.1",
"_appFooter": "Discord.Net (c) 2015-2022 3.9.0",
"_enableSearch": true,
"_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg",
"_appFaviconPath": "favicon.ico"


+ 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
- 1
docs/guides/int_basics/application-commands/slash-commands/parameters.md View File

@@ -45,7 +45,7 @@ public async Task Client_Ready()
{
ulong guildId = 848176216011046962;

var guildCommand = new SlashCommandBuilder()
var guildCommand = new Discord.SlashCommandBuilder()
.WithName("list-roles")
.WithDescription("Lists all roles of a user.")
.AddOption("user", ApplicationCommandOptionType.User, "The users whos roles you want to be listed", isRequired: true);


+ 1
- 1
docs/guides/int_framework/autocompletion.md View File

@@ -7,7 +7,7 @@ title: Command Autocompletion

[Autocompleters] provide a similar pattern to TypeConverters.
[Autocompleters] are cached, singleton services and they are used by the
Interaction Service to handle Autocomplete Interations targeted to a specific Slash Command parameter.
Interaction Service to handle Autocomplete Interactions targeted to a specific Slash Command parameter.

To start using AutocompleteHandlers, use the `[AutocompleteAttribute(Type type)]` overload of the [AutocompleteAttribute].
This will dynamically link the parameter to the [AutocompleteHandler] type.


+ 3
- 0
docs/guides/int_framework/intro.md View File

@@ -208,6 +208,9 @@ You may use as many wild card characters as you want.
Unlike button interactions, select menu interactions also contain the values of the selected menu items.
In this case, you should structure your method to accept a string array.

> [!NOTE]
> Use arrays of `IUser`, `IChannel`, `IRole`, `IMentionable` or their implementations to get data from a select menu with respective type.

[!code-csharp[Dropdown](samples/intro/dropdown.cs)]

> [!NOTE]


+ 5
- 0
docs/guides/int_framework/permissions.md View File

@@ -55,5 +55,10 @@ The amount of nesting you can do is realistically endless.
> If the nested class is marked with `Group`, as required for setting up subcommands, this example will not work.
> As mentioned before, subcommands cannot have seperate permissions from the top level command.

### NSFW Commands
Commands can be limited to only age restricted channels and DMs:

[!code-csharp[Nsfw-Permissions](samples/permissions/nsfw-permissions.cs)]

[permissions]: xref:Discord.GuildPermission


+ 6
- 0
docs/guides/int_framework/samples/permissions/nsfw-permissions.cs View File

@@ -0,0 +1,6 @@
[NsfwCommand(true)]
[SlashCommand("beautiful-code", "Get an image of perfect code")]
public async Task BeautifulCodeAsync(...)
{
...
}

+ 1
- 0
src/Discord.Net.Core/Audio/IAudioClient.cs View File

@@ -13,6 +13,7 @@ namespace Discord.Audio
event Func<ulong, AudioInStream, Task> StreamCreated;
event Func<ulong, Task> StreamDestroyed;
event Func<ulong, bool, Task> SpeakingUpdated;
event Func<ulong, Task> ClientDisconnected;

/// <summary> Gets the current connection state of this client. </summary>
ConnectionState ConnectionState { get; }


+ 14
- 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,10 +174,10 @@ namespace Discord
ServerRequiresMonetization = 50097,
ServerRequiresBoosts = 50101,
RequestBodyContainsInvalidJSON = 50109,
FailedToResizeAssetBelowTheMaximumSize = 50138,
OwnershipCannotBeTransferredToABotUser = 50132,
AssetResizeBelowTheMaximumSize= 50138,
FailedToResizeAssetBelowTheMaximumSize = 50138,
UploadedFileNotFound = 50146,
FeatureInProcessOfRollingOut = 50155,
MissingPermissionToSendThisSticker = 50600,
#endregion

@@ -213,8 +215,8 @@ namespace Discord
LottieCantContainRasters = 170002,
StickerMaximumFramerateExceeded = 170003,
StickerMaximumFrameCountExceeded = 170004,
LottieMaximumDimentionsExceeded = 170005,
StickerFramerateBoundsExceeed = 170006,
LottieMaximumDimensionsExceeded = 170005,
StickerFramerateBoundsExceeded = 170006,
StickerAnimationDurationTooLong = 170007,
#endregion

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


+ 26
- 15
src/Discord.Net.Core/Entities/ApplicationFlags.cs View File

@@ -4,20 +4,31 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
namespace Discord;

/// <summary>
/// Represents public flags for an application.
/// </summary>
public enum ApplicationFlags
{
/// <summary>
/// Represents public flags for an application.
/// </summary>
public enum ApplicationFlags
{
GatewayPresence = 1 << 12,
GatewayPresenceLimited = 1 << 13,
GatewayGuildMembers = 1 << 14,
GatewayGuildMembersLimited = 1 << 15,
VerificationPendingGuildLimit = 1 << 16,
Embedded = 1 << 17,
GatewayMessageContent = 1 << 18,
GatewayMessageContentLimited = 1 << 19
}
GatewayPresence = 1 << 12,

GatewayPresenceLimited = 1 << 13,

GatewayGuildMembers = 1 << 14,

GatewayGuildMembersLimited = 1 << 15,

VerificationPendingGuildLimit = 1 << 16,

Embedded = 1 << 17,

GatewayMessageContent = 1 << 18,

GatewayMessageContentLimited = 1 << 19,

ApplicationCommandBadge = 1 << 23,

ActiveApplication = 1 << 24
}


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

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

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

/// <summary>
/// Gets or sets the rule used to display posts in a forum channel.
/// </summary>
public Optional<ForumLayout> DefaultLayout { get; set; }
}

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

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

/// <summary>
/// Represents the layout type used to display posts in a forum channel.
/// </summary>
public enum ForumLayout
{
/// <summary>
/// A preferred forum layout hasn't been set by a server admin
/// </summary>
Default = 0,

/// <summary>
/// List View: display forum posts in a text-focused list
/// </summary>
List = 1,

/// <summary>
/// Gallery View: display forum posts in a media-focused gallery
/// </summary>
Grid = 2
}

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

+ 66
- 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,60 @@ 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 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>
/// Gets the rule used to display posts in a forum channel.
/// </summary>
ForumLayout DefaultLayout { 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 +106,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 +133,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 +162,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 +189,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 +213,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>


+ 10
- 0
src/Discord.Net.Core/Entities/Channels/ITextChannel.cs View File

@@ -160,5 +160,15 @@ namespace Discord
/// </returns>
Task<IThreadChannel> CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay,
IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null);

/// <summary>
/// Gets a collection of active threads within this channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents an asynchronous get operation for retrieving the threads. The task result contains
/// a collection of active threads.
/// </returns>
Task<IReadOnlyCollection<IThreadChannel>> GetActiveThreadsAsync(RequestOptions options = null);
}
}

+ 25
- 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>
@@ -65,6 +74,11 @@ namespace Discord
/// </remarks>
new DateTimeOffset CreatedAt { get; }

/// <summary>
/// Gets the id of the creator of the thread.
/// </summary>
ulong OwnerId { get; }

/// <summary>
/// Joins the current thread.
/// </summary>
@@ -102,5 +116,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,
}
}

+ 28
- 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>
@@ -1239,5 +1251,21 @@ namespace Discord
/// </returns>
Task<IReadOnlyCollection<IApplicationCommand>> BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties,
RequestOptions options = null);

/// <summary>
/// Gets the welcome screen of the guild. Returns <see langword="null"/> if the welcome channel is not set.
/// </summary>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains a <see cref="WelcomeScreen"/>.
/// </returns>
Task<WelcomeScreen> GetWelcomeScreenAsync(RequestOptions options = null);

/// <summary>
/// Modifies the welcome screen of the guild. Returns <see langword="null"/> if welcome screen is removed.
/// </summary>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains a <see cref="WelcomeScreen"/>.
/// </returns>
Task<WelcomeScreen> ModifyWelcomeScreenAsync(bool enabled, WelcomeScreenChannelProperties[] channels, string description = null, RequestOptions options = null);
}
}

+ 24
- 0
src/Discord.Net.Core/Entities/Guilds/WelcomeScreen.cs View File

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

namespace Discord;

public class WelcomeScreen
{
/// <summary>
/// Gets the server description shown in the welcome screen. <see langword="null"/> if not set.
/// </summary>
public string Description { get; }

/// <summary>
/// Gets the channels shown in the welcome screen, up to 5 channels.
/// </summary>
public IReadOnlyCollection<WelcomeScreenChannel> Channels { get; }

internal WelcomeScreen(string description, IReadOnlyCollection<WelcomeScreenChannel> channels)
{
Description = description;

Channels = channels.ToImmutableArray();
}
}

+ 41
- 0
src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannel.cs View File

@@ -0,0 +1,41 @@
using System;

namespace Discord;

public class WelcomeScreenChannel : ISnowflakeEntity
{
/// <summary>
/// Gets the channel's id.
/// </summary>
public ulong Id { get; }

/// <summary>
/// Gets the description shown for the channel.
/// </summary>
public string Description { get; }

/// <summary>
/// Gets the emoji for this channel. <see cref="Emoji"/> if it is unicode emoji, <see cref="Emote"/> if it is a custom one and <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>
public IEmote Emoji { get; }

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

internal WelcomeScreenChannel(ulong id, string description, string emojiName = null, ulong? emoteId = null)
{
Id = id;
Description = description;

if (emoteId.HasValue && emoteId.Value != 0)
Emoji = new Emote(emoteId.Value, emojiName, false);
else if (emojiName != null)
Emoji = new Emoji(emojiName);
else
Emoji = null;
}
}

+ 54
- 0
src/Discord.Net.Core/Entities/Guilds/WelcomeScreenChannelProperties.cs View File

@@ -0,0 +1,54 @@
using System;
using System.Xml.Linq;

namespace Discord;

public class WelcomeScreenChannelProperties : ISnowflakeEntity
{
/// <summary>
/// Gets or sets the channel's id.
/// </summary>
public ulong Id { get; set; }

/// <summary>
/// Gets or sets the description shown for the channel.
/// </summary>
public string Description { get; set; }

/// <summary>
/// Gets or sets the emoji for this channel. <see cref="Emoji"/> if it is unicode emoji, <see cref="Emote"/> if it is a custom one and <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>
public IEmote Emoji { get; set; }

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

/// <summary>
/// Initializes a new instance of <see cref="WelcomeScreenChannelProperties"/>.
/// </summary>
/// <param name="id">Id if a channel.</param>
/// <param name="description">Description for the channel in the welcome screen.</param>
/// <param name="emoji">The emoji for the channel in the welcome screen.</param>
public WelcomeScreenChannelProperties(ulong id, string description, IEmote emoji = null)
{
Id = id;
Description = description;
Emoji = emoji;
}

/// <summary>
/// Initializes a new instance of <see cref="WelcomeScreenChannelProperties"/>.
/// </summary>
public WelcomeScreenChannelProperties() { }
/// <summary>
/// Initializes a new instance of <see cref="WelcomeScreenChannelProperties"/>.
/// </summary>
/// <param name="channel">A welcome screen channel to modify.</param>
/// <returns>A new instance of <see cref="WelcomeScreenChannelProperties"/>.</returns>
public static WelcomeScreenChannelProperties FromWelcomeScreenChannel(WelcomeScreenChannel channel)
=> new (channel.Id, channel.Description, channel.Emoji);
}

+ 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,



+ 5
- 0
src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs View File

@@ -83,6 +83,11 @@ namespace Discord
/// </summary>
public Optional<bool> IsDMEnabled { get; set; }

/// <summary>
/// Gets or sets whether or not this command is age restricted.
/// </summary>
public Optional<bool> IsNsfw { get; set; }

/// <summary>
/// Gets or sets the default permissions required by a user to execute this application command.
/// </summary>


+ 19
- 2
src/Discord.Net.Core/Entities/Interactions/ContextMenus/MessageCommandBuilder.cs View File

@@ -46,6 +46,11 @@ namespace Discord
/// </summary>
public bool IsDMEnabled { get; set; } = true;

/// <summary>
/// Gets or sets whether or not this command is age restricted.
/// </summary>
public bool IsNsfw{ get; set; } = false;

/// <summary>
/// Gets or sets the default permission required to use this slash command.
/// </summary>
@@ -68,7 +73,8 @@ namespace Discord
IsDefaultPermission = IsDefaultPermission,
IsDMEnabled = IsDMEnabled,
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified,
NameLocalizations = NameLocalizations
NameLocalizations = NameLocalizations,
IsNsfw = IsNsfw,
};

return props;
@@ -123,7 +129,7 @@ namespace Discord
}

/// <summary>
/// Sets whether or not this command can be used in dms
/// Sets whether or not this command can be used in dms.
/// </summary>
/// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param>
/// <returns>The current builder.</returns>
@@ -133,6 +139,17 @@ namespace Discord
return this;
}

/// <summary>
/// Sets whether or not this command is age restricted.
/// </summary>
/// <param name="permission"><see langword="true"/> if the command is age restricted, otherwise <see langword="false"/>.</param>
/// <returns>The current builder.</returns>
public MessageCommandBuilder WithNsfw(bool permission)
{
IsNsfw = permission;
return this;
}

/// <summary>
/// Adds a new entry to the <see cref="NameLocalizations"/> collection.
/// </summary>


+ 19
- 2
src/Discord.Net.Core/Entities/Interactions/ContextMenus/UserCommandBuilder.cs View File

@@ -46,6 +46,11 @@ namespace Discord
/// </summary>
public bool IsDMEnabled { get; set; } = true;

/// <summary>
/// Gets or sets whether or not this command is age restricted.
/// </summary>
public bool IsNsfw { get; set; } = false;

/// <summary>
/// Gets or sets the default permission required to use this slash command.
/// </summary>
@@ -66,7 +71,8 @@ namespace Discord
IsDefaultPermission = IsDefaultPermission,
IsDMEnabled = IsDMEnabled,
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified,
NameLocalizations = NameLocalizations
NameLocalizations = NameLocalizations,
IsNsfw = IsNsfw,
};

return props;
@@ -121,7 +127,7 @@ namespace Discord
}
/// <summary>
/// Sets whether or not this command can be used in dms
/// Sets whether or not this command can be used in dms.
/// </summary>
/// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param>
/// <returns>The current builder.</returns>
@@ -131,6 +137,17 @@ namespace Discord
return this;
}

/// <summary>
/// Sets whether or not this command is age restricted.
/// </summary>
/// <param name="permission"><see langword="true"/> if the command is age restricted, otherwise <see langword="false"/>.</param>
/// <returns>The current builder.</returns>
public UserCommandBuilder WithNsfw(bool permission)
{
IsNsfw = permission;
return this;
}

/// <summary>
/// Adds a new entry to the <see cref="NameLocalizations"/> collection.
/// </summary>


+ 5
- 0
src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs View File

@@ -42,6 +42,11 @@ namespace Discord
/// </remarks>
bool IsEnabledInDm { get; }

/// <summary>
/// Indicates whether the command is age restricted.
/// </summary>
bool IsNsfw { get; }

/// <summary>
/// Set of default <see cref="GuildPermission"/> required to invoke the command.
/// </summary>


+ 93
- 21
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs View File

@@ -1,7 +1,7 @@
using Discord.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using Discord.Utils;

namespace Discord
{
@@ -92,9 +92,11 @@ namespace Discord
/// <param name="maxValues">The max values of the placeholder.</param>
/// <param name="disabled">Whether or not the menu is disabled.</param>
/// <param name="row">The row to add the menu to.</param>
/// <param name="type">The type of the select menu.</param>
/// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param>
/// <returns></returns>
public ComponentBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options,
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0)
public ComponentBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options = null,
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false, int row = 0, ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null)
{
return WithSelectMenu(new SelectMenuBuilder()
.WithCustomId(customId)
@@ -102,7 +104,9 @@ namespace Discord
.WithPlaceholder(placeholder)
.WithMaxValues(maxValues)
.WithMinValues(minValues)
.WithDisabled(disabled),
.WithDisabled(disabled)
.WithType(type)
.WithChannelTypes(channelTypes),
row);
}

@@ -118,7 +122,7 @@ namespace Discord
public ComponentBuilder WithSelectMenu(SelectMenuBuilder menu, int row = 0)
{
Preconditions.LessThan(row, MaxActionRowCount, nameof(row));
if (menu.Options.Distinct().Count() != menu.Options.Count)
if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count)
throw new InvalidOperationException("Please make sure that there is no duplicates values.");

var builtMenu = menu.Build();
@@ -278,9 +282,7 @@ namespace Discord
{
if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.TextInput) ?? false)
throw new ArgumentException("TextInputComponents are not allowed in messages.", nameof(ActionRows));
if (_actionRows?.SelectMany(x => x.Components)?.Any(x => x.Type == ComponentType.ModalSubmit) ?? false)
throw new ArgumentException("ModalSubmit components are not allowed in messages.", nameof(ActionRows));

return _actionRows != null
? new MessageComponent(_actionRows.Select(x => x.Build()).ToList())
: MessageComponent.Empty;
@@ -356,10 +358,13 @@ namespace Discord
/// <param name="placeholder">The placeholder of the menu.</param>
/// <param name="minValues">The min values of the placeholder.</param>
/// <param name="maxValues">The max values of the placeholder.</param>
/// <param name="disabled">Whether or not the menu is disabled.</param>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options,
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false)
/// <param name="disabled">Whether or not the menu is disabled.</param>
/// <param name="type">The type of the select menu.</param>
/// <param name="channelTypes">Menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>)</param>
/// <returns>The current builder.</returns>
public ActionRowBuilder WithSelectMenu(string customId, List<SelectMenuOptionBuilder> options = null,
string placeholder = null, int minValues = 1, int maxValues = 1, bool disabled = false,
ComponentType type = ComponentType.SelectMenu, ChannelType[] channelTypes = null)
{
return WithSelectMenu(new SelectMenuBuilder()
.WithCustomId(customId)
@@ -367,7 +372,9 @@ namespace Discord
.WithPlaceholder(placeholder)
.WithMaxValues(maxValues)
.WithMinValues(minValues)
.WithDisabled(disabled));
.WithDisabled(disabled)
.WithType(type)
.WithChannelTypes(channelTypes));
}

/// <summary>
@@ -378,7 +385,7 @@ namespace Discord
/// <returns>The current builder.</returns>
public ActionRowBuilder WithSelectMenu(SelectMenuBuilder menu)
{
if (menu.Options.Distinct().Count() != menu.Options.Count)
if (menu.Options is not null && menu.Options.Distinct().Count() != menu.Options.Count)
throw new InvalidOperationException("Please make sure that there is no duplicates values.");

var builtMenu = menu.Build();
@@ -431,10 +438,10 @@ namespace Discord
{
var builtButton = button.Build();

if(Components.Count >= 5)
if (Components.Count >= 5)
throw new InvalidOperationException($"Components count reached {MaxChildCount}");

if (Components.Any(x => x.Type == ComponentType.SelectMenu))
if (Components.Any(x => x.Type.IsSelectType()))
throw new InvalidOperationException($"A button cannot be added to a row with a SelectMenu");

AddComponent(builtButton);
@@ -458,11 +465,15 @@ namespace Discord
case ComponentType.ActionRow:
return false;
case ComponentType.Button:
if (Components.Any(x => x.Type == ComponentType.SelectMenu))
if (Components.Any(x => x.Type.IsSelectType()))
return false;
else
return Components.Count < 5;
case ComponentType.SelectMenu:
case ComponentType.ChannelSelect:
case ComponentType.MentionableSelect:
case ComponentType.RoleSelect:
case ComponentType.UserSelect:
return Components.Count == 0;
default:
return false;
@@ -759,6 +770,18 @@ namespace Discord
};
}

/// <summary>
/// Gets or sets the type of the current select menu.
/// </summary>
/// <exception cref="ArgumentException">Type must be a select menu type.</exception>
public ComponentType Type
{
get => _type;
set => _type = value.IsSelectType()
? value
: throw new ArgumentException("Type must be a select menu type.", nameof(value));
}

/// <summary>
/// Gets or sets the placeholder text of the current select menu.
/// </summary>
@@ -815,8 +838,6 @@ namespace Discord
{
if (value != null)
Preconditions.AtMost(value.Count, MaxOptionCount, nameof(Options));
else
throw new ArgumentNullException(nameof(value), $"{nameof(Options)} cannot be null.");

_options = value;
}
@@ -827,11 +848,17 @@ namespace Discord
/// </summary>
public bool IsDisabled { get; set; }

/// <summary>
/// Gets or sets the menu's channel types (only valid on <see cref="ComponentType.ChannelSelect"/>s).
/// </summary>
public List<ChannelType> ChannelTypes { get; set; }

private List<SelectMenuOptionBuilder> _options = new List<SelectMenuOptionBuilder>();
private int _minValues = 1;
private int _maxValues = 1;
private string _placeholder;
private string _customId;
private ComponentType _type = ComponentType.SelectMenu;

/// <summary>
/// Creates a new instance of a <see cref="SelectMenuBuilder"/>.
@@ -862,7 +889,9 @@ namespace Discord
/// <param name="maxValues">The max values of this select menu.</param>
/// <param name="minValues">The min values of this select menu.</param>
/// <param name="isDisabled">Disabled this select menu or not.</param>
public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false)
/// <param name="type">The <see cref="ComponentType"/> of this select menu.</param>
/// <param name="channelTypes">The types of channels this menu can select (only valid on <see cref="ComponentType.ChannelSelect"/>s)</param>
public SelectMenuBuilder(string customId, List<SelectMenuOptionBuilder> options = null, string placeholder = null, int maxValues = 1, int minValues = 1, bool isDisabled = false, ComponentType type = ComponentType.SelectMenu, List<ChannelType> channelTypes = null)
{
CustomId = customId;
Options = options;
@@ -870,6 +899,8 @@ namespace Discord
IsDisabled = isDisabled;
MaxValues = maxValues;
MinValues = minValues;
Type = type;
ChannelTypes = channelTypes ?? new();
}

/// <summary>
@@ -990,6 +1021,47 @@ namespace Discord
return this;
}

/// <summary>
/// Sets the menu's current type.
/// </summary>
/// <param name="type">The type of the menu.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithType(ComponentType type)
{
Type = type;
return this;
}

/// <summary>
/// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s).
/// </summary>
/// <param name="channelTypes">The valid channel types of the menu.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithChannelTypes(List<ChannelType> channelTypes)
{
ChannelTypes = channelTypes;
return this;
}

/// <summary>
/// Sets the menus valid channel types (only for <see cref="ComponentType.ChannelSelect"/>s).
/// </summary>
/// <param name="channelTypes">The valid channel types of the menu.</param>
/// <returns>
/// The current builder.
/// </returns>
public SelectMenuBuilder WithChannelTypes(params ChannelType[] channelTypes)
{
ChannelTypes = channelTypes is null
? ChannelTypeUtils.AllChannelTypes()
: channelTypes.ToList();
return this;
}

/// <summary>
/// Builds a <see cref="SelectMenuComponent"/>
/// </summary>
@@ -998,7 +1070,7 @@ namespace Discord
{
var options = Options?.Select(x => x.Build()).ToList();

return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled);
return new SelectMenuComponent(CustomId, options, Placeholder, MinValues, MaxValues, IsDisabled, Type, ChannelTypes);
}
}



+ 17
- 2
src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentType.cs View File

@@ -26,8 +26,23 @@ namespace Discord
TextInput = 4,

/// <summary>
/// An interaction sent when a model is submitted.
/// A select menu for picking from users.
/// </summary>
ModalSubmit = 5,
UserSelect = 5,

/// <summary>
/// A select menu for picking from roles.
/// </summary>
RoleSelect = 6,

/// <summary>
/// A select menu for picking from roles and users.
/// </summary>
MentionableSelect = 7,

/// <summary>
/// A select menu for picking from channels.
/// </summary>
ChannelSelect = 8,
}
}

+ 22
- 2
src/Discord.Net.Core/Entities/Interactions/MessageComponents/IComponentInteractionData.cs View File

@@ -18,12 +18,32 @@ namespace Discord
ComponentType Type { get; }

/// <summary>
/// Gets the value(s) of a <see cref="SelectMenuComponent"/> interaction response.
/// Gets the value(s) of a <see cref="ComponentType.SelectMenu"/> interaction response. <see langword="null"/> if select type is different.
/// </summary>
IReadOnlyCollection<string> Values { get; }

/// <summary>
/// Gets the value of a <see cref="TextInputComponent"/> interaction response.
/// Gets the channels(s) of a <see cref="ComponentType.ChannelSelect"/> interaction response. <see langword="null"/> if select type is different.
/// </summary>
IReadOnlyCollection<IChannel> Channels { get; }

/// <summary>
/// Gets the user(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. <see langword="null"/> if select type is different.
/// </summary>
IReadOnlyCollection<IUser> Users { get; }

/// <summary>
/// Gets the roles(s) of a <see cref="ComponentType.RoleSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. <see langword="null"/> if select type is different.
/// </summary>
IReadOnlyCollection<IRole> Roles { get; }

/// <summary>
/// Gets the guild member(s) of a <see cref="ComponentType.UserSelect"/> or <see cref="ComponentType.MentionableSelect"/> interaction response. <see langword="null"/> if type select is different.
/// </summary>
IReadOnlyCollection<IGuildUser> Members { get; }

/// <summary>
/// Gets the value of a <see cref="ComponentType.TextInput"/> interaction response.
/// </summary>
public string Value { get; }
}


+ 11
- 3
src/Discord.Net.Core/Entities/Interactions/MessageComponents/SelectMenuComponent.cs View File

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

@@ -9,7 +10,7 @@ namespace Discord
public class SelectMenuComponent : IMessageComponent
{
/// <inheritdoc/>
public ComponentType Type => ComponentType.SelectMenu;
public ComponentType Type { get; }

/// <inheritdoc/>
public string CustomId { get; }
@@ -39,6 +40,11 @@ namespace Discord
/// </summary>
public bool IsDisabled { get; }

/// <summary>
/// Gets the allowed channel types for this modal
/// </summary>
public IReadOnlyCollection<ChannelType> ChannelTypes { get; }

/// <summary>
/// Turns this select menu into a builder.
/// </summary>
@@ -52,9 +58,9 @@ namespace Discord
Placeholder,
MaxValues,
MinValues,
IsDisabled);
IsDisabled, Type, ChannelTypes.ToList());

internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled)
internal SelectMenuComponent(string customId, List<SelectMenuOption> options, string placeholder, int minValues, int maxValues, bool disabled, ComponentType type, IEnumerable<ChannelType> channelTypes = null)
{
CustomId = customId;
Options = options;
@@ -62,6 +68,8 @@ namespace Discord
MinValues = minValues;
MaxValues = maxValues;
IsDisabled = disabled;
Type = type;
ChannelTypes = channelTypes?.ToArray() ?? Array.Empty<ChannelType>();
}
}
}

+ 2
- 2
src/Discord.Net.Core/Entities/Interactions/Modals/Modal.cs View File

@@ -7,12 +7,12 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a modal interaction.
/// Represents a modal interaction.
/// </summary>
public class Modal : IMessageComponent
{
/// <inheritdoc/>
public ComponentType Type => ComponentType.ModalSubmit;
public ComponentType Type => throw new NotSupportedException("Modals do not have a component type.");

/// <summary>
/// Gets the title of the modal.


+ 19
- 2
src/Discord.Net.Core/Entities/Interactions/SlashCommands/SlashCommandBuilder.cs View File

@@ -84,6 +84,11 @@ namespace Discord
/// Gets or sets whether or not this command can be used in DMs.
/// </summary>
public bool IsDMEnabled { get; set; } = true;
/// <summary>
/// Gets or sets whether or not this command is age restricted.
/// </summary>
public bool IsNsfw { get; set; } = false;

/// <summary>
/// Gets or sets the default permission required to use this slash command.
@@ -110,7 +115,8 @@ namespace Discord
NameLocalizations = _nameLocalizations,
DescriptionLocalizations = _descriptionLocalizations,
IsDMEnabled = IsDMEnabled,
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified
DefaultMemberPermissions = DefaultMemberPermissions ?? Optional<GuildPermission>.Unspecified,
IsNsfw = IsNsfw,
};

if (Options != null && Options.Any())
@@ -161,7 +167,7 @@ namespace Discord
}

/// <summary>
/// Sets whether or not this command can be used in dms
/// Sets whether or not this command can be used in dms.
/// </summary>
/// <param name="permission"><see langword="true"/> if the command is available in dms, otherwise <see langword="false"/>.</param>
/// <returns>The current builder.</returns>
@@ -171,6 +177,17 @@ namespace Discord
return this;
}

/// <summary>
/// Sets whether or not this command is age restricted.
/// </summary>
/// <param name="permission"><see langword="true"/> if the command is age restricted, otherwise <see langword="false"/>.</param>
/// <returns>The current builder.</returns>
public SlashCommandBuilder WithNsfw(bool permission)
{
IsNsfw = permission;
return this;
}

/// <summary>
/// Sets the default member permissions required to use this application command.
/// </summary>


+ 1
- 0
src/Discord.Net.Core/Entities/Invites/IInvite.cs View File

@@ -60,6 +60,7 @@ namespace Discord
/// A guild object representing the guild that the invite points to.
/// </returns>
IGuild Guild { get; }

/// <summary>
/// Gets the ID of the guild this invite is linked to.
/// </summary>


+ 156
- 0
src/Discord.Net.Core/Entities/Invites/InviteGuild.cs View File

@@ -0,0 +1,156 @@
using System;

namespace Discord;

public class InviteGuild : ISnowflakeEntity
{
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);

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

/// <summary>
/// Gets the name of this guild.
/// </summary>
/// <returns>
/// A string containing the name of this guild.
/// </returns>
public string Name { get; private set; }

/// <summary>
/// Gets the description for the guild.
/// </summary>
/// <returns>
/// The description for the guild; <see langword="null" /> if none is set.
/// </returns>
public string Description { get; private set; }

/// <summary>
/// Gets the ID of this guild's splash image.
/// </summary>
/// <returns>
/// An identifier for the splash image; <see langword="null" /> if none is set.
/// </returns>
public string SplashId { get; private set; }

/// <summary>
/// Gets the URL of this guild's splash image.
/// </summary>
/// <returns>
/// A URL pointing to the guild's splash image; <see langword="null" /> if none is set.
/// </returns>
public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId);

/// <summary>
/// Gets the identifier for this guilds banner image.
/// </summary>
/// <returns>
/// An identifier for the banner image; <see langword="null" /> if none is set.
/// </returns>
public string BannerId { get; private set; }

/// <summary>
/// Gets the URL of this guild's banner image.
/// </summary>
/// <returns>
/// A URL pointing to the guild's banner image; <see langword="null" /> if none is set.
/// </returns>
public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId, ImageFormat.Auto);

/// <summary>
/// Gets the features for this guild.
/// </summary>
/// <returns>
/// A flags enum containing all the features for the guild.
/// </returns>
public GuildFeatures Features { get; private set; }

/// <summary>
/// Gets the ID of this guild's icon.
/// </summary>
/// <returns>
/// An identifier for the splash image; <see langword="null" /> if none is set.
/// </returns>
public string IconId { get; private set; }

/// <summary>
/// Gets the URL of this guild's icon.
/// </summary>
/// <returns>
/// A URL pointing to the guild's icon; <see langword="null" /> if none is set.
/// </returns>
public string IconUrl => CDN.GetGuildIconUrl(Id, IconId);
/// <summary>
///
/// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild.
/// </summary>
/// <returns>
/// The level of requirements.
/// </returns>
public VerificationLevel VerificationLevel { get; private set; }

/// <summary>
/// Gets the code for this guild's vanity invite URL.
/// </summary>
/// <returns>
/// A string containing the vanity invite code for this guild; <see langword="null" /> if none is set.
/// </returns>
public string VanityURLCode { get; private set; }

/// <summary>
/// Gets the number of premium subscribers of this guild.
/// </summary>
/// <remarks>
/// This is the number of users who have boosted this guild.
/// </remarks>
/// <returns>
/// The number of premium subscribers of this guild;
/// </returns>
public int PremiumSubscriptionCount { get; private set; }

/// <summary>
/// Gets the NSFW level of this guild.
/// </summary>
/// <returns>
/// The NSFW level of this guild.
/// </returns>
public NsfwLevel NsfwLevel { get; private set; }

/// <summary>
/// Gets the Welcome Screen of this guild
/// </summary>
/// <returns>
/// The welcome screen of this guild. <see langword="null" /> if none is set.
/// </returns>
public WelcomeScreen WelcomeScreen { get; private set; }

internal InviteGuild(
ulong id,
string name,
string description,
string splashId,
string bannerId,
GuildFeatures features,
string iconId,
VerificationLevel verificationLevel,
string vanityURLCode,
int premiumSubscriptionCount,
NsfwLevel nsfwLevel,
WelcomeScreen welcomeScreen)
{
Id = id;
Name = name;
Description = description;
SplashId = splashId;
BannerId = bannerId;
Features = features;
IconId = iconId;
VerificationLevel = verificationLevel;
VanityURLCode = vanityURLCode;
PremiumSubscriptionCount = premiumSubscriptionCount;
NsfwLevel = nsfwLevel;
WelcomeScreen = welcomeScreen;
}
}

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

+ 5
- 0
src/Discord.Net.Core/Entities/Users/UserProperties.cs View File

@@ -69,5 +69,10 @@ namespace Discord
/// Flag given to bots that use only outgoing webhooks, exclusively.
/// </summary>
BotHTTPInteractions = 1 << 19,

/// <summary>
/// Flag given to users that are active developers.
/// </summary>
ActiveDeveloper = 1 << 22
}
}

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


+ 91
- 0
src/Discord.Net.Core/Extensions/UserExtensions.cs View File

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

namespace Discord
@@ -163,6 +164,96 @@ namespace Discord
return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false);
}

/// <summary>
/// Sends a file via DM with an optional caption.
/// </summary>
/// <remarks>
/// This method attempts to send an attachment as a direct-message to the user.
/// <note type="warning">
/// <para>
/// Please note that this method <strong>will</strong> throw an <see cref="Discord.Net.HttpException"/>
/// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked.
/// </para>
/// <para>
/// You may want to consider catching for <see cref="Discord.Net.HttpException.DiscordCode"/>
/// <c>50007</c> when using this method.
/// </para>
/// </note>
/// <note>
/// If you wish to upload an image and have it embedded in a <see cref="Discord.EmbedType.Rich"/> embed,
/// you may upload the file and refer to the file with "attachment://filename.ext" in the
/// <see cref="Discord.EmbedBuilder.ImageUrl"/>. See the example section for its usage.
/// </note>
/// </remarks>
/// <param name="user">The user to send the DM to.</param>
/// <param name="attachment">The attachment containing the file and description.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="components">The message component to be included with this message. Used for interactions.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
public static async Task<IUserMessage> SendFileAsync(this IUser user,
FileAttachment attachment,
string text = null,
bool isTTS = false,
Embed embed = null,
RequestOptions options = null,
MessageComponent components = null,
Embed[] embeds = null)
{
return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(attachment, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false);
}

/// <summary>
/// Sends a collection of files via DM.
/// </summary>
/// <remarks>
/// This method attempts to send an attachments as a direct-message to the user.
/// <note type="warning">
/// <para>
/// Please note that this method <strong>will</strong> throw an <see cref="Discord.Net.HttpException"/>
/// if the user cannot receive DMs due to privacy reasons or if the user has the sender blocked.
/// </para>
/// <para>
/// You may want to consider catching for <see cref="Discord.Net.HttpException.DiscordCode"/>
/// <c>50007</c> when using this method.
/// </para>
/// </note>
/// <note>
/// If you wish to upload an image and have it embedded in a <see cref="Discord.EmbedType.Rich"/> embed,
/// you may upload the file and refer to the file with "attachment://filename.ext" in the
/// <see cref="Discord.EmbedBuilder.ImageUrl"/>. See the example section for its usage.
/// </note>
/// </remarks>
/// <param name="user">The user to send the DM to.</param>
/// <param name="attachments">A collection of attachments to upload.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <param name="components">The message component to be included with this message. Used for interactions.</param>
/// <param name="embeds">A array of <see cref="Embed"/>s to send with this response. Max 10.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
public static async Task<IUserMessage> SendFilesAsync(this IUser user,
IEnumerable<FileAttachment> attachments,
string text = null,
bool isTTS = false,
Embed embed = null,
RequestOptions options = null,
MessageComponent components = null,
Embed[] embeds = null)
{
return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFilesAsync(attachments, text, isTTS, embed, options, components: components, embeds: embeds).ConfigureAwait(false);
}

/// <summary>
/// Bans the user from the guild and optionally prunes their recent messages.
/// </summary>


+ 14
- 0
src/Discord.Net.Core/Utils/ChannelTypeUtils.cs View File

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

namespace Discord.Utils;

public static class ChannelTypeUtils
{
public static List<ChannelType> AllChannelTypes()
=> new List<ChannelType>()
{
ChannelType.Forum, ChannelType.Category, ChannelType.DM, ChannelType.Group, ChannelType.GuildDirectory,
ChannelType.News, ChannelType.NewsThread, ChannelType.PrivateThread, ChannelType.PublicThread,
ChannelType.Stage, ChannelType.Store, ChannelType.Text, ChannelType.Voice
};
}

+ 8
- 0
src/Discord.Net.Core/Utils/ComponentType.cs View File

@@ -0,0 +1,8 @@
namespace Discord.Utils;

public static class ComponentTypeUtils
{
public static bool IsSelectType(this ComponentType type) => type is ComponentType.ChannelSelect
or ComponentType.SelectMenu or ComponentType.RoleSelect or ComponentType.UserSelect
or ComponentType.MentionableSelect;
}

+ 6
- 0
src/Discord.Net.Interactions/Attributes/Commands/ComponentInteractionAttribute.cs View File

@@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;

namespace Discord.Interactions
{
@@ -28,6 +29,11 @@ namespace Discord.Interactions
/// </summary>
public RunMode RunMode { get; }

/// <summary>
/// Gets or sets whether the <see cref="CustomId"/> should be treated as a raw Regex pattern.
/// </summary>
public bool TreatAsRegex { get; set; } = false;

/// <summary>
/// Create a command for component interaction handling.
/// </summary>


+ 5
- 0
src/Discord.Net.Interactions/Attributes/Commands/ModalInteractionAttribute.cs View File

@@ -28,6 +28,11 @@ namespace Discord.Interactions
/// </summary>
public RunMode RunMode { get; }

/// <summary>
/// Gets or sets whether the <see cref="CustomId"/> should be treated as a raw Regex pattern.
/// </summary>
public bool TreatAsRegex { get; set; } = false;

/// <summary>
/// Create a command for modal interaction handling.
/// </summary>


+ 25
- 0
src/Discord.Net.Interactions/Attributes/NsfwCommandAttribute.cs View File

@@ -0,0 +1,25 @@
using System;

namespace Discord.Interactions
{
/// <summary>
/// Sets the <see cref="IApplicationCommandInfo.IsNsfw"/> property of an application command or module.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class NsfwCommandAttribute : Attribute
{
/// <summary>
/// Gets whether or not this command is age restricted.
/// </summary>
public bool IsNsfw { get; }

/// <summary>
/// Sets the <see cref="IApplicationCommandInfo.IsNsfw"/> property of an application command or module.
/// </summary>
/// <param name="isNsfw">Whether or not this command is age restricted.</param>
public NsfwCommandAttribute(bool isNsfw)
{
IsNsfw = isNsfw;
}
}
}

+ 20
- 0
src/Discord.Net.Interactions/Builders/Commands/CommandBuilder.cs View File

@@ -35,6 +35,9 @@ namespace Discord.Interactions.Builders
/// <inheritdoc/>
public bool IgnoreGroupNames { get; set; }

/// <inheritdoc/>
public bool TreatNameAsRegex { get; set; }

/// <inheritdoc/>
public RunMode RunMode { get; set; }

@@ -117,6 +120,19 @@ namespace Discord.Interactions.Builders
return Instance;
}

/// <summary>
/// Sets <see cref="TreatNameAsRegex"/>.
/// </summary>
/// <param name="value">New value of the <see cref="TreatNameAsRegex"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public TBuilder WithNameAsRegex (bool value)
{
TreatNameAsRegex = value;
return Instance;
}

/// <summary>
/// Adds parameter builders to <see cref="Parameters"/>.
/// </summary>
@@ -163,6 +179,10 @@ namespace Discord.Interactions.Builders
ICommandBuilder ICommandBuilder.SetRunMode (RunMode runMode) =>
SetRunMode(runMode);

/// <inheritdoc/>
ICommandBuilder ICommandBuilder.WithNameAsRegex(bool value) =>
WithNameAsRegex(value);

/// <inheritdoc/>
ICommandBuilder ICommandBuilder.AddParameters (params IParameterBuilder[] parameters) =>
AddParameters(parameters as TParamBuilder);


+ 18
- 0
src/Discord.Net.Interactions/Builders/Commands/ContextCommandBuilder.cs View File

@@ -25,6 +25,11 @@ namespace Discord.Interactions.Builders
/// </summary>
public bool IsEnabledInDm { get; set; } = true;

/// <summary>
/// Gets whether this command is age restricted.
/// </summary>
public bool IsNsfw { get; set; } = false;

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>
@@ -95,6 +100,19 @@ namespace Discord.Interactions.Builders
return this;
}

/// <summary>
/// Sets <see cref="IsNsfw"/>.
/// </summary>
/// <param name="isNsfw">New value of the <see cref="IsNsfw"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ContextCommandBuilder SetNsfw(bool isNsfw)
{
IsNsfw = isNsfw;
return this;
}

/// <summary>
/// Sets <see cref="DefaultMemberPermissions"/>.
/// </summary>


+ 14
- 0
src/Discord.Net.Interactions/Builders/Commands/ICommandBuilder.cs View File

@@ -34,6 +34,11 @@ namespace Discord.Interactions.Builders
/// </summary>
bool IgnoreGroupNames { get; set; }

/// <summary>
/// Gets or sets whether the <see cref="Name"/> should be directly used as a Regex pattern.
/// </summary>
bool TreatNameAsRegex { get; set; }

/// <summary>
/// Gets or sets the run mode this command gets executed with.
/// </summary>
@@ -90,6 +95,15 @@ namespace Discord.Interactions.Builders
/// </returns>
ICommandBuilder SetRunMode (RunMode runMode);

/// <summary>
/// Sets <see cref="TreatNameAsRegex"/>.
/// </summary>
/// <param name="value">New value of the <see cref="TreatNameAsRegex"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
ICommandBuilder WithNameAsRegex(bool value);

/// <summary>
/// Adds parameter builders to <see cref="Parameters"/>.
/// </summary>


+ 18
- 0
src/Discord.Net.Interactions/Builders/Commands/SlashCommandBuilder.cs View File

@@ -25,6 +25,11 @@ namespace Discord.Interactions.Builders
/// </summary>
public bool IsEnabledInDm { get; set; } = true;

/// <summary>
/// Gets whether this command is age restricted.
/// </summary>
public bool IsNsfw { get; set; } = false;

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>
@@ -95,6 +100,19 @@ namespace Discord.Interactions.Builders
return this;
}

/// <summary>
/// Sets <see cref="IsNsfw"/>.
/// </summary>
/// <param name="isNsfw">New value of the <see cref="IsNsfw"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public SlashCommandBuilder SetNsfw(bool isNsfw)
{
IsNsfw = isNsfw;
return this;
}

/// <summary>
/// Sets <see cref="DefaultMemberPermissions"/>.
/// </summary>


+ 18
- 0
src/Discord.Net.Interactions/Builders/ModuleBuilder.cs View File

@@ -59,6 +59,11 @@ namespace Discord.Interactions.Builders
/// </summary>
public bool IsEnabledInDm { get; set; } = true;

/// <summary>
/// Gets whether this command is age restricted.
/// </summary>
public bool IsNsfw { get; set; } = false;

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>
@@ -190,6 +195,19 @@ namespace Discord.Interactions.Builders
return this;
}

/// <summary>
/// Sets <see cref="IsNsfw"/>.
/// </summary>
/// <param name="isNsfw">New value of the <see cref="IsNsfw"/>.</param>
/// <returns>
/// The builder instance.
/// </returns>
public ModuleBuilder SetNsfw(bool isNsfw)
{
IsNsfw = isNsfw;
return this;
}

/// <summary>
/// Sets <see cref="DefaultMemberPermissions"/>.
/// </summary>


+ 12
- 1
src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs View File

@@ -101,6 +101,9 @@ namespace Discord.Interactions.Builders
case DontAutoRegisterAttribute dontAutoRegister:
builder.DontAutoRegister = true;
break;
case NsfwCommandAttribute nsfwCommand:
builder.SetNsfw(nsfwCommand.IsNsfw);
break;
default:
builder.AddAttributes(attribute);
break;
@@ -192,6 +195,9 @@ namespace Discord.Interactions.Builders
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
case NsfwCommandAttribute nsfwCommand:
builder.SetNsfw(nsfwCommand.IsNsfw);
break;
default:
builder.WithAttributes(attribute);
break;
@@ -244,6 +250,9 @@ namespace Discord.Interactions.Builders
case PreconditionAttribute precondition:
builder.WithPreconditions(precondition);
break;
case NsfwCommandAttribute nsfwCommand:
builder.SetNsfw(nsfwCommand.IsNsfw);
break;
default:
builder.WithAttributes(attribute);
break;
@@ -274,6 +283,7 @@ namespace Discord.Interactions.Builders
builder.Name = interaction.CustomId;
builder.RunMode = interaction.RunMode;
builder.IgnoreGroupNames = interaction.IgnoreGroupNames;
builder.TreatNameAsRegex = interaction.TreatAsRegex;
}
break;
case PreconditionAttribute precondition:
@@ -287,7 +297,7 @@ namespace Discord.Interactions.Builders

var parameters = methodInfo.GetParameters();

var wildCardCount = Regex.Matches(Regex.Escape(builder.Name), Regex.Escape(commandService._wildCardExp)).Count;
var wildCardCount = RegexUtils.GetWildCardCount(builder.Name, commandService._wildCardExp);

foreach (var parameter in parameters)
builder.AddParameter(x => BuildComponentParameter(x, parameter, parameter.Position >= wildCardCount));
@@ -355,6 +365,7 @@ namespace Discord.Interactions.Builders
builder.Name = modal.CustomId;
builder.RunMode = modal.RunMode;
builder.IgnoreGroupNames = modal.IgnoreGroupNames;
builder.TreatNameAsRegex = modal.TreatAsRegex;
}
break;
case PreconditionAttribute precondition:


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


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

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

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

public bool TreatNameAsRegex { get; }

internal CommandInfo(Builders.ICommandBuilder builder, ModuleInfo module, InteractionService commandService)
{
@@ -78,6 +80,7 @@ namespace Discord.Interactions
RunMode = builder.RunMode != RunMode.Default ? builder.RunMode : commandService._runMode;
Attributes = builder.Attributes.ToImmutableArray();
Preconditions = builder.Preconditions.ToImmutableArray();
TreatNameAsRegex = builder.TreatNameAsRegex && SupportsWildCards;

_action = builder.Callback;
_groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal);
@@ -85,71 +88,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 +107,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 +186,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)
{


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

@@ -20,11 +20,14 @@ namespace Discord.Interactions
/// <inheritdoc/>
public bool IsEnabledInDm { get; }

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

/// <inheritdoc/>
public GuildPermission? DefaultMemberPermissions { get; }

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

/// <inheritdoc/>
public override bool SupportsWildCards => false;
@@ -37,6 +40,7 @@ namespace Discord.Interactions
{
CommandType = builder.CommandType;
DefaultPermission = builder.DefaultPermission;
IsNsfw = builder.IsNsfw;
IsEnabledInDm = builder.IsEnabledInDm;
DefaultMemberPermissions = builder.DefaultMemberPermissions;
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();


+ 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)
{


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

@@ -29,11 +29,14 @@ namespace Discord.Interactions
/// <inheritdoc/>
public bool IsEnabledInDm { get; }

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

/// <inheritdoc/>
public GuildPermission? DefaultMemberPermissions { get; }

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

/// <inheritdoc/>
public override bool SupportsWildCards => false;
@@ -41,13 +44,14 @@ 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;
IsEnabledInDm = builder.IsEnabledInDm;
IsNsfw = builder.IsNsfw;
DefaultMemberPermissions = builder.DefaultMemberPermissions;
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray();
@@ -60,49 +64,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 +111,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)


+ 5
- 0
src/Discord.Net.Interactions/Info/IApplicationCommandInfo.cs View File

@@ -28,6 +28,11 @@ namespace Discord.Interactions
/// </summary>
public bool IsEnabledInDm { get; }

/// <summary>
/// Gets whether this command can is age restricted.
/// </summary>
public bool IsNsfw { get; }

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>


+ 2
- 0
src/Discord.Net.Interactions/Info/ICommandInfo.cs View File

@@ -65,6 +65,8 @@ namespace Discord.Interactions
/// </summary>
IReadOnlyCollection<IParameterInfo> Parameters { get; }

bool TreatNameAsRegex { get; }

/// <summary>
/// Executes the command with the provided context.
/// </summary>


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

+ 6
- 0
src/Discord.Net.Interactions/Info/ModuleInfo.cs View File

@@ -49,6 +49,11 @@ namespace Discord.Interactions
/// </summary>
public bool IsEnabledInDm { get; }

/// <summary>
/// Gets whether this command is age restricted.
/// </summary>
public bool IsNsfw { get; }

/// <summary>
/// Gets the default permissions needed for executing this command.
/// </summary>
@@ -121,6 +126,7 @@ namespace Discord.Interactions
Description = builder.Description;
Parent = parent;
DefaultPermission = builder.DefaultPermission;
IsNsfw = builder.IsNsfw;
IsEnabledInDm = builder.IsEnabledInDm;
DefaultMemberPermissions = BuildDefaultMemberPermissions(builder);
SlashCommands = BuildSlashCommands(builder).ToImmutableArray();


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


+ 3
- 6
src/Discord.Net.Interactions/Map/CommandMapNode.cs View File

@@ -2,14 +2,13 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Discord.Interactions
{
internal class CommandMapNode<T> where T : class, ICommandInfo
{
private const string RegexWildCardExp = "(\\S+)?";

{
private readonly string _wildCardStr = "*";
private readonly ConcurrentDictionary<string, CommandMapNode<T>> _nodes;
private readonly ConcurrentDictionary<string, T> _commands;
@@ -35,10 +34,8 @@ namespace Discord.Interactions
{
if (keywords.Count == index + 1)
{
if (commandInfo.SupportsWildCards && commandInfo.Name.Contains(_wildCardStr))
if (commandInfo.SupportsWildCards && RegexUtils.TryBuildRegexPattern(commandInfo, _wildCardStr, out var patternStr))
{
var escapedStr = RegexUtils.EscapeExcluding(commandInfo.Name, _wildCardStr.ToArray());
var patternStr = "\\A" + escapedStr.Replace(_wildCardStr, RegexWildCardExp) + "\\Z";
var regex = new Regex(patternStr, RegexOptions.Singleline | RegexOptions.Compiled);

if (!_wildCardCommands.TryAdd(regex, commandInfo))


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


+ 41
- 10
src/Discord.Net.Interactions/TypeConverters/ComponentInteractions/DefaultArrayComponentConverter.cs View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;

namespace Discord.Interactions
@@ -17,27 +19,56 @@ namespace Discord.Interactions
throw new InvalidOperationException($"{nameof(DefaultArrayComponentConverter<T>)} cannot be used to convert a non-array type.");

_underlyingType = typeof(T).GetElementType();
_typeReader = interactionService.GetTypeReader(_underlyingType);

_typeReader = true switch
{
_ when typeof(IUser).IsAssignableFrom(_underlyingType)
|| typeof(IChannel).IsAssignableFrom(_underlyingType)
|| typeof(IMentionable).IsAssignableFrom(_underlyingType)
|| typeof(IRole).IsAssignableFrom(_underlyingType) => null,
_ => interactionService.GetTypeReader(_underlyingType)
};
}

public override async Task<TypeConverterResult> ReadAsync(IInteractionContext context, IComponentInteractionData option, IServiceProvider services)
{
var results = new List<TypeConverterResult>();
var objs = new List<object>();

if(_typeReader is not null && option.Values.Count > 0)
foreach (var value in option.Values)
{
var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false);

if (!result.IsSuccess)
return result;

foreach (var value in option.Values)
objs.Add(result.Value);
}
else
{
var result = await _typeReader.ReadAsync(context, value, services).ConfigureAwait(false);
var users = new Dictionary<ulong, IUser>();

if (option.Users is not null)
foreach (var user in option.Users)
users[user.Id] = user;

if(option.Members is not null)
foreach(var member in option.Members)
users[member.Id] = member;

objs.AddRange(users.Values);

if (!result.IsSuccess)
return result;
if(option.Roles is not null)
objs.AddRange(option.Roles);

results.Add(result);
if (option.Channels is not null)
objs.AddRange(option.Channels);
}

var destination = Array.CreateInstance(_underlyingType, results.Count);
var destination = Array.CreateInstance(_underlyingType, objs.Count);

for (var i = 0; i < results.Count; i++)
destination.SetValue(results[i].Value, i);
for (var i = 0; i < objs.Count; i++)
destination.SetValue(objs[i], i);

return TypeConverterResult.FromSuccess(destination);
}


+ 8
- 1
src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs View File

@@ -54,6 +54,7 @@ namespace Discord.Interactions
Description = commandInfo.Description,
IsDefaultPermission = commandInfo.DefaultPermission,
IsDMEnabled = commandInfo.IsEnabledInDm,
IsNsfw = commandInfo.IsNsfw,
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
}.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty)
.WithDescriptionLocalizations(localizationManager?.GetAllDescriptions(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty)
@@ -97,7 +98,8 @@ namespace Discord.Interactions
Name = commandInfo.Name,
IsDefaultPermission = commandInfo.DefaultPermission,
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
IsDMEnabled = commandInfo.IsEnabledInDm
IsDMEnabled = commandInfo.IsEnabledInDm,
IsNsfw = commandInfo.IsNsfw,
}
.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty)
.Build(),
@@ -106,6 +108,7 @@ namespace Discord.Interactions
Name = commandInfo.Name,
IsDefaultPermission = commandInfo.DefaultPermission,
DefaultMemberPermissions = ((commandInfo.DefaultMemberPermissions ?? 0) | (commandInfo.Module.DefaultMemberPermissions ?? 0)).SanitizeGuildPermissions(),
IsNsfw = commandInfo.IsNsfw,
IsDMEnabled = commandInfo.IsEnabledInDm
}
.WithNameLocalizations(localizationManager?.GetAllNames(commandPath, LocalizationTarget.Command) ?? ImmutableDictionary<string, string>.Empty)
@@ -162,6 +165,7 @@ namespace Discord.Interactions
Description = moduleInfo.Description,
IsDefaultPermission = moduleInfo.DefaultPermission,
IsDMEnabled = moduleInfo.IsEnabledInDm,
IsNsfw = moduleInfo.IsNsfw,
DefaultMemberPermissions = moduleInfo.DefaultMemberPermissions
}
.WithNameLocalizations(localizationManager?.GetAllNames(modulePath, LocalizationTarget.Group) ?? ImmutableDictionary<string, string>.Empty)
@@ -225,6 +229,7 @@ namespace Discord.Interactions
IsDefaultPermission = command.IsDefaultPermission,
DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue,
IsDMEnabled = command.IsEnabledInDm,
IsNsfw = command.IsNsfw,
Options = command.Options?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional<List<ApplicationCommandOptionProperties>>.Unspecified,
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty,
@@ -234,6 +239,7 @@ namespace Discord.Interactions
Name = command.Name,
IsDefaultPermission = command.IsDefaultPermission,
DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue,
IsNsfw = command.IsNsfw,
IsDMEnabled = command.IsEnabledInDm,
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty
@@ -243,6 +249,7 @@ namespace Discord.Interactions
Name = command.Name,
IsDefaultPermission = command.IsDefaultPermission,
DefaultMemberPermissions = (GuildPermission)command.DefaultMemberPermissions.RawValue,
IsNsfw = command.IsNsfw,
IsDMEnabled = command.IsEnabledInDm,
NameLocalizations = command.NameLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty,
DescriptionLocalizations = command.DescriptionLocalizations?.ToImmutableDictionary() ?? ImmutableDictionary<string, string>.Empty


+ 33
- 0
src/Discord.Net.Interactions/Utilities/RegexUtils.cs View File

@@ -1,3 +1,4 @@
using Discord.Interactions;
using System;
using System.Linq;

@@ -81,5 +82,37 @@ namespace System.Text.RegularExpressions
{
return (ch <= '|' && _category[ch] >= E);
}

internal static int GetWildCardCount(string input, string wildCardExpression)
{
var escapedWildCard = Regex.Escape(wildCardExpression);
var match = Regex.Matches(input, $@"(?<!\\){escapedWildCard}|(?<!\\){{[0-9]+(?:,[0-9]*)?(?<!\\)}}");
return match.Count;
}

internal static bool TryBuildRegexPattern<T>(T commandInfo, string wildCardStr, out string pattern) where T: class, ICommandInfo
{
if (commandInfo.TreatNameAsRegex)
{
pattern = commandInfo.Name;
return true;
}

if (GetWildCardCount(commandInfo.Name, wildCardStr) == 0)
{
pattern = null;
return false;
}

var escapedWildCard = Regex.Escape(wildCardStr);
var unquantified = Regex.Replace(commandInfo.Name, $@"(?<!\\){escapedWildCard}(?<delimiter>[^{escapedWildCard}]?)",
@"([^\n\t${delimiter}]+)${delimiter}");

var quantified = Regex.Replace(unquantified, $@"(?<!\\){{(?<start>[0-9]+)(?<end>,[0-9]*)?(?<!\\)}}(?<delimiter>[^{escapedWildCard}]?)",
@"([^\n\t${delimiter}]{${start}${end}})${delimiter}");

pattern = "\\A" + quantified + "\\Z";
return true;
}
}
}

+ 4
- 0
src/Discord.Net.Rest/API/Common/ActionRowComponent.cs View File

@@ -21,6 +21,10 @@ namespace Discord.API
{
ComponentType.Button => new ButtonComponent(x as Discord.ButtonComponent),
ComponentType.SelectMenu => new SelectMenuComponent(x as Discord.SelectMenuComponent),
ComponentType.ChannelSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent),
ComponentType.UserSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent),
ComponentType.RoleSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent),
ComponentType.MentionableSelect => new SelectMenuComponent(x as Discord.SelectMenuComponent),
ComponentType.TextInput => new TextInputComponent(x as Discord.TextInputComponent),
_ => null
};


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

@@ -44,5 +44,8 @@ namespace Discord.API

[JsonProperty("default_member_permissions")]
public Optional<GuildPermission?> DefaultMemberPermission { get; set; }

[JsonProperty("nsfw")]
public Optional<bool?> Nsfw { get; set; }
}
}

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

@@ -70,8 +70,27 @@ 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; }

[JsonProperty("default_forum_layout")]
public Optional<ForumLayout> DefaultForumLayout { 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; }
}
}

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

@@ -83,5 +83,8 @@ namespace Discord.API
public Sticker[] Stickers { get; set; }
[JsonProperty("premium_progress_bar_enabled")]
public Optional<bool> IsBoostProgressBarEnabled { get; set; }

[JsonProperty("welcome_screen")]
public Optional<WelcomeScreen> WelcomeScreen { get; set; }
}
}

+ 34
- 2
src/Discord.Net.Rest/API/Common/InviteGuild.cs View File

@@ -6,9 +6,41 @@ namespace Discord.API
{
[JsonProperty("id")]
public ulong Id { get; set; }

[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("splash_hash")]
public string SplashHash { get; set; }

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

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

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

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

[JsonProperty("features")]
public GuildFeatures Features { get; set; }

[JsonProperty("verification_level")]
public VerificationLevel VerificationLevel { get; set; }

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

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

[JsonProperty("nsfw")]
public Optional<bool?> Nsfw { get; set; }

[JsonProperty("nsfw_level")]
public NsfwLevel NsfwLevel { get; set; }

[JsonProperty("welcome_screen")]
public Optional<WelcomeScreen> WelcomeScreen { get; set; }
}
}

+ 4
- 0
src/Discord.Net.Rest/API/Common/MessageComponentInteractionData.cs View File

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

namespace Discord.API
{
@@ -15,5 +16,8 @@ namespace Discord.API

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

[JsonProperty("resolved")]
public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; }
}
}

+ 19
- 0
src/Discord.Net.Rest/API/Common/MessageComponentInteractionDataResolved.cs View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
using System.Collections.Generic;

namespace Discord.API;

internal class MessageComponentInteractionDataResolved
{
[JsonProperty("users")]
public Optional<Dictionary<string, User>> Users { get; set; }

[JsonProperty("members")]
public Optional<Dictionary<string, GuildMember>> Members { get; set; }

[JsonProperty("channels")]
public Optional<Dictionary<string, Channel>> Channels { get; set; }

[JsonProperty("roles")]
public Optional<Dictionary<string, Role>> Roles { get; set; }
}

+ 8
- 1
src/Discord.Net.Rest/API/Common/SelectMenuComponent.cs View File

@@ -26,6 +26,12 @@ namespace Discord.API
[JsonProperty("disabled")]
public bool Disabled { get; set; }

[JsonProperty("channel_types")]
public Optional<ChannelType[]> ChannelTypes { get; set; }

[JsonProperty("resolved")]
public Optional<MessageComponentInteractionDataResolved> Resolved { get; set; }

[JsonProperty("values")]
public Optional<string[]> Values { get; set; }
public SelectMenuComponent() { }
@@ -34,11 +40,12 @@ namespace Discord.API
{
Type = component.Type;
CustomId = component.CustomId;
Options = component.Options.Select(x => new SelectMenuOption(x)).ToArray();
Options = component.Options?.Select(x => new SelectMenuOption(x)).ToArray();
Placeholder = component.Placeholder;
MinValues = component.MinValues;
MaxValues = component.MaxValues;
Disabled = component.IsDisabled;
ChannelTypes = component.ChannelTypes.ToArray();
}
}
}

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

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

namespace Discord.API;

internal class WelcomeScreen
{
[JsonProperty("description")]
public Optional<string> Description { get; set; }

[JsonProperty("welcome_channels")]
public WelcomeScreenChannel[] WelcomeChannels { get; set; }
}

+ 18
- 0
src/Discord.Net.Rest/API/Common/WelcomeScreenChannel.cs View File

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

namespace Discord.API;

internal class WelcomeScreenChannel
{
[JsonProperty("channel_id")]
public ulong ChannelId { get; set; }

[JsonProperty("description")]
public string Description { get; set; }

[JsonProperty("emoji_id")]
public Optional<ulong?> EmojiId { get; set; }

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

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

@@ -35,9 +35,12 @@ namespace Discord.API.Rest
[JsonProperty("default_member_permissions")]
public Optional<GuildPermission?> DefaultMemberPermission { get; set; }

[JsonProperty("nsfw")]
public Optional<bool> Nsfw { get; set; }

public CreateApplicationCommandParams() { }
public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null,
IDictionary<string, string> nameLocalizations = null, IDictionary<string, string> descriptionLocalizations = null)
IDictionary<string, string> nameLocalizations = null, IDictionary<string, string> descriptionLocalizations = null, bool nsfw = false)
{
Name = name;
Description = description;
@@ -45,6 +48,7 @@ namespace Discord.API.Rest
Type = type;
NameLocalizations = nameLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>>.Unspecified;
DescriptionLocalizations = descriptionLocalizations?.ToDictionary(x => x.Key, x => x.Value) ?? Optional<Dictionary<string, string>>.Unspecified;
Nsfw = nsfw;
}
}
}

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

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

@@ -17,6 +17,12 @@ namespace Discord.API.Rest
[JsonProperty("default_permission")]
public Optional<bool> DefaultPermission { get; set; }

[JsonProperty("nsfw")]
public Optional<bool> Nsfw { get; set; }

[JsonProperty("default_member_permissions")]
public Optional<GuildPermission?> DefaultMemberPermission { get; set; }

[JsonProperty("name_localizations")]
public Optional<Dictionary<string, string>> NameLocalizations { get; set; }



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

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

[JsonProperty("default_forum_layout")]
public Optional<ForumLayout> DefaultLayout { 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; }
}



Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save