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