diff --git a/Discord.Net.SlashCommands/Builders/SlashCommandBuilder.cs b/Discord.Net.SlashCommands/Builders/SlashCommandBuilder.cs
new file mode 100644
index 000000000..75995525a
--- /dev/null
+++ b/Discord.Net.SlashCommands/Builders/SlashCommandBuilder.cs
@@ -0,0 +1,491 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace Discord.SlashCommands.Builders
+{
+ ///
+ /// A class used to build slash commands.
+ ///
+ public class SlashCommandBuilder
+ {
+ ///
+ /// Returns the maximun length a commands name allowed by Discord
+ ///
+ public const int MaxNameLength = 32;
+ ///
+ /// Returns the maximum length of a commands description allowed by Discord.
+ ///
+ public const int MaxDescriptionLength = 100;
+ ///
+ /// Returns the maximum count of command options allowed by Discord
+ ///
+ public const int MaxOptionsCount = 10;
+
+ ///
+ /// The name of this slash 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));
+
+ // Discord updated the docs, this regex prevents special characters like @!$%(... etc,
+ // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
+ if (!Regex.IsMatch(value, @"^[\w-]{3,32}$"))
+ throw new ArgumentException("Command name cannot contian any special characters or whitespaces!");
+
+ _name = value;
+ }
+ }
+
+ ///
+ /// A 1-100 length description of this slash command
+ ///
+ public string Description
+ {
+ get
+ {
+ return _description;
+ }
+ set
+ {
+ Preconditions.AtLeast(value.Length, 1, nameof(Description));
+ Preconditions.AtMost(value.Length, MaxDescriptionLength, nameof(Description));
+
+ _description = value;
+ }
+ }
+
+ public ulong GuildId
+ {
+ get
+ {
+ return _guildId ?? 0;
+ }
+ set
+ {
+ if (value == 0)
+ {
+ throw new ArgumentException("Guild ID cannot be 0!");
+ }
+
+ _guildId = value;
+
+ if (isGlobal)
+ isGlobal = false;
+ }
+ }
+ ///
+ /// Gets or sets the options for this command.
+ ///
+ public List Options
+ {
+ get
+ {
+ return _options;
+ }
+ set
+ {
+ if (value != null)
+ if (value.Count > MaxOptionsCount)
+ throw new ArgumentException(message: $"Option count must be less than or equal to {MaxOptionsCount}.", paramName: nameof(Options));
+
+ _options = value;
+ }
+ }
+
+ private ulong? _guildId { get; set; }
+ private string _name { get; set; }
+ private string _description { get; set; }
+ private List _options { get; set; }
+
+ internal bool isGlobal { get; set; }
+
+
+ public SlashCommandCreationProperties Build()
+ {
+ SlashCommandCreationProperties props = new SlashCommandCreationProperties()
+ {
+ Name = this.Name,
+ Description = this.Description,
+ };
+
+ if(this.Options != null || this.Options.Any())
+ {
+ var options = new List();
+
+ this.Options.ForEach(x => options.Add(x.Build()));
+
+ props.Options = options;
+ }
+
+ return props;
+
+ }
+
+ ///
+ /// Makes this command a global application command .
+ ///
+ /// The current builder.
+ public SlashCommandBuilder MakeGlobal()
+ {
+ this.isGlobal = true;
+ return this;
+ }
+
+ ///
+ /// Makes this command a guild specific command.
+ ///
+ /// The Id of the target guild.
+ /// The current builder.
+ public SlashCommandBuilder ForGuild(ulong GuildId)
+ {
+ this.GuildId = GuildId;
+ return this;
+ }
+
+ public SlashCommandBuilder WithName(string Name)
+ {
+ this.Name = Name;
+ return this;
+ }
+
+ ///
+ /// Sets the description of the current command.
+ ///
+ /// The description of this command.
+ /// The current builder.
+ public SlashCommandBuilder WithDescription(string Description)
+ {
+ this.Description = Description;
+ return this;
+ }
+
+ ///
+ /// Adds an option to the current slash command.
+ ///
+ /// The name of the option to add.
+ /// The type of this option.
+ /// The description of this option.
+ /// If this option is required for this command.
+ /// If this option is the default option.
+ /// The options of the option to add.
+ /// The choices of this option.
+ /// The current builder.
+ public SlashCommandBuilder AddOption(string Name, ApplicationCommandOptionType Type,
+ string Description, bool Required = true, bool Default = false, List Options = null, params ApplicationCommandOptionChoiceProperties[] Choices)
+ {
+ // Make sure the name matches the requirements from discord
+ Preconditions.NotNullOrEmpty(Name, nameof(Name));
+ Preconditions.AtLeast(Name.Length, 3, nameof(Name));
+ Preconditions.AtMost(Name.Length, MaxNameLength, nameof(Name));
+
+ // Discord updated the docs, this regex prevents special characters like @!$%( and s p a c e s.. etc,
+ // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
+ if (!Regex.IsMatch(Name, @"^[\w-]{3,32}$"))
+ throw new ArgumentException("Command name cannot contian any special characters or whitespaces!", nameof(Name));
+
+ // same with description
+ Preconditions.NotNullOrEmpty(Description, nameof(Description));
+ Preconditions.AtLeast(Description.Length, 3, nameof(Description));
+ Preconditions.AtMost(Description.Length, MaxDescriptionLength, nameof(Description));
+
+ // make sure theres only one option with default set to true
+ if (Default)
+ {
+ if (this.Options != null)
+ if (this.Options.Any(x => x.Default))
+ throw new ArgumentException("There can only be one command option with default set to true!", nameof(Default));
+ }
+
+ SlashCommandOptionBuilder option = new SlashCommandOptionBuilder();
+ option.Name = Name;
+ option.Description = Description;
+ option.Required = Required;
+ option.Default = Default;
+ option.Options = Options;
+ option.Choices = Choices != null ? new List(Choices) : null;
+
+ return AddOption(option);
+ }
+
+ ///
+ /// Adds an option to the current slash command.
+ ///
+ /// The name of the option to add.
+ /// The type of this option.
+ /// The description of this option.
+ /// If this option is required for this command.
+ /// If this option is the default option.
+ /// The choices of this option.
+ /// The current builder.
+ public SlashCommandBuilder AddOption(string Name, ApplicationCommandOptionType Type,
+ string Description, bool Required = true, bool Default = false, params ApplicationCommandOptionChoiceProperties[] Choices)
+ => AddOption(Name, Type, Description, Required, Default, null, Choices);
+
+ ///
+ /// Adds an option to the current slash command.
+ ///
+ /// The name of the option to add.
+ /// The type of this option.
+ /// The sescription of this option.
+ /// The current builder.
+ public SlashCommandBuilder AddOption(string Name, ApplicationCommandOptionType Type, string Description)
+ => AddOption(Name, Type, Description, Options: null, Choices: null);
+
+ ///
+ /// Adds an option to this slash command.
+ ///
+ /// The option to add.
+ /// The current builder.
+ public SlashCommandBuilder AddOption(SlashCommandOptionBuilder Option)
+ {
+ if (this.Options == null)
+ this.Options = new List();
+
+ if (this.Options.Count >= MaxOptionsCount)
+ throw new ArgumentOutOfRangeException(nameof(Options), $"Cannot have more than {MaxOptionsCount} options!");
+
+ if (Option == null)
+ throw new ArgumentNullException(nameof(Option), "Option cannot be null");
+
+ this.Options.Add(Option);
+ return this;
+ }
+ ///
+ /// Adds a collection of options to the current slash command.
+ ///
+ /// The collection of options to add.
+ /// The current builder.
+ public SlashCommandBuilder AddOptions(params SlashCommandOptionBuilder[] Options)
+ {
+ if (Options == null)
+ throw new ArgumentNullException(nameof(Options), "Options cannot be null!");
+
+ if (Options.Length == 0)
+ throw new ArgumentException(nameof(Options), "Options cannot be empty!");
+
+ if (this.Options == null)
+ this.Options = new List();
+
+ if (this.Options.Count + Options.Length > MaxOptionsCount)
+ throw new ArgumentOutOfRangeException(nameof(Options), $"Cannot have more than {MaxOptionsCount} options!");
+
+ this.Options.AddRange(Options);
+ return this;
+ }
+ }
+
+ ///
+ /// Represents a class used to build options for the .
+ ///
+ public class SlashCommandOptionBuilder
+ {
+ ///
+ /// The max length of a choice's name allowed by Discord.
+ ///
+ public const int ChoiceNameMaxLength = 100;
+
+ ///
+ /// The maximum number of choices allowed by Discord.
+ ///
+ public const int MaxChoiceCount = 10;
+
+ private string _name;
+ private string _description;
+
+ ///
+ /// The name of this option.
+ ///
+ public string Name
+ {
+ get => _name;
+ set
+ {
+ if (value?.Length > SlashCommandBuilder.MaxNameLength)
+ throw new ArgumentException("Name length must be less than or equal to 32");
+ if(value?.Length < 3)
+ throw new ArgumentException("Name length must at least 3 characters in length");
+
+ // Discord updated the docs, this regex prevents special characters like @!$%(... etc,
+ // https://discord.com/developers/docs/interactions/slash-commands#applicationcommand
+ if (!Regex.IsMatch(value, @"^[\w-]{3,32}$"))
+ throw new ArgumentException("Option name cannot contian any special characters or whitespaces!");
+
+ _name = value;
+ }
+ }
+
+ ///
+ /// The description of this option.
+ ///
+ public string Description
+ {
+ get => _description;
+ set
+ {
+ if (value?.Length > SlashCommandBuilder.MaxDescriptionLength)
+ throw new ArgumentException("Description length must be less than or equal to 100");
+ if (value?.Length < 1)
+ throw new ArgumentException("Name length must at least 1 character in length");
+
+ _description = value;
+ }
+ }
+
+ ///
+ /// The type of this option.
+ ///
+ public ApplicationCommandOptionType Type { get; set; }
+
+ ///
+ /// The first required option for the user to complete. only one option can be default.
+ ///
+ public bool Default { get; set; }
+
+ ///
+ /// if this option is required for this command, otherwise .
+ ///
+ public bool Required { get; set; }
+
+ ///
+ /// choices for string and int types for the user to pick from.
+ ///
+ public List Choices { get; set; }
+
+ ///
+ /// If the option is a subcommand or subcommand group type, this nested options will be the parameters.
+ ///
+ public List Options { get; set; }
+
+ ///
+ /// Builds the current option.
+ ///
+ /// The build version of this option
+ public ApplicationCommandOptionProperties Build()
+ {
+ 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 (!isSubType && (Options != null && Options.Any()))
+ throw new ArgumentException(nameof(Options), $"Cannot have options on {Type} type");
+
+ return new ApplicationCommandOptionProperties()
+ {
+ Name = this.Name,
+ Description = this.Description,
+ Default = this.Default,
+ Required = this.Required,
+ Type = this.Type,
+ Options = new List(this.Options.Select(x => x.Build())),
+ Choices = this.Choices
+ };
+ }
+
+ ///
+ /// Adds a sub
+ ///
+ ///
+ ///
+ public SlashCommandOptionBuilder AddOption(SlashCommandOptionBuilder option)
+ {
+ if (this.Options == null)
+ this.Options = new List();
+
+ if (this.Options.Count >= SlashCommandBuilder.MaxOptionsCount)
+ throw new ArgumentOutOfRangeException(nameof(Choices), $"There can only be {SlashCommandBuilder.MaxOptionsCount} options per sub command group!");
+
+ if (option == null)
+ throw new ArgumentNullException(nameof(option), "Option cannot be null");
+
+ Options.Add(option);
+ return this;
+ }
+
+ public SlashCommandOptionBuilder AddChoice(string Name, int Value)
+ {
+ if (Choices == null)
+ Choices = new List();
+
+ if (Choices.Count >= MaxChoiceCount)
+ throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!");
+
+ Choices.Add(new ApplicationCommandOptionChoiceProperties()
+ {
+ Name = Name,
+ Value = Value
+ });
+
+ return this;
+ }
+ public SlashCommandOptionBuilder AddChoice(string Name, string Value)
+ {
+ if (Choices == null)
+ Choices = new List();
+
+ if (Choices.Count >= MaxChoiceCount)
+ throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!");
+
+ Choices.Add(new ApplicationCommandOptionChoiceProperties()
+ {
+ Name = Name,
+ Value = Value
+ });
+
+ return this;
+ }
+
+ public SlashCommandOptionBuilder WithName(string Name, int Value)
+ {
+ if (Choices == null)
+ Choices = new List();
+
+ if (Choices.Count >= MaxChoiceCount)
+ throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!");
+
+ Choices.Add(new ApplicationCommandOptionChoiceProperties()
+ {
+ Name = Name,
+ Value = Value
+ });
+
+ return this;
+ }
+
+ public SlashCommandOptionBuilder WithDescription(string Description)
+ {
+ this.Description = Description;
+ return this;
+ }
+
+ public SlashCommandOptionBuilder WithRequired(bool value)
+ {
+ this.Required = value;
+ return this;
+ }
+
+ public SlashCommandOptionBuilder WithDefault(bool value)
+ {
+ this.Default = value;
+ return this;
+ }
+ public SlashCommandOptionBuilder WithType(ApplicationCommandOptionType Type)
+ {
+ this.Type = Type;
+ return this;
+ }
+ }
+}
diff --git a/Discord.Net.SlashCommands/Discord.Net.SlashCommands.csproj b/Discord.Net.SlashCommands/Discord.Net.SlashCommands.csproj
new file mode 100644
index 000000000..70b29c74b
--- /dev/null
+++ b/Discord.Net.SlashCommands/Discord.Net.SlashCommands.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Discord.Net.SlashCommands
+ Discord.SlashCommands
+ A Discord.Net extension adding support for slash commands.
+ net461;netstandard2.0;netstandard2.1
+ netstandard2.0;netstandard2.1
+
+
+
+
+
+
+
diff --git a/Discord.Net.sln b/Discord.Net.sln
index 1a32f1270..084d8a834 100644
--- a/Discord.Net.sln
+++ b/Discord.Net.sln
@@ -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
diff --git a/src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs b/src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs
index 523137858..6087825b4 100644
--- a/src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs
+++ b/src/Discord.Net.Commands/Builders/SlashCommandBuilder.cs
@@ -390,7 +390,7 @@ namespace Discord.Commands.Builders
Default = this.Default,
Required = this.Required,
Type = this.Type,
- Options = new List(this.Options.Select(x => x.Build())),
+ Options = Options != null ? new List(this.Options.Select(x => x.Build())) : null,
Choices = this.Choices
};
}
diff --git a/src/Discord.Net.Commands/SlashCommands/Attributes/SlashCommand.cs b/src/Discord.Net.Commands/SlashCommands/Attributes/SlashCommand.cs
new file mode 100644
index 000000000..8b60eabc1
--- /dev/null
+++ b/src/Discord.Net.Commands/SlashCommands/Attributes/SlashCommand.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.SlashCommands
+{
+ ///
+ /// Defines the current class or function as a slash command.
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
+ public class SlashCommand : Attribute
+ {
+ ///
+ /// The name of this slash command.
+ ///
+ public string CommandName;
+
+ ///
+ /// Tells the that this class/function is a slash command.
+ ///
+ /// The name of this slash command.
+ public SlashCommand(string CommandName)
+ {
+ this.CommandName = CommandName;
+ }
+ }
+}
diff --git a/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs b/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs
new file mode 100644
index 000000000..f075dd833
--- /dev/null
+++ b/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs
@@ -0,0 +1,30 @@
+using Discord.Commands;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.SlashCommands
+{
+ public class SlashCommandService
+ {
+ private List _modules;
+
+ public SlashCommandService() // TODO: possible config?
+ {
+
+ }
+
+ public void AddAssembly()
+ {
+
+ }
+
+ public async Task ExecuteAsync()
+ {
+ // TODO: handle execution
+ return null;
+ }
+ }
+}
diff --git a/src/Discord.Net.Commands/SlashCommands/Types/SlashCommandBase.cs b/src/Discord.Net.Commands/SlashCommands/Types/SlashCommandBase.cs
new file mode 100644
index 000000000..0fe947e6d
--- /dev/null
+++ b/src/Discord.Net.Commands/SlashCommands/Types/SlashCommandBase.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.Commands.SlashCommands.Types
+{
+ ///
+ /// The base class to inherit for your slash command class's.
+ ///
+ /// The type of the interaction.
+ public abstract class SlashCommandBase where T : IDiscordInteraction
+ {
+ public event Func, Task> CommandExecuted;
+ public SlashCommandBase()
+ {
+
+ }
+
+ public void Register(SlashCommandCreationProperties command)
+ {
+ // TODO: register it, make sure that this command is what discord is expecting
+ }
+
+ internal Task ExecuteInternalAsync(IDiscordInteraction interaction)
+ {
+ // try catch?
+ return this.CommandExecuted?.Invoke(new SlashCommandContext(interaction));
+ }
+ }
+
+ ///
+ /// The base class to inherit for your slash command class's.
+ ///
+ public abstract class SlashCommandBase : SlashCommandBase { }
+}
diff --git a/src/Discord.Net.Commands/SlashCommands/Types/SlashCommandContext.cs b/src/Discord.Net.Commands/SlashCommands/Types/SlashCommandContext.cs
new file mode 100644
index 000000000..edb6b9f3f
--- /dev/null
+++ b/src/Discord.Net.Commands/SlashCommands/Types/SlashCommandContext.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.Commands.SlashCommands.Types
+{
+ public class SlashCommandContext where T : IDiscordInteraction
+ {
+ public T Interaction;
+
+ public IGuild Guild
+ => Interaction.Guild;
+
+ internal SlashCommandContext(IDiscordInteraction interaction)
+ {
+ this.Interaction = (T)interaction;
+ }
+ }
+}
diff --git a/src/Discord.Net.Commands/SlashCommands/Types/SlashCommandModule.cs b/src/Discord.Net.Commands/SlashCommands/Types/SlashCommandModule.cs
new file mode 100644
index 000000000..6e54e920a
--- /dev/null
+++ b/src/Discord.Net.Commands/SlashCommands/Types/SlashCommandModule.cs
@@ -0,0 +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
+ {
+
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
index 2cc0faf4f..037852eff 100644
--- a/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
+++ b/src/Discord.Net.Core/Entities/Interactions/IDiscordInteraction.cs
@@ -20,6 +20,11 @@ namespace Discord
///
ulong Id { get; }
+ ///
+ ///
+ ///
+ IGuild Guild { get; }
+
///
/// The type of this .
///
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
index 4a8277fd4..06a5d53c7 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteraction.cs
@@ -211,5 +211,6 @@ namespace Discord.WebSocket
}
IApplicationCommandInteractionData IDiscordInteraction.Data => Data;
+ IGuild IDiscordInteraction.Guild => Guild;
}
}
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs
index 62097689d..fea0bf3b6 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs
@@ -38,6 +38,7 @@ namespace Discord.WebSocket
this.Options = model.Options.IsSpecified
? model.Options.Value.Select(x => new SocketInteractionDataOption(x, discord, guild)).ToImmutableArray()
: null;
+
}
// Converters