Basic Slash Command Servicepull/1733/head
@@ -42,6 +42,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Examples", "src | |||
EndProject | |||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" | |||
EndProject | |||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlashCommandsExample", "SlashCommandsExample\SlashCommandsExample.csproj", "{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}" | |||
ProjectSection(ProjectDependencies) = postProject | |||
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} | |||
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} | |||
{BFC6DC28-0351-4573-926A-D4124244C04F} = {BFC6DC28-0351-4573-926A-D4124244C04F} | |||
{E169E15A-E82C-45BF-8C24-C2CADB7093AA} = {E169E15A-E82C-45BF-8C24-C2CADB7093AA} | |||
{47820065-3CFB-401C-ACEA-862BD564A404} = {47820065-3CFB-401C-ACEA-862BD564A404} | |||
{DBF8B16E-5967-4480-8EDE-15D98A0DF0C4} = {DBF8B16E-5967-4480-8EDE-15D98A0DF0C4} | |||
{FC67057C-E92F-4E1C-98BE-46F839C8AD71} = {FC67057C-E92F-4E1C-98BE-46F839C8AD71} | |||
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2} = {91E9E7BD-75C9-4E98-84AA-2C271922E5C2} | |||
{688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {688FD1D8-7F01-4539-B2E9-F473C5D699C7} | |||
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C} = {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C} | |||
{BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {BBA8E7FB-C834-40DC-822F-B112CB7F0140} | |||
EndProjectSection | |||
EndProject | |||
Global | |||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
Debug|Any CPU = Debug|Any CPU | |||
@@ -232,6 +247,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 | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Debug|x64.ActiveCfg = Debug|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Debug|x64.Build.0 = Debug|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Debug|x86.ActiveCfg = Debug|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Debug|x86.Build.0 = Debug|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Release|Any CPU.Build.0 = Release|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Release|x64.ActiveCfg = Release|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Release|x64.Build.0 = Release|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Release|x86.ActiveCfg = Release|Any CPU | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27}.Release|x86.Build.0 = Release|Any CPU | |||
EndGlobalSection | |||
GlobalSection(SolutionProperties) = preSolution | |||
HideSolutionNode = FALSE | |||
@@ -251,6 +278,7 @@ Global | |||
{FC67057C-E92F-4E1C-98BE-46F839C8AD71} = {C7CF5621-7D36-433B-B337-5A2E3C101A71} | |||
{47820065-3CFB-401C-ACEA-862BD564A404} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} | |||
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} | |||
{2CB2A016-CCEB-4A67-BC7B-098F114D7C27} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} | |||
EndGlobalSection | |||
GlobalSection(ExtensibilityGlobals) = postSolution | |||
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} | |||
@@ -0,0 +1,155 @@ | |||
using Discord; | |||
using Discord.Commands; | |||
using Discord.SlashCommands; | |||
using Discord.WebSocket; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Reflection; | |||
using System.Threading.Tasks; | |||
namespace SlashCommandsExample | |||
{ | |||
class DiscordClient | |||
{ | |||
public static DiscordSocketClient socketClient { get; set; } = new DiscordSocketClient(); | |||
public static SlashCommandService _commands { get; set; } | |||
public static IServiceProvider _services { get; set; } | |||
private string botToken = "<YOUR TOKEN HERE>"; | |||
public DiscordClient() | |||
{ | |||
_commands = new SlashCommandService(); | |||
_services = new ServiceCollection() | |||
.AddSingleton(socketClient) | |||
.AddSingleton(_commands) | |||
.BuildServiceProvider(); | |||
socketClient.Log += SocketClient_Log; | |||
_commands.Log += SocketClient_Log; | |||
socketClient.InteractionCreated += InteractionHandler; | |||
socketClient.Ready += RegisterCommand; | |||
// This is for dev purposes. | |||
// To avoid the situation in which you accidentally push your bot token to upstream, you can use | |||
// EnviromentVariables to store your key. | |||
botToken = Environment.GetEnvironmentVariable("DiscordSlashCommandsBotToken", EnvironmentVariableTarget.User); | |||
// Uncomment the next line of code to set the environment variable. | |||
// ------------------------------------------------------------------ | |||
// | WARNING! | | |||
// | | | |||
// | MAKE SURE TO DELETE YOUR TOKEN AFTER YOU HAVE SET THE VARIABLE | | |||
// | | | |||
// ------------------------------------------------------------------ | |||
//Environment.SetEnvironmentVariable("DiscordSlashCommandsBotToken", | |||
// "[YOUR TOKEN GOES HERE DELETE & COMMENT AFTER USE]", | |||
// EnvironmentVariableTarget.User); | |||
} | |||
public async Task RegisterCommand() | |||
{ | |||
// Use this to manually register a command for testing. | |||
return; | |||
await socketClient.Rest.CreateGuildCommand(new SlashCommandCreationProperties() | |||
{ | |||
Name = "root", | |||
Description = "Root Command", | |||
Options = new List<ApplicationCommandOptionProperties>() | |||
{ | |||
new ApplicationCommandOptionProperties() | |||
{ | |||
Name = "usr", | |||
Description = "User Folder", | |||
Type = ApplicationCommandOptionType.SubCommandGroup, | |||
Options = new List<ApplicationCommandOptionProperties>() | |||
{ | |||
// This doesn't work. This is good! | |||
//new ApplicationCommandOptionProperties() | |||
//{ | |||
// Name = "strstr", | |||
// Description = "Some random string I guess.", | |||
// Type = ApplicationCommandOptionType.String, | |||
//}, | |||
new ApplicationCommandOptionProperties() | |||
{ | |||
Name = "zero", | |||
Description = "Zero's Home Folder - COMMAND", | |||
Type = ApplicationCommandOptionType.SubCommand, | |||
Options = new List<ApplicationCommandOptionProperties>() | |||
{ | |||
new ApplicationCommandOptionProperties() | |||
{ | |||
Name = "file", | |||
Description = "the file you want accessed.", | |||
Type = ApplicationCommandOptionType.String | |||
} | |||
} | |||
}, | |||
new ApplicationCommandOptionProperties() | |||
{ | |||
Name = "johhny", | |||
Description = "Johnny Test's Home Folder - COMMAND", | |||
Type = ApplicationCommandOptionType.SubCommand, | |||
Options = new List<ApplicationCommandOptionProperties>() | |||
{ | |||
new ApplicationCommandOptionProperties() | |||
{ | |||
Name = "file", | |||
Description = "the file you want accessed.", | |||
Type = ApplicationCommandOptionType.String | |||
} | |||
} | |||
} | |||
} | |||
}, | |||
new ApplicationCommandOptionProperties() | |||
{ | |||
Name = "random", | |||
Description = "Random things", | |||
Type = ApplicationCommandOptionType.SubCommand | |||
} | |||
} | |||
}, 386658607338618891) ; | |||
} | |||
public async Task RunAsync() | |||
{ | |||
await socketClient.LoginAsync(TokenType.Bot, botToken); | |||
await socketClient.StartAsync(); | |||
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | |||
await _commands.RegisterCommandsAsync(socketClient, new List<ulong>() | |||
{ | |||
386658607338618891 | |||
}, | |||
new CommandRegistrationOptions(OldCommandOptions.DELETE_UNUSED,ExistingCommandOptions.OVERWRITE)); | |||
// If you would like to register your commands manually use: | |||
//-----------------------------------------// | |||
// | |||
// await _commands.BuildCommands(); | |||
// | |||
//-----------------------------------------// | |||
// Though I wouldn't highly recommend it unless you want to do something very specific with them | |||
// such as only registering some commands on only some guilds, or editing them manually. | |||
await Task.Delay(-1); | |||
} | |||
private async Task InteractionHandler(SocketInteraction arg) | |||
{ | |||
if(arg.Type == InteractionType.ApplicationCommand) | |||
{ | |||
await _commands.ExecuteAsync(arg); | |||
} | |||
} | |||
private Task SocketClient_Log(LogMessage arg) | |||
{ | |||
Console.WriteLine("[Discord] " + arg.ToString()); | |||
return Task.CompletedTask; | |||
} | |||
} | |||
} |
@@ -0,0 +1,104 @@ | |||
using Discord.Commands; | |||
using Discord.Commands.SlashCommands.Types; | |||
using Discord.SlashCommands; | |||
using Discord.WebSocket; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace SlashCommandsExample.Modules | |||
{ | |||
// You can make the whole module Global | |||
//[Global] | |||
public class DevModule : SlashCommandModule<SocketInteraction> | |||
{ | |||
[SlashCommand("ping", "Ping the bot to see if it's alive!")] | |||
[Global] | |||
public async Task PingAsync() | |||
{ | |||
await Reply(":white_check_mark: **Bot Online**"); | |||
} | |||
[SlashCommand("echo", "I'll repeate everything you said to me, word for word.")] | |||
public async Task EchoAsync( | |||
[Description("The message you want repetead")] | |||
[Required] | |||
string message) | |||
{ | |||
await Reply($"{Interaction.Member?.Nickname ?? Interaction.Member?.Username} told me to say this: \r\n{message}"); | |||
} | |||
[SlashCommand("overload","Just hit me with every type of data you got, man!")] | |||
public async Task OverloadAsync( | |||
[ParameterName("var1")] | |||
bool? boolean, | |||
[ParameterName("var2")] | |||
int? integer, | |||
[ParameterName("var3")] | |||
string myString, | |||
SocketGuildChannel channel, | |||
SocketGuildUser user, | |||
SocketRole role | |||
) | |||
{ | |||
await Reply($"You gave me:\r\n {boolean}, {integer}, {myString}, <#{channel?.Id}>, {user?.Mention}, {role?.Mention}"); | |||
} | |||
[SlashCommand("stats","Get the stats from Game(tm) for players or teams.")] | |||
public async Task GetStatsAsync( | |||
[Required] | |||
[Choice("XBOX","xbox")] | |||
[Choice("PlayStation","ps")] | |||
[Choice("PC","pc")] | |||
string platform, | |||
[Choice("Player",1)] | |||
[Choice("Team",2)] | |||
int searchType | |||
) | |||
{ | |||
await Reply($"Well I got this: {platform}, {searchType}"); | |||
} | |||
[CommandGroup("root")] | |||
//[Global] | |||
public class DevModule_Root : SlashCommandModule<SocketInteraction> | |||
{ | |||
[SlashCommand("rng", "Gives you a random number from this \"machine\"")] | |||
public async Task RNGAsync() | |||
{ | |||
var rand = new Random(); | |||
await Reply(rand.Next(0, 101).ToString()); | |||
} | |||
[CommandGroup("usr")] | |||
public class DevModule_Root_Usr : SlashCommandModule<SocketInteraction> | |||
{ | |||
[SlashCommand("zero", "Gives you a file from user zero from this \"machine\"")] | |||
public async Task ZeroAsync([Description("The file you want.")] string file) | |||
{ | |||
await Reply($"You don't have permissiont to access {file} from user \"zero\"."); | |||
} | |||
[SlashCommand("johnny", "Gives you a file from user Johnny Test from this \"machine\"")] | |||
public async Task JohnnyAsync([Description("The file you want.")] string file) | |||
{ | |||
await Reply($"You don't have permissiont to access {file} from user \"johnny\"."); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
/* | |||
The base way of defining a command using the regular command service: | |||
public class PingModule : ModuleBase<SocketCommandContext> | |||
{ | |||
[Command("ping")] | |||
[Summary("Pong! Check if the bot is alive.")] | |||
public async Task PingAsync() | |||
{ | |||
await ReplyAsync(":white_check_mark: **Bot Online**"); | |||
} | |||
} | |||
*/ |
@@ -0,0 +1,20 @@ | |||
using Discord.SlashCommands; | |||
using Discord.WebSocket; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace SlashCommandsExample.Modules | |||
{ | |||
// Doesn't inherit from SlashCommandModule | |||
public class InvalidDefinition : Object | |||
{ | |||
// commands | |||
} | |||
// Isn't public | |||
class PrivateDefinition : SlashCommandModule<SocketInteraction> | |||
{ | |||
// commands | |||
} | |||
} |
@@ -0,0 +1,26 @@ | |||
/* | |||
* This project, is at this moment used for testing and debugging the new and experimental Slash Commands. | |||
* After all testing has been done, and the project is ready to be integrated into the main Discord.Net ecosystem | |||
* this project should be re-made into one that could be used as an example usage of the new Slash Command Service. | |||
*/ | |||
using System; | |||
using System.Threading.Tasks; | |||
using Discord; | |||
using Discord.Commands; | |||
using Discord.SlashCommands; | |||
using Discord.WebSocket; | |||
namespace SlashCommandsExample | |||
{ | |||
class Program | |||
{ | |||
static void Main(string[] args) | |||
{ | |||
Console.WriteLine("Hello World!"); | |||
DiscordClient discordClient = new DiscordClient(); | |||
// This could instead be handled in another thread, if for whatever reason you want to continue execution in the main Thread. | |||
discordClient.RunAsync().GetAwaiter().GetResult(); | |||
} | |||
} | |||
} |
@@ -0,0 +1,7 @@ | |||
{ | |||
"profiles": { | |||
"SlashCommandsExample": { | |||
"commandName": "Project" | |||
} | |||
} | |||
} |
@@ -0,0 +1,22 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<OutputType>Exe</OutputType> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.0" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj" /> | |||
<ProjectReference Include="..\src\Discord.Net.Commands\Discord.Net.Commands.csproj" /> | |||
<ProjectReference Include="..\src\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||
<ProjectReference Include="..\src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" /> | |||
<ProjectReference Include="..\src\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | |||
<ProjectReference Include="..\src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" /> | |||
<ProjectReference Include="..\src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@@ -10,6 +10,7 @@ | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | |||
<ProjectReference Include="..\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@@ -0,0 +1,41 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
/// <summary> | |||
/// Defines the parameter as a choice. | |||
/// </summary> | |||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] | |||
public class Choice : Attribute | |||
{ | |||
/// <summary> | |||
/// The internal value of this choice. | |||
/// </summary> | |||
public string choiceStringValue; | |||
/// <summary> | |||
/// The internal value of this choice. | |||
/// </summary> | |||
public int? choiceIntValue = null; | |||
/// <summary> | |||
/// The display value of this choice. | |||
/// </summary> | |||
public string choiceName; | |||
public Choice(string choiceName, string choiceValue) | |||
{ | |||
this.choiceName = choiceName; | |||
this.choiceStringValue = choiceValue; | |||
} | |||
public Choice(string choiceName, int choiceValue) | |||
{ | |||
this.choiceName = choiceName; | |||
this.choiceIntValue = choiceValue; | |||
} | |||
} | |||
} |
@@ -0,0 +1,35 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
/// <summary> | |||
/// Defines the current as being a group of slash commands. | |||
/// </summary> | |||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] | |||
public class CommandGroup : Attribute | |||
{ | |||
/// <summary> | |||
/// The name of this slash command. | |||
/// </summary> | |||
public string groupName; | |||
/// <summary> | |||
/// The description of this slash command. | |||
/// </summary> | |||
public string description; | |||
/// <summary> | |||
/// Tells the <see cref="SlashCommandService"/> that this class/function is a slash command. | |||
/// </summary> | |||
/// <param name="commandName">The name of this slash command.</param> | |||
public CommandGroup(string groupName, string description = "No description.") | |||
{ | |||
this.groupName = groupName.ToLower(); | |||
this.description = description; | |||
} | |||
} | |||
} |
@@ -0,0 +1,31 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
/// <summary> | |||
/// An Attribute that gives the command parameter a description. | |||
/// </summary> | |||
[AttributeUsage(AttributeTargets.Parameter , AllowMultiple = false)] | |||
public class Description : Attribute | |||
{ | |||
public static string DefaultDescription = "No description."; | |||
/// <summary> | |||
/// The description of this slash command parameter. | |||
/// </summary> | |||
public string description; | |||
/// <summary> | |||
/// Tells the <see cref="SlashCommandService"/> that this parameter has a description. | |||
/// </summary> | |||
/// <param name="commandName">The name of this slash command.</param> | |||
public Description(string description) | |||
{ | |||
this.description = description; | |||
} | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
/// <summary> | |||
/// An Attribute that gives the command parameter a description. | |||
/// </summary> | |||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] | |||
public class Global : Attribute | |||
{ | |||
} | |||
} |
@@ -0,0 +1,29 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
/// <summary> | |||
/// An Attribute that gives the command parameter a custom name. | |||
/// </summary> | |||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] | |||
public class ParameterName : Attribute | |||
{ | |||
/// <summary> | |||
/// The name of this slash command parameter. | |||
/// </summary> | |||
public string name; | |||
/// <summary> | |||
/// Tells the <see cref="SlashCommandService"/> that this parameter has a custom name. | |||
/// </summary> | |||
/// <param name="name">The name of this slash command.</param> | |||
public ParameterName(string name) | |||
{ | |||
this.name = name; | |||
} | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
/// <summary> | |||
/// An Attribute that gives the command parameter a description. | |||
/// </summary> | |||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] | |||
public class Required : Attribute | |||
{ | |||
} | |||
} |
@@ -9,21 +9,27 @@ namespace Discord.SlashCommands | |||
/// <summary> | |||
/// Defines the current class or function as a slash command. | |||
/// </summary> | |||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] | |||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] | |||
public class SlashCommand : Attribute | |||
{ | |||
/// <summary> | |||
/// The name of this slash command. | |||
/// </summary> | |||
public string CommandName; | |||
public string commandName; | |||
/// <summary> | |||
/// The description of this slash command. | |||
/// </summary> | |||
public string description; | |||
/// <summary> | |||
/// Tells the <see cref="SlashCommandService"/> that this class/function is a slash command. | |||
/// </summary> | |||
/// <param name="CommandName">The name of this slash command.</param> | |||
public SlashCommand(string CommandName) | |||
/// <param name="commandName">The name of this slash command.</param> | |||
public SlashCommand(string commandName, string description = "No description.") | |||
{ | |||
this.CommandName = CommandName; | |||
this.commandName = commandName.ToLower(); | |||
this.description = description; | |||
} | |||
} | |||
} |
@@ -377,8 +377,8 @@ namespace Discord.Commands.Builders | |||
{ | |||
bool isSubType = this.Type == ApplicationCommandOptionType.SubCommand || this.Type == ApplicationCommandOptionType.SubCommandGroup; | |||
if (isSubType && (Options == null || !Options.Any())) | |||
throw new ArgumentException(nameof(Options), "SubCommands/SubCommandGroups must have at least one option"); | |||
if (this.Type == ApplicationCommandOptionType.SubCommandGroup && (Options == null || !Options.Any())) | |||
throw new ArgumentException(nameof(Options), "SubCommandGroups must have at least one option"); | |||
if (!isSubType && (Options != null && Options.Any())) | |||
throw new ArgumentException(nameof(Options), $"Cannot have options on {Type} type"); | |||
@@ -448,20 +448,9 @@ namespace Discord.Commands.Builders | |||
return this; | |||
} | |||
public SlashCommandOptionBuilder WithName(string Name, int Value) | |||
public SlashCommandOptionBuilder WithName(string Name) | |||
{ | |||
if (Choices == null) | |||
Choices = new List<ApplicationCommandOptionChoiceProperties>(); | |||
if (Choices.Count >= MaxChoiceCount) | |||
throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!"); | |||
Choices.Add(new ApplicationCommandOptionChoiceProperties() | |||
{ | |||
Name = Name, | |||
Value = Value | |||
}); | |||
this.Name = Name; | |||
return this; | |||
} | |||
@@ -0,0 +1,39 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
/// <summary> | |||
/// The options that should be kept in mind when registering the slash commands to discord. | |||
/// </summary> | |||
public class CommandRegistrationOptions | |||
{ | |||
/// <summary> | |||
/// The options that should be kept in mind when registering the slash commands to discord. | |||
/// </summary> | |||
/// <param name="oldCommands">What to do with the old commands that are already registered with discord</param> | |||
/// <param name="existingCommands"> What to do with the old commands (if they weren't wiped) that we re-define.</param> | |||
public CommandRegistrationOptions(OldCommandOptions oldCommands, ExistingCommandOptions existingCommands) | |||
{ | |||
OldCommands = oldCommands; | |||
ExistingCommands = existingCommands; | |||
} | |||
/// <summary> | |||
/// What to do with the old commands that are already registered with discord | |||
/// </summary> | |||
public OldCommandOptions OldCommands { get; set; } | |||
/// <summary> | |||
/// What to do with the old commands (if they weren't wiped) that we re-define. | |||
/// </summary> | |||
public ExistingCommandOptions ExistingCommands { get; set; } | |||
/// <summary> | |||
/// The default, and reccomended options - Keep the old commands, and overwrite existing commands we re-defined. | |||
/// </summary> | |||
public static CommandRegistrationOptions Default => | |||
new CommandRegistrationOptions(OldCommandOptions.KEEP, ExistingCommandOptions.OVERWRITE); | |||
} | |||
} |
@@ -1,14 +1,14 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
internal class SlashCommandModule | |||
public enum ExistingCommandOptions | |||
{ | |||
OVERWRITE, | |||
KEEP_EXISTING | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
public enum OldCommandOptions | |||
{ | |||
/// <summary> | |||
/// Keep the old commands intact - do nothing to them. | |||
/// </summary> | |||
KEEP, | |||
/// <summary> | |||
/// Delete the old commands that we won't be re-defined this time around. | |||
/// </summary> | |||
DELETE_UNUSED, | |||
/// <summary> | |||
/// Delete everything discord has. | |||
/// </summary> | |||
WIPE | |||
} | |||
} |
@@ -0,0 +1,160 @@ | |||
using Discord.Commands; | |||
using Discord.Commands.Builders; | |||
using Discord.WebSocket; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
public class SlashCommandInfo | |||
{ | |||
/// <summary> | |||
/// Gets the module that the command belongs in. | |||
/// </summary> | |||
public SlashModuleInfo Module { get; } | |||
/// <summary> | |||
/// Gets the name of the command. | |||
/// </summary> | |||
public string Name { get; } | |||
/// <summary> | |||
/// Gets the name of the command. | |||
/// </summary> | |||
public string Description { get; } | |||
/// <summary> | |||
/// The parameters we are expecting - an extension of SlashCommandOptionBuilder | |||
/// </summary> | |||
public List<SlashParameterInfo> Parameters { get; } | |||
public bool isGlobal { get; } | |||
/// <summary> | |||
/// The user method as a delegate. We need to use Delegate because there is an unknown number of parameters | |||
/// </summary> | |||
public Delegate userMethod; | |||
/// <summary> | |||
/// The callback that we call to start the delegate. | |||
/// </summary> | |||
public Func<object[], Task<IResult>> callback; | |||
public SlashCommandInfo(SlashModuleInfo module, string name, string description,List<SlashParameterInfo> parameters , Delegate userMethod , bool isGlobal = false) | |||
{ | |||
Module = module; | |||
Name = name; | |||
Description = description; | |||
Parameters = parameters; | |||
this.userMethod = userMethod; | |||
this.isGlobal = isGlobal; | |||
this.callback = new Func<object[], Task<IResult>>(async (args) => | |||
{ | |||
// Try-catch it and see what we get - error or success | |||
try | |||
{ | |||
await Task.Run(() => | |||
{ | |||
userMethod.DynamicInvoke(args); | |||
}).ConfigureAwait(false); | |||
} | |||
catch(Exception e) | |||
{ | |||
return ExecuteResult.FromError(e); | |||
} | |||
return ExecuteResult.FromSuccess(); | |||
}); | |||
} | |||
/// <summary> | |||
/// Execute the function based on the interaction data we get. | |||
/// </summary> | |||
/// <param name="data">Interaction data from interaction</param> | |||
public async Task<IResult> ExecuteAsync(IReadOnlyCollection<SocketInteractionDataOption> data) | |||
{ | |||
// List of arguments to be passed to the Delegate | |||
List<object> args = new List<object>(); | |||
try | |||
{ | |||
foreach (var parameter in Parameters) | |||
{ | |||
// For each parameter to try find its coresponding DataOption based on the name. | |||
// !!! names from `data` will always be lowercase regardless if we defined the command with any | |||
// number of upercase letters !!! | |||
if (TryGetInteractionDataOption(data, parameter.Name, out SocketInteractionDataOption dataOption)) | |||
{ | |||
// Parse the dataOption to one corresponding argument type, and then add it to the list of arguments | |||
args.Add(parameter.Parse(dataOption)); | |||
} | |||
else | |||
{ | |||
// There was no input from the user on this field. | |||
args.Add(null); | |||
} | |||
} | |||
} | |||
catch(Exception e) | |||
{ | |||
return ExecuteResult.FromError(e); | |||
} | |||
return await callback.Invoke(args.ToArray()).ConfigureAwait(false); | |||
} | |||
/// <summary> | |||
/// Get the interaction data from the name of the parameter we want to fill in. | |||
/// </summary> | |||
private bool TryGetInteractionDataOption(IReadOnlyCollection<SocketInteractionDataOption> data, string name, out SocketInteractionDataOption dataOption) | |||
{ | |||
if (data == null) | |||
{ | |||
dataOption = null; | |||
return false; | |||
} | |||
foreach (var option in data) | |||
{ | |||
if (option.Name == name.ToLower()) | |||
{ | |||
dataOption = option; | |||
return true; | |||
} | |||
} | |||
dataOption = null; | |||
return false; | |||
} | |||
/// <summary> | |||
/// Build the command and put it in a state in which we can use to define it to Discord. | |||
/// </summary> | |||
public SlashCommandCreationProperties BuildCommand() | |||
{ | |||
SlashCommandBuilder builder = new SlashCommandBuilder(); | |||
builder.WithName(Name); | |||
builder.WithDescription(Description); | |||
builder.Options = new List<SlashCommandOptionBuilder>(); | |||
foreach (var parameter in Parameters) | |||
{ | |||
builder.AddOptions(parameter); | |||
} | |||
return builder.Build(); | |||
} | |||
/// <summary> | |||
/// Build the command AS A SUBCOMMAND and put it in a state in which we can use to define it to Discord. | |||
/// </summary> | |||
public SlashCommandOptionBuilder BuildSubCommand() | |||
{ | |||
SlashCommandOptionBuilder builder = new SlashCommandOptionBuilder(); | |||
builder.WithName(Name); | |||
builder.WithDescription(Description); | |||
builder.WithType(ApplicationCommandOptionType.SubCommand); | |||
builder.Options = new List<SlashCommandOptionBuilder>(); | |||
foreach (var parameter in Parameters) | |||
{ | |||
builder.AddOption(parameter); | |||
} | |||
return builder; | |||
} | |||
} | |||
} |
@@ -0,0 +1,142 @@ | |||
using Discord.Commands; | |||
using Discord.Commands.Builders; | |||
using Discord.WebSocket; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
public class SlashModuleInfo | |||
{ | |||
public const string PathSeperator = "//"; | |||
public const string RootModuleName = "TOP"; | |||
public const string RootCommandPrefix = RootModuleName + PathSeperator; | |||
public SlashModuleInfo(SlashCommandService service) | |||
{ | |||
Service = service; | |||
} | |||
public bool isCommandGroup { get; set; } = false; | |||
public CommandGroup commandGroupInfo { get; set; } | |||
public SlashModuleInfo parent { get; set; } | |||
public List<SlashModuleInfo> commandGroups { get; set; } | |||
public string Path { get; set; } = RootModuleName; | |||
public bool isGlobal { get; set; } = false; | |||
/// <summary> | |||
/// Gets the command service associated with this module. | |||
/// </summary> | |||
public SlashCommandService Service { get; } | |||
/// <summary> | |||
/// Gets a read-only list of commands associated with this module. | |||
/// </summary> | |||
public List<SlashCommandInfo> Commands { get; private set; } | |||
/// <summary> | |||
/// The user command module defined as the interface ISlashCommandModule | |||
/// Used to set context. | |||
/// </summary> | |||
public ISlashCommandModule userCommandModule; | |||
public Type moduleType; | |||
public void SetCommands(List<SlashCommandInfo> commands) | |||
{ | |||
if (this.Commands == null) | |||
{ | |||
this.Commands = commands; | |||
} | |||
} | |||
public void SetCommandModule(object userCommandModule) | |||
{ | |||
if (this.userCommandModule == null) | |||
{ | |||
this.userCommandModule = userCommandModule as ISlashCommandModule; | |||
} | |||
} | |||
public void SetType(Type type) | |||
{ | |||
moduleType = type; | |||
} | |||
public void MakeCommandGroup(CommandGroup commandGroupInfo, SlashModuleInfo parent) | |||
{ | |||
isCommandGroup = true; | |||
this.commandGroupInfo = commandGroupInfo; | |||
this.parent = parent; | |||
} | |||
public void SetSubCommandGroups(List<SlashModuleInfo> subCommandGroups) | |||
{ | |||
// this.commandGroups = new List<SlashModuleInfo>(subCommandGroups); | |||
this.commandGroups = subCommandGroups; | |||
} | |||
public void MakePath() | |||
{ | |||
Path = parent.Path + SlashModuleInfo.PathSeperator + commandGroupInfo.groupName; | |||
} | |||
public List<SlashCommandCreationProperties> BuildCommands() | |||
{ | |||
List<SlashCommandCreationProperties> builtCommands = new List<SlashCommandCreationProperties>(); | |||
foreach (var command in Commands) | |||
{ | |||
var builtCommand = command.BuildCommand(); | |||
if (isGlobal || command.isGlobal) | |||
{ | |||
builtCommand.Global = true; | |||
} | |||
builtCommands.Add(builtCommand); | |||
} | |||
foreach(var commandGroup in commandGroups) | |||
{ | |||
var builtCommand = commandGroup.BuildTopLevelCommandGroup(); | |||
if (isGlobal || commandGroup.isGlobal) | |||
{ | |||
builtCommand.Global = true; | |||
} | |||
builtCommands.Add(builtCommand); | |||
} | |||
return builtCommands; | |||
} | |||
public SlashCommandCreationProperties BuildTopLevelCommandGroup() | |||
{ | |||
SlashCommandBuilder builder = new SlashCommandBuilder(); | |||
builder.WithName(commandGroupInfo.groupName); | |||
builder.WithDescription(commandGroupInfo.description); | |||
foreach (var command in Commands) | |||
{ | |||
builder.AddOption(command.BuildSubCommand()); | |||
} | |||
foreach (var commandGroup in commandGroups) | |||
{ | |||
builder.AddOption(commandGroup.BuildNestedCommandGroup()); | |||
} | |||
return builder.Build(); | |||
} | |||
private SlashCommandOptionBuilder BuildNestedCommandGroup() | |||
{ | |||
SlashCommandOptionBuilder builder = new SlashCommandOptionBuilder(); | |||
builder.WithName(commandGroupInfo.groupName); | |||
builder.WithDescription(commandGroupInfo.description); | |||
builder.WithType(ApplicationCommandOptionType.SubCommandGroup); | |||
foreach (var command in Commands) | |||
{ | |||
builder.AddOption(command.BuildSubCommand()); | |||
} | |||
foreach (var commandGroup in commandGroups) | |||
{ | |||
builder.AddOption(commandGroup.BuildNestedCommandGroup()); | |||
} | |||
return builder; | |||
} | |||
} | |||
} |
@@ -0,0 +1,45 @@ | |||
using Discord.Commands.Builders; | |||
using Discord.WebSocket; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
public class SlashParameterInfo : SlashCommandOptionBuilder | |||
{ | |||
public bool Nullable { get; internal set; } | |||
public object Parse(SocketInteractionDataOption dataOption) | |||
{ | |||
switch (Type) | |||
{ | |||
case ApplicationCommandOptionType.Boolean: | |||
if (Nullable) | |||
return (bool?)dataOption; | |||
else | |||
return (bool)dataOption; | |||
case ApplicationCommandOptionType.Integer: | |||
if(Nullable) | |||
return (int?)dataOption; | |||
else | |||
return (int)dataOption; | |||
case ApplicationCommandOptionType.String: | |||
return (string)dataOption; | |||
case ApplicationCommandOptionType.Channel: | |||
return (SocketGuildChannel)dataOption; | |||
case ApplicationCommandOptionType.Role: | |||
return (SocketRole)dataOption; | |||
case ApplicationCommandOptionType.User: | |||
return (SocketGuildUser)dataOption; | |||
case ApplicationCommandOptionType.SubCommandGroup: | |||
throw new NotImplementedException(); | |||
case ApplicationCommandOptionType.SubCommand: | |||
throw new NotImplementedException(); | |||
} | |||
throw new NotImplementedException($"There is no such type of data... unless we missed it. Please report this error on the Discord.Net github page! Type: {Type}"); | |||
} | |||
} | |||
} |
@@ -1,30 +1,178 @@ | |||
using Discord.Commands; | |||
using Discord.Logging; | |||
using Discord.WebSocket; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Reflection; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
public class SlashCommandService | |||
{ | |||
private List<SlashCommandModule> _modules; | |||
// This semaphore is used to prevent race conditions. | |||
private readonly SemaphoreSlim _moduleLock; | |||
// This contains a dictionary of all definde SlashCommands, based on it's name | |||
public Dictionary<string, SlashCommandInfo> commandDefs; | |||
// This contains a list of all slash command modules defined by their user in their assembly. | |||
public Dictionary<Type, SlashModuleInfo> moduleDefs; | |||
// This is such a complicated method to log stuff... | |||
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } | |||
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>(); | |||
internal Logger _logger; | |||
internal LogManager _logManager; | |||
public SlashCommandService() // TODO: possible config? | |||
{ | |||
// max one thread | |||
_moduleLock = new SemaphoreSlim(1, 1); | |||
_logManager = new LogManager(LogSeverity.Info); | |||
_logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); | |||
_logger = new Logger(_logManager, "SlshCommand"); | |||
} | |||
/// <summary> | |||
/// Execute a slash command. | |||
/// </summary> | |||
/// <param name="interaction">Interaction data recieved from discord.</param> | |||
/// <returns></returns> | |||
public async Task<IResult> ExecuteAsync(SocketInteraction interaction) | |||
{ | |||
SlashCommandInfo commandInfo; | |||
// Get the name of the actual command - be it a normal slash command or subcommand, and return the options we can give it. | |||
string name = GetSearchName(interaction.Data, out var resultingOptions); | |||
// We still need to make sure it is registerd. | |||
if (commandDefs.TryGetValue(name, out commandInfo)) | |||
{ | |||
// Then, set the context in which the command will be executed | |||
commandInfo.Module.userCommandModule.SetContext(interaction); | |||
// Then run the command and pass the interaction data over to the CommandInfo class | |||
return await commandInfo.ExecuteAsync(resultingOptions).ConfigureAwait(false); | |||
} | |||
else | |||
{ | |||
return SearchResult.FromError(CommandError.UnknownCommand, $"There is no registered slash command with the name {interaction.Data.Name}"); | |||
} | |||
} | |||
/// <summary> | |||
/// Get the name of the command we want to search for - be it a normal slash command or a sub command. Returns as out the options to be given to the method. | |||
/// /// </summary> | |||
/// <param name="interactionData"></param> | |||
/// <param name="resultingOptions"></param> | |||
/// <returns></returns> | |||
public string GetSearchName(SocketInteractionData interactionData, out IReadOnlyCollection<SocketInteractionDataOption> resultingOptions) | |||
{ | |||
// The names are stored as such: | |||
// TOP//top-level-command-name | |||
// TOP//command-group//command-group//sub-command-name | |||
// What we are looking for is to get from the interaction the specific (sub)command and what we need to pass to the method. | |||
// So we start the search at TOP//{interactionData.name} | |||
// because we are going to go through each sub-option it has. If it is a subcommand/ command group then it's going to be | |||
// inside the dictionary as TOP//{interactionData.name}//{option.name} | |||
// If the option is a parameter we then know that we've reached the end of the call chain - this should be our coomand! | |||
string nameToSearch = SlashModuleInfo.RootCommandPrefix + interactionData.Name; | |||
var options = interactionData.Options; | |||
while(options != null && options.Count == 1) | |||
{ | |||
string newName = nameToSearch + SlashModuleInfo.PathSeperator + GetFirstOption(options).Name; | |||
if (AnyKeyContains(commandDefs,newName)) | |||
{ | |||
nameToSearch = newName; | |||
options = GetFirstOption(options).Options; | |||
} | |||
else | |||
{ | |||
break; | |||
} | |||
} | |||
resultingOptions = options; | |||
return nameToSearch; | |||
} | |||
/// <summary> | |||
/// Test to see if any <b>string</b> key contains another string inside it. | |||
/// </summary> | |||
private bool AnyKeyContains(Dictionary<string, SlashCommandInfo> commandDefs, string newName) | |||
{ | |||
foreach (var pair in commandDefs) | |||
{ | |||
if (pair.Key.Contains(newName)) | |||
return true; | |||
} | |||
return false; | |||
} | |||
private SocketInteractionDataOption GetFirstOption(IReadOnlyCollection<SocketInteractionDataOption> options) | |||
{ | |||
var it = options.GetEnumerator(); | |||
it.MoveNext(); | |||
return it.Current; | |||
} | |||
public void AddAssembly() | |||
/// <summary> | |||
/// Registers with discord all previously scanned commands. | |||
/// </summary> | |||
public async Task RegisterCommandsAsync(DiscordSocketClient socketClient, List<ulong> guildIDs, CommandRegistrationOptions registrationOptions) | |||
{ | |||
// First take a hold of the module lock, as to make sure we aren't editing stuff while we do our business | |||
await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
// Build and register all of the commands. | |||
await SlashCommandServiceHelper.RegisterCommands(socketClient, moduleDefs, commandDefs, this, guildIDs, registrationOptions).ConfigureAwait(false); | |||
} | |||
finally | |||
{ | |||
_moduleLock.Release(); | |||
} | |||
await _logger.InfoAsync("All commands have been registered!").ConfigureAwait(false); | |||
} | |||
public async Task<IResult> ExecuteAsync() | |||
/// <summary> | |||
/// Build all the commands and return them, for manual registration with Discord. This is automatically done in <see cref="RegisterCommandsAsync(DiscordSocketClient, List{ulong}, CommandRegistrationOptions)"/> | |||
/// </summary> | |||
/// <returns>A list of all the valid commands found within this Assembly.</returns> | |||
public async Task<List<SlashCommandCreationProperties>> BuildCommands() | |||
{ | |||
// TODO: handle execution | |||
return null; | |||
// First take a hold of the module lock, as to make sure we aren't editing stuff while we do our business | |||
await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
List<SlashCommandCreationProperties> result; | |||
try | |||
{ | |||
result = await SlashCommandServiceHelper.BuildCommands(moduleDefs).ConfigureAwait(false); | |||
} | |||
finally | |||
{ | |||
_moduleLock.Release(); | |||
} | |||
await _logger.InfoAsync("All commands have been built!").ConfigureAwait(false); | |||
return result; | |||
} | |||
/// <summary> | |||
/// Scans the program for Attribute-based SlashCommandModules | |||
/// </summary> | |||
public async Task AddModulesAsync(Assembly assembly, IServiceProvider services) | |||
{ | |||
// First take a hold of the module lock, as to make sure we aren't editing stuff while we do our business | |||
await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
// Get all of the modules that were properly defined by the user. | |||
IReadOnlyList<TypeInfo> types = await SlashCommandServiceHelper.GetValidModuleClasses(assembly, this).ConfigureAwait(false); | |||
// Then, based on that, make an instance out of each of them, and get the resulting SlashModuleInfo s | |||
moduleDefs = await SlashCommandServiceHelper.InstantiateModules(types, this).ConfigureAwait(false); | |||
// After that, internally register all of the commands into SlashCommandInfo | |||
commandDefs = await SlashCommandServiceHelper.CreateCommandInfos(types,moduleDefs,this).ConfigureAwait(false); | |||
} | |||
finally | |||
{ | |||
_moduleLock.Release(); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,478 @@ | |||
using Discord.Commands.Builders; | |||
using Discord.WebSocket; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Linq.Expressions; | |||
using System.Reflection; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
internal static class SlashCommandServiceHelper | |||
{ | |||
/// <summary> | |||
/// Get all of the valid user-defined slash command modules | |||
/// </summary> | |||
public static async Task<IReadOnlyList<TypeInfo>> GetValidModuleClasses(Assembly assembly, SlashCommandService service) | |||
{ | |||
var result = new List<TypeInfo>(); | |||
foreach (TypeInfo typeInfo in assembly.DefinedTypes) | |||
{ | |||
if (IsValidModuleDefinition(typeInfo)) | |||
{ | |||
// To simplify our lives, we need the modules to be public. | |||
if (typeInfo.IsPublic || typeInfo.IsNestedPublic) | |||
{ | |||
result.Add(typeInfo); | |||
} | |||
else | |||
{ | |||
await service._logger.WarningAsync($"Found class {typeInfo.FullName} as a valid SlashCommand Module, but it's not public!"); | |||
} | |||
} | |||
} | |||
return result; | |||
} | |||
private static bool IsValidModuleDefinition(TypeInfo typeInfo) | |||
{ | |||
// See if the base type (SlashCommandInfo<T>) implements interface ISlashCommandModule | |||
return typeInfo.BaseType.GetInterfaces() | |||
.Any(n => n == typeof(ISlashCommandModule)) && | |||
typeInfo.GetCustomAttributes(typeof(CommandGroup)).Count() == 0; | |||
} | |||
/// <summary> | |||
/// Create an instance of each user-defined module | |||
/// </summary> | |||
public static async Task<Dictionary<Type, SlashModuleInfo>> InstantiateModules(IReadOnlyList<TypeInfo> types, SlashCommandService slashCommandService) | |||
{ | |||
var result = new Dictionary<Type, SlashModuleInfo>(); | |||
// Here we get all modules thate are NOT sub command groups and instantiate them. | |||
foreach (Type userModuleType in types) | |||
{ | |||
SlashModuleInfo moduleInfo = new SlashModuleInfo(slashCommandService); | |||
moduleInfo.SetType(userModuleType); | |||
// If they want a constructor with different parameters, this is the place to add them. | |||
object instance = userModuleType.GetConstructor(Type.EmptyTypes).Invoke(null); | |||
moduleInfo.SetCommandModule(instance); | |||
moduleInfo.isGlobal = IsCommandModuleGlobal(userModuleType); | |||
moduleInfo.SetSubCommandGroups(InstantiateSubModules(userModuleType, moduleInfo, slashCommandService)); | |||
result.Add(userModuleType, moduleInfo); | |||
} | |||
return result; | |||
} | |||
public static List<SlashModuleInfo> InstantiateSubModules(Type rootModule,SlashModuleInfo rootModuleInfo, SlashCommandService slashCommandService) | |||
{ | |||
// Instantiate all of the nested modules. | |||
List<SlashModuleInfo> commandGroups = new List<SlashModuleInfo>(); | |||
foreach(Type commandGroupType in rootModule.GetNestedTypes()) | |||
{ | |||
if(TryGetCommandGroupAttribute(commandGroupType, out CommandGroup commandGroup)) | |||
{ | |||
SlashModuleInfo groupInfo = new SlashModuleInfo(slashCommandService); | |||
groupInfo.SetType(commandGroupType); | |||
object instance = commandGroupType.GetConstructor(Type.EmptyTypes).Invoke(null); | |||
groupInfo.SetCommandModule(instance); | |||
groupInfo.MakeCommandGroup(commandGroup,rootModuleInfo); | |||
groupInfo.MakePath(); | |||
groupInfo.isGlobal = IsCommandModuleGlobal(commandGroupType); | |||
groupInfo.SetSubCommandGroups(InstantiateSubModules(commandGroupType, groupInfo, slashCommandService)); | |||
commandGroups.Add(groupInfo); | |||
} | |||
} | |||
return commandGroups; | |||
} | |||
public static bool TryGetCommandGroupAttribute(Type module, out CommandGroup commandGroup) | |||
{ | |||
if(!module.IsPublic && !module.IsNestedPublic) | |||
{ | |||
commandGroup = null; | |||
return false; | |||
} | |||
var commandGroupAttributes = module.GetCustomAttributes(typeof(CommandGroup)); | |||
if( commandGroupAttributes.Count() == 0) | |||
{ | |||
commandGroup = null; | |||
return false; | |||
} | |||
else if(commandGroupAttributes.Count() > 1) | |||
{ | |||
throw new Exception($"Too many CommandGroup attributes on a single class ({module.FullName}). It can only contain one!"); | |||
} | |||
else | |||
{ | |||
commandGroup = commandGroupAttributes.First() as CommandGroup; | |||
return true; | |||
} | |||
} | |||
public static bool IsCommandModuleGlobal(Type userModuleType) | |||
{ | |||
// Verify that we only have one [Global] attribute | |||
IEnumerable<Attribute> slashCommandAttributes = userModuleType.GetCustomAttributes(typeof(Global)); | |||
if (slashCommandAttributes.Count() > 1) | |||
{ | |||
throw new Exception("Too many Global attributes on a single method. It can only contain one!"); | |||
} | |||
// And at least one | |||
if (slashCommandAttributes.Count() == 0) | |||
{ | |||
return false; | |||
} | |||
return true; | |||
} | |||
/// <summary> | |||
/// Prepare all of the commands and register them internally. | |||
/// </summary> | |||
public static async Task<Dictionary<string, SlashCommandInfo>> CreateCommandInfos(IReadOnlyList<TypeInfo> types, Dictionary<Type, SlashModuleInfo> moduleDefs, SlashCommandService slashCommandService) | |||
{ | |||
// Create the resulting dictionary ahead of time | |||
var result = new Dictionary<string, SlashCommandInfo>(); | |||
// For each user-defined module ... | |||
foreach (var userModule in types) | |||
{ | |||
// Get its associated information. If there isn't any it means something went wrong, but it's not a critical error. | |||
SlashModuleInfo moduleInfo; | |||
if (moduleDefs.TryGetValue(userModule, out moduleInfo)) | |||
{ | |||
// Create the root-level commands | |||
var commandInfos = CreateSameLevelCommands(result, userModule, moduleInfo); | |||
moduleInfo.SetCommands(commandInfos); | |||
// Then create all of the command groups it has. | |||
CreateSubCommandInfos(result, moduleInfo.commandGroups, slashCommandService); | |||
} | |||
} | |||
return result; | |||
} | |||
public static void CreateSubCommandInfos(Dictionary<string, SlashCommandInfo> result, List<SlashModuleInfo> subCommandGroups, SlashCommandService slashCommandService) | |||
{ | |||
foreach (var subCommandGroup in subCommandGroups) | |||
{ | |||
// Create the commands that is on the same hierarchical level as this ... | |||
var commandInfos = CreateSameLevelCommands(result, subCommandGroup.moduleType.GetTypeInfo(), subCommandGroup); | |||
subCommandGroup.SetCommands(commandInfos); | |||
// ... and continue with the lower sub command groups. | |||
CreateSubCommandInfos(result, subCommandGroup.commandGroups, slashCommandService); | |||
} | |||
} | |||
private static List<SlashCommandInfo> CreateSameLevelCommands(Dictionary<string, SlashCommandInfo> result, TypeInfo userModule, SlashModuleInfo moduleInfo) | |||
{ | |||
var commandMethods = userModule.GetMethods(); | |||
List<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>(); | |||
foreach (var commandMethod in commandMethods) | |||
{ | |||
// Get the SlashCommand attribute | |||
SlashCommand slashCommand; | |||
if (IsValidSlashCommand(commandMethod, out slashCommand)) | |||
{ | |||
// Create the delegate for the method we want to call once the user interacts with the bot. | |||
// We use a delegate because of the unknown number and type of parameters we will have. | |||
Delegate delegateMethod = CreateDelegate(commandMethod, moduleInfo.userCommandModule); | |||
SlashCommandInfo commandInfo = new SlashCommandInfo( | |||
module: moduleInfo, | |||
name: slashCommand.commandName, | |||
description: slashCommand.description, | |||
// Generate the parameters. Due to it's complicated way the algorithm has been moved to its own function. | |||
parameters: ConstructCommandParameters(commandMethod), | |||
userMethod: delegateMethod, | |||
isGlobal: IsCommandGlobal(commandMethod) | |||
); | |||
result.Add(commandInfo.Module.Path + SlashModuleInfo.PathSeperator + commandInfo.Name, commandInfo); | |||
commandInfos.Add(commandInfo); | |||
} | |||
} | |||
return commandInfos; | |||
} | |||
/// <summary> | |||
/// Determines wheater a method can be clasified as a slash command | |||
/// </summary> | |||
private static bool IsValidSlashCommand(MethodInfo method, out SlashCommand slashCommand) | |||
{ | |||
// Verify that we only have one [SlashCommand(...)] attribute | |||
IEnumerable<Attribute> slashCommandAttributes = method.GetCustomAttributes(typeof(SlashCommand)); | |||
if (slashCommandAttributes.Count() > 1) | |||
{ | |||
throw new Exception("Too many SlashCommand attributes on a single method. It can only contain one!"); | |||
} | |||
// And at least one | |||
if (slashCommandAttributes.Count() == 0) | |||
{ | |||
slashCommand = null; | |||
return false; | |||
} | |||
// And return the first (and only) attribute | |||
slashCommand = slashCommandAttributes.First() as SlashCommand; | |||
return true; | |||
} | |||
/// <summary> | |||
/// Determins if the method has a [Global] Attribute. | |||
/// </summary> | |||
private static bool IsCommandGlobal(MethodInfo method) | |||
{ | |||
// Verify that we only have one [Global] attribute | |||
IEnumerable<Attribute> slashCommandAttributes = method.GetCustomAttributes(typeof(Global)); | |||
if (slashCommandAttributes.Count() > 1) | |||
{ | |||
throw new Exception("Too many Global attributes on a single method. It can only contain one!"); | |||
} | |||
// And at least one | |||
if (slashCommandAttributes.Count() == 0) | |||
{ | |||
return false; | |||
} | |||
return true; | |||
} | |||
/// <summary> | |||
/// Process the parameters of this method, including all the attributes. | |||
/// </summary> | |||
private static List<SlashParameterInfo> ConstructCommandParameters(MethodInfo method) | |||
{ | |||
// Prepare the final list of parameters | |||
List<SlashParameterInfo> finalParameters = new List<SlashParameterInfo>(); | |||
// For each mehod parameter ... | |||
// ex: ... MyCommand(string abc, int myInt) | |||
// `abc` and `myInt` are parameters | |||
foreach (var methodParameter in method.GetParameters()) | |||
{ | |||
SlashParameterInfo newParameter = new SlashParameterInfo(); | |||
// Test for the [ParameterName] Attribute. If we have it, then use that as the name, | |||
// if not just use the parameter name as the option name. | |||
var customNameAttributes = methodParameter.GetCustomAttributes(typeof(ParameterName)); | |||
if (customNameAttributes.Count() == 0) | |||
newParameter.Name = methodParameter.Name; | |||
else if (customNameAttributes.Count() > 1) | |||
throw new Exception($"Too many ParameterName attributes on a single parameter ({method.Name} -> {methodParameter.Name}). It can only contain one!"); | |||
else | |||
newParameter.Name = (customNameAttributes.First() as ParameterName).name; | |||
// Get to see if it has a Description Attribute. | |||
// If it has | |||
// 0 -> then use the default description | |||
// 1 -> Use the value from that attribute | |||
// 2+ -> Throw an error. This shouldn't normaly happen, but we check for sake of sanity | |||
var descriptions = methodParameter.GetCustomAttributes(typeof(Description)); | |||
if (descriptions.Count() == 0) | |||
newParameter.Description = Description.DefaultDescription; | |||
else if (descriptions.Count() > 1) | |||
throw new Exception($"Too many Description attributes on a single parameter ({method.Name} -> {methodParameter.Name}). It can only contain one!"); | |||
else | |||
newParameter.Description = (descriptions.First() as Description).description; | |||
// Set the Type of the parameter. | |||
// In the case of int and int? it returns the same type - INTEGER. | |||
// Same with bool and bool?. | |||
newParameter.Type = TypeFromMethodParameter(methodParameter); | |||
// If we have a nullble type (int? or bool?) mark it as such. | |||
newParameter.Nullable = GetNullableStatus(methodParameter); | |||
// Test for the [Required] Attribute | |||
var requiredAttributes = methodParameter.GetCustomAttributes(typeof(Required)); | |||
if (requiredAttributes.Count() == 1) | |||
newParameter.Required = true; | |||
else if (requiredAttributes.Count() > 1) | |||
throw new Exception($"Too many Required attributes on a single parameter ({method.Name} -> {methodParameter.Name}). It can only contain one!"); | |||
// Test for the [Choice] Attribute | |||
// A parameter cna have multiple Choice attributes, and for each we're going to add it's key-value pair. | |||
foreach (Choice choice in methodParameter.GetCustomAttributes(typeof(Choice))) | |||
{ | |||
// If the parameter expects a string but the value of the choice is of type int, then throw an error. | |||
if (newParameter.Type == ApplicationCommandOptionType.String) | |||
{ | |||
if(String.IsNullOrEmpty(choice.choiceStringValue)) | |||
{ | |||
throw new Exception($"Parameter ({method.Name} -> {methodParameter.Name}) is of type string, but choice is of type int!"); | |||
} | |||
newParameter.AddChoice(choice.choiceName, choice.choiceStringValue); | |||
} | |||
// If the parameter expects a int but the value of the choice is of type string, then throw an error. | |||
if (newParameter.Type == ApplicationCommandOptionType.Integer) | |||
{ | |||
if (choice.choiceIntValue == null) | |||
{ | |||
throw new Exception($"Parameter ({method.Name} -> {methodParameter.Name}) is of type int, but choice is of type string!"); | |||
} | |||
newParameter.AddChoice(choice.choiceName, (int)choice.choiceIntValue); | |||
} | |||
} | |||
finalParameters.Add(newParameter); | |||
} | |||
return finalParameters; | |||
} | |||
/// <summary> | |||
/// Get the type of command option from a method parameter info. | |||
/// </summary> | |||
private static ApplicationCommandOptionType TypeFromMethodParameter(ParameterInfo methodParameter) | |||
{ | |||
// Can't do switch -- who knows why? | |||
if (methodParameter.ParameterType == typeof(int) || | |||
methodParameter.ParameterType == typeof(int?)) | |||
return ApplicationCommandOptionType.Integer; | |||
if (methodParameter.ParameterType == typeof(string)) | |||
return ApplicationCommandOptionType.String; | |||
if (methodParameter.ParameterType == typeof(bool) || | |||
methodParameter.ParameterType == typeof(bool?)) | |||
return ApplicationCommandOptionType.Boolean; | |||
if (methodParameter.ParameterType == typeof(SocketGuildChannel)) | |||
return ApplicationCommandOptionType.Channel; | |||
if (methodParameter.ParameterType == typeof(SocketRole)) | |||
return ApplicationCommandOptionType.Role; | |||
if (methodParameter.ParameterType == typeof(SocketGuildUser)) | |||
return ApplicationCommandOptionType.User; | |||
throw new Exception($"Got parameter type other than int, string, bool, guild, role, or user. {methodParameter.Name}"); | |||
} | |||
/// <summary> | |||
/// Gets whater the parameter can be set as null, in the case that parameter type usually does not allow null. | |||
/// More specifically tests to see if it is a type of 'int?' or 'bool?', | |||
/// </summary> | |||
private static bool GetNullableStatus(ParameterInfo methodParameter) | |||
{ | |||
if(methodParameter.ParameterType == typeof(int?) || | |||
methodParameter.ParameterType == typeof(bool?)) | |||
{ | |||
return true; | |||
} | |||
return false; | |||
} | |||
/// <summary> | |||
/// Creae a delegate from methodInfo. Taken from | |||
/// https://stackoverflow.com/a/40579063/8455128 | |||
/// </summary> | |||
private static Delegate CreateDelegate(MethodInfo methodInfo, object target) | |||
{ | |||
Func<Type[], Type> getType; | |||
var isAction = methodInfo.ReturnType.Equals((typeof(void))); | |||
var types = methodInfo.GetParameters().Select(p => p.ParameterType); | |||
if (isAction) | |||
{ | |||
getType = Expression.GetActionType; | |||
} | |||
else | |||
{ | |||
getType = Expression.GetFuncType; | |||
types = types.Concat(new[] { methodInfo.ReturnType }); | |||
} | |||
if (methodInfo.IsStatic) | |||
{ | |||
return Delegate.CreateDelegate(getType(types.ToArray()), methodInfo); | |||
} | |||
return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo.Name); | |||
} | |||
public static async Task RegisterCommands(DiscordSocketClient socketClient, Dictionary<Type, SlashModuleInfo> rootModuleInfos, Dictionary<string, SlashCommandInfo> commandDefs, SlashCommandService slashCommandService, List<ulong> guildIDs,CommandRegistrationOptions options) | |||
{ | |||
// TODO: see how we should handle if user wants to register two commands with the same name, one global and one not. | |||
// Build the commands | |||
List<SlashCommandCreationProperties> builtCommands = await BuildCommands(rootModuleInfos).ConfigureAwait(false); | |||
// Scan for each existing command on discord so we know what is already there. | |||
List<Rest.RestGuildCommand> existingGuildCommands = new List<Rest.RestGuildCommand>(); | |||
List<Rest.RestGlobalCommand> existingGlobalCommands = new List<Rest.RestGlobalCommand>(); | |||
existingGlobalCommands.AddRange(await socketClient.Rest.GetGlobalApplicationCommands().ConfigureAwait(false)); | |||
foreach (ulong guildID in guildIDs) | |||
{ | |||
existingGuildCommands.AddRange(await socketClient.Rest.GetGuildApplicationCommands(guildID).ConfigureAwait(false)); | |||
} | |||
// If we want to keep the existing commands that are already registered | |||
// remove the commands that share the same name from the builtCommands list as to not overwrite. | |||
if (options.ExistingCommands == ExistingCommandOptions.KEEP_EXISTING) | |||
{ | |||
foreach (var existingCommand in existingGuildCommands) | |||
{ | |||
builtCommands.RemoveAll(x => (!x.Global && x.Name == existingCommand.Name)); | |||
} | |||
foreach (var existingCommand in existingGlobalCommands) | |||
{ | |||
builtCommands.RemoveAll(x => (x.Global && x.Name == existingCommand.Name)); | |||
} | |||
} | |||
// If we want to delete commands that are not going to be re-implemented in builtCommands | |||
// or if we just want a blank slate | |||
if (options.OldCommands == OldCommandOptions.DELETE_UNUSED || | |||
options.OldCommands == OldCommandOptions.WIPE) | |||
{ | |||
foreach (var existingCommand in existingGuildCommands) | |||
{ | |||
// If we want to wipe all GUILD commands | |||
// or if the existing command isn't re-defined and re-built | |||
// remove it from discord. | |||
if (options.OldCommands == OldCommandOptions.WIPE || | |||
// There are no commands which contain this existing command. | |||
!builtCommands.Any(x => !x.Global && x.Name.Contains(SlashModuleInfo.PathSeperator + existingCommand.Name))) | |||
{ | |||
await existingCommand.DeleteAsync(); | |||
} | |||
} | |||
foreach (var existingCommand in existingGlobalCommands) | |||
{ | |||
// If we want to wipe all GLOBAL commands | |||
// or if the existing command isn't re-defined and re-built | |||
// remove it from discord. | |||
if (options.OldCommands == OldCommandOptions.WIPE || | |||
// There are no commands which contain this existing command. | |||
!builtCommands.Any(x => x.Global && x.Name.Contains(SlashModuleInfo.PathSeperator + existingCommand.Name))) | |||
{ | |||
await existingCommand.DeleteAsync(); | |||
} | |||
} | |||
} | |||
// And now register them. Globally if the 'Global' flag is set. | |||
// If not then just register them as guild commands on all of the guilds given to us. | |||
foreach (var builtCommand in builtCommands) | |||
{ | |||
if (builtCommand.Global) | |||
{ | |||
await socketClient.Rest.CreateGlobalCommand(builtCommand).ConfigureAwait(false); | |||
} | |||
else | |||
{ | |||
foreach (ulong guildID in guildIDs) | |||
{ | |||
await socketClient.Rest.CreateGuildCommand(builtCommand, guildID).ConfigureAwait(false); | |||
} | |||
} | |||
} | |||
return; | |||
} | |||
/// <summary> | |||
/// Build and return all of the commands this assembly contians. | |||
/// </summary> | |||
public static async Task<List<SlashCommandCreationProperties>> BuildCommands(Dictionary<Type, SlashModuleInfo> rootModuleInfos) | |||
{ | |||
List<SlashCommandCreationProperties> builtCommands = new List<SlashCommandCreationProperties>(); | |||
foreach (var pair in rootModuleInfos) | |||
{ | |||
var rootModuleInfo = pair.Value; | |||
builtCommands.AddRange(rootModuleInfo.BuildCommands()); | |||
} | |||
return builtCommands; | |||
} | |||
} | |||
} |
@@ -0,0 +1,21 @@ | |||
using Discord.Commands.Builders; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
public interface ISlashCommandModule | |||
{ | |||
void SetContext(IDiscordInteraction interaction); | |||
//void BeforeExecute(CommandInfo command); | |||
//void AfterExecute(CommandInfo command); | |||
//void OnModuleBuilding(CommandService commandService, ModuleBuilder builder); | |||
} | |||
} |
@@ -0,0 +1,34 @@ | |||
using Discord.Commands.SlashCommands.Types; | |||
using Discord.WebSocket; | |||
using System; | |||
using System.Threading.Tasks; | |||
namespace Discord.SlashCommands | |||
{ | |||
public class SlashCommandModule<T> : ISlashCommandModule where T : class, IDiscordInteraction | |||
{ | |||
/// <summary> | |||
/// The underlying interaction of the command. | |||
/// </summary> | |||
/// <seealso cref="T:Discord.IDiscordInteraction"/> | |||
/// <seealso cref="T:Discord.WebSocket.SocketInteraction" /> | |||
public T Interaction { get; private set; } | |||
void ISlashCommandModule.SetContext(IDiscordInteraction interaction) | |||
{ | |||
var newValue = interaction as T; | |||
Interaction = newValue ?? throw new InvalidOperationException($"Invalid interaction type. Expected {typeof(T).Name}, got {interaction.GetType().Name}."); | |||
} | |||
public async Task<IMessage> Reply(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource, | |||
AllowedMentions allowedMentions = null, RequestOptions options = null) | |||
{ | |||
if(Interaction is SocketInteraction) | |||
{ | |||
return await (Interaction as SocketInteraction).FollowupAsync(text, isTTS, embed, Type, allowedMentions, options); | |||
} | |||
return null; | |||
} | |||
} | |||
} |
@@ -21,6 +21,10 @@ namespace Discord | |||
/// </summary> | |||
public string Description { get; set; } | |||
/// <summary> | |||
/// If the command should be defined as a global command. | |||
/// </summary> | |||
public bool Global { get; set; } = false; | |||
/// <summary> | |||
/// Gets or sets the options for this command. | |||
@@ -67,7 +67,9 @@ namespace Discord.API | |||
? option.Options.Select(x => new ApplicationCommandOption(x)).ToArray() | |||
: Optional<ApplicationCommandOption[]>.Unspecified; | |||
this.Required = option.Required.Value; | |||
this.Required = option.Required.HasValue | |||
? option.Required.Value | |||
: Optional<bool>.Unspecified; | |||
this.Default = option.Default.HasValue | |||
? option.Default.Value | |||
: Optional<bool>.Unspecified; | |||
@@ -60,7 +60,7 @@ namespace Discord.Rest | |||
this.Description = model.Description; | |||
this.Options = model.Options.IsSpecified | |||
? model.Options.Value.Select(x => RestApplicationCommandOption.Create(x)).ToImmutableArray() | |||
? model.Options.Value.Select(x => RestApplicationCommandOption.Create(x)).ToImmutableArray().ToReadOnlyCollection() | |||
: null; | |||
} | |||
@@ -60,11 +60,11 @@ namespace Discord.Rest | |||
this.Required = model.Required.Value; | |||
this.Options = model.Options.IsSpecified | |||
? model.Options.Value.Select(x => Create(x)).ToImmutableArray() | |||
? model.Options.Value.Select(x => Create(x)).ToImmutableArray().ToReadOnlyCollection() | |||
: null; | |||
this.Choices = model.Choices.IsSpecified | |||
? model.Choices.Value.Select(x => new RestApplicationCommandChoice(x)).ToImmutableArray() | |||
? model.Choices.Value.Select(x => new RestApplicationCommandChoice(x)).ToImmutableArray().ToReadOnlyCollection() | |||
: null; | |||
} | |||
@@ -37,8 +37,8 @@ namespace Discord.Rest | |||
public static RoleTags ToEntity(this API.RoleTags model) | |||
{ | |||
return new RoleTags( | |||
model.BotId.IsSpecified ? model.BotId.Value : null, | |||
model.IntegrationId.IsSpecified ? model.IntegrationId.Value : null, | |||
model.BotId.IsSpecified ? model.BotId.Value : (ulong?)null, | |||
model.IntegrationId.IsSpecified ? model.IntegrationId.Value : (ulong?)null, | |||
model.IsPremiumSubscriber.IsSpecified ? true : false); | |||
} | |||
public static API.Embed ToModel(this Embed entity) | |||
@@ -54,19 +54,19 @@ namespace Discord.WebSocket | |||
this.Default = model.Default.IsSpecified | |||
? model.Default.Value | |||
: null; | |||
: (bool?)null; | |||
this.Required = model.Required.IsSpecified | |||
? model.Required.Value | |||
: null; | |||
: (bool?)null; | |||
this.Choices = model.Choices.IsSpecified | |||
? model.Choices.Value.Select(x => SocketApplicationCommandChoice.Create(x)).ToImmutableArray() | |||
: new ImmutableArray<SocketApplicationCommandChoice>(); | |||
? model.Choices.Value.Select(x => SocketApplicationCommandChoice.Create(x)).ToImmutableArray().ToReadOnlyCollection() | |||
: null; | |||
this.Options = model.Options.IsSpecified | |||
? model.Options.Value.Select(x => SocketApplicationCommandOption.Create(x)).ToImmutableArray() | |||
: new ImmutableArray<SocketApplicationCommandOption>(); | |||
? model.Options.Value.Select(x => SocketApplicationCommandOption.Create(x)).ToImmutableArray().ToReadOnlyCollection() | |||
: null; | |||
} | |||
IReadOnlyCollection<IApplicationCommandOptionChoice> IApplicationCommandOption.Choices => Choices; | |||
@@ -38,7 +38,7 @@ namespace Discord.WebSocket | |||
this.guildId = guildId; | |||
this.Options = model.Options.IsSpecified | |||
? model.Options.Value.Select(x => new SocketInteractionDataOption(x, this.Discord, guildId)).ToImmutableArray() | |||
? model.Options.Value.Select(x => new SocketInteractionDataOption(x, this.Discord, guildId)).ToImmutableArray().ToReadOnlyCollection() | |||
: null; | |||
} | |||
@@ -30,13 +30,13 @@ namespace Discord.WebSocket | |||
internal SocketInteractionDataOption() { } | |||
internal SocketInteractionDataOption(Model model, DiscordSocketClient discord, ulong guild) | |||
{ | |||
this.Name = Name; | |||
this.Name = model.Name; | |||
this.Value = model.Value.IsSpecified ? model.Value.Value : null; | |||
this.discord = discord; | |||
this.guild = guild; | |||
this.Options = model.Options.IsSpecified | |||
? model.Options.Value.Select(x => new SocketInteractionDataOption(x, discord, guild)).ToImmutableArray() | |||
? model.Options.Value.Select(x => new SocketInteractionDataOption(x, discord, guild)).ToImmutableArray().ToReadOnlyCollection() | |||
: null; | |||
} | |||
@@ -44,14 +44,32 @@ namespace Discord.WebSocket | |||
// Converters | |||
public static explicit operator bool(SocketInteractionDataOption option) | |||
=> (bool)option.Value; | |||
// The default value is of type long, so an implementaiton of of the long option is trivial | |||
public static explicit operator int(SocketInteractionDataOption option) | |||
=> (int)option.Value; | |||
=> unchecked( | |||
(int)( (long)option.Value ) | |||
); | |||
public static explicit operator string(SocketInteractionDataOption option) | |||
=> option.Value.ToString(); | |||
public static explicit operator bool?(SocketInteractionDataOption option) | |||
{ | |||
if (option.Value == null) | |||
return null; | |||
else | |||
return (bool)option; | |||
} | |||
public static explicit operator int?(SocketInteractionDataOption option) | |||
{ | |||
if (option.Value == null) | |||
return null; | |||
else | |||
return (int)option; | |||
} | |||
public static explicit operator SocketGuildChannel(SocketInteractionDataOption option) | |||
{ | |||
if (option.Value is ulong id) | |||
if (ulong.TryParse((string)option.Value, out ulong id)) | |||
{ | |||
var guild = option.discord.GetGuild(option.guild); | |||
@@ -66,7 +84,7 @@ namespace Discord.WebSocket | |||
public static explicit operator SocketRole(SocketInteractionDataOption option) | |||
{ | |||
if (option.Value is ulong id) | |||
if (ulong.TryParse((string)option.Value, out ulong id)) | |||
{ | |||
var guild = option.discord.GetGuild(option.guild); | |||
@@ -81,7 +99,7 @@ namespace Discord.WebSocket | |||
public static explicit operator SocketGuildUser(SocketInteractionDataOption option) | |||
{ | |||
if(option.Value is ulong id) | |||
if (ulong.TryParse((string)option.Value, out ulong id)) | |||
{ | |||
var guild = option.discord.GetGuild(option.guild); | |||