using Discord.Interactions.Builders; using Discord.Logging; using Discord.Rest; using Discord.WebSocket; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; namespace Discord.Interactions { /// /// Provides the framework for building and registering Discord Application Commands. /// public class InteractionService : IDisposable { /// /// Occurs when a Slash Command related information is recieved. /// public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new (); /// /// Occurs when a Slash Command is executed. /// public event Func SlashCommandExecuted { add { _slashCommandExecutedEvent.Add(value); } remove { _slashCommandExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _slashCommandExecutedEvent = new (); /// /// Occurs when a Context Command is executed. /// public event Func ContextCommandExecuted { add { _contextCommandExecutedEvent.Add(value); } remove { _contextCommandExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _contextCommandExecutedEvent = new (); /// /// Occurs when a Message Component command is executed. /// public event Func ComponentCommandExecuted { add { _componentCommandExecutedEvent.Add(value); } remove { _componentCommandExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _componentCommandExecutedEvent = new (); /// /// Occurs when a Autocomplete command is executed. /// public event Func AutocompleteCommandExecuted { add { _autocompleteCommandExecutedEvent.Add(value); } remove { _autocompleteCommandExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _autocompleteCommandExecutedEvent = new(); /// /// Occurs when a AutocompleteHandler is executed. /// public event Func AutocompleteHandlerExecuted { add { _autocompleteHandlerExecutedEvent.Add(value); } remove { _autocompleteHandlerExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _autocompleteHandlerExecutedEvent = new(); private readonly ConcurrentDictionary _typedModuleDefs; private readonly CommandMap _slashCommandMap; private readonly ConcurrentDictionary> _contextCommandMaps; private readonly CommandMap _componentCommandMap; private readonly CommandMap _autocompleteCommandMap; private readonly HashSet _moduleDefs; private readonly ConcurrentDictionary _typeConverters; private readonly ConcurrentDictionary _genericTypeConverters; private readonly ConcurrentDictionary _autocompleteHandlers = new(); private readonly SemaphoreSlim _lock; internal readonly Logger _cmdLogger; internal readonly LogManager _logManager; internal readonly Func _getRestClient; internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes; internal readonly string _wildCardExp; internal readonly RunMode _runMode; internal readonly RestResponseCallback _restResponseCallback; /// /// Rest client to be used to register application commands. /// public DiscordRestClient RestClient { get => _getRestClient(); } /// /// Represents all modules loaded within . /// public IReadOnlyList Modules => _moduleDefs.ToList(); /// /// Represents all Slash Commands loaded within . /// public IReadOnlyList SlashCommands => _moduleDefs.SelectMany(x => x.SlashCommands).ToList(); /// /// Represents all Context Commands loaded within . /// public IReadOnlyList ContextCommands => _moduleDefs.SelectMany(x => x.ContextCommands).ToList(); /// /// Represents all Component Commands loaded within . /// public IReadOnlyCollection ComponentCommands => _moduleDefs.SelectMany(x => x.ComponentCommands).ToList(); /// /// Initialize a with provided configurations. /// /// The discord client. /// The configuration class. public InteractionService (DiscordSocketClient discord, InteractionServiceConfig config = null) : this(() => discord.Rest, config ?? new InteractionServiceConfig()) { } /// /// Initialize a with provided configurations. /// /// The discord client. /// The configuration class. public InteractionService (DiscordShardedClient discord, InteractionServiceConfig config = null) : this(() => discord.Rest, config ?? new InteractionServiceConfig()) { } /// /// Initialize a with provided configurations. /// /// The discord client. /// The configuration class. public InteractionService (BaseSocketClient discord, InteractionServiceConfig config = null) :this(() => discord.Rest, config ?? new InteractionServiceConfig()) { } /// /// Initialize a with provided configurations. /// /// The discord client. /// The configuration class. public InteractionService (DiscordRestClient discord, InteractionServiceConfig config = null) :this(() => discord, config ?? new InteractionServiceConfig()) { } private InteractionService (Func getRestClient, InteractionServiceConfig config = null) { config ??= new InteractionServiceConfig(); _lock = new SemaphoreSlim(1, 1); _typedModuleDefs = new ConcurrentDictionary(); _moduleDefs = new HashSet(); _logManager = new LogManager(config.LogLevel); _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); _cmdLogger = _logManager.CreateLogger("App Commands"); _slashCommandMap = new CommandMap(this); _contextCommandMaps = new ConcurrentDictionary>(); _componentCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters); _autocompleteCommandMap = new CommandMap(this); _getRestClient = getRestClient; _runMode = config.DefaultRunMode; if (_runMode == RunMode.Default) throw new InvalidOperationException($"RunMode cannot be set to {RunMode.Default}"); _throwOnError = config.ThrowOnError; _wildCardExp = config.WildCardExpression; _useCompiledLambda = config.UseCompiledLambda; _enableAutocompleteHandlers = config.EnableAutocompleteHandlers; _autoServiceScopes = config.AutoServiceScopes; _restResponseCallback = config.RestResponseCallback; _genericTypeConverters = new ConcurrentDictionary { [typeof(IChannel)] = typeof(DefaultChannelConverter<>), [typeof(IRole)] = typeof(DefaultRoleConverter<>), [typeof(IUser)] = typeof(DefaultUserConverter<>), [typeof(IMentionable)] = typeof(DefaultMentionableConverter<>), [typeof(IConvertible)] = typeof(DefaultValueConverter<>), [typeof(Enum)] = typeof(EnumConverter<>), [typeof(Nullable<>)] = typeof(NullableConverter<>), }; _typeConverters = new ConcurrentDictionary { [typeof(TimeSpan)] = new TimeSpanConverter() }; } /// /// Create and loads a using a builder factory. /// /// Name of the module. /// The for your dependency injection solution if using one; otherwise, pass null. /// Module builder factory. /// /// A task representing the operation for adding modules. The task result contains the built module instance. /// public async Task CreateModuleAsync(string name, IServiceProvider services, Action buildFunc) { services ??= EmptyServiceProvider.Instance; await _lock.WaitAsync().ConfigureAwait(false); try { var builder = new ModuleBuilder(this, name); buildFunc(builder); var moduleInfo = builder.Build(this, services); LoadModuleInternal(moduleInfo); return moduleInfo; } finally { _lock.Release(); } } /// /// Discover and load command modules from an . /// /// the command modules are defined in. /// The for your dependency injection solution if using one; otherwise, pass null. /// /// A task representing the operation for adding modules. The task result contains a collection of the modules added. /// public async Task> AddModulesAsync (Assembly assembly, IServiceProvider services) { services ??= EmptyServiceProvider.Instance; await _lock.WaitAsync().ConfigureAwait(false); try { var types = await ModuleClassBuilder.SearchAsync(assembly, this); var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services); foreach (var info in moduleDefs) { _typedModuleDefs[info.Key] = info.Value; LoadModuleInternal(info.Value); } return moduleDefs.Values; } finally { _lock.Release(); } } /// /// Add a command module from a . /// /// Type of the module. /// The for your dependency injection solution if using one; otherwise, pass null . /// /// A task representing the operation for adding the module. The task result contains the built module. /// /// /// Thrown if this module has already been added. /// /// /// Thrown when the is not a valid module definition. /// public Task AddModuleAsync (IServiceProvider services) where T : class => AddModuleAsync(typeof(T), services); /// /// Add a command module from a . /// /// Type of the module. /// The for your dependency injection solution if using one; otherwise, pass null . /// /// A task representing the operation for adding the module. The task result contains the built module. /// /// /// Thrown if this module has already been added. /// /// /// Thrown when the is not a valid module definition. /// public async Task AddModuleAsync (Type type, IServiceProvider services) { if (!typeof(IInteractionModuleBase).IsAssignableFrom(type)) throw new ArgumentException("Type parameter must be a type of Slash Module", "T"); services ??= EmptyServiceProvider.Instance; await _lock.WaitAsync().ConfigureAwait(false); try { var typeInfo = type.GetTypeInfo(); if (_typedModuleDefs.ContainsKey(typeInfo)) throw new ArgumentException("Module definition for this type already exists."); var moduleDef = ( await ModuleClassBuilder.BuildAsync(new List { typeInfo }, this, services).ConfigureAwait(false) ).FirstOrDefault(); if (moduleDef.Value == default) throw new InvalidOperationException($"Could not build the module {typeInfo.FullName}, did you pass an invalid type?"); if (!_typedModuleDefs.TryAdd(type, moduleDef.Value)) throw new ArgumentException("Module definition for this type already exists."); _typedModuleDefs[moduleDef.Key] = moduleDef.Value; LoadModuleInternal(moduleDef.Value); return moduleDef.Value; } finally { _lock.Release(); } } /// /// Register Application Commands from and to a guild. /// /// Id of the target guild. /// If , this operation will not delete the commands that are missing from . /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. /// public async Task> RegisterCommandsToGuildAsync (ulong guildId, bool deleteMissing = true) { EnsureClientReady(); var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); if (!deleteMissing) { var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false); var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false); } /// /// Register Application Commands from and to Discord on in global scope. /// /// If , this operation will not delete the commands that are missing from . /// /// A task representing the command registration process. The task result contains the active global application commands of bot. /// public async Task> RegisterCommandsGloballyAsync (bool deleteMissing = true) { EnsureClientReady(); var topLevelModules = _moduleDefs.Where(x => !x.IsSubModule); var props = topLevelModules.SelectMany(x => x.ToApplicationCommandProps()).ToList(); if (!deleteMissing) { var existing = await RestClient.GetGlobalApplicationCommands().ConfigureAwait(false); var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); } /// /// Register Application Commands from to a guild. /// /// /// Commands will be registered as standalone commands, if you want the to take effect, /// use . Registering a commands without group names might cause the command traversal to fail. /// /// The target guild. /// Commands to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. /// public async Task> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands) { EnsureClientReady(); if (guild is null) throw new ArgumentNullException(nameof(guild)); var props = new List(); foreach (var command in commands) { switch (command) { case SlashCommandInfo slashCommand: props.Add(slashCommand.ToApplicationCommandProps()); break; case ContextCommandInfo contextCommand: props.Add(contextCommand.ToApplicationCommandProps()); break; default: throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); } } if (!deleteMissing) { var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false); } /// /// Register Application Commands from modules provided in to a guild. /// /// The target guild. /// Modules to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. /// public async Task> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules) { EnsureClientReady(); if (guild is null) throw new ArgumentNullException(nameof(guild)); var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList(); if (!deleteMissing) { var existing = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guild.Id).ConfigureAwait(false); } /// /// Register Application Commands from modules provided in as global commands. /// /// Modules to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. /// public async Task> AddModulesGloballyAsync(bool deleteMissing = false, params ModuleInfo[] modules) { EnsureClientReady(); var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList(); if (!deleteMissing) { var existing = await RestClient.GetGlobalApplicationCommands().ConfigureAwait(false); var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); } /// /// Register Application Commands from as global commands. /// /// /// Commands will be registered as standalone commands, if you want the to take effect, /// use . Registering a commands without group names might cause the command traversal to fail. /// /// Commands to be registered to Discord. /// /// A task representing the command registration process. The task result contains the active application commands of the target guild. /// public async Task> AddCommandsGloballyAsync(bool deleteMissing = false, params IApplicationCommandInfo[] commands) { EnsureClientReady(); var props = new List(); foreach (var command in commands) { switch (command) { case SlashCommandInfo slashCommand: props.Add(slashCommand.ToApplicationCommandProps()); break; case ContextCommandInfo contextCommand: props.Add(contextCommand.ToApplicationCommandProps()); break; default: throw new InvalidOperationException($"Command type {command.GetType().FullName} isn't supported yet"); } } if (!deleteMissing) { var existing = await RestClient.GetGlobalApplicationCommands().ConfigureAwait(false); var missing = existing.Where(x => !props.Any(y => y.Name.IsSpecified && y.Name.Value == x.Name)); props.AddRange(missing.Select(x => x.ToApplicationCommandProps())); } return await RestClient.BulkOverwriteGlobalCommands(props.ToArray()).ConfigureAwait(false); } private void LoadModuleInternal (ModuleInfo module) { _moduleDefs.Add(module); foreach (var command in module.SlashCommands) _slashCommandMap.AddCommand(command, command.IgnoreGroupNames); foreach (var command in module.ContextCommands) _contextCommandMaps.GetOrAdd(command.CommandType, new CommandMap(this)).AddCommand(command, command.IgnoreGroupNames); foreach (var interaction in module.ComponentCommands) _componentCommandMap.AddCommand(interaction, interaction.IgnoreGroupNames); foreach (var command in module.AutocompleteCommands) _autocompleteCommandMap.AddCommand(command.GetCommandKeywords(), command); foreach (var subModule in module.SubModules) LoadModuleInternal(subModule); } /// /// Remove a command module. /// /// The of the module. /// /// A task that represents the asynchronous removal operation. The task result contains a value that /// indicates whether the module is successfully removed. /// public Task RemoveModuleAsync ( ) => RemoveModuleAsync(typeof(T)); /// /// Remove a command module. /// /// The of the module. /// /// A task that represents the asynchronous removal operation. The task result contains a value that /// indicates whether the module is successfully removed. /// public async Task RemoveModuleAsync (Type type) { await _lock.WaitAsync().ConfigureAwait(false); try { if (!_typedModuleDefs.TryRemove(type, out var module)) return false; return RemoveModuleInternal(module); } finally { _lock.Release(); } } /// /// Remove a command module. /// /// The to be removed from the service. /// /// A task that represents the asynchronous removal operation. The task result contains a value that /// indicates whether the is successfully removed. /// public async Task RemoveModuleAsync(ModuleInfo module) { await _lock.WaitAsync().ConfigureAwait(false); try { var typeModulePair = _typedModuleDefs.FirstOrDefault(x => x.Value.Equals(module)); if (!typeModulePair.Equals(default(KeyValuePair))) _typedModuleDefs.TryRemove(typeModulePair.Key, out var _); return RemoveModuleInternal(module); } finally { _lock.Release(); } } private bool RemoveModuleInternal (ModuleInfo moduleInfo) { if (!_moduleDefs.Remove(moduleInfo)) return false; foreach (var command in moduleInfo.SlashCommands) { _slashCommandMap.RemoveCommand(command); } return true; } /// /// Execute a Command from a given . /// /// Name context of the command. /// The service to be used in the command's dependency injection. /// /// A task representing the command execution process. The task result contains the result of the execution. /// public async Task ExecuteCommandAsync (IInteractionContext context, IServiceProvider services) { var interaction = context.Interaction; return interaction switch { ISlashCommandInteraction slashCommand => await ExecuteSlashCommandAsync(context, slashCommand, services).ConfigureAwait(false), IComponentInteraction messageComponent => await ExecuteComponentCommandAsync(context, messageComponent.Data.CustomId, services).ConfigureAwait(false), IUserCommandInteraction userCommand => await ExecuteContextCommandAsync(context, userCommand.Data.Name, ApplicationCommandType.User, services).ConfigureAwait(false), IMessageCommandInteraction messageCommand => await ExecuteContextCommandAsync(context, messageCommand.Data.Name, ApplicationCommandType.Message, services).ConfigureAwait(false), IAutocompleteInteraction autocomplete => await ExecuteAutocompleteAsync(context, autocomplete, services).ConfigureAwait(false), _ => throw new InvalidOperationException($"{interaction.Type} interaction type cannot be executed by the Interaction service"), }; } private async Task ExecuteSlashCommandAsync (IInteractionContext context, ISlashCommandInteraction interaction, IServiceProvider services) { var keywords = interaction.Data.GetCommandKeywords(); var result = _slashCommandMap.GetCommand(keywords); if (!result.IsSuccess) { await _cmdLogger.DebugAsync($"Unknown slash command, skipping execution ({string.Join(" ", keywords).ToUpper()})"); await _slashCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); return result; } return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); } private async Task ExecuteContextCommandAsync (IInteractionContext context, string input, ApplicationCommandType commandType, IServiceProvider services) { if (!_contextCommandMaps.TryGetValue(commandType, out var map)) return SearchResult.FromError(input, InteractionCommandError.UnknownCommand, $"No {commandType} command found."); var result = map.GetCommand(input); if (!result.IsSuccess) { await _cmdLogger.DebugAsync($"Unknown context command, skipping execution ({result.Text.ToUpper()})"); await _contextCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); return result; } return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false); } private async Task ExecuteComponentCommandAsync (IInteractionContext context, string input, IServiceProvider services) { var result = _componentCommandMap.GetCommand(input); if (!result.IsSuccess) { await _cmdLogger.DebugAsync($"Unknown custom interaction id, skipping execution ({input.ToUpper()})"); await _componentCommandExecutedEvent.InvokeAsync(null, context, result).ConfigureAwait(false); return result; } return await result.Command.ExecuteAsync(context, services, result.RegexCaptureGroups).ConfigureAwait(false); } private async Task ExecuteAutocompleteAsync (IInteractionContext context, IAutocompleteInteraction interaction, IServiceProvider services ) { var keywords = interaction.Data.GetCommandKeywords(); if(_enableAutocompleteHandlers) { var autocompleteHandlerResult = _slashCommandMap.GetCommand(keywords); if(autocompleteHandlerResult.IsSuccess) { var parameter = autocompleteHandlerResult.Command.Parameters.FirstOrDefault(x => string.Equals(x.Name, interaction.Data.Current.Name, StringComparison.Ordinal)); if(parameter?.AutocompleteHandler is not null) return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false); } } keywords.Add(interaction.Data.Current.Name); var commandResult = _autocompleteCommandMap.GetCommand(keywords); if(!commandResult.IsSuccess) { await _cmdLogger.DebugAsync($"Unknown command name, skipping autocomplete process ({interaction.Data.CommandName.ToUpper()})"); await _autocompleteCommandExecutedEvent.InvokeAsync(null, context, commandResult).ConfigureAwait(false); return commandResult; } return await commandResult.Command.ExecuteAsync(context, services).ConfigureAwait(false); } internal TypeConverter GetTypeConverter (Type type, IServiceProvider services = null) { if (_typeConverters.TryGetValue(type, out var specific)) return specific; else if (_genericTypeConverters.Any(x => x.Key.IsAssignableFrom(type) || (x.Key.IsGenericTypeDefinition && type.IsGenericType && x.Key.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()))) { services ??= EmptyServiceProvider.Instance; var converterType = GetMostSpecificTypeConverter(type); var converter = ReflectionUtils.CreateObject(converterType.MakeGenericType(type).GetTypeInfo(), this, services); _typeConverters[type] = converter; return converter; } else if (_typeConverters.Any(x => x.Value.CanConvertTo(type))) return _typeConverters.First(x => x.Value.CanConvertTo(type)).Value; throw new ArgumentException($"No type {nameof(TypeConverter)} is defined for this {type.FullName}", "type"); } /// /// Add a concrete type . /// /// Primary target of the . /// The instance. public void AddTypeConverter (TypeConverter converter) => AddTypeConverter(typeof(T), converter); /// /// Add a concrete type . /// /// Primary target of the . /// The instance. public void AddTypeConverter (Type type, TypeConverter converter) { if (!converter.CanConvertTo(type)) throw new ArgumentException($"This {converter.GetType().FullName} cannot read {type.FullName} and cannot be registered as its {nameof(TypeConverter)}"); _typeConverters[type] = converter; } /// /// Add a generic type . /// /// Generic Type constraint of the of the . /// Type of the . public void AddGenericTypeConverter (Type converterType) => AddGenericTypeConverter(typeof(T), converterType); /// /// Add a generic type . /// /// Generic Type constraint of the of the . /// Type of the . public void AddGenericTypeConverter (Type targetType, Type converterType) { if (!converterType.IsGenericTypeDefinition) throw new ArgumentException($"{converterType.FullName} is not generic."); var genericArguments = converterType.GetGenericArguments(); if (genericArguments.Count() > 1) throw new InvalidOperationException($"Valid generic {converterType.FullName}s cannot have more than 1 generic type parameter"); var constraints = genericArguments.SelectMany(x => x.GetGenericParameterConstraints()); if (!constraints.Any(x => x.IsAssignableFrom(targetType))) throw new InvalidOperationException($"This generic class does not support type {targetType.FullName}"); _genericTypeConverters[targetType] = converterType; } internal IAutocompleteHandler GetAutocompleteHandler(Type autocompleteHandlerType, IServiceProvider services = null) { services ??= EmptyServiceProvider.Instance; if (!_enableAutocompleteHandlers) throw new InvalidOperationException($"{nameof(IAutocompleteHandler)}s are not enabled. To use this feature set {nameof(InteractionServiceConfig.EnableAutocompleteHandlers)} to TRUE"); if (_autocompleteHandlers.TryGetValue(autocompleteHandlerType, out var autocompleteHandler)) return autocompleteHandler; else { autocompleteHandler = ReflectionUtils.CreateObject(autocompleteHandlerType.GetTypeInfo(), this, services); _autocompleteHandlers[autocompleteHandlerType] = autocompleteHandler; return autocompleteHandler; } } /// /// Modify the command permissions of the matching Discord Slash Command. /// /// Module representing the top level Slash Command. /// Target guild. /// New permission values. /// /// The active command permissions after the modification. /// public async Task ModifySlashCommandPermissionsAsync (ModuleInfo module, IGuild guild, params ApplicationCommandPermission[] permissions) { if (!module.IsSlashGroup) throw new InvalidOperationException($"This module does not have a {nameof(GroupAttribute)} and does not represent an Application Command"); if (!module.IsTopLevelGroup) throw new InvalidOperationException("This module is not a top level application command. You cannot change its permissions"); if (guild is null) throw new ArgumentNullException("guild"); var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); var appCommand = commands.First(x => x.Name == module.SlashGroupName); return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); } /// /// Modify the command permissions of the matching Discord Slash Command. /// /// The Slash Command. /// Target guild. /// New permission values. /// /// The active command permissions after the modification. /// public async Task ModifySlashCommandPermissionsAsync (SlashCommandInfo command, IGuild guild, params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false); /// /// Modify the command permissions of the matching Discord Slash Command. /// /// The Context Command. /// Target guild. /// New permission values. /// /// The active command permissions after the modification. /// public async Task ModifyContextCommandPermissionsAsync (ContextCommandInfo command, IGuild guild, params ApplicationCommandPermission[] permissions) => await ModifyApplicationCommandPermissionsAsync(command, guild, permissions).ConfigureAwait(false); private async Task ModifyApplicationCommandPermissionsAsync (T command, IGuild guild, params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo { if (!command.IsTopLevelCommand) throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions"); if (guild is null) throw new ArgumentNullException("guild"); var commands = await RestClient.GetGuildApplicationCommands(guild.Id).ConfigureAwait(false); var appCommand = commands.First(x => x.Name == ( command as IApplicationCommandInfo ).Name); return await appCommand.ModifyCommandPermissions(permissions).ConfigureAwait(false); } /// /// Gets a . /// /// Declaring module type of this command, must be a type of . /// Method name of the handler, use of is recommended. /// /// instance for this command. /// /// Module or Slash Command couldn't be found. public SlashCommandInfo GetSlashCommandInfo (string methodName) where TModule : class { var module = GetModuleInfo(); return module.SlashCommands.First(x => x.MethodName == methodName); } /// /// Gets a . /// /// Declaring module type of this command, must be a type of . /// Method name of the handler, use of is recommended. /// /// instance for this command. /// /// Module or Context Command couldn't be found. public ContextCommandInfo GetContextCommandInfo (string methodName) where TModule : class { var module = GetModuleInfo(); return module.ContextCommands.First(x => x.MethodName == methodName); } /// /// Gets a . /// /// Declaring module type of this command, must be a type of . /// Method name of the handler, use of is recommended. /// /// instance for this command. /// /// Module or Component Command couldn't be found. public ComponentCommandInfo GetComponentCommandInfo (string methodName) where TModule : class { var module = GetModuleInfo(); return module.ComponentCommands.First(x => x.MethodName == methodName); } /// /// Gets a built . /// /// Type of the module, must be a type of . /// /// instance for this module. /// public ModuleInfo GetModuleInfo ( ) where TModule : class { if (!typeof(IInteractionModuleBase).IsAssignableFrom(typeof(TModule))) throw new ArgumentException("Type parameter must be a type of Slash Module", "TModule"); var module = _typedModuleDefs[typeof(TModule)]; if (module is null) throw new InvalidOperationException($"{typeof(TModule).FullName} is not loaded to the Slash Command Service"); return module; } /// public void Dispose ( ) { _lock.Dispose(); } private Type GetMostSpecificTypeConverter (Type type) { if (_genericTypeConverters.TryGetValue(type, out var matching)) return matching; if (type.IsGenericType && _genericTypeConverters.TryGetValue(type.GetGenericTypeDefinition(), out var genericDefinition)) return genericDefinition; var typeInterfaces = type.GetInterfaces(); var candidates = _genericTypeConverters.Where(x => x.Key.IsAssignableFrom(type)) .OrderByDescending(x => typeInterfaces.Count(y => y.IsAssignableFrom(x.Key))); return candidates.First().Value; } private void EnsureClientReady() { if (RestClient?.CurrentUser is null || RestClient?.CurrentUser?.Id == 0) throw new InvalidOperationException($"Provided client is not ready to execute this operation, invoke this operation after a `Client Ready` event"); } } }