using Discord.Interactions.Builders;
using Discord.Logging;
using Discord.Rest;
using Discord.WebSocket;
using System;
using System.Collections;
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 received.
///
public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } }
internal readonly AsyncEvent> _logEvent = new();
///
/// Occurs when any type of interaction is executed.
///
public event Func InteractionExecuted
{
add
{
SlashCommandExecuted += value;
ContextCommandExecuted += value;
ComponentCommandExecuted += value;
AutocompleteCommandExecuted += value;
ModalCommandExecuted += value;
}
remove
{
SlashCommandExecuted -= value;
ContextCommandExecuted -= value;
ComponentCommandExecuted -= value;
AutocompleteCommandExecuted -= value;
ModalCommandExecuted -= value;
}
}
///
/// 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();
///
/// Occurs when a Modal command is executed.
///
public event Func ModalCommandExecuted { add { _modalCommandExecutedEvent.Add(value); } remove { _modalCommandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent> _modalCommandExecutedEvent = new();
///
/// Get the used by this Interaction Service instance to localize strings.
///
public ILocalizationManager LocalizationManager { get; set; }
private readonly ConcurrentDictionary _typedModuleDefs;
private readonly CommandMap _slashCommandMap;
private readonly ConcurrentDictionary> _contextCommandMaps;
private readonly CommandMap _componentCommandMap;
private readonly CommandMap _autocompleteCommandMap;
private readonly CommandMap _modalCommandMap;
private readonly HashSet _moduleDefs;
private readonly TypeMap _typeConverterMap;
private readonly TypeMap _compTypeConverterMap;
private readonly TypeMap _typeReaderMap;
private readonly ConcurrentDictionary _autocompleteHandlers = new();
private readonly ConcurrentDictionary _modalInfos = new();
private readonly SemaphoreSlim _lock;
internal readonly Logger _cmdLogger;
internal readonly LogManager _logManager;
internal readonly Func _getRestClient;
internal readonly bool _throwOnError, _useCompiledLambda, _enableAutocompleteHandlers, _autoServiceScopes, _exitOnMissingModalField;
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();
///
/// Represents all Modal Commands loaded within .
///
public IReadOnlyCollection ModalCommands => _moduleDefs.SelectMany(x => x.ModalCommands).ToList();
///
/// Gets a collection of the cached classes that are referenced in registered s.
///
public IReadOnlyCollection Modals => ModalUtils.Modals;
///
/// 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);
_modalCommandMap = new CommandMap(this, config.InteractionCustomIdDelimiters);
_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;
_exitOnMissingModalField = config.ExitOnMissingModalField;
_enableAutocompleteHandlers = config.EnableAutocompleteHandlers;
_autoServiceScopes = config.AutoServiceScopes;
_restResponseCallback = config.RestResponseCallback;
LocalizationManager = config.LocalizationManager;
_typeConverterMap = new TypeMap(this, new ConcurrentDictionary
{
[typeof(TimeSpan)] = new TimeSpanConverter()
}, new ConcurrentDictionary
{
[typeof(IChannel)] = typeof(DefaultChannelConverter<>),
[typeof(IRole)] = typeof(DefaultRoleConverter<>),
[typeof(IAttachment)] = typeof(DefaultAttachmentConverter<>),
[typeof(IUser)] = typeof(DefaultUserConverter<>),
[typeof(IMentionable)] = typeof(DefaultMentionableConverter<>),
[typeof(IConvertible)] = typeof(DefaultValueConverter<>),
[typeof(Enum)] = typeof(EnumConverter<>),
[typeof(Nullable<>)] = typeof(NullableConverter<>)
});
_compTypeConverterMap = new TypeMap(this, new ConcurrentDictionary(),
new ConcurrentDictionary
{
[typeof(Array)] = typeof(DefaultArrayComponentConverter<>),
[typeof(IConvertible)] = typeof(DefaultValueComponentConverter<>),
[typeof(Nullable<>)] = typeof(NullableComponentConverter<>)
});
_typeReaderMap = new TypeMap(this, new ConcurrentDictionary(),
new ConcurrentDictionary
{
[typeof(IChannel)] = typeof(DefaultChannelReader<>),
[typeof(IRole)] = typeof(DefaultRoleReader<>),
[typeof(IUser)] = typeof(DefaultUserReader<>),
[typeof(IMessage)] = typeof(DefaultMessageReader<>),
[typeof(IConvertible)] = typeof(DefaultValueReader<>),
[typeof(Enum)] = typeof(EnumReader<>),
[typeof(Nullable<>)] = typeof(NullableReader<>)
});
}
///
/// Create and loads a using a builder factory.
///
/// Name of the module.
/// The for your dependency injection solution if using one; otherwise, pass .
/// 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 .
///
/// 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 .
///
/// 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 .
///
/// 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", nameof(type));
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, true).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(true).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.
/// If , this operation will not delete the commands that are missing from .
/// 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 Task> AddCommandsToGuildAsync(IGuild guild, bool deleteMissing = false, params ICommandInfo[] commands)
{
if (guild is null)
throw new ArgumentNullException(nameof(guild));
return AddCommandsToGuildAsync(guild.Id, deleteMissing, commands);
}
///
/// 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 ID.
/// If , this operation will not delete the commands that are missing from .
/// 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(ulong guildId, bool deleteMissing = false, params ICommandInfo[] 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.GetGuildApplicationCommands(guildId, true).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 modules provided in to a guild.
///
/// The target guild.
/// If , this operation will not delete the commands that are missing from .
/// 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 Task> AddModulesToGuildAsync(IGuild guild, bool deleteMissing = false, params ModuleInfo[] modules)
{
if (guild is null)
throw new ArgumentNullException(nameof(guild));
return AddModulesToGuildAsync(guild.Id, deleteMissing, modules);
}
///
/// Register Application Commands from modules provided in to a guild.
///
/// The target guild ID.
/// If , this operation will not delete the commands that are missing from .
/// 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(ulong guildId, bool deleteMissing = false, params ModuleInfo[] modules)
{
EnsureClientReady();
var props = modules.SelectMany(x => x.ToApplicationCommandProps(true)).Distinct().ToList();
if (!deleteMissing)
{
var existing = await RestClient.GetGuildApplicationCommands(guildId, true).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 modules provided in as global commands.
///
/// If , this operation will not delete the commands that are missing from .
/// 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)).Distinct().ToList();
if (!deleteMissing)
{
var existing = await RestClient.GetGlobalApplicationCommands(true).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.
///
/// If , this operation will not delete the commands that are missing from .
/// 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(true).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 command in module.ModalCommands)
_modalCommandMap.AddCommand(command, command.IgnoreGroupNames);
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();
}
}
///
/// Unregister Application Commands from modules provided in from a guild.
///
/// The target guild.
/// Modules to be deregistered from Discord.
///
/// A task representing the command de-registration process. The task result contains the active application commands of the target guild.
///
public Task> RemoveModulesFromGuildAsync(IGuild guild, params ModuleInfo[] modules)
{
if (guild is null)
throw new ArgumentNullException(nameof(guild));
return RemoveModulesFromGuildAsync(guild.Id, modules);
}
///
/// Unregister Application Commands from modules provided in from a guild.
///
/// The target guild ID.
/// Modules to be deregistered from Discord.
///
/// A task representing the command de-registration process. The task result contains the active application commands of the target guild.
///
public async Task> RemoveModulesFromGuildAsync(ulong guildId, params ModuleInfo[] modules)
{
EnsureClientReady();
var exclude = modules.SelectMany(x => x.ToApplicationCommandProps(true)).ToList();
var existing = await RestClient.GetGuildApplicationCommands(guildId).ConfigureAwait(false);
var props = existing.Where(x => !exclude.Any(y => y.Name.IsSpecified && x.Name == y.Name.Value)).Select(x => x.ToApplicationCommandProps());
return await RestClient.BulkOverwriteGuildCommands(props.ToArray(), guildId).ConfigureAwait(false);
}
private bool RemoveModuleInternal(ModuleInfo moduleInfo)
{
if (!_moduleDefs.Remove(moduleInfo))
return false;
foreach (var command in moduleInfo.SlashCommands)
{
_slashCommandMap.RemoveCommand(command);
}
return true;
}
///
/// Search the registered slash commands using a .
///
/// Interaction entity to perform the search with.
///
/// The search result. When successful, result contains the found .
///
public SearchResult SearchSlashCommand(ISlashCommandInteraction slashCommandInteraction)
=> _slashCommandMap.GetCommand(slashCommandInteraction.Data.GetCommandKeywords());
///
/// Search the registered slash commands using a .
///
/// Interaction entity to perform the search with.
///
/// The search result. When successful, result contains the found .
///
public SearchResult SearchComponentCommand(IComponentInteraction componentInteraction)
=> _componentCommandMap.GetCommand(componentInteraction.Data.CustomId);
///
/// Search the registered slash commands using a .
///
/// Interaction entity to perform the search with.
///
/// The search result. When successful, result contains the found .
///
public SearchResult SearchUserCommand(IUserCommandInteraction userCommandInteraction)
=> _contextCommandMaps[ApplicationCommandType.User].GetCommand(userCommandInteraction.Data.Name);
///
/// Search the registered slash commands using a .
///
/// Interaction entity to perform the search with.
///
/// The search result. When successful, result contains the found .
///
public SearchResult SearchMessageCommand(IMessageCommandInteraction messageCommandInteraction)
=> _contextCommandMaps[ApplicationCommandType.Message].GetCommand(messageCommandInteraction.Data.Name);
///
/// Search the registered slash commands using a .
///
/// Interaction entity to perform the search with.
///
/// The search result. When successful, result contains the found .
///
public SearchResult SearchAutocompleteCommand(IAutocompleteInteraction autocompleteInteraction)
{
var keywords = autocompleteInteraction.Data.GetCommandKeywords();
keywords.Add(autocompleteInteraction.Data.Current.Name);
return _autocompleteCommandMap.GetCommand(keywords);
}
///
/// 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),
IModalInteraction modalCommand => await ExecuteModalCommandAsync(context, modalCommand.Data.CustomId, 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;
}
SetMatchesIfApplicable(context, result);
return await result.Command.ExecuteAsync(context, services).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)
{
if (autocompleteHandlerResult.Command._flattenedParameterDictionary.TryGetValue(interaction.Data.Current.Name, out var parameter) && 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);
}
private async Task ExecuteModalCommandAsync(IInteractionContext context, string input, IServiceProvider services)
{
var result = _modalCommandMap.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;
}
SetMatchesIfApplicable(context, result);
return await result.Command.ExecuteAsync(context, services).ConfigureAwait(false);
}
private static void SetMatchesIfApplicable(IInteractionContext context, SearchResult searchResult)
where T : class, ICommandInfo
{
if (!searchResult.Command.SupportsWildCards || context is not IRouteMatchContainer matchContainer)
return;
if (searchResult.RegexCaptureGroups?.Length > 0)
{
var matches = new RouteSegmentMatch[searchResult.RegexCaptureGroups.Length];
for (var i = 0; i < searchResult.RegexCaptureGroups.Length; i++)
matches[i] = new RouteSegmentMatch(searchResult.RegexCaptureGroups[i]);
matchContainer.SetSegmentMatches(matches);
}
else
matchContainer.SetSegmentMatches(Array.Empty());
}
internal TypeConverter GetTypeConverter(Type type, IServiceProvider services = null)
=> _typeConverterMap.Get(type, services);
///
/// Add a concrete type .
///
/// Primary target of the .
/// The instance.
public void AddTypeConverter(TypeConverter converter) =>
_typeConverterMap.AddConcrete(converter);
///
/// Add a concrete type .
///
/// Primary target of the .
/// The instance.
public void AddTypeConverter(Type type, TypeConverter converter) =>
_typeConverterMap.AddConcrete(type, converter);
///
/// Add a generic type .
///
/// Generic Type constraint of the of the .
/// Type of the .
public void AddGenericTypeConverter(Type converterType) =>
_typeConverterMap.AddGeneric(converterType);
///
/// Add a generic type .
///
/// Generic Type constraint of the of the .
/// Type of the .
public void AddGenericTypeConverter(Type targetType, Type converterType) =>
_typeConverterMap.AddGeneric(targetType, converterType);
internal ComponentTypeConverter GetComponentTypeConverter(Type type, IServiceProvider services = null) =>
_compTypeConverterMap.Get(type, services);
///
/// Add a concrete type .
///
/// Primary target of the .
/// The instance.
public void AddComponentTypeConverter(ComponentTypeConverter converter) =>
AddComponentTypeConverter(typeof(T), converter);
///
/// Add a concrete type .
///
/// Primary target of the .
/// The instance.
public void AddComponentTypeConverter(Type type, ComponentTypeConverter converter) =>
_compTypeConverterMap.AddConcrete(type, converter);
///
/// Add a generic type .
///
/// Generic Type constraint of the of the .
/// Type of the .
public void AddGenericComponentTypeConverter(Type converterType) =>
AddGenericComponentTypeConverter(typeof(T), converterType);
///
/// Add a generic type .
///
/// Generic Type constraint of the of the .
/// Type of the .
public void AddGenericComponentTypeConverter(Type targetType, Type converterType) =>
_compTypeConverterMap.AddGeneric(targetType, converterType);
internal TypeReader GetTypeReader(Type type, IServiceProvider services = null) =>
_typeReaderMap.Get(type, services);
///
/// Add a concrete type .
///
/// Primary target of the .
/// The instance.
public void AddTypeReader(TypeReader reader) =>
AddTypeReader(typeof(T), reader);
///
/// Add a concrete type .
///
/// Primary target of the .
/// The instance.
public void AddTypeReader(Type type, TypeReader reader) =>
_typeReaderMap.AddConcrete(type, reader);
///
/// Add a generic type .
///
/// Generic Type constraint of the of the .
/// Type of the .
public void AddGenericTypeReader(Type readerType) =>
AddGenericTypeReader(typeof(T), readerType);
///
/// Add a generic type .
///
/// Generic Type constraint of the of the .
/// Type of the .
public void AddGenericTypeReader(Type targetType, Type readerType) =>
_typeReaderMap.AddGeneric(targetType, readerType);
///
/// Removes a type reader for the type .
///
/// The type to remove the readers from.
/// The reader if the resulting remove operation was successful.
/// if the remove operation was successful; otherwise .
public bool TryRemoveTypeReader(out TypeReader reader)
=> TryRemoveTypeReader(typeof(T), out reader);
///
/// Removes a type reader for the given type.
///
///
/// Removing a from the will not dereference the from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
///
/// The type to remove the reader from.
/// The reader if the resulting remove operation was successful.
/// if the remove operation was successful; otherwise .
public bool TryRemoveTypeReader(Type type, out TypeReader reader)
=> _typeReaderMap.TryRemoveConcrete(type, out reader);
///
/// Removes a generic type reader from the type .
///
///
/// Removing a from the will not dereference the from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
///
/// The type to remove the readers from.
/// The removed readers type.
/// if the remove operation was successful; otherwise .
public bool TryRemoveGenericTypeReader(out Type readerType)
=> TryRemoveGenericTypeReader(typeof(T), out readerType);
///
/// Removes a generic type reader from the given type.
///
///
/// Removing a from the will not dereference the from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
///
/// The type to remove the reader from.
/// The readers type if the remove operation was successful.
/// if the remove operation was successful; otherwise .
public bool TryRemoveGenericTypeReader(Type type, out Type readerType)
=> _typeReaderMap.TryRemoveGeneric(type, out readerType);
///
/// Serialize an object using a into a to be placed in a Component CustomId.
///
///
/// Removing a from the will not dereference the from the loaded module/command instances.
/// You need to reload the modules for the changes to take effect.
///
/// Type of the object to be serialized.
/// Object to be serialized.
/// Services that will be passed on to the .
///
/// A task representing the conversion process. The task result contains the result of the conversion.
///
public Task SerializeValueAsync(T obj, IServiceProvider services) =>
_typeReaderMap.Get(typeof(T), services).SerializeAsync(obj, services);
///
/// Serialize and format multiple objects into a Custom Id string.
///
/// A composite format string.
/// >Services that will be passed on to the s.
/// Objects to be serialized.
///
/// A task representing the conversion process. The task result contains the result of the conversion.
///
public async Task GenerateCustomIdStringAsync(string format, IServiceProvider services, params object[] args)
{
var serializedValues = new string[args.Length];
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
var typeReader = _typeReaderMap.Get(arg.GetType(), null);
var result = await typeReader.SerializeAsync(arg, services).ConfigureAwait(false);
serializedValues[i] = result;
}
return string.Format(format, serializedValues);
}
///
/// Loads and caches an for the provided .
///
/// Type of to be loaded.
///
/// The built instance.
///
///
public ModalInfo AddModalInfo() where T : class, IModal
{
var type = typeof(T);
if (_modalInfos.ContainsKey(type))
throw new InvalidOperationException($"Modal type {type.FullName} already exists.");
return ModalUtils.GetOrAdd(type, this);
}
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 Task ModifySlashCommandPermissionsAsync(ModuleInfo module, IGuild guild,
params ApplicationCommandPermission[] permissions)
{
if (module is null)
throw new ArgumentNullException(nameof(module));
if (guild is null)
throw new ArgumentNullException(nameof(guild));
return ModifySlashCommandPermissionsAsync(module, guild.Id, permissions);
}
///
/// Modify the command permissions of the matching Discord Slash Command.
///
/// Module representing the top level Slash Command.
/// Target guild ID.
/// New permission values.
///
/// The active command permissions after the modification.
///
public async Task ModifySlashCommandPermissionsAsync(ModuleInfo module, ulong guildId,
params ApplicationCommandPermission[] permissions)
{
if (module is null)
throw new ArgumentNullException(nameof(module));
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");
var commands = await RestClient.GetGuildApplicationCommands(guildId).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 Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, IGuild guild,
params ApplicationCommandPermission[] permissions)
{
if (command is null)
throw new ArgumentNullException(nameof(command));
if (guild is null)
throw new ArgumentNullException(nameof(guild));
return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions);
}
///
/// Modify the command permissions of the matching Discord Slash Command.
///
/// The Slash Command.
/// Target guild ID.
/// New permission values.
///
/// The active command permissions after the modification.
///
public Task ModifySlashCommandPermissionsAsync(SlashCommandInfo command, ulong guildId,
params ApplicationCommandPermission[] permissions)
=> ModifyApplicationCommandPermissionsAsync(command, guildId, permissions);
///
/// 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 Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, IGuild guild,
params ApplicationCommandPermission[] permissions)
{
if (command is null)
throw new ArgumentNullException(nameof(command));
if (guild is null)
throw new ArgumentNullException(nameof(guild));
return ModifyApplicationCommandPermissionsAsync(command, guild.Id, permissions);
}
///
/// Modify the command permissions of the matching Discord Slash Command.
///
/// The Context Command.
/// Target guild ID.
/// New permission values.
///
/// The active command permissions after the modification.
///
public Task ModifyContextCommandPermissionsAsync(ContextCommandInfo command, ulong guildId,
params ApplicationCommandPermission[] permissions)
=> ModifyApplicationCommandPermissionsAsync(command, guildId, permissions);
private async Task ModifyApplicationCommandPermissionsAsync(T command, ulong guildId,
params ApplicationCommandPermission[] permissions) where T : class, IApplicationCommandInfo, ICommandInfo
{
if (command is null)
throw new ArgumentNullException(nameof(command));
if (!command.IsTopLevelCommand)
throw new InvalidOperationException("This command is not a top level application command. You cannot change its permissions");
var commands = await RestClient.GetGuildApplicationCommands(guildId).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", nameof(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 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");
}
}
}