diff --git a/Discord.Net.sln b/Discord.Net.sln index 1a32f1270..9f888cd12 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28407.52 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31521.260 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject @@ -40,7 +40,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers.Tests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Examples", "src\Discord.Net.Examples\Discord.Net.Examples.csproj", "{47820065-3CFB-401C-ACEA-862BD564A404}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -232,6 +232,18 @@ Global {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Debug|x64.ActiveCfg = Debug|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Debug|x64.Build.0 = Debug|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Debug|x86.ActiveCfg = Debug|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Debug|x86.Build.0 = Debug|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Release|Any CPU.Build.0 = Release|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Release|x64.ActiveCfg = Release|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Release|x64.Build.0 = Release|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Release|x86.ActiveCfg = Release|Any CPU + {0CC57A32-3AC7-489D-8DF5-C431925E4675}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/docs/guides/application-commands/01-getting-started.md b/docs/guides/application-commands/01-getting-started.md new file mode 100644 index 000000000..28d5fc817 --- /dev/null +++ b/docs/guides/application-commands/01-getting-started.md @@ -0,0 +1,25 @@ +# Getting started with application commands. + +Welcome! This guide will show you how to use application commands. If you have extra questions that aren't covered here you can come to our [Discord](https://discord.com/invite/dvSfUTet3K) server and ask around there. + +## What is an application command? + +Application commands consist of three different types. Slash commands, context menu User commands and context menu Message commands. +Slash commands are made up of a name, description, and a block of options, which you can think of like arguments to a function. The name and description help users find your command among many others, and the options validate user input as they fill out your command. +Message and User commands are only a name, to the user. So try to make the name descriptive. They're accessed by right clicking (or long press, on mobile) a user or a message, respectively. + +All three varieties of application commands have both Global and Guild variants. Your global commands are available in every guild that adds your application. You can also make commands for a specific guild; they're only available in that guild. The User and Message commands are more limited in quantity than the slash commands. For specifics, check out their respective guide pages. + +An Interaction is the message that your application receives when a user uses a command. It includes the values that the user submitted, as well as some metadata about this particular instance of the command being used: the guild_id, channel_id, member and other fields. You can find all the values in our data models. + +## Authorizing your bot for application commands + +There is a new special OAuth2 scope for applications called `applications.commands`. In order to make Application Commands work within a guild, the guild must authorize your application with the `applications.commands` scope. The bot scope is not enough. + +Head over to your discord applications OAuth2 screen and make sure to select the `application.commands` scope. + +![OAuth2 scoping](images/oauth.png) + +From there you can then use the link to add your bot to a server. + +**Note**: In order for users in your guild to use your slash commands, they need to have the "Use Slash Command" permission on the guild. diff --git a/docs/guides/application-commands/context-menu-commands/creating-context-menu-commands.md b/docs/guides/application-commands/context-menu-commands/creating-context-menu-commands.md new file mode 100644 index 000000000..9100c3b42 --- /dev/null +++ b/docs/guides/application-commands/context-menu-commands/creating-context-menu-commands.md @@ -0,0 +1,91 @@ +# Creating context menu commands. + +There are two kinds of Context Menu Commands: User Commands and Message Commands. +Each of these have a Global and Guild variant. +Global menu commands are available for every guild that adds your app. An individual app's global commands are also available in DMs if that app has a bot that shares a mutual guild with the user. + +Guild commands are specific to the guild you specify when making them. Guild commands are not available in DMs. Command names are unique per application within each scope (global and guild). That means: + +- Your app cannot have two global commands with the same name +- Your app cannot have two guild commands within the same name on the same guild +- Your app can have a global and guild command with the same name +- Multiple apps can have commands with the same names + +**Note**: Apps can have a maximum of 5 global context menu commands, and an additional 5 guild-specific context menu commands per guild. + +If you don't have the code for a bot ready yet please follow [this guide](https://docs.stillu.cc/guides/getting_started/first-bot.html). + +## UserCommandBuilder + +The context menu user command builder will help you create user commands. The builder has these available fields and methods: + +| Name | Type | Description | +| --------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | +| Name | string | The name of this context menu command. | +| WithName | Function | Sets the field name. | +| Build | Function | Builds the builder into the appropriate `CommandCreationProperties` class used to make Menu commands | + +## MessageCommandBuilder + +The context menu message command builder will help you create message commands. The builder has these available fields and methods: + +| Name | Type | Description | +| --------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | +| Name | string | The name of this context menu command. | +| WithName | Function | Sets the field name. | +| Build | Function | Builds the builder into the appropriate `CommandCreationProperties` class used to make Menu commands | + +**Note**: Context Menu command names can be upper and lowercase, and use spaces. + +Let's use the user command builder to make a global and guild command. + +```cs +// Let's hook the ready event for creating our commands in. +client.Ready += Client_Ready; + +... + +public async Task Client_Ready() +{ + // Let's build a guild command! We're going to need a guild id so lets just put that in a variable. + ulong guildId = 848176216011046962; + + // Next, lets create our user and message command builder. This is like the embed builder but for context menu commands. + var guildUserCommand = new UserCommandBuilder(); + var guildMessageCommand = new MessageCommandBuilder(); + + // Note: Names have to be all lowercase and match the regular expression ^[\w -]{3,32}$ + guildUserCommand.WithName("Guild User Command"); + guildMessageCommand.WithName("Guild Message Command"); + + // Descriptions are not used with User and Message commands + //guildCommand.WithDescription(""); + + // Let's do our global commands + var globalCommand = new UserCommandBuilder(); + globalCommand.WithName("Global User Command"); + var globalMessageCommand = new MessageCommandBuilder(); + globalMessageCommand.WithName("Global Message Command"); + + try + { + // Now that we have our builder, we can call the rest API to make our slash command. + await client.Rest.CreateGuildUserCommand(guildUserCommand.Build(), guildId); + await client.Rest.CreateGuildMessageCommand(guildMessageCommand.Build(), guildId); + + // With global commands we dont need the guild id. + await client.Rest.CreateGlobalUserCommand(globalUserCommand.Build()); + await client.Rest.CreateGlobalMessageCommand(globalMessageCommand.Build()); + } + catch(ApplicationCommandException exception) + { + // If our command was invalid, we should catch an ApplicationCommandException. This exception contains the path of the error as well as the error message. You can serialize the Error field in the exception to get a visual of where your error is. + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + + // You can send this error somewhere or just print it to the console, for this example we're just going to print it. + Console.WriteLine(json); + } +} + +``` +**Note**: Application commands only need to be created once. They do _not_ have to be 'created' on every startup or connection. The example simple shows creating them in the ready event as it's simpler than creating normal bot commands to register application commands. diff --git a/docs/guides/application-commands/context-menu-commands/receiving-context-menu-command-events.md b/docs/guides/application-commands/context-menu-commands/receiving-context-menu-command-events.md new file mode 100644 index 000000000..52bc303e7 --- /dev/null +++ b/docs/guides/application-commands/context-menu-commands/receiving-context-menu-command-events.md @@ -0,0 +1,40 @@ +# Receiving Context Menu events + +User commands and Message commands have their own unique objects returned. Different from Slash commands. To get the appropriate object returned, you can use a similar method to the slash commands. + +```cs +client.InteractionCreated += InteractionCreatedHandler; + +... + +public async Task InteractionCreatedHandler(SocketInteraction arg) +{ + if ( arg.Type == InteractionType.ApplicationCommand) + Task.Run(() => ApplicationCommandHandler(arg)); +} + +public async Task ApplicationCommandHandler(SocketInteraction arg) +{ + switch (arg) + { + case SocketSlashCommand slashCommand: + Console.Writeline("Slash command received!"); + break; + case SocketUserCommand userCommand: + Console.Writeline("User command received!") + // userCommand.User = User who ran command. + // userCommand.Data.Member = User who was clicked. + break; + case SocketMessageCommand messageCommand: + Console.Writeline("Message command received!") + // messageCommand.User = User who ran command. + // messageCommand.Data.Message = Message that was clicked. + break; + } +} +``` + +User commands return a SocketUser object, showing the user that was clicked to run the command. +Message commands return a SocketMessage object, showing the message that was clicked to run the command. + +Both return the user who ran the command, the guild (if any), channel, etc. \ No newline at end of file diff --git a/docs/guides/slash-commands/02-creating-slash-commands.md b/docs/guides/application-commands/slash-commands/02-creating-slash-commands.md similarity index 100% rename from docs/guides/slash-commands/02-creating-slash-commands.md rename to docs/guides/application-commands/slash-commands/02-creating-slash-commands.md diff --git a/docs/guides/slash-commands/03-responding-to-slash-commands.md b/docs/guides/application-commands/slash-commands/03-responding-to-slash-commands.md similarity index 100% rename from docs/guides/slash-commands/03-responding-to-slash-commands.md rename to docs/guides/application-commands/slash-commands/03-responding-to-slash-commands.md diff --git a/docs/guides/slash-commands/04-parameters.md b/docs/guides/application-commands/slash-commands/04-parameters.md similarity index 100% rename from docs/guides/slash-commands/04-parameters.md rename to docs/guides/application-commands/slash-commands/04-parameters.md diff --git a/docs/guides/slash-commands/05-responding-ephemerally.md b/docs/guides/application-commands/slash-commands/05-responding-ephemerally.md similarity index 100% rename from docs/guides/slash-commands/05-responding-ephemerally.md rename to docs/guides/application-commands/slash-commands/05-responding-ephemerally.md diff --git a/docs/guides/slash-commands/06-subcommands.md b/docs/guides/application-commands/slash-commands/06-subcommands.md similarity index 100% rename from docs/guides/slash-commands/06-subcommands.md rename to docs/guides/application-commands/slash-commands/06-subcommands.md diff --git a/docs/guides/slash-commands/07-choice-slash-command.md b/docs/guides/application-commands/slash-commands/07-choice-slash-command.md similarity index 100% rename from docs/guides/slash-commands/07-choice-slash-command.md rename to docs/guides/application-commands/slash-commands/07-choice-slash-command.md diff --git a/docs/guides/slash-commands/README.md b/docs/guides/application-commands/slash-commands/README.md similarity index 99% rename from docs/guides/slash-commands/README.md rename to docs/guides/application-commands/slash-commands/README.md index 702aec6de..3b672792c 100644 --- a/docs/guides/slash-commands/README.md +++ b/docs/guides/application-commands/slash-commands/README.md @@ -1,11 +1,11 @@ -## Slash command guides - -Here you can find some guides on how to use slash commands. - -1. [Getting started](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/01-getting-started.md) -2. [Creating a slash command](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/02-creating-slash-commands.md) -3. [Responding to slash commands](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/03-responding-to-slash-commands.md) -4. [Parameters in slash commands](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/04-parameters.md) -5. [Responding ephemerally](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/05-responding-ephemerally.md) -6. [Subcommands](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/06-subcommands.md) -7. [Choices](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/07-choice-slash-command.md) +## Slash command guides + +Here you can find some guides on how to use slash commands. + +1. [Getting started](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/01-getting-started.md) +2. [Creating a slash command](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/02-creating-slash-commands.md) +3. [Responding to slash commands](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/03-responding-to-slash-commands.md) +4. [Parameters in slash commands](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/04-parameters.md) +5. [Responding ephemerally](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/05-responding-ephemerally.md) +6. [Subcommands](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/06-subcommands.md) +7. [Choices](https://github.com/Discord-Net-Labs/Discord.Net-Labs/blob/Interactions/docs/guides/slash-commands/07-choice-slash-command.md) diff --git a/docs/guides/slash-commands/images/ephemeral1.png b/docs/guides/application-commands/slash-commands/images/ephemeral1.png similarity index 100% rename from docs/guides/slash-commands/images/ephemeral1.png rename to docs/guides/application-commands/slash-commands/images/ephemeral1.png diff --git a/docs/guides/slash-commands/images/feedback1.png b/docs/guides/application-commands/slash-commands/images/feedback1.png similarity index 100% rename from docs/guides/slash-commands/images/feedback1.png rename to docs/guides/application-commands/slash-commands/images/feedback1.png diff --git a/docs/guides/slash-commands/images/feedback2.png b/docs/guides/application-commands/slash-commands/images/feedback2.png similarity index 100% rename from docs/guides/slash-commands/images/feedback2.png rename to docs/guides/application-commands/slash-commands/images/feedback2.png diff --git a/docs/guides/slash-commands/images/listroles1.png b/docs/guides/application-commands/slash-commands/images/listroles1.png similarity index 100% rename from docs/guides/slash-commands/images/listroles1.png rename to docs/guides/application-commands/slash-commands/images/listroles1.png diff --git a/docs/guides/slash-commands/images/listroles2.png b/docs/guides/application-commands/slash-commands/images/listroles2.png similarity index 100% rename from docs/guides/slash-commands/images/listroles2.png rename to docs/guides/application-commands/slash-commands/images/listroles2.png diff --git a/docs/guides/slash-commands/images/oauth.png b/docs/guides/application-commands/slash-commands/images/oauth.png similarity index 100% rename from docs/guides/slash-commands/images/oauth.png rename to docs/guides/application-commands/slash-commands/images/oauth.png diff --git a/docs/guides/slash-commands/images/settings1.png b/docs/guides/application-commands/slash-commands/images/settings1.png similarity index 100% rename from docs/guides/slash-commands/images/settings1.png rename to docs/guides/application-commands/slash-commands/images/settings1.png diff --git a/docs/guides/slash-commands/images/settings2.png b/docs/guides/application-commands/slash-commands/images/settings2.png similarity index 100% rename from docs/guides/slash-commands/images/settings2.png rename to docs/guides/application-commands/slash-commands/images/settings2.png diff --git a/docs/guides/slash-commands/images/settings3.png b/docs/guides/application-commands/slash-commands/images/settings3.png similarity index 100% rename from docs/guides/slash-commands/images/settings3.png rename to docs/guides/application-commands/slash-commands/images/settings3.png diff --git a/docs/guides/slash-commands/images/slashcommand1.png b/docs/guides/application-commands/slash-commands/images/slashcommand1.png similarity index 100% rename from docs/guides/slash-commands/images/slashcommand1.png rename to docs/guides/application-commands/slash-commands/images/slashcommand1.png diff --git a/docs/guides/slash-commands/images/slashcommand2.png b/docs/guides/application-commands/slash-commands/images/slashcommand2.png similarity index 100% rename from docs/guides/slash-commands/images/slashcommand2.png rename to docs/guides/application-commands/slash-commands/images/slashcommand2.png diff --git a/docs/guides/slash-commands/01-getting-started.md b/docs/guides/slash-commands/01-getting-started.md deleted file mode 100644 index 093178878..000000000 --- a/docs/guides/slash-commands/01-getting-started.md +++ /dev/null @@ -1,23 +0,0 @@ -# Getting started with slash commands. - -Welcome! This guide will show you how to use slash commands. If you have extra questions that aren't covered here you can come to our [Discord](https://discord.com/invite/dvSfUTet3K) server and ask around there. - -## What is a slash command? - -Slash Commands _(synonymous with application commands)_ are made up of a name, description, and a block of options, which you can think of like arguments to a function. The name and description help users find your command among many others, and the options validate user input as they fill out your command. - -Your global commands are available in every guild that adds your application. You can also make commands for a specific guild; they're only available in that guild. - -An Interaction is the message that your application receives when a user uses a command. It includes the values that the user submitted, as well as some metadata about this particular instance of the command being used: the guild_id, channel_id, member and other fields. You can find all the values in our data models. - -## Authorizing your bot for slash commands - -There is a new special OAuth2 scope for applications called `applications.commands`. In order to make Slash Commands work within a guild, the guild must authorize your application with the `applications.commands` scope. The bot scope is not enough. - -Head over to your discord applications OAuth2 screen and make sure to select the `application.commands` scope. - -![OAuth2 scoping](images/oauth.png) - -From there you can then use the link to add your bot to a server. - -**Note**: In order for users in your guild to use your slash commands, they need to have the "Use Slash Command" permission on the guild. diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 309c45fb6..ac54b259c 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/Discord.Net.Core/Discord.Net.Core.xml b/src/Discord.Net.Core/Discord.Net.Core.xml index f60adb42c..bcd7040d7 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.xml +++ b/src/Discord.Net.Core/Discord.Net.Core.xml @@ -4007,7 +4007,7 @@ - Gets this guilds slash commands commands + Gets this guilds application commands. The options to be used when sending the request. @@ -4015,6 +4015,38 @@ of application commands found within the guild. + + + Gets an application command within this guild with the specified id. + + The id of the application command to get. + The that determines whether the object should be fetched from cache. + The options to be used when sending the request. + + A ValueTask that represents the asynchronous get operation. The task result contains a + if found, otherwise . + + + + + Creates an application command within this guild. + + The properties to use when creating the command. + The options to be used when sending the request. + + A task that represents the asynchronous creation operation. The task result contains the command that was created. + + + + + Overwrites the application commands within this guild. + + A collection of properties to use when creating the commands. + The options to be used when sending the request. + + A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. + + Holds information for a guild integration feature. @@ -4552,7 +4584,7 @@ - Provides properties that are used to modify a with the specified changes. + Represents the base class to create/modify application commands. @@ -4560,24 +4592,101 @@ Gets or sets the name of this command. - + - Gets or sets the discription of this command. + ApplicationCommandType is enum of current valid Application Command Types: Slash, User, Message - + - Gets or sets the options for this command. + ApplicationCommandType.Slash is Slash command type - + - Whether the command is enabled by default when the app is added to a guild. Default is + ApplicationCommandType.User is Context Menu User command type + + + + + ApplicationCommandType.Message is Context Menu Message command type + + + + + A class used to build Message commands. + + + + + Returns the maximun length a commands name allowed by Discord + + + + + The name of this Message command. + + + + + Build the current builder into a class. + + + A that can be used to create message commands. + + + + + Sets the field name. + + The value to set the field name to. + + The current builder. + + + + + A class used to create message commands. + + + + + A class used to build user commands. + + + + + Returns the maximun length a commands name allowed by Discord + + + + + The name of this User command. + + + + + Build the current builder into a class. + + A that can be used to create user commands. + + + + Sets the field name. + + The value to set the field name to. + + The current builder. + + + + + A class used to create User commands. - The base command model that belongs to an application. see + The base command model that belongs to an application. @@ -4585,6 +4694,11 @@ Gets the unique id of the parent application. + + + The type of the command + + The name of the command. @@ -4605,6 +4719,16 @@ If the option is a subcommand or subcommand group type, this nested options will be the parameters. + + + Modifies the current application command. + + The new properties to use when modifying the command. + The options to be used when sending the request. + + A task that represents the asynchronous modification operation. + + Represents data of an Interaction Command, see . @@ -5659,9 +5783,9 @@ - Build the current builder into a class. + Build the current builder into a class. - A that can be used to create slash commands over rest. + A that can be used to create slash commands over rest. @@ -5849,27 +5973,22 @@ The type to set. The current builder. - + A class used to create slash commands. - - - The name of this command. - - - + The discription of this command. - + Gets or sets the options for this command. - + Whether the command is enabled by default when the app is added to a guild. Default is @@ -11121,6 +11240,47 @@ A task that represents the asynchronous get operation. The task result contains a read-only collection of connections. + + + Gets a global application command. + + The id of the command. + The options to be used when sending the request. + + A task that represents the asynchronous get operation. The task result contains the application command if found, otherwise + . + + + + + Gets a collection of all global commands. + + The options to be used when sending the request. + + A task that represents the asynchronous get operation. The task result contains a read-only collection of global + application commands. + + + + + Creates a global application command. + + The properties to use when creating the command. + The options to be used when sending the request. + + A task that represents the asynchronous creation operation. The task result contains the created application command. + + + + + Bulk overwrites all global application commands. + + A collection of properties to use when creating the commands. + The options to be used when sending the request. + + A task that represents the asynchronous creation operation. The task result contains a collection of application commands that were created. + + Gets a guild. diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 314e7e6c3..83efb158b 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1024,7 +1024,7 @@ namespace Discord Task DeleteStickerAsync(ICustomSticker sticker, RequestOptions options = null); /// - /// Gets this guilds slash commands commands + /// Gets this guilds application commands. /// /// The options to be used when sending the request. /// @@ -1032,5 +1032,39 @@ namespace Discord /// of application commands found within the guild. /// Task> GetApplicationCommandsAsync (RequestOptions options = null); + + /// + /// Gets an application command within this guild with the specified id. + /// + /// The id of the application command to get. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A ValueTask that represents the asynchronous get operation. The task result contains a + /// if found, otherwise . + /// + Task GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, + RequestOptions options = null); + + /// + /// Creates an application command within this guild. + /// + /// The properties to use when creating the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the command that was created. + /// + Task CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null); + + /// + /// Overwrites the application commands within this guild. + /// + /// A collection of properties to use when creating the commands. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. + /// + Task> BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, + RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs index e0d10af87..2ccb0148b 100644 --- a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandProperties.cs @@ -7,29 +7,17 @@ using System.Threading.Tasks; namespace Discord { /// - /// Provides properties that are used to modify a with the specified changes. + /// Represents the base class to create/modify application commands. /// - public class ApplicationCommandProperties + public abstract class ApplicationCommandProperties { + internal abstract ApplicationCommandType Type { get; } + /// /// Gets or sets the name of this command. /// public Optional Name { get; set; } - /// - /// Gets or sets the discription of this command. - /// - public Optional Description { get; set; } - - - /// - /// Gets or sets the options for this command. - /// - public Optional> Options { get; set; } - - /// - /// Whether the command is enabled by default when the app is added to a guild. Default is - /// - public Optional DefaultPermission { get; set; } + internal ApplicationCommandProperties() { } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandTypes.cs b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandTypes.cs new file mode 100644 index 000000000..3cfa97a5a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/ApplicationCommandTypes.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// ApplicationCommandType is enum of current valid Application Command Types: Slash, User, Message + /// + public enum ApplicationCommandType : byte + { + /// + /// ApplicationCommandType.Slash is Slash command type + /// + Slash = 1, + + /// + /// ApplicationCommandType.User is Context Menu User command type + /// + User = 2, + + /// + /// ApplicationCommandType.Message is Context Menu Message command type + /// + Message = 3 + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Context Menus/MessageCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/Context Menus/MessageCommandBuilder.cs new file mode 100644 index 000000000..801aca302 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Context Menus/MessageCommandBuilder.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// A class used to build Message commands. + /// + public class MessageCommandBuilder + { + /// + /// Returns the maximun length a commands name allowed by Discord + /// + public const int MaxNameLength = 32; + + /// + /// The name of this Message command. + /// + public string Name + { + get + { + return _name; + } + set + { + Preconditions.NotNullOrEmpty(value, nameof(Name)); + Preconditions.AtLeast(value.Length, 3, nameof(Name)); + Preconditions.AtMost(value.Length, MaxNameLength, nameof(Name)); + + _name = value; + } + } + + private string _name { get; set; } + + /// + /// Build the current builder into a class. + /// + /// + /// A that can be used to create message commands. + /// + public MessageCommandProperties Build() + { + MessageCommandProperties props = new MessageCommandProperties() + { + Name = this.Name, + }; + + return props; + + } + + /// + /// Sets the field name. + /// + /// The value to set the field name to. + /// + /// The current builder. + /// + public MessageCommandBuilder WithName(string name) + { + this.Name = name; + return this; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Context Menus/MessageCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/Context Menus/MessageCommandProperties.cs new file mode 100644 index 000000000..3af9b47f3 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Context Menus/MessageCommandProperties.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// A class used to create message commands. + /// + public class MessageCommandProperties : ApplicationCommandProperties + { + internal override ApplicationCommandType Type => ApplicationCommandType.Message; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Context Menus/UserCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/Context Menus/UserCommandBuilder.cs new file mode 100644 index 000000000..5629a7014 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Context Menus/UserCommandBuilder.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// A class used to build user commands. + /// + public class UserCommandBuilder + { + /// + /// Returns the maximun length a commands name allowed by Discord + /// + public const int MaxNameLength = 32; + + /// + /// The name of this User command. + /// + public string Name + { + get + { + return _name; + } + set + { + Preconditions.NotNullOrEmpty(value, nameof(Name)); + Preconditions.AtLeast(value.Length, 3, nameof(Name)); + Preconditions.AtMost(value.Length, MaxNameLength, nameof(Name)); + + _name = value; + } + } + + private string _name { get; set; } + + /// + /// Build the current builder into a class. + /// + /// A that can be used to create user commands. + public UserCommandProperties Build() + { + UserCommandProperties props = new UserCommandProperties() + { + Name = this.Name, + }; + + return props; + + } + + /// + /// Sets the field name. + /// + /// The value to set the field name to. + /// + /// The current builder. + /// + public UserCommandBuilder WithName(string name) + { + this.Name = name; + return this; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/Context Menus/UserCommandProperties.cs b/src/Discord.Net.Core/Entities/Interactions/Context Menus/UserCommandProperties.cs new file mode 100644 index 000000000..091166a17 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Interactions/Context Menus/UserCommandProperties.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + /// + /// A class used to create User commands. + /// + public class UserCommandProperties : ApplicationCommandProperties + { + internal override ApplicationCommandType Type => ApplicationCommandType.User; + } +} diff --git a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs index a1a33acea..e0cf605d2 100644 --- a/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs +++ b/src/Discord.Net.Core/Entities/Interactions/IApplicationCommand.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// The base command model that belongs to an application. see + /// The base command model that belongs to an application. /// public interface IApplicationCommand : ISnowflakeEntity, IDeletable { @@ -16,6 +16,11 @@ namespace Discord /// ulong ApplicationId { get; } + /// + /// The type of the command + /// + ApplicationCommandType Type { get; } + /// /// The name of the command. /// @@ -35,5 +40,15 @@ namespace Discord /// If the option is a subcommand or subcommand group type, this nested options will be the parameters. /// IReadOnlyCollection Options { get; } + + /// + /// Modifies the current application command. + /// + /// The new properties to use when modifying the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommandBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommandBuilder.cs index e0afde50c..8a1bd8314 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommandBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommandBuilder.cs @@ -93,16 +93,16 @@ namespace Discord private List _options { get; set; } /// - /// Build the current builder into a class. + /// Build the current builder into a class. /// - /// A that can be used to create slash commands over rest. - public SlashCommandCreationProperties Build() + /// A that can be used to create slash commands over rest. + public SlashCommandProperties Build() { - SlashCommandCreationProperties props = new SlashCommandCreationProperties() + SlashCommandProperties props = new SlashCommandProperties() { Name = this.Name, Description = this.Description, - DefaultPermission = this.DefaultPermission + DefaultPermission = this.DefaultPermission, }; if (this.Options != null && this.Options.Any()) diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommandProperties.cs similarity index 74% rename from src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs rename to src/Discord.Net.Core/Entities/Interactions/SlashCommandProperties.cs index 7f4a3a62d..72c7c322b 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommandProperties.cs @@ -9,18 +9,13 @@ namespace Discord /// /// A class used to create slash commands. /// - public class SlashCommandCreationProperties + public class SlashCommandProperties : ApplicationCommandProperties { - /// - /// The name of this command. - /// - public string Name { get; set; } - + internal override ApplicationCommandType Type => ApplicationCommandType.Slash; /// /// The discription of this command. /// - public string Description { get; set; } - + public Optional Description { get; set; } /// /// Gets or sets the options for this command. @@ -31,5 +26,7 @@ namespace Discord /// Whether the command is enabled by default when the app is added to a guild. Default is /// public Optional DefaultPermission { get; set; } + + internal SlashCommandProperties() { } } } diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index d7d6d2856..f6981d552 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -141,6 +141,47 @@ namespace Discord /// Task> GetConnectionsAsync(RequestOptions options = null); + /// + /// Gets a global application command. + /// + /// The id of the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the application command if found, otherwise + /// . + /// + Task GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null); + + /// + /// Gets a collection of all global commands. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global + /// application commands. + /// + Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null); + + /// + /// Creates a global application command. + /// + /// The properties to use when creating the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the created application command. + /// + Task CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options = null); + + /// + /// Bulk overwrites all global application commands. + /// + /// A collection of properties to use when creating the commands. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains a collection of application commands that were created. + /// + Task> BulkOverwriteGlobalApplicationCommand(ApplicationCommandProperties[] properties, RequestOptions options = null); + /// /// Gets a guild. /// diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs index 39c40a1ee..9de272706 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommand.cs @@ -11,14 +11,22 @@ namespace Discord.API { [JsonProperty("id")] public ulong Id { get; set; } + + [JsonProperty("type")] + public ApplicationCommandType Type { get; set; } = ApplicationCommandType.Slash; // defaults to 1 which is slash. + [JsonProperty("application_id")] public ulong ApplicationId { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("description")] public string Description { get; set; } + [JsonProperty("options")] public Optional Options { get; set; } + [JsonProperty("default_permission")] public Optional DefaultPermissions { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs index c0ced154a..bb395df13 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionData.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Discord.API { - internal class ApplicationCommandInteractionData : IDiscordInteractionData + internal class ApplicationCommandInteractionData : IResolvable, IDiscordInteractionData { [JsonProperty("id")] public ulong Id { get; set; } @@ -17,5 +17,8 @@ namespace Discord.API [JsonProperty("resolved")] public Optional Resolved { get; set; } + [JsonProperty("type")] + public ApplicationCommandType Type { get; set; } + } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs index 46eca6b71..5b4b83e23 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandInteractionDataResolved.cs @@ -16,5 +16,7 @@ namespace Discord.API [JsonProperty("roles")] public Optional> Roles { get; set; } + [JsonProperty("messages")] + public Optional> Messages { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs b/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs index ba233cc6b..2101f79f4 100644 --- a/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs +++ b/src/Discord.Net.Rest/API/Common/InteractionCallbackData.cs @@ -16,7 +16,6 @@ namespace Discord.API [JsonProperty("allowed_mentions")] public Optional AllowedMentions { get; set; } - // New flags prop. this make the response "ephemeral". see https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata [JsonProperty("flags")] public Optional Flags { get; set; } diff --git a/src/Discord.Net.Rest/API/Net/IResolvable.cs b/src/Discord.Net.Rest/API/Net/IResolvable.cs new file mode 100644 index 000000000..7485f5de8 --- /dev/null +++ b/src/Discord.Net.Rest/API/Net/IResolvable.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API +{ + internal interface IResolvable + { + Optional Resolved { get; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs index 2e66245d7..ff72429a3 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateApplicationCommandParams.cs @@ -13,6 +13,9 @@ namespace Discord.API.Rest [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("type")] + public ApplicationCommandType Type { get; set; } + [JsonProperty("description")] public string Description { get; set; } @@ -23,11 +26,12 @@ namespace Discord.API.Rest public Optional DefaultPermission { get; set; } public CreateApplicationCommandParams() { } - public CreateApplicationCommandParams(string name, string description, ApplicationCommandOption[] options = null) + public CreateApplicationCommandParams(string name, string description, ApplicationCommandType type, ApplicationCommandOption[] options = null) { this.Name = name; this.Description = description; this.Options = Optional.Create(options); + this.Type = type; } } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 68589a4f1..93b82c929 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -216,6 +216,19 @@ namespace Discord.Rest Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) => Task.FromResult(null); + /// + Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) + => Task.FromResult(null); + + /// + Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + Task IDiscordClient.CreateGlobalApplicationCommand(ApplicationCommandProperties properties, RequestOptions options) + => Task.FromResult(null); + Task> IDiscordClient.BulkOverwriteGlobalApplicationCommand(ApplicationCommandProperties[] properties, + RequestOptions options) + => Task.FromResult>(ImmutableArray.Create()); + /// Task IDiscordClient.StartAsync() => Task.Delay(0); diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 0161483c9..2fc382900 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -194,7 +194,8 @@ namespace Discord.Rest }; } - public static async Task> GetGlobalApplicationCommands(BaseDiscordClient client, RequestOptions options) + public static async Task> GetGlobalApplicationCommands(BaseDiscordClient client, + RequestOptions options = null) { var response = await client.ApiClient.GetGlobalApplicationCommandsAsync(options).ConfigureAwait(false); @@ -203,8 +204,16 @@ namespace Discord.Rest return response.Select(x => RestGlobalCommand.Create(client, x)).ToArray(); } + public static async Task GetGlobalApplicationCommand(BaseDiscordClient client, ulong id, + RequestOptions options = null) + { + var model = await client.ApiClient.GetGlobalApplicationCommandAsync(id, options); + + return model != null ? RestGlobalCommand.Create(client, model) : null; + } - public static async Task> GetGuildApplicationCommands(BaseDiscordClient client, ulong guildId, RequestOptions options) + public static async Task> GetGuildApplicationCommands(BaseDiscordClient client, ulong guildId, + RequestOptions options = null) { var response = await client.ApiClient.GetGuildApplicationCommandsAsync(guildId, options).ConfigureAwait(false); @@ -213,7 +222,41 @@ namespace Discord.Rest return response.Select(x => RestGuildCommand.Create(client, x, guildId)).ToImmutableArray(); } + public static async Task GetGuildApplicationCommand(BaseDiscordClient client, ulong id, ulong guildId, + RequestOptions options = null) + { + var model = await client.ApiClient.GetGuildApplicationCommandAsync(guildId, id, options); + return model != null ? RestGuildCommand.Create(client, model, guildId) : null; + } + public static async Task CreateGuildApplicationCommand(BaseDiscordClient client, ulong guildId, ApplicationCommandProperties properties, + RequestOptions options = null) + { + var model = await InteractionHelper.CreateGuildCommand(client, guildId, properties, options); + + return RestGuildCommand.Create(client, model, guildId); + } + public static async Task CreateGlobalApplicationCommand(BaseDiscordClient client, ApplicationCommandProperties properties, + RequestOptions options = null) + { + var model = await InteractionHelper.CreateGlobalCommand(client, properties, options); + + return RestGlobalCommand.Create(client, model); + } + public static async Task> BulkOverwriteGlobalApplicationCommand(BaseDiscordClient client, ApplicationCommandProperties[] properties, + RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGlobalCommands(client, properties, options); + + return models.Select(x => RestGlobalCommand.Create(client, x)).ToImmutableArray(); + } + public static async Task> BulkOverwriteGuildApplicationCommand(BaseDiscordClient client, ulong guildId, + ApplicationCommandProperties[] properties, RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGuildCommands(client, guildId, properties, options); + + return models.Select(x => RestGuildCommand.Create(client, x, guildId)).ToImmutableArray(); + } public static Task AddRoleAsync(BaseDiscordClient client, ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) => client.ApiClient.AddRoleAsync(guildId, userId, roleId, options); diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.xml b/src/Discord.Net.Rest/Discord.Net.Rest.xml index 2c53beefc..196c93168 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.xml +++ b/src/Discord.Net.Rest/Discord.Net.Rest.xml @@ -226,6 +226,12 @@ + + + + + + @@ -299,6 +305,12 @@ + + + + + + Represents a configuration class for . @@ -3503,6 +3515,37 @@ of application commands found within the guild. + + + Gets an application command within this guild with the specified id. + + The id of the application command to get. + The options to be used when sending the request. + + A ValueTask that represents the asynchronous get operation. The task result contains a + if found, otherwise . + + + + + Creates an application command within this guild. + + The properties to use when creating the command. + The options to be used when sending the request. + + A task that represents the asynchronous creation operation. The task result contains the command that was created. + + + + + Overwrites the application commands within this guild. + + A collection of properties to use when creating the commands. + The options to be used when sending the request. + + A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. + + Returns the name of the guild. @@ -3752,6 +3795,15 @@ + + + + + + + + + @@ -3834,6 +3886,9 @@ + + + @@ -3848,17 +3903,15 @@ The options of this command. - - - The type of this rest application command. - - + + + Represents a Rest-based implementation of . @@ -3900,24 +3953,9 @@ A collection of 's for this command. - - - Represents a type of Rest-based command. - - - - - Specifies that this command is a Global command. - - - - - Specifies that this command is a Guild specific command. - - - Represents a global Slash command. + Represents a Rest-based global application command. @@ -3935,7 +3973,7 @@ - Represents a Rest-based guild command. + Represents a Rest-based guild application command. diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 14aba69ea..09d284785 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -235,6 +235,7 @@ namespace Discord.API options.BucketId = bucketId; string json = payload != null ? SerializeJson(payload) : null; + var request = new JsonRestRequest(RestClient, method, endpoint, json, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); } @@ -1123,6 +1124,18 @@ namespace Discord.API return await SendAsync("GET", () => $"applications/{this.CurrentUserId}/commands", new BucketIds(), options: options).ConfigureAwait(false); } + public async Task GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) + { + Preconditions.NotEqual(id, 0, nameof(id)); + + options = RequestOptions.CreateOrClone(options); + + try + { + return await SendAsync("GET", () => $"applications/{this.CurrentUserId}/commands/{id}", new BucketIds(), options: options).ConfigureAwait(false); + } + catch(HttpException x) when (x.HttpCode == HttpStatusCode.NotFound) { return null; } + } public async Task CreateGlobalApplicationCommandAsync(CreateApplicationCommandParams command, RequestOptions options = null) { @@ -1134,7 +1147,6 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); - return await TrySendApplicationCommand(SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/commands", command, new BucketIds(), options: options)).ConfigureAwait(false); } public async Task ModifyGlobalApplicationCommandAsync(ModifyApplicationCommandParams command, ulong commandId, RequestOptions options = null) @@ -1143,6 +1155,18 @@ namespace Discord.API return await TrySendApplicationCommand(SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options)).ConfigureAwait(false); } + public async Task ModifyGlobalApplicationUserCommandAsync(ModifyApplicationCommandParams command, ulong commandId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommand(SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options)).ConfigureAwait(false); + } + public async Task ModifyGlobalApplicationMessageCommandAsync(ModifyApplicationCommandParams command, ulong commandId, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommand(SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/commands/{commandId}", command, new BucketIds(), options: options)).ConfigureAwait(false); + } public async Task DeleteGlobalApplicationCommandAsync(ulong commandId, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); @@ -1156,6 +1180,40 @@ namespace Discord.API return await TrySendApplicationCommand(SendJsonAsync("PUT", () => $"applications/{this.CurrentUserId}/commands", commands, new BucketIds(), options: options)).ConfigureAwait(false); } + public async Task CreateGlobalApplicationUserCommandAsync(CreateApplicationCommandParams command, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name)); + + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommand(SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/commands", command, new BucketIds(), options: options)).ConfigureAwait(false); + } + public async Task CreateGlobalApplicationMessageCommandAsync(CreateApplicationCommandParams command, RequestOptions options = null) + { + Preconditions.NotNull(command, nameof(command)); + Preconditions.AtMost(command.Name.Length, 32, nameof(command.Name)); + Preconditions.AtLeast(command.Name.Length, 3, nameof(command.Name)); + + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommand(SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/commands", command, new BucketIds(), options: options)).ConfigureAwait(false); + } + + public async Task BulkOverwriteGlobalApplicationUserCommands(CreateApplicationCommandParams[] commands, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommand(SendJsonAsync("PUT", () => $"applications/{this.CurrentUserId}/commands", commands, new BucketIds(), options: options)).ConfigureAwait(false); + } + + public async Task BulkOverwriteGlobalApplicationMessageCommands(CreateApplicationCommandParams[] commands, RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + + return await TrySendApplicationCommand(SendJsonAsync("PUT", () => $"applications/{this.CurrentUserId}/commands", commands, new BucketIds(), options: options)).ConfigureAwait(false); + } public async Task GetGuildApplicationCommandsAsync(ulong guildId, RequestOptions options = null) { @@ -1172,7 +1230,11 @@ namespace Discord.API var bucket = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", bucket, options: options); + try + { + return await SendAsync("GET", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", bucket, options: options); + } + catch(HttpException x) when (x.HttpCode == HttpStatusCode.NotFound) { return null; } } public async Task CreateGuildApplicationCommandAsync(CreateApplicationCommandParams command, ulong guildId, RequestOptions options = null) @@ -1182,7 +1244,6 @@ namespace Discord.API var bucket = new BucketIds(guildId: guildId); return await TrySendApplicationCommand(SendJsonAsync("POST", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands", command, bucket, options: options)).ConfigureAwait(false); - } public async Task ModifyGuildApplicationCommandAsync(ModifyApplicationCommandParams command, ulong guildId, ulong commandId, RequestOptions options = null) { @@ -1190,21 +1251,7 @@ namespace Discord.API var bucket = new BucketIds(guildId: guildId); - try - { - return await SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, bucket, options: options).ConfigureAwait(false); - } - catch (HttpException x) - { - if (x.HttpCode == HttpStatusCode.BadRequest) - { - var json = (x.Request as JsonRestRequest).Json; - throw new ApplicationCommandException(json, x); - } - - // Re-throw the http exception - throw; - } + return await TrySendApplicationCommand(SendJsonAsync("PATCH", () => $"applications/{this.CurrentUserId}/guilds/{guildId}/commands/{commandId}", command, bucket, options: options)).ConfigureAwait(false); } public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index a8849525e..10a4c40a9 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -1,3 +1,4 @@ +//using Discord.Rest.Entities.Interactions; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -107,22 +108,18 @@ namespace Discord.Rest public Task GetWebhookAsync(ulong id, RequestOptions options = null) => ClientHelper.GetWebhookAsync(this, id, options); - public Task CreateGlobalCommand(SlashCommandCreationProperties properties, RequestOptions options = null) - => InteractionHelper.CreateGlobalCommand(this, properties, options); - public Task CreateGlobalCommand(Action func, RequestOptions options = null) - => InteractionHelper.CreateGlobalCommand(this, func, options); - public Task CreateGuildCommand(SlashCommandCreationProperties properties, ulong guildId, RequestOptions options = null) - => InteractionHelper.CreateGuildCommand(this, guildId, properties, options); - public Task CreateGuildCommand(Action func, ulong guildId, RequestOptions options = null) - => InteractionHelper.CreateGuildCommand(this, guildId, func, options); + public Task CreateGlobalCommand(ApplicationCommandProperties properties, RequestOptions options = null) + => ClientHelper.CreateGlobalApplicationCommand(this, properties, options); + public Task CreateGuildCommand(ApplicationCommandProperties properties, ulong guildId, RequestOptions options = null) + => ClientHelper.CreateGuildApplicationCommand(this, guildId, properties, options); public Task> GetGlobalApplicationCommands(RequestOptions options = null) => ClientHelper.GetGlobalApplicationCommands(this, options); public Task> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) => ClientHelper.GetGuildApplicationCommands(this, guildId, options); - public Task> BulkOverwriteGlobalCommands(SlashCommandCreationProperties[] commandProperties, RequestOptions options = null) - => InteractionHelper.BulkOverwriteGlobalCommands(this, commandProperties, options); - public Task> BulkOverwriteGuildCommands(SlashCommandCreationProperties[] commandProperties, ulong guildId, RequestOptions options = null) - => InteractionHelper.BulkOverwriteGuildCommands(this, guildId, commandProperties, options); + public Task> BulkOverwriteGlobalCommands(ApplicationCommandProperties[] commandProperties, RequestOptions options = null) + => ClientHelper.BulkOverwriteGlobalApplicationCommand(this, commandProperties, options); + public Task> BulkOverwriteGuildCommands(ApplicationCommandProperties[] commandProperties, ulong guildId, RequestOptions options = null) + => ClientHelper.BulkOverwriteGuildApplicationCommand(this, guildId, commandProperties, options); public Task> BatchEditGuildCommandPermissions(ulong guildId, IDictionary permissions, RequestOptions options = null) => InteractionHelper.BatchEditGuildCommandPermissionsAsync(this, guildId, permissions, options); public Task DeleteAllGlobalCommandsAsync(RequestOptions options = null) @@ -225,5 +222,12 @@ namespace Discord.Rest /// async Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options).ConfigureAwait(false); + + /// + async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) + => await GetGlobalApplicationCommands(options).ConfigureAwait(false); + /// + async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) + => await ClientHelper.GetGlobalApplicationCommand(this, id, options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index b27bcd996..f257ef898 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -898,8 +898,48 @@ namespace Discord.Rest /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of application commands found within the guild. /// - public async Task> GetApplicationCommandsAsync (RequestOptions options = null) + public async Task> GetApplicationCommandsAsync (RequestOptions options = null) => await ClientHelper.GetGuildApplicationCommands(Discord, Id, options).ConfigureAwait(false); + /// + /// Gets an application command within this guild with the specified id. + /// + /// The id of the application command to get. + /// The options to be used when sending the request. + /// + /// A ValueTask that represents the asynchronous get operation. The task result contains a + /// if found, otherwise . + /// + public async Task GetApplicationCommandAsync(ulong id, RequestOptions options = null) + => await ClientHelper.GetGuildApplicationCommand(Discord, id, this.Id, options); + /// + /// Creates an application command within this guild. + /// + /// The properties to use when creating the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the command that was created. + /// + public async Task CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null) + { + var model = await InteractionHelper.CreateGuildCommand(Discord, this.Id, properties, options); + + return RestGuildCommand.Create(Discord, model, this.Id); + } + /// + /// Overwrites the application commands within this guild. + /// + /// A collection of properties to use when creating the commands. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. + /// + public async Task> BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, + RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGuildCommands(Discord, this.Id, properties, options); + + return models.Select(x => RestGuildCommand.Create(Discord, x, this.Id)).ToImmutableArray(); + } /// /// Returns the name of the guild. @@ -1327,5 +1367,22 @@ namespace Discord.Rest /// Task IGuild.DeleteStickerAsync(ICustomSticker sticker, RequestOptions options) => sticker.DeleteAsync(); + /// + async Task IGuild.CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options) + => await CreateApplicationCommandAsync(properties, options); + /// + async Task> IGuild.BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, + RequestOptions options) + => await BulkOverwriteApplicationCommandsAsync(properties, options); + /// + async Task IGuild.GetApplicationCommandAsync(ulong id, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + { + return await GetApplicationCommandAsync(id, options); + } + else + return null; + } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs index 7ed48a04d..59d5c4f2b 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/InteractionHelper.cs @@ -1,6 +1,7 @@ using Discord.API; using Discord.API.Rest; using Discord.Net; +//using Discord.Rest.Entities.Interactions; using System; using System.Collections.Generic; using System.Linq; @@ -43,40 +44,52 @@ namespace Discord.Rest } // Global commands - public static async Task CreateGlobalCommand(BaseDiscordClient client, - Action func, RequestOptions options = null) + public static async Task GetGlobalCommandAsync(BaseDiscordClient client, ulong id, + RequestOptions options = null) { - var args = new SlashCommandCreationProperties(); - func(args); - return await CreateGlobalCommand(client, args, options).ConfigureAwait(false); + var model = await client.ApiClient.GetGlobalApplicationCommandAsync(id, options).ConfigureAwait(false); + + + return RestGlobalCommand.Create(client, model); } - public static async Task CreateGlobalCommand(BaseDiscordClient client, - SlashCommandCreationProperties arg, RequestOptions options = null) + public static Task CreateGlobalCommand(BaseDiscordClient client, + Action func, RequestOptions options = null) where TArg : ApplicationCommandProperties + { + var args = Activator.CreateInstance(typeof(TArg)); + func((TArg)args); + return CreateGlobalCommand(client, (TArg)args, options); + } + public static async Task CreateGlobalCommand(BaseDiscordClient client, + ApplicationCommandProperties arg, RequestOptions options = null) { Preconditions.NotNullOrEmpty(arg.Name, nameof(arg.Name)); - Preconditions.NotNullOrEmpty(arg.Description, nameof(arg.Description)); - - if (arg.Options.IsSpecified) - Preconditions.AtMost(arg.Options.Value.Count, 25, nameof(arg.Options)); var model = new CreateApplicationCommandParams() { - Name = arg.Name, - Description = arg.Description, - Options = arg.Options.IsSpecified - ? arg.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified, - DefaultPermission = arg.DefaultPermission.IsSpecified - ? arg.DefaultPermission.Value - : Optional.Unspecified + Name = arg.Name.Value, + Type = arg.Type, }; - var cmd = await client.ApiClient.CreateGlobalApplicationCommandAsync(model, options).ConfigureAwait(false); - return RestGlobalCommand.Create(client, cmd); + if (arg is SlashCommandProperties slashProps) + { + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); + + model.Description = slashProps.Description.Value; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + + model.DefaultPermission = slashProps.DefaultPermission.IsSpecified + ? slashProps.DefaultPermission.Value + : Optional.Unspecified; + } + + return await client.ApiClient.CreateGlobalApplicationCommandAsync(model, options).ConfigureAwait(false); } - public static async Task> BulkOverwriteGlobalCommands(BaseDiscordClient client, - SlashCommandCreationProperties[] args, RequestOptions options = null) + public static async Task BulkOverwriteGlobalCommands(BaseDiscordClient client, + ApplicationCommandProperties[] args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); @@ -85,33 +98,36 @@ namespace Discord.Rest foreach (var arg in args) { Preconditions.NotNullOrEmpty(arg.Name, nameof(arg.Name)); - Preconditions.NotNullOrEmpty(arg.Description, nameof(arg.Description)); - - if (arg.Options.IsSpecified) - Preconditions.AtMost(arg.Options.Value.Count, 25, nameof(arg.Options)); var model = new CreateApplicationCommandParams() { - Name = arg.Name, - Description = arg.Description, - Options = arg.Options.IsSpecified - ? arg.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified, - DefaultPermission = arg.DefaultPermission.IsSpecified - ? arg.DefaultPermission.Value - : Optional.Unspecified + Name = arg.Name.Value, + Type = arg.Type, }; + if (arg is SlashCommandProperties slashProps) + { + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); + + model.Description = slashProps.Description.Value; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + + model.DefaultPermission = slashProps.DefaultPermission.IsSpecified + ? slashProps.DefaultPermission.Value + : Optional.Unspecified; + } + models.Add(model); } - var apiModels = await client.ApiClient.BulkOverwriteGlobalApplicationCommands(models.ToArray(), options); - - return apiModels.Select(x => RestGlobalCommand.Create(client, x)).ToArray(); + return await client.ApiClient.BulkOverwriteGlobalApplicationCommands(models.ToArray(), options).ConfigureAwait(false); } - public static async Task> BulkOverwriteGuildCommands(BaseDiscordClient client, ulong guildId, - SlashCommandCreationProperties[] args, RequestOptions options = null) + public static async Task> BulkOverwriteGuildCommands(BaseDiscordClient client, ulong guildId, + ApplicationCommandProperties[] args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); @@ -120,74 +136,86 @@ namespace Discord.Rest foreach (var arg in args) { Preconditions.NotNullOrEmpty(arg.Name, nameof(arg.Name)); - Preconditions.NotNullOrEmpty(arg.Description, nameof(arg.Description)); - - if (arg.Options.IsSpecified) - Preconditions.AtMost(arg.Options.Value.Count, 25, nameof(arg.Options)); var model = new CreateApplicationCommandParams() { - Name = arg.Name, - Description = arg.Description, - Options = arg.Options.IsSpecified - ? arg.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified, - DefaultPermission = arg.DefaultPermission.IsSpecified - ? arg.DefaultPermission.Value - : Optional.Unspecified + Name = arg.Name.Value, + Type = arg.Type, }; + if (arg is SlashCommandProperties slashProps) + { + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); + + model.Description = slashProps.Description.Value; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + + model.DefaultPermission = slashProps.DefaultPermission.IsSpecified + ? slashProps.DefaultPermission.Value + : Optional.Unspecified; + } + models.Add(model); } - var apiModels = await client.ApiClient.BulkOverwriteGuildApplicationCommands(guildId, models.ToArray(), options); - - return apiModels.Select(x => RestGuildCommand.Create(client, x, guildId)).ToArray(); + return await client.ApiClient.BulkOverwriteGuildApplicationCommands(guildId, models.ToArray(), options).ConfigureAwait(false); } - public static async Task ModifyGlobalCommand(BaseDiscordClient client, RestGlobalCommand command, - Action func, RequestOptions options = null) + public static Task ModifyGlobalCommand(BaseDiscordClient client, IApplicationCommand command, + Action func, RequestOptions options = null) where TArg : ApplicationCommandProperties { - ApplicationCommandProperties args = new ApplicationCommandProperties(); - func(args); + var arg = (TArg)Activator.CreateInstance(typeof(TArg)); + func(arg); + return ModifyGlobalCommand(client, command, arg, options); + } + public static async Task ModifyGlobalCommand(BaseDiscordClient client, IApplicationCommand command, + ApplicationCommandProperties args, RequestOptions options = null) + { if (args.Name.IsSpecified) { Preconditions.AtMost(args.Name.Value.Length, 32, nameof(args.Name)); Preconditions.AtLeast(args.Name.Value.Length, 3, nameof(args.Name)); } - if (args.Description.IsSpecified) - { - Preconditions.AtMost(args.Description.Value.Length, 100, nameof(args.Description)); - Preconditions.AtLeast(args.Description.Value.Length, 1, nameof(args.Description)); - } - - - if (args.Options.IsSpecified) - { - if (args.Options.Value.Count > 10) - throw new ArgumentException("Option count must be 10 or less"); - } var model = new Discord.API.Rest.ModifyApplicationCommandParams() { Name = args.Name, - Description = args.Description, - Options = args.Options.IsSpecified - ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified, - DefaultPermission = args.DefaultPermission.IsSpecified - ? args.DefaultPermission.Value - : Optional.Unspecified }; - var msg = await client.ApiClient.ModifyGlobalApplicationCommandAsync(model, command.Id, options).ConfigureAwait(false); - command.Update(msg); - return command; + if(args is SlashCommandProperties slashProps) + { + if (slashProps.Description.IsSpecified) + { + Preconditions.AtMost(slashProps.Description.Value.Length, 100, nameof(slashProps.Description)); + Preconditions.AtLeast(slashProps.Description.Value.Length, 1, nameof(slashProps.Description)); + } + + if (slashProps.Options.IsSpecified) + { + if (slashProps.Options.Value.Count > 10) + throw new ArgumentException("Option count must be 10 or less"); + } + + model.Description = slashProps.Description; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + + model.DefaultPermission = slashProps.DefaultPermission.IsSpecified + ? slashProps.DefaultPermission.Value + : Optional.Unspecified; + } + + return await client.ApiClient.ModifyGlobalApplicationCommandAsync(model, command.Id, options).ConfigureAwait(false); } - public static async Task DeleteGlobalCommand(BaseDiscordClient client, RestGlobalCommand command, RequestOptions options = null) + public static async Task DeleteGlobalCommand(BaseDiscordClient client, IApplicationCommand command, RequestOptions options = null) { Preconditions.NotNull(command, nameof(command)); Preconditions.NotEqual(command.Id, 0, nameof(command.Id)); @@ -196,90 +224,73 @@ namespace Discord.Rest } // Guild Commands - public static async Task CreateGuildCommand(BaseDiscordClient client, ulong guildId, - Action func, RequestOptions options = null) + public static Task CreateGuildCommand(BaseDiscordClient client, ulong guildId, + Action func, RequestOptions options) where TArg : ApplicationCommandProperties { - var args = new SlashCommandCreationProperties(); - func(args); - - return await CreateGuildCommand(client, guildId, args, options).ConfigureAwait(false); + var args = Activator.CreateInstance(typeof(TArg)); + func((TArg)args); + return CreateGuildCommand(client, guildId, (TArg)args, options); } - public static async Task CreateGuildCommand(BaseDiscordClient client, ulong guildId, - SlashCommandCreationProperties args, RequestOptions options = null) - { - Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - Preconditions.NotNullOrEmpty(args.Description, nameof(args.Description)); - Preconditions.AtMost(args.Name.Length, 32, nameof(args.Name)); - Preconditions.AtLeast(args.Name.Length, 3, nameof(args.Name)); - Preconditions.AtMost(args.Description.Length, 100, nameof(args.Description)); - Preconditions.AtLeast(args.Description.Length, 1, nameof(args.Description)); + public static async Task CreateGuildCommand(BaseDiscordClient client, ulong guildId, + ApplicationCommandProperties arg, RequestOptions options = null) + { + var model = new CreateApplicationCommandParams() + { + Name = arg.Name.Value, + Type = arg.Type, + }; - if (args.Options.IsSpecified) + if (arg is SlashCommandProperties slashProps) { - if (args.Options.Value.Count > 10) - throw new ArgumentException("Option count must be 10 or less"); + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); - foreach (var item in args.Options.Value) - { - Preconditions.NotNullOrEmpty(item.Name, nameof(item.Name)); - Preconditions.NotNullOrEmpty(item.Description, nameof(item.Description)); - } - } + model.Description = slashProps.Description.Value; - var model = new CreateApplicationCommandParams() - { - Name = args.Name, - Description = args.Description, - Options = args.Options.IsSpecified - ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified, - DefaultPermission = args.DefaultPermission.IsSpecified - ? args.DefaultPermission.Value - : Optional.Unspecified - }; + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; - var cmd = await client.ApiClient.CreateGuildApplicationCommandAsync(model, guildId, options).ConfigureAwait(false); - return RestGuildCommand.Create(client, cmd, guildId); + model.DefaultPermission = slashProps.DefaultPermission.IsSpecified + ? slashProps.DefaultPermission.Value + : Optional.Unspecified; + } + + return await client.ApiClient.CreateGuildApplicationCommandAsync(model, guildId, options).ConfigureAwait(false); } - public static async Task ModifyGuildCommand(BaseDiscordClient client, RestGuildCommand command, - Action func, RequestOptions options = null) + + public static Task ModifyGuildCommand(BaseDiscordClient client, IApplicationCommand command, ulong guildId, + Action func, RequestOptions options = null) where TArg : ApplicationCommandProperties { - ApplicationCommandProperties args = new ApplicationCommandProperties(); - func(args); + var arg = (TArg)Activator.CreateInstance(typeof(TArg)); + func(arg); + return ModifyGuildCommand(client, command, guildId, arg, options); + } - if (args.Name.IsSpecified) - { - Preconditions.AtMost(args.Name.Value.Length, 32, nameof(args.Name)); - Preconditions.AtLeast(args.Name.Value.Length, 3, nameof(args.Name)); - } - if (args.Description.IsSpecified) + public static async Task ModifyGuildCommand(BaseDiscordClient client, IApplicationCommand command, ulong guildId, + ApplicationCommandProperties arg, RequestOptions options = null) + { + var model = new ModifyApplicationCommandParams() { - Preconditions.AtMost(args.Description.Value.Length, 100, nameof(args.Description)); - Preconditions.AtLeast(args.Description.Value.Length, 1, nameof(args.Description)); - } + Name = arg.Name.Value, + }; - if (args.Options.IsSpecified) + if (arg is SlashCommandProperties slashProps) { - if (args.Options.Value.Count > 10) - throw new ArgumentException("Option count must be 10 or less"); - } + Preconditions.NotNullOrEmpty(slashProps.Description, nameof(slashProps.Description)); - var model = new Discord.API.Rest.ModifyApplicationCommandParams() - { - Name = args.Name, - Description = args.Description, - Options = args.Options.IsSpecified - ? args.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() - : Optional.Unspecified, - DefaultPermission = args.DefaultPermission.IsSpecified - ? args.DefaultPermission.Value - : Optional.Unspecified - }; + model.Description = slashProps.Description.Value; + + model.Options = slashProps.Options.IsSpecified + ? slashProps.Options.Value.Select(x => new Discord.API.ApplicationCommandOption(x)).ToArray() + : Optional.Unspecified; + + model.DefaultPermission = slashProps.DefaultPermission.IsSpecified + ? slashProps.DefaultPermission.Value + : Optional.Unspecified; + } - var msg = await client.ApiClient.ModifyGuildApplicationCommandAsync(model, command.GuildId, command.Id, options).ConfigureAwait(false); - command.Update(msg); - return command; + return await client.ApiClient.ModifyGuildApplicationCommandAsync(model, guildId, command.Id, options).ConfigureAwait(false); } public static async Task DeleteGuildCommand(BaseDiscordClient client, ulong guildId, IApplicationCommand command, RequestOptions options = null) @@ -290,6 +301,19 @@ namespace Discord.Rest await client.ApiClient.DeleteGuildApplicationCommandAsync(guildId, command.Id, options).ConfigureAwait(false); } + public static Task DeleteUnknownApplicationCommand(BaseDiscordClient client, ulong? guildId, IApplicationCommand command, RequestOptions options = null) + { + if (guildId.HasValue) + { + return DeleteGuildCommand(client, guildId.Value, command, options); + } + else + { + return DeleteGlobalCommand(client, command, options); + } + } + + // Responses public static async Task ModifyFollowupMessage(BaseDiscordClient client, RestFollowupMessage message, Action func, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs index e1a854187..dcba1ee87 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommand.cs @@ -16,6 +16,9 @@ namespace Discord.Rest /// public ulong ApplicationId { get; private set; } + /// + public ApplicationCommandType Type { get; private set; } + /// public string Name { get; private set; } @@ -30,11 +33,6 @@ namespace Discord.Rest /// public IReadOnlyCollection Options { get; private set; } - /// - /// The type of this rest application command. - /// - public RestApplicationCommandType CommandType { get; internal set; } - /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(this.Id); @@ -45,15 +43,16 @@ namespace Discord.Rest } - internal static RestApplicationCommand Create(BaseDiscordClient client, Model model, RestApplicationCommandType type, ulong guildId = 0) + internal static RestApplicationCommand Create(BaseDiscordClient client, Model model, ulong? guildId) { - if (type == RestApplicationCommandType.GlobalCommand) + if (guildId.HasValue) + { + return RestGuildCommand.Create(client, model, guildId.Value); + } + else + { return RestGlobalCommand.Create(client, model); - - if (type == RestApplicationCommandType.GuildCommand) - return RestGuildCommand.Create(client, model, guildId); - - return null; + } } internal virtual void Update(Model model) @@ -72,7 +71,9 @@ namespace Discord.Rest /// public abstract Task DeleteAsync(RequestOptions options = null); - IReadOnlyCollection IApplicationCommand.Options => Options; + /// + public abstract Task ModifyAsync(Action func, RequestOptions options = null); + IReadOnlyCollection IApplicationCommand.Options => Options; } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs index a8e37873e..b135ae578 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandOption.cs @@ -68,7 +68,10 @@ namespace Discord.Rest : null; } - IReadOnlyCollection IApplicationCommandOption.Options => Options; - IReadOnlyCollection IApplicationCommandOption.Choices => Choices; + //IApplicationCommandOption + IReadOnlyCollection IApplicationCommandOption.Options + => Options; + IReadOnlyCollection IApplicationCommandOption.Choices + => Choices; } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs b/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs deleted file mode 100644 index 96ba07053..000000000 --- a/src/Discord.Net.Rest/Entities/Interactions/RestApplicationCommandType.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.Rest -{ - /// - /// Represents a type of Rest-based command. - /// - public enum RestApplicationCommandType - { - /// - /// Specifies that this command is a Global command. - /// - GlobalCommand, - - /// - /// Specifies that this command is a Guild specific command. - /// - GuildCommand - } -} diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs index 230243c7a..7e3ca0a4e 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestGlobalCommand.cs @@ -8,14 +8,14 @@ using Model = Discord.API.ApplicationCommand; namespace Discord.Rest { /// - /// Represents a global Slash command. + /// Represents a Rest-based global application command. /// public class RestGlobalCommand : RestApplicationCommand { internal RestGlobalCommand(BaseDiscordClient client, ulong id) : base(client, id) { - this.CommandType = RestApplicationCommandType.GlobalCommand; + } internal static RestGlobalCommand Create(BaseDiscordClient client, Model model) @@ -37,7 +37,10 @@ namespace Discord.Rest /// /// The modified command. /// - public async Task ModifyAsync(Action func, RequestOptions options = null) - => await InteractionHelper.ModifyGlobalCommand(Discord, this, func, options).ConfigureAwait(false); + public override async Task ModifyAsync(Action func, RequestOptions options = null) + { + var cmd = await InteractionHelper.ModifyGlobalCommand(Discord, this, func, options).ConfigureAwait(false); + this.Update(cmd); + } } } diff --git a/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs index 33ab78dbb..aa236d4b1 100644 --- a/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs +++ b/src/Discord.Net.Rest/Entities/Interactions/RestGuildCommand.cs @@ -8,7 +8,7 @@ using Model = Discord.API.ApplicationCommand; namespace Discord.Rest { /// - /// Represents a Rest-based guild command. + /// Represents a Rest-based guild application command. /// public class RestGuildCommand : RestApplicationCommand { @@ -20,7 +20,6 @@ namespace Discord.Rest internal RestGuildCommand(BaseDiscordClient client, ulong id, ulong guildId) : base(client, id) { - this.CommandType = RestApplicationCommandType.GuildCommand; this.GuildId = guildId; } @@ -43,8 +42,11 @@ namespace Discord.Rest /// /// The modified command /// - public async Task ModifyAsync(Action func, RequestOptions options = null) - => await InteractionHelper.ModifyGuildCommand(Discord, this, func, options).ConfigureAwait(false); + public override async Task ModifyAsync(Action func, RequestOptions options = null) + { + var model = await InteractionHelper.ModifyGuildCommand(Discord, this, GuildId, func, options).ConfigureAwait(false); + this.Update(model); + } /// /// Gets this commands permissions inside of the current guild. diff --git a/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs index 190eca89d..9d41ecf48 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/ApplicationCommandCreatedUpdatedEvent.cs @@ -7,27 +7,9 @@ using System.Threading.Tasks; namespace Discord.API.Gateway { - internal class ApplicationCommandCreatedUpdatedEvent + internal class ApplicationCommandCreatedUpdatedEvent : API.ApplicationCommand { - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("id")] - public ulong Id { get; set; } - - [JsonProperty("description")] - public string Description { get; set; } - - [JsonProperty("application_id")] - public ulong ApplicationId { get; set; } - [JsonProperty("guild_id")] - public ulong GuildId { get; set; } - - [JsonProperty("options")] - public Optional> Options { get; set; } - - [JsonProperty("default_permission")] - public Optional DefaultPermission { get; set; } + public Optional GuildId { get; set; } } } diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index f2e370d02..7129feb48 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -16,12 +16,14 @@ namespace Discord.WebSocket private readonly ConcurrentDictionary _guilds; private readonly ConcurrentDictionary _users; private readonly ConcurrentHashSet _groupChannels; + private readonly ConcurrentDictionary _commands; internal IReadOnlyCollection Channels => _channels.ToReadOnlyCollection(); internal IReadOnlyCollection DMChannels => _dmChannels.ToReadOnlyCollection(); internal IReadOnlyCollection GroupChannels => _groupChannels.Select(x => GetChannel(x) as SocketGroupChannel).ToReadOnlyCollection(_groupChannels); internal IReadOnlyCollection Guilds => _guilds.ToReadOnlyCollection(); internal IReadOnlyCollection Users => _users.ToReadOnlyCollection(); + internal IReadOnlyCollection Commands => _commands.ToReadOnlyCollection(); internal IReadOnlyCollection PrivateChannels => _dmChannels.Select(x => x.Value as ISocketPrivateChannel).Concat( @@ -37,6 +39,7 @@ namespace Discord.WebSocket _guilds = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(guildCount * CollectionMultiplier)); _users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(estimatedUsersCount * CollectionMultiplier)); _groupChannels = new ConcurrentHashSet(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(10 * CollectionMultiplier)); + _commands = new ConcurrentDictionary(); } internal SocketChannel GetChannel(ulong id) @@ -139,5 +142,33 @@ namespace Discord.WebSocket foreach (var guild in _guilds.Values) guild.PurgeGuildUserCache(); } + + internal SocketApplicationCommand GetCommand(ulong id) + { + if (_commands.TryGetValue(id, out SocketApplicationCommand command)) + return command; + return null; + } + internal void AddCommand(SocketApplicationCommand command) + { + _commands[command.Id] = command; + } + internal SocketApplicationCommand GetOrAddCommand(ulong id, Func commandFactory) + { + return _commands.GetOrAdd(id, commandFactory); + } + internal SocketApplicationCommand RemoveCommand(ulong id) + { + if (_commands.TryRemove(id, out SocketApplicationCommand command)) + return command; + return null; + } + internal void PurgeCommands(Func precondition) + { + var ids = _commands.Where(x => precondition(x.Value)).Select(x => x.Key); + + foreach (var id in ids) + _commands.TryRemove(id, out var _); + } } } diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index bcaf81f31..e6352780e 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -20,7 +20,7 @@ 3.0.1 - TRACE;DEBUG;DEBUG_PACKETS + TRACE diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml index 2649e0aac..9366cf1f7 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xml @@ -1118,6 +1118,27 @@ + + + Gets a global application command. + + The id of the command. + The options to be used when sending the request. + + A ValueTask that represents the asynchronous get operation. The task result contains the application command if found, otherwise + . + + + + + Gets a collection of all global commands. + + The options to be used when sending the request. + + A task that represents the asynchronous get operation. The task result contains a read-only collection of global + application commands. + + Clears cached users from the client. @@ -1216,6 +1237,12 @@ + + + + + + @@ -3299,16 +3326,16 @@ voice regions the guild can access. - + - Deletes all slash commands in the current guild. + Deletes all application commands in the current guild. The options to be used when sending the request. A task that represents the asynchronous delete operation. - + Gets a collection of slash commands created by the current user in this guild. @@ -3318,15 +3345,36 @@ slash commands created by the current user. - + - Gets a slash command in the current guild. + Gets an application command within this guild with the specified id. - The unique identifier of the slash command. + The id of the application command to get. + The that determines whether the object should be fetched from cache. The options to be used when sending the request. - A task that represents the asynchronous get operation. The task result contains a - slash command created by the current user. + A ValueTask that represents the asynchronous get operation. The task result contains a + if found, otherwise . + + + + + Creates an application command within this guild. + + The properties to use when creating the command. + The options to be used when sending the request. + + A task that represents the asynchronous creation operation. The task result contains the command that was created. + + + + + Overwrites the application commands within this guild. + + A collection of properties to use when creating the commands. + The options to be used when sending the request. + + A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. @@ -3467,16 +3515,6 @@ of webhooks found within the guild. - - - Gets this guilds slash commands commands - - The options to be used when sending the request. - - A task that represents the asynchronous get operation. The task result contains a read-only collection - of application commands found within the guild. - - @@ -3713,6 +3751,58 @@ + + + Represents a Websocket-based slash command received over the gateway. + + + + + The data associated with this interaction. + + + + + Represents the data tied with the interaction. + + + + + Gets the messagte associated with this message command. + + + + + + Note Not implemented for + + + + + Represents a Websocket-based slash command received over the gateway. + + + + + The data associated with this interaction. + + + + + Represents the data tied with the interaction. + + + + + Gets the user who this command targets. + + + + + + Note Not implemented for + + Represents a Websocket-based interaction type for Message Components. @@ -3775,9 +3865,48 @@ The value(s) of a interaction response. + + + Represents a Websocket-based slash command received over the gateway. + + + + + The data associated with this interaction. + + + + + Represents the data tied with the interaction. + + + + + Represents a Websocket-based recieved by the gateway + + + + + + + + + + + + + + The sub command options received for this sub command group. + + - Represends a Websocket-based recieved over the gateway. + Represends a Websocket-based . + + + + + if this command is a global command, otherwise . @@ -3786,6 +3915,9 @@ + + + @@ -3794,20 +3926,34 @@ - A collection of 's recieved over the gateway. + A collection of 's for this command. + + If the is not a slash command, this field will be an empty collection. + - The where this application was created. + Returns the guild this command resides in, if this command is a global command then it will return + + + Modifies the current application command. + + The new properties to use when modifying the command. + The options to be used when sending the request. + + A task that represents the asynchronous modification operation. + + Thrown when you pass in an invalid type. + Represents a choice for a . @@ -3849,55 +3995,46 @@ If the option is a subcommand or subcommand group type, this nested options will be the parameters. - + - Represents a Websocket-based slash command received over the gateway. + Base class for User, Message, and Slash command interactions - + The data associated with this interaction. - - - - + - + - + - Represents the data tied with the interaction. - - - - - - - - The 's received with this interaction. + Acknowledges this interaction with the . + + A task that represents the asynchronous operation of acknowledging the interaction. + - + - Represents a Websocket-based recieved by the gateway + Represents the base data tied with the interaction. - - - - + - - + + + The received with this interaction. + - + - The sub command options received for this sub command group. + Represents the base data tied with the interaction. diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index fcef7463a..1eb034434 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -381,6 +381,83 @@ namespace Discord.WebSocket /// public override SocketUser GetUser(string username, string discriminator) => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); + + /// + /// Gets a global application command. + /// + /// The id of the command. + /// The options to be used when sending the request. + /// + /// A ValueTask that represents the asynchronous get operation. The task result contains the application command if found, otherwise + /// . + /// + public async ValueTask GetGlobalApplicationCommandAsync(ulong id, RequestOptions options = null) + { + var command = State.GetCommand(id); + + if (command != null) + return command; + + var model = await ApiClient.GetGlobalApplicationCommandAsync(id, options); + + if (model == null) + return null; + + command = SocketApplicationCommand.Create(this, model); + + State.AddCommand(command); + + return command; + } + /// + /// Gets a collection of all global commands. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection of global + /// application commands. + /// + public async Task> GetGlobalApplicationCommandsAsync(RequestOptions options = null) + { + var commands = (await ApiClient.GetGlobalApplicationCommandsAsync(options)).Select(x => SocketApplicationCommand.Create(this, x)); + + foreach(var command in commands) + { + State.AddCommand(command); + } + + return commands.ToImmutableArray(); + } + + public async Task CreateGlobalApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null) + { + var model = await InteractionHelper.CreateGlobalCommand(this, properties, options).ConfigureAwait(false); + + var entity = State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(this, model)); + + // update it incase it was cached + entity.Update(model); + + return entity; + } + public async Task> BulkOverwriteGlobalApplicationCommandsAsync( + ApplicationCommandProperties[] properties, RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGlobalCommands(this, properties, options); + + var entities = models.Select(x => SocketApplicationCommand.Create(this, x)); + + // purge our previous commands + State.PurgeCommands(x => x.IsGlobalCommand); + + foreach(var entity in entities) + { + State.AddCommand(entity); + } + + return entities.ToImmutableArray(); + } + /// /// Clears cached users from the client. /// @@ -1974,8 +2051,6 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Received Dispatch (INTERACTION_CREATE)").ConfigureAwait(false); - // 0x546861742062696720656e6469616e20656e636f64696e67206d616b6573206d79316d687a20636c6f636b207469636b - var data = (payload as JToken).ToObject(_serializer); SocketChannel channel = null; @@ -2029,15 +2104,20 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if(guild == null) + if (data.GuildId.IsSpecified) { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } } var applicationCommand = SocketApplicationCommand.Create(this, data); + State.AddCommand(applicationCommand); + await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false); } break; @@ -2047,15 +2127,20 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild == null) + if (data.GuildId.IsSpecified) { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } } var applicationCommand = SocketApplicationCommand.Create(this, data); + State.AddCommand(applicationCommand); + await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false); } break; @@ -2065,15 +2150,20 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); - var guild = State.GetGuild(data.GuildId); - if (guild == null) + if (data.GuildId.IsSpecified) { - await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); - return; + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) + { + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } } var applicationCommand = SocketApplicationCommand.Create(this, data); + State.RemoveCommand(applicationCommand.Id); + await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false); } break; @@ -2738,6 +2828,13 @@ namespace Discord.WebSocket async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); + /// + async Task IDiscordClient.GetGlobalApplicationCommandAsync(ulong id, RequestOptions options) + => await GetGlobalApplicationCommandAsync(id, options); + /// + async Task> IDiscordClient.GetGlobalApplicationCommandsAsync(RequestOptions options) + => await GetGlobalApplicationCommandsAsync(options); + /// async Task IDiscordClient.StartAsync() => await StartAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 205642634..e34c49811 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -815,13 +815,13 @@ namespace Discord.WebSocket //Interactions /// - /// Deletes all slash commands in the current guild. + /// Deletes all application commands in the current guild. /// /// The options to be used when sending the request. /// /// A task that represents the asynchronous delete operation. /// - public Task DeleteSlashCommandsAsync(RequestOptions options = null) + public Task DeleteApplicationCommandsAsync(RequestOptions options = null) => InteractionHelper.DeleteAllGuildCommandsAsync(Discord, this.Id, options); /// @@ -832,20 +832,93 @@ namespace Discord.WebSocket /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// slash commands created by the current user. /// - public Task> GetSlashCommandsAsync(RequestOptions options = null) - => GuildHelper.GetSlashCommandsAsync(this, Discord, options); + public async Task> GetApplicationCommandsAsync(RequestOptions options = null) + { + var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Id, options)).Select(x => SocketApplicationCommand.Create(Discord, x, this.Id)); + + foreach (var command in commands) + { + Discord.State.AddCommand(command); + } + + return commands.ToImmutableArray(); + } + + /// + /// Gets an application command within this guild with the specified id. + /// + /// The id of the application command to get. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// A ValueTask that represents the asynchronous get operation. The task result contains a + /// if found, otherwise . + /// + public async ValueTask GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) + { + var command = Discord.State.GetCommand(id); + + if (command != null) + return command; + + if (mode == CacheMode.CacheOnly) + return null; + + var model = await Discord.ApiClient.GetGlobalApplicationCommandAsync(id, options); + + if (model == null) + return null; + + command = SocketApplicationCommand.Create(Discord, model, this.Id); + + Discord.State.AddCommand(command); + + return command; + } + + /// + /// Creates an application command within this guild. + /// + /// The properties to use when creating the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the command that was created. + /// + public async Task CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null) + { + var model = await InteractionHelper.CreateGuildCommand(Discord, this.Id, properties, options); + + var entity = Discord.State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model)); + + entity.Update(model); + + return entity; + } /// - /// Gets a slash command in the current guild. + /// Overwrites the application commands within this guild. /// - /// The unique identifier of the slash command. + /// A collection of properties to use when creating the commands. /// The options to be used when sending the request. /// - /// A task that represents the asynchronous get operation. The task result contains a - /// slash command created by the current user. + /// A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. /// - public Task GetSlashCommandAsync(ulong id, RequestOptions options = null) - => GuildHelper.GetSlashCommandAsync(this, id, Discord, options); + public async Task> BulkOverwriteApplicationCommandAsync(ApplicationCommandProperties[] properties, + RequestOptions options = null) + { + var models = await InteractionHelper.BulkOverwriteGuildCommands(Discord, this.Id, properties, options); + + var entities = models.Select(x => SocketApplicationCommand.Create(Discord, x)); + + Discord.State.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == this.Id); + + foreach(var entity in entities) + { + Discord.State.AddCommand(entity); + } + + return entities.ToImmutableArray(); + } //Invites /// @@ -1136,18 +1209,6 @@ namespace Discord.WebSocket public Task> GetWebhooksAsync(RequestOptions options = null) => GuildHelper.GetWebhooksAsync(this, Discord, options); - //Interactions - /// - /// Gets this guilds slash commands commands - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection - /// of application commands found within the guild. - /// - public async Task> GetApplicationCommandsAsync(RequestOptions options = null) - => await Discord.Rest.GetGuildApplicationCommands(this.Id, options); - //Emotes /// public Task> GetEmotesAsync(RequestOptions options = null) @@ -1687,6 +1748,16 @@ namespace Discord.WebSocket /// Task IGuild.DeleteStickerAsync(ICustomSticker sticker, RequestOptions options) => DeleteStickerAsync(_stickers[sticker.Id], options); + /// + async Task IGuild.GetApplicationCommandAsync(ulong id, CacheMode mode, RequestOptions options) + => await GetApplicationCommandAsync(id, mode, options); + /// + async Task IGuild.CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options) + => await CreateApplicationCommandAsync(properties, options); + /// + async Task> IGuild.BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, + RequestOptions options) + => await BulkOverwriteApplicationCommandAsync(properties, options); void IDisposable.Dispose() { diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketMessageCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketMessageCommand.cs new file mode 100644 index 000000000..72f040cc6 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketMessageCommand.cs @@ -0,0 +1,41 @@ +using Discord.Rest; +using System; +using System.Linq; +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based slash command received over the gateway. + /// + public class SocketMessageCommand : SocketCommandBase + { + /// + /// The data associated with this interaction. + /// + public new SocketMessageCommandData Data { get; } + + internal SocketMessageCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model, channel) + { + var dataModel = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + + ulong? guildId = null; + if (this.Channel is SocketGuildChannel guildChannel) + guildId = guildChannel.Guild.Id; + + Data = SocketMessageCommandData.Create(client, dataModel, model.Id, guildId); + } + + new internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketMessageCommand(client, model, channel); + entity.Update(model); + return entity; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketMessageCommandData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketMessageCommandData.cs new file mode 100644 index 000000000..e78da223d --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/Message Commands/SocketMessageCommandData.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the data tied with the interaction. + /// + public class SocketMessageCommandData : SocketCommandBaseData + { + /// + /// Gets the messagte associated with this message command. + /// + public SocketMessage Message + => ResolvableData?.Messages.FirstOrDefault().Value; + + /// + /// + /// Note Not implemented for + /// + public override IReadOnlyCollection Options + => throw new System.NotImplementedException(); + + internal SocketMessageCommandData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model, guildId) { } + + internal new static SocketMessageCommandData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId) + { + var entity = new SocketMessageCommandData(client, model, guildId); + entity.Update(model); + return entity; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketUserCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketUserCommand.cs new file mode 100644 index 000000000..5345c08f7 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketUserCommand.cs @@ -0,0 +1,41 @@ +using Discord.Rest; +using System; +using System.Linq; +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Represents a Websocket-based slash command received over the gateway. + /// + public class SocketUserCommand : SocketCommandBase + { + /// + /// The data associated with this interaction. + /// + public new SocketUserCommandData Data { get; } + + internal SocketUserCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model, channel) + { + var dataModel = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + + ulong? guildId = null; + if (this.Channel is SocketGuildChannel guildChannel) + guildId = guildChannel.Guild.Id; + + Data = SocketUserCommandData.Create(client, dataModel, model.Id, guildId); + } + + new internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketUserCommand(client, model, channel); + entity.Update(model); + return entity; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketUserCommandData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketUserCommandData.cs new file mode 100644 index 000000000..d6c2a7990 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Context Menu Commands/User Commands/SocketUserCommandData.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the data tied with the interaction. + /// + public class SocketUserCommandData : SocketCommandBaseData + { + /// + /// Gets the user who this command targets. + /// + public SocketUser Member + => (SocketUser)ResolvableData.GuildMembers.Values.FirstOrDefault() ?? ResolvableData.Users.Values.FirstOrDefault(); + + /// + /// + /// Note Not implemented for + /// + public override IReadOnlyCollection Options + => throw new System.NotImplementedException(); + + internal SocketUserCommandData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model, guildId) { } + + internal new static SocketUserCommandData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId) + { + var entity = new SocketUserCommandData(client, model, guildId); + entity.Update(model); + return entity; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommand.cs deleted file mode 100644 index 81decb4be..000000000 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommand.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Discord.Rest; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Model = Discord.API.Gateway.ApplicationCommandCreatedUpdatedEvent; - -namespace Discord.WebSocket -{ - /// - /// Represends a Websocket-based recieved over the gateway. - /// - public class SocketApplicationCommand : SocketEntity, IApplicationCommand - { - /// - public ulong ApplicationId { get; private set; } - - /// - public string Name { get; private set; } - - /// - public string Description { get; private set; } - - /// - public bool DefaultPermission { get; private set; } - - /// - /// A collection of 's recieved over the gateway. - /// - public IReadOnlyCollection Options { get; private set; } - - /// - public DateTimeOffset CreatedAt - => SnowflakeUtils.FromSnowflake(this.Id); - - /// - /// The where this application was created. - /// - public SocketGuild Guild - => Discord.GetGuild(this.GuildId); - private ulong GuildId { get; set; } - - internal SocketApplicationCommand(DiscordSocketClient client, ulong id) - : base(client, id) - { - - } - internal static SocketApplicationCommand Create(DiscordSocketClient client, Model model) - { - var entity = new SocketApplicationCommand(client, model.Id); - entity.Update(model); - return entity; - } - - internal void Update(Model model) - { - this.ApplicationId = model.ApplicationId; - this.Description = model.Description; - this.Name = model.Name; - this.GuildId = model.GuildId; - this.DefaultPermission = model.DefaultPermission.GetValueOrDefault(true); - - - this.Options = model.Options.IsSpecified - ? model.Options.Value.Select(x => SocketApplicationCommandOption.Create(x)).ToImmutableArray() - : new ImmutableArray(); - } - - /// - public Task DeleteAsync(RequestOptions options = null) - => InteractionHelper.DeleteGuildCommand(Discord, this.GuildId, this, options); - - IReadOnlyCollection IApplicationCommand.Options => Options; - } -} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs index bff5292c5..05c051f12 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommand.cs @@ -10,7 +10,7 @@ namespace Discord.WebSocket /// /// Represents a Websocket-based slash command received over the gateway. /// - public class SocketSlashCommand : SocketInteraction + public class SocketSlashCommand : SocketCommandBase { /// /// The data associated with this interaction. @@ -18,7 +18,7 @@ namespace Discord.WebSocket new public SocketSlashCommandData Data { get; } internal SocketSlashCommand(DiscordSocketClient client, Model model, ISocketMessageChannel channel) - : base(client, model.Id, channel) + : base(client, model, channel) { var dataModel = model.Data.IsSpecified ? (DataModel)model.Data.Value @@ -28,7 +28,7 @@ namespace Discord.WebSocket if (this.Channel is SocketGuildChannel guildChannel) guildId = guildChannel.Guild.Id; - Data = SocketSlashCommandData.Create(client, dataModel, model.Id, guildId); + Data = SocketSlashCommandData.Create(client, dataModel, guildId); } new internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) @@ -37,125 +37,5 @@ namespace Discord.WebSocket entity.Update(model); return entity; } - - internal override void Update(Model model) - { - var data = model.Data.IsSpecified ? - (DataModel)model.Data.Value - : null; - - this.Data.Update(data); - - base.Update(model); - } - - /// - public override async Task RespondAsync( - string text = null, - Embed[] embeds = null, - bool isTTS = false, - bool ephemeral = false, - AllowedMentions allowedMentions = null, - RequestOptions options = null, - MessageComponent component = null, - Embed embed = null) - { - if (!IsValidToken) - throw new InvalidOperationException("Interaction token is no longer valid"); - - if (embeds == null && embed != null) - embeds = new[] { embed }; - - if (Discord.AlwaysAcknowledgeInteractions) - { - await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, options, component); - return; - } - - Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); - Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); - Preconditions.AtMost(embeds?.Length ?? 0, 10, nameof(embeds), "A max of 10 embeds are allowed."); - - // check that user flag and user Id list are exclusive, same with role flag and role Id list - if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) - { - if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && - allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) - { - throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); - } - - if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && - allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) - { - throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); - } - } - - var response = new API.InteractionResponse - { - Type = InteractionResponseType.ChannelMessageWithSource, - Data = new API.InteractionCallbackData - { - Content = text, - AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, - Embeds = embeds?.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, - TTS = isTTS ? true : Optional.Unspecified, - Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified - } - }; - - if (ephemeral) - response.Data.Value.Flags = MessageFlags.Ephemeral; - - await InteractionHelper.SendInteractionResponse(this.Discord, response, this.Id, Token, options); - } - - /// - public override async Task FollowupAsync( - string text = null, - Embed[] embeds = null, - bool isTTS = false, - bool ephemeral = false, - AllowedMentions allowedMentions = null, - RequestOptions options = null, - MessageComponent component = null, - Embed embed = null) - { - if (!IsValidToken) - throw new InvalidOperationException("Interaction token is no longer valid"); - - if (embeds == null && embed != null) - embeds = new[] { embed }; - Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); - Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); - Preconditions.AtMost(embeds?.Length ?? 0, 10, nameof(embeds), "A max of 10 embeds are allowed."); - - var args = new API.Rest.CreateWebhookMessageParams - { - Content = text, - AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, - IsTTS = isTTS, - Embeds = embeds?.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, - Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified - }; - - if (ephemeral) - args.Flags = MessageFlags.Ephemeral; - - return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); - } - - /// - public override Task DeferAsync(bool ephemeral = false, RequestOptions options = null) - { - var response = new API.InteractionResponse - { - Type = InteractionResponseType.DeferredChannelMessageWithSource, - Data = ephemeral ? new API.InteractionCallbackData() { Flags = MessageFlags.Ephemeral } : Optional.Unspecified - }; - - return Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, this.Token, options); - } } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandCache.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandCache.cs deleted file mode 100644 index 7dd30151d..000000000 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandCache.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.WebSocket.Entities.Interaction -{ - internal class SlashCommandCache - { - private readonly ConcurrentDictionary _slashCommands; - private readonly ConcurrentQueue _orderedSlashCommands; - private readonly int _size; - - public IReadOnlyCollection Messages => _slashCommands.ToReadOnlyCollection(); - - public SlashCommandCache(DiscordSocketClient client) - { - _size = 256; - _slashCommands = new ConcurrentDictionary(); - - } - - public void Add(SocketSlashCommand slashCommand) - { - if (_slashCommands.TryAdd(slashCommand.Id, slashCommand)) - { - _orderedSlashCommands.Enqueue(slashCommand.Id); - - while (_orderedSlashCommands.Count > _size && _orderedSlashCommands.TryDequeue(out ulong msgId)) - _slashCommands.TryRemove(msgId, out _); - } - } - - public SocketSlashCommand Remove(ulong id) - { - _slashCommands.TryRemove(id, out var slashCommand); - return slashCommand; - } - - public SocketSlashCommand Get(ulong id) - { - _slashCommands.TryGetValue(id, out var slashCommands); - return slashCommands; - } - } -} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandData.cs index 6ed77b997..4b6764bf7 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandData.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandData.cs @@ -8,108 +8,24 @@ namespace Discord.WebSocket /// /// Represents the data tied with the interaction. /// - public class SocketSlashCommandData : SocketEntity, IApplicationCommandInteractionData + public class SocketSlashCommandData : SocketCommandBaseData { - /// - public string Name { get; private set; } - - /// - /// The 's received with this interaction. - /// - public IReadOnlyCollection Options { get; private set; } - - internal Dictionary guildMembers { get; private set; } - = new Dictionary(); - internal Dictionary users { get; private set; } - = new Dictionary(); - internal Dictionary channels { get; private set; } - = new Dictionary(); - internal Dictionary roles { get; private set; } - = new Dictionary(); - - private ulong? guildId; - internal SocketSlashCommandData(DiscordSocketClient client, Model model, ulong? guildId) - : base(client, model.Id) - { - this.guildId = guildId; - - if (model.Resolved.IsSpecified) - { - var guild = this.guildId.HasValue ? Discord.GetGuild(this.guildId.Value) : null; - - var resolved = model.Resolved.Value; - - if (resolved.Users.IsSpecified) - { - foreach (var user in resolved.Users.Value) - { - var socketUser = Discord.GetOrCreateUser(this.Discord.State, user.Value); - - this.users.Add(ulong.Parse(user.Key), socketUser); - } - } + : base(client, model, guildId) { } - if (resolved.Channels.IsSpecified) - { - foreach (var channel in resolved.Channels.Value) - { - SocketChannel socketChannel = guild != null - ? guild.GetChannel(channel.Value.Id) - : Discord.GetChannel(channel.Value.Id); - - if (socketChannel == null) - { - var channelModel = guild != null - ? Discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult() - : Discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); - - socketChannel = guild != null - ? SocketGuildChannel.Create(guild, Discord.State, channelModel) - : (SocketChannel)SocketChannel.CreatePrivate(Discord, Discord.State, channelModel); - } - - Discord.State.AddChannel(socketChannel); - this.channels.Add(ulong.Parse(channel.Key), socketChannel); - } - } - - if (resolved.Members.IsSpecified) - { - foreach (var member in resolved.Members.Value) - { - member.Value.User = resolved.Users.Value[member.Key]; - var user = guild.AddOrUpdateUser(member.Value); - this.guildMembers.Add(ulong.Parse(member.Key), user); - } - } - - if (resolved.Roles.IsSpecified) - { - foreach (var role in resolved.Roles.Value) - { - var socketRole = guild.AddOrUpdateRole(role.Value); - this.roles.Add(ulong.Parse(role.Key), socketRole); - } - } - } - } - - internal static SocketSlashCommandData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId) + internal static SocketSlashCommandData Create(DiscordSocketClient client, Model model, ulong? guildId) { var entity = new SocketSlashCommandData(client, model, guildId); entity.Update(model); return entity; } - internal void Update(Model model) + internal override void Update(Model model) { - this.Name = model.Name; + base.Update(model); this.Options = model.Options.IsSpecified ? model.Options.Value.Select(x => new SocketSlashCommandDataOption(this, x)).ToImmutableArray() : null; } - - IReadOnlyCollection IApplicationCommandInteractionData.Options => Options; } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandDataOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandDataOption.cs index f9c12257e..a79a9724c 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandDataOption.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketSlashCommandDataOption.cs @@ -44,34 +44,34 @@ namespace Discord.WebSocket { case ApplicationCommandOptionType.User: { - var guildUser = data.guildMembers.FirstOrDefault(x => x.Key == valueId).Value; + var guildUser = data.ResolvableData.GuildMembers.FirstOrDefault(x => x.Key == valueId).Value; if (guildUser != null) this.Value = guildUser; else - this.Value = data.users.FirstOrDefault(x => x.Key == valueId).Value; + this.Value = data.ResolvableData.Users.FirstOrDefault(x => x.Key == valueId).Value; } break; case ApplicationCommandOptionType.Channel: - this.Value = data.channels.FirstOrDefault(x => x.Key == valueId).Value; + this.Value = data.ResolvableData.Channels.FirstOrDefault(x => x.Key == valueId).Value; break; case ApplicationCommandOptionType.Role: - this.Value = data.roles.FirstOrDefault(x => x.Key == valueId).Value; + this.Value = data.ResolvableData.Roles.FirstOrDefault(x => x.Key == valueId).Value; break; case ApplicationCommandOptionType.Mentionable: { - if(data.guildMembers.Any(x => x.Key == valueId) || data.users.Any(x => x.Key == valueId)) + if(data.ResolvableData.GuildMembers.Any(x => x.Key == valueId) || data.ResolvableData.Users.Any(x => x.Key == valueId)) { - var guildUser = data.guildMembers.FirstOrDefault(x => x.Key == valueId).Value; + var guildUser = data.ResolvableData.GuildMembers.FirstOrDefault(x => x.Key == valueId).Value; if (guildUser != null) this.Value = guildUser; else - this.Value = data.users.FirstOrDefault(x => x.Key == valueId).Value; + this.Value = data.ResolvableData.Users.FirstOrDefault(x => x.Key == valueId).Value; } - else if(data.roles.Any(x => x.Key == valueId)) + else if(data.ResolvableData.Roles.Any(x => x.Key == valueId)) { - this.Value = data.roles.FirstOrDefault(x => x.Key == valueId).Value; + this.Value = data.ResolvableData.Roles.FirstOrDefault(x => x.Key == valueId).Value; } } break; @@ -125,6 +125,8 @@ namespace Discord.WebSocket public static explicit operator string(SocketSlashCommandDataOption option) => option.Value.ToString(); - IReadOnlyCollection IApplicationCommandInteractionDataOption.Options => this.Options; + // IApplicationCommandInteractionDataOption + IReadOnlyCollection IApplicationCommandInteractionDataOption.Options + => this.Options; } } diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs new file mode 100644 index 000000000..77a43a1e3 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommand.cs @@ -0,0 +1,132 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using GatewayModel = Discord.API.Gateway.ApplicationCommandCreatedUpdatedEvent; +using Model = Discord.API.ApplicationCommand; + +namespace Discord.WebSocket +{ + /// + /// Represends a Websocket-based . + /// + public class SocketApplicationCommand : SocketEntity, IApplicationCommand + { + /// + /// if this command is a global command, otherwise . + /// + public bool IsGlobalCommand + => Guild == null; + + /// + public ulong ApplicationId { get; private set; } + + /// + public string Name { get; private set; } + + /// + public ApplicationCommandType Type { get; private set; } + + /// + public string Description { get; private set; } + + /// + public bool DefaultPermission { get; private set; } + + /// + /// A collection of 's for this command. + /// + /// + /// If the is not a slash command, this field will be an empty collection. + /// + public IReadOnlyCollection Options { get; private set; } + + /// + public DateTimeOffset CreatedAt + => SnowflakeUtils.FromSnowflake(this.Id); + + /// + /// Returns the guild this command resides in, if this command is a global command then it will return + /// + public SocketGuild Guild + => GuildId.HasValue ? Discord.GetGuild(this.GuildId.Value) : null; + + private ulong? GuildId { get; set; } + + internal SocketApplicationCommand(DiscordSocketClient client, ulong id, ulong? guildId) + : base(client, id) + { + this.GuildId = guildId; + } + internal static SocketApplicationCommand Create(DiscordSocketClient client, GatewayModel model) + { + var entity = new SocketApplicationCommand(client, model.Id, model.GuildId.ToNullable()); + entity.Update(model); + return entity; + } + + internal static SocketApplicationCommand Create(DiscordSocketClient client, Model model, ulong? guildId = null) + { + var entity = new SocketApplicationCommand(client, model.Id, guildId); + entity.Update(model); + return entity; + } + + internal void Update(Model model) + { + this.ApplicationId = model.ApplicationId; + this.Description = model.Description; + this.Name = model.Name; + this.DefaultPermission = model.DefaultPermissions.GetValueOrDefault(true); + + this.Options = model.Options.IsSpecified + ? model.Options.Value.Select(x => SocketApplicationCommandOption.Create(x)).ToImmutableArray() + : new ImmutableArray(); + } + + /// + public Task DeleteAsync(RequestOptions options = null) + => InteractionHelper.DeleteUnknownApplicationCommand(Discord, this.GuildId, this, options); + + /// + /// Modifies the current application command. + /// + /// The new properties to use when modifying the command. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + /// Thrown when you pass in an invalid type. + public async Task ModifyAsync(Action func, RequestOptions options = null) where TArg : ApplicationCommandProperties + { + switch (typeof(TArg)) + { + case Type messageCommand when messageCommand == typeof(MessageCommandProperties) && this.Type != ApplicationCommandType.Message: + case Type slashCommand when slashCommand == typeof(SlashCommandProperties) && this.Type != ApplicationCommandType.Slash: + case Type userCommand when userCommand == typeof(UserCommandProperties) && this.Type != ApplicationCommandType.User: + throw new InvalidOperationException($"Cannot modify this application command with the parameter type {nameof(TArg)}"); + } + + Model command = null; + + if (this.IsGlobalCommand) + { + command = await InteractionHelper.ModifyGlobalCommand(Discord, this, func, options).ConfigureAwait(false); + } + else + { + command = await InteractionHelper.ModifyGuildCommand(Discord, this, this.GuildId.Value, func, options); + } + + this.Update(command); + } + + // IApplicationCommand + IReadOnlyCollection IApplicationCommand.Options => Options; + Task IApplicationCommand.ModifyAsync(Action func, RequestOptions options) + => ModifyAsync(func, options); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandChoice.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs similarity index 100% rename from src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandChoice.cs rename to src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandChoice.cs diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs similarity index 100% rename from src/Discord.Net.WebSocket/Entities/Interaction/Slash Commands/SocketApplicationCommandOption.cs rename to src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketApplicationCommandOption.cs diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs new file mode 100644 index 000000000..8ca20aa7c --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBase.cs @@ -0,0 +1,165 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DataModel = Discord.API.ApplicationCommandInteractionData; +using Model = Discord.API.Interaction; + +namespace Discord.WebSocket +{ + /// + /// Base class for User, Message, and Slash command interactions + /// + public class SocketCommandBase : SocketInteraction + { + /// + /// The data associated with this interaction. + /// + new internal SocketCommandBaseData Data { get; } + + internal SocketCommandBase(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + : base(client, model.Id, channel) + { + var dataModel = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + + ulong? guildId = null; + if (this.Channel is SocketGuildChannel guildChannel) + guildId = guildChannel.Guild.Id; + + Data = SocketCommandBaseData.Create(client, dataModel, model.Id, guildId); + } + + new internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) + { + var entity = new SocketCommandBase(client, model, channel); + entity.Update(model); + return entity; + } + + internal override void Update(Model model) + { + var data = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + + this.Data.Update(data); + + base.Update(model); + } + + /// + public override async Task RespondAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (embeds == null && embed != null) + embeds = new[] { embed }; + + if (Discord.AlwaysAcknowledgeInteractions) + { + await FollowupAsync(text, embeds, isTTS, ephemeral, allowedMentions, options, component); + return; + } + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds?.Length ?? 0, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var response = new API.InteractionResponse + { + Type = InteractionResponseType.ChannelMessageWithSource, + Data = new API.InteractionCallbackData + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + Embeds = embeds?.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + TTS = isTTS ? true : Optional.Unspecified, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Flags = ephemeral ? MessageFlags.Ephemeral : Optional.Unspecified + } + }; + + await InteractionHelper.SendInteractionResponse(this.Discord, response, this.Id, Token, options); + } + + /// + public override async Task FollowupAsync( + string text = null, + Embed[] embeds = null, + bool isTTS = false, + bool ephemeral = false, + AllowedMentions allowedMentions = null, + RequestOptions options = null, + MessageComponent component = null, + Embed embed = null) + { + if (!IsValidToken) + throw new InvalidOperationException("Interaction token is no longer valid"); + + if (embeds == null && embed != null) + embeds = new[] { embed }; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds?.Length ?? 0, 10, nameof(embeds), "A max of 10 embeds are allowed."); + + var args = new API.Rest.CreateWebhookMessageParams + { + Content = text, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + IsTTS = isTTS, + Embeds = embeds?.Select(x => x.ToModel()).ToArray() ?? Optional.Unspecified, + Components = component?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified + }; + + if (ephemeral) + args.Flags = MessageFlags.Ephemeral; + + return await InteractionHelper.SendFollowupAsync(Discord.Rest, args, Token, Channel, options); + } + + /// + /// Acknowledges this interaction with the . + /// + /// + /// A task that represents the asynchronous operation of acknowledging the interaction. + /// + public override Task DeferAsync(bool ephemeral = false, RequestOptions options = null) + { + var response = new API.InteractionResponse + { + Type = InteractionResponseType.DeferredChannelMessageWithSource, + }; + + return Discord.Rest.ApiClient.CreateInteractionResponse(response, this.Id, this.Token, options); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBaseData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBaseData.cs new file mode 100644 index 000000000..64aeb5d24 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketCommandBaseData.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.ApplicationCommandInteractionData; + +namespace Discord.WebSocket +{ + /// + /// Represents the base data tied with the interaction. + /// + public class SocketCommandBaseData : SocketEntity, IApplicationCommandInteractionData where TOption : IApplicationCommandInteractionDataOption + { + /// + public string Name { get; private set; } + + /// + /// The received with this interaction. + /// + public virtual IReadOnlyCollection Options { get; internal set; } + + internal readonly SocketResolvableData ResolvableData; + + private ApplicationCommandType Type { get; set; } + + internal SocketCommandBaseData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model.Id) + { + this.Type = model.Type; + + if (model.Resolved.IsSpecified) + { + ResolvableData = new SocketResolvableData(client, guildId, model); + } + } + + internal static SocketCommandBaseData Create(DiscordSocketClient client, Model model, ulong id, ulong? guildId) + { + var entity = new SocketCommandBaseData(client, model, guildId); + entity.Update(model); + return entity; + } + + internal virtual void Update(Model model) + { + this.Name = model.Name; + } + + IReadOnlyCollection IApplicationCommandInteractionData.Options + => (IReadOnlyCollection)Options; + } + + /// + /// Represents the base data tied with the interaction. + /// + public class SocketCommandBaseData : SocketCommandBaseData + { + internal SocketCommandBaseData(DiscordSocketClient client, Model model, ulong? guildId) + : base(client, model, guildId) { } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs new file mode 100644 index 000000000..17c96724c --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketBaseCommand/SocketResolvableData.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal class SocketResolvableData where T : API.IResolvable + { + internal readonly Dictionary GuildMembers + = new Dictionary(); + internal readonly Dictionary Users + = new Dictionary(); + internal readonly Dictionary Channels + = new Dictionary(); + internal readonly Dictionary Roles + = new Dictionary(); + + internal readonly Dictionary Messages + = new Dictionary(); + + internal SocketResolvableData(DiscordSocketClient discord, ulong? guildId, T model) + { + var guild = guildId.HasValue ? discord.GetGuild(guildId.Value) : null; + + var resolved = model.Resolved.Value; + + if (resolved.Users.IsSpecified) + { + foreach (var user in resolved.Users.Value) + { + var socketUser = discord.GetOrCreateUser(discord.State, user.Value); + + this.Users.Add(ulong.Parse(user.Key), socketUser); + } + } + + if (resolved.Channels.IsSpecified) + { + foreach (var channel in resolved.Channels.Value) + { + SocketChannel socketChannel = guild != null + ? guild.GetChannel(channel.Value.Id) + : discord.GetChannel(channel.Value.Id); + + if (socketChannel == null) + { + var channelModel = guild != null + ? discord.Rest.ApiClient.GetChannelAsync(guild.Id, channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult() + : discord.Rest.ApiClient.GetChannelAsync(channel.Value.Id).ConfigureAwait(false).GetAwaiter().GetResult(); + + socketChannel = guild != null + ? SocketGuildChannel.Create(guild, discord.State, channelModel) + : (SocketChannel)SocketChannel.CreatePrivate(discord, discord.State, channelModel); + } + + discord.State.AddChannel(socketChannel); + this.Channels.Add(ulong.Parse(channel.Key), socketChannel); + } + } + + if (resolved.Members.IsSpecified) + { + foreach (var member in resolved.Members.Value) + { + member.Value.User = resolved.Users.Value[member.Key]; + var user = guild.AddOrUpdateUser(member.Value); + this.GuildMembers.Add(ulong.Parse(member.Key), user); + } + } + + if (resolved.Roles.IsSpecified) + { + foreach (var role in resolved.Roles.Value) + { + var socketRole = guild.AddOrUpdateRole(role.Value); + this.Roles.Add(ulong.Parse(role.Key), socketRole); + } + } + + if (resolved.Messages.IsSpecified) + { + foreach (var msg in resolved.Messages.Value) + { + var channel = discord.GetChannel(msg.Value.ChannelId) as ISocketMessageChannel; + + SocketUser author; + if (guild != null) + { + if (msg.Value.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, discord.State, msg.Value.Author.Value, msg.Value.WebhookId.Value); + else + author = guild.GetUser(msg.Value.Author.Value.Id); + } + else + author = (channel as SocketChannel).GetUser(msg.Value.Author.Value.Id); + + if (channel == null) + { + if (!msg.Value.GuildId.IsSpecified) // assume it is a DM + { + channel = discord.CreateDMChannel(msg.Value.ChannelId, msg.Value.Author.Value, discord.State); + } + } + + var message = SocketMessage.Create(discord, discord.State, author, channel, msg.Value); + this.Messages.Add(message.Id, message); + } + } + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs index e7f5873d6..9b42ed0e2 100644 --- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs +++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs @@ -2,6 +2,7 @@ using Discord.Rest; using System; using System.Threading.Tasks; using Model = Discord.API.Interaction; +using DataModel = Discord.API.ApplicationCommandInteractionData; namespace Discord.WebSocket { @@ -61,7 +62,22 @@ namespace Discord.WebSocket internal static SocketInteraction Create(DiscordSocketClient client, Model model, ISocketMessageChannel channel) { if (model.Type == InteractionType.ApplicationCommand) + { + if (model.ApplicationId != null) + { + var dataModel = model.Data.IsSpecified ? + (DataModel)model.Data.Value + : null; + if (dataModel != null) + { + if (dataModel.Type.Equals(ApplicationCommandType.User)) + return SocketUserCommand.Create(client, model, channel); + if (dataModel.Type.Equals(ApplicationCommandType.Message)) + return SocketMessageCommand.Create(client, model, channel); + } + } return SocketSlashCommand.Create(client, model, channel); + } if (model.Type == InteractionType.MessageComponent) return SocketMessageComponent.Create(client, model, channel); else