using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Discord.Commands.Builders; using Discord.Logging; namespace Discord.Commands { /// /// Provides a framework for building Discord commands. /// /// /// /// The service provides a framework for building Discord commands both dynamically via runtime builders or /// statically via compile-time modules. To create a command module at compile-time, see /// (most common); otherwise, see . /// /// /// This service also provides several events for monitoring command usages; such as /// for any command-related log events, and /// for information about commands that have /// been successfully executed. /// /// public class CommandService : IDisposable { /// /// Occurs when a command-related information is received. /// public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); /// /// Occurs when a command is executed. /// /// /// This event is fired when a command has been executed, successfully or not. When a command fails to /// execute during parsing or precondition stage, the CommandInfo may not be returned. /// public event Func, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } internal readonly AsyncEvent, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent, ICommandContext, IResult, Task>>(); private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _typedModuleDefs; private readonly ConcurrentDictionary> _typeReaders; private readonly ConcurrentDictionary _defaultTypeReaders; private readonly ImmutableList<(Type EntityType, Type TypeReaderType)> _entityTypeReaders; private readonly HashSet _moduleDefs; private readonly CommandMap _map; internal readonly bool _caseSensitive, _throwOnError, _ignoreExtraArgs; internal readonly char _separatorChar; internal readonly RunMode _defaultRunMode; internal readonly Logger _cmdLogger; internal readonly LogManager _logManager; internal readonly IReadOnlyDictionary _quotationMarkAliasMap; internal bool _isDisposed; /// /// Represents all modules loaded within . /// public IEnumerable Modules => _moduleDefs.Select(x => x); /// /// Represents all commands loaded within . /// public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); /// /// Represents all loaded within . /// public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); /// /// Initializes a new class. /// public CommandService() : this(new CommandServiceConfig()) { } /// /// Initializes a new class with the provided configuration. /// /// The configuration class. /// /// The cannot be set to . /// public CommandService(CommandServiceConfig config) { _caseSensitive = config.CaseSensitiveCommands; _throwOnError = config.ThrowOnError; _ignoreExtraArgs = config.IgnoreExtraArgs; _separatorChar = config.SeparatorChar; _defaultRunMode = config.DefaultRunMode; _quotationMarkAliasMap = (config.QuotationMarkAliasMap ?? new Dictionary()).ToImmutableDictionary(); if (_defaultRunMode == RunMode.Default) throw new InvalidOperationException("The default run mode cannot be set to Default."); _logManager = new LogManager(config.LogLevel); _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); _cmdLogger = _logManager.CreateLogger("Command"); _moduleLock = new SemaphoreSlim(1, 1); _typedModuleDefs = new ConcurrentDictionary(); _moduleDefs = new HashSet(); _map = new CommandMap(this); _typeReaders = new ConcurrentDictionary>(); _defaultTypeReaders = new ConcurrentDictionary(); foreach (var type in PrimitiveParsers.SupportedTypes) { _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); _defaultTypeReaders[typeof(Nullable<>).MakeGenericType(type)] = NullableTypeReader.Create(type, _defaultTypeReaders[type]); } var tsreader = new TimeSpanTypeReader(); _defaultTypeReaders[typeof(TimeSpan)] = tsreader; _defaultTypeReaders[typeof(TimeSpan?)] = NullableTypeReader.Create(typeof(TimeSpan), tsreader); _defaultTypeReaders[typeof(string)] = new PrimitiveTypeReader((string x, out string y) => { y = x; return true; }, 0); var entityTypeReaders = ImmutableList.CreateBuilder<(Type, Type)>(); entityTypeReaders.Add((typeof(IMessage), typeof(MessageTypeReader<>))); entityTypeReaders.Add((typeof(IChannel), typeof(ChannelTypeReader<>))); entityTypeReaders.Add((typeof(IRole), typeof(RoleTypeReader<>))); entityTypeReaders.Add((typeof(IUser), typeof(UserTypeReader<>))); _entityTypeReaders = entityTypeReaders.ToImmutable(); } //Modules public async Task CreateModuleAsync(string primaryAlias, Action buildFunc) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { var builder = new ModuleBuilder(this, null, primaryAlias); buildFunc(builder); var module = builder.Build(this, null); return LoadModuleInternal(module); } finally { _moduleLock.Release(); } } /// /// Add a command module from a . /// /// /// The following example registers the module MyModule to commandService. /// /// await commandService.AddModuleAsync<MyModule>(serviceProvider); /// /// /// The type of module. /// The for your dependency injection solution if using one; otherwise, pass null. /// This module has already been added. /// /// The fails to be built; an invalid type may have been provided. /// /// /// A task that represents the asynchronous operation for adding the module. The task result contains the /// built module. /// public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); /// /// Adds a command module from a . /// /// The type of module. /// The for your dependency injection solution if using one; otherwise, pass null . /// This module has already been added. /// /// The fails to be built; an invalid type may have been provided. /// /// /// A task that represents the asynchronous operation for adding the module. The task result contains the /// built module. /// public async Task AddModuleAsync(Type type, IServiceProvider services) { services = services ?? EmptyServiceProvider.Instance; await _moduleLock.WaitAsync().ConfigureAwait(false); try { var typeInfo = type.GetTypeInfo(); if (_typedModuleDefs.ContainsKey(type)) throw new ArgumentException("This module has already been added."); var module = (await ModuleClassBuilder.BuildAsync(this, services, typeInfo).ConfigureAwait(false)).FirstOrDefault(); if (module.Value == default(ModuleInfo)) throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); _typedModuleDefs[module.Key] = module.Value; return LoadModuleInternal(module.Value); } finally { _moduleLock.Release(); } } /// /// Add command modules from an . /// /// The containing command modules. /// The for your dependency injection solution if using one; otherwise, pass null. /// /// A task that represents the asynchronous operation for adding the command modules. The task result /// contains an enumerable collection of modules added. /// public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) { services = services ?? EmptyServiceProvider.Instance; await _moduleLock.WaitAsync().ConfigureAwait(false); try { var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services).ConfigureAwait(false); foreach (var info in moduleDefs) { _typedModuleDefs[info.Key] = info.Value; LoadModuleInternal(info.Value); } return moduleDefs.Select(x => x.Value).ToImmutableArray(); } finally { _moduleLock.Release(); } } private ModuleInfo LoadModuleInternal(ModuleInfo module) { _moduleDefs.Add(module); foreach (var command in module.Commands) _map.AddCommand(command); foreach (var submodule in module.Submodules) LoadModuleInternal(submodule); return module; } /// /// Removes the 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 _moduleLock.WaitAsync().ConfigureAwait(false); try { return RemoveModuleInternal(module); } finally { _moduleLock.Release(); } } /// /// Removes the 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)); /// /// Removes the 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 _moduleLock.WaitAsync().ConfigureAwait(false); try { if (!_typedModuleDefs.TryRemove(type, out var module)) return false; return RemoveModuleInternal(module); } finally { _moduleLock.Release(); } } private bool RemoveModuleInternal(ModuleInfo module) { if (!_moduleDefs.Remove(module)) return false; foreach (var cmd in module.Commands) _map.RemoveCommand(cmd); foreach (var submodule in module.Submodules) { RemoveModuleInternal(submodule); } return true; } //Type Readers /// /// Adds a custom to this for the supplied object /// type. /// If is a , a nullable will /// also be added. /// If a default exists for , a warning will be logged /// and the default will be replaced. /// /// The object type to be read by the . /// An instance of the to be added. public void AddTypeReader(TypeReader reader) => AddTypeReader(typeof(T), reader); /// /// Adds a custom to this for the supplied object /// type. /// If is a , a nullable for the /// value type will also be added. /// If a default exists for , a warning will be logged and /// the default will be replaced. /// /// A instance for the type to be read. /// An instance of the to be added. public void AddTypeReader(Type type, TypeReader reader) { if (_defaultTypeReaders.ContainsKey(type)) _ = _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." + "To suppress this message, use AddTypeReader(reader, true)."); AddTypeReader(type, reader, true); } /// /// Adds a custom to this for the supplied object /// type. /// If is a , a nullable will /// also be added. /// /// The object type to be read by the . /// An instance of the to be added. /// /// Defines whether the should replace the default one for /// if it exists. /// public void AddTypeReader(TypeReader reader, bool replaceDefault) => AddTypeReader(typeof(T), reader, replaceDefault); /// /// Adds a custom to this for the supplied object /// type. /// If is a , a nullable for the /// value type will also be added. /// /// A instance for the type to be read. /// An instance of the to be added. /// /// Defines whether the should replace the default one for if /// it exists. /// public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) { if (replaceDefault && HasDefaultTypeReader(type)) { _defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader); if (type.GetTypeInfo().IsValueType) { var nullableType = typeof(Nullable<>).MakeGenericType(type); var nullableReader = NullableTypeReader.Create(type, reader); _defaultTypeReaders.AddOrUpdate(nullableType, nullableReader, (k, v) => nullableReader); } } else { var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); readers[reader.GetType()] = reader; if (type.GetTypeInfo().IsValueType) AddNullableTypeReader(type, reader); } } internal bool HasDefaultTypeReader(Type type) { if (_defaultTypeReaders.ContainsKey(type)) return true; var typeInfo = type.GetTypeInfo(); if (typeInfo.IsEnum) return true; return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.TypeReaderType)); } internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) { var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary()); var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); readers[nullableReader.GetType()] = nullableReader; } internal IDictionary GetTypeReaders(Type type) { if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) return definedTypeReaders; return null; } internal TypeReader GetDefaultTypeReader(Type type) { if (_defaultTypeReaders.TryGetValue(type, out var reader)) return reader; var typeInfo = type.GetTypeInfo(); //Is this an enum? if (typeInfo.IsEnum) { reader = EnumTypeReader.GetReader(type); _defaultTypeReaders[type] = reader; return reader; } //Is this an entity? for (int i = 0; i < _entityTypeReaders.Count; i++) { if (type == _entityTypeReaders[i].EntityType || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].EntityType)) { reader = Activator.CreateInstance(_entityTypeReaders[i].TypeReaderType.MakeGenericType(type)) as TypeReader; _defaultTypeReaders[type] = reader; return reader; } } return null; } //Execution /// /// Searches for the command. /// /// The context of the command. /// The position of which the command starts at. /// The result containing the matching commands. public SearchResult Search(ICommandContext context, int argPos) => Search(context.Message.Content.Substring(argPos)); /// /// Searches for the command. /// /// The context of the command. /// The command string. /// The result containing the matching commands. public SearchResult Search(ICommandContext context, string input) => Search(input); public SearchResult Search(string input) { string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); if (matches.Length > 0) return SearchResult.FromSuccess(input, matches); else return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } /// /// Executes the command. /// /// The context of the command. /// The position of which the command starts at. /// The service to be used in the command's dependency injection. /// The handling mode when multiple command matches are found. /// /// A task that represents the asynchronous execution operation. The task result contains the result of the /// command execution. /// public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); /// /// Executes the command. /// /// The context of the command. /// The command string. /// The service to be used in the command's dependency injection. /// The handling mode when multiple command matches are found. /// /// A task that represents the asynchronous execution operation. The task result contains the result of the /// command execution. /// public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { services = services ?? EmptyServiceProvider.Instance; var searchResult = Search(input); if (!searchResult.IsSuccess) { await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); return searchResult; } var commands = searchResult.Commands; var preconditionResults = new Dictionary(); foreach (var match in commands) { preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); } var successfulPreconditions = preconditionResults .Where(x => x.Value.IsSuccess) .ToArray(); if (successfulPreconditions.Length == 0) { //All preconditions failed, return the one from the highest priority command var bestCandidate = preconditionResults .OrderByDescending(x => x.Key.Command.Priority) .FirstOrDefault(x => !x.Value.IsSuccess); await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false); return bestCandidate.Value; } //If we get this far, at least one precondition was successful. var parseResultsDict = new Dictionary(); foreach (var pair in successfulPreconditions) { var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); if (parseResult.Error == CommandError.MultipleMatches) { IReadOnlyList argList, paramList; switch (multiMatchHandling) { case MultiMatchHandling.Best: argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); parseResult = ParseResult.FromSuccess(argList, paramList); break; } } parseResultsDict[pair.Key] = parseResult; } // Calculates the 'score' of a command given a parse result float CalculateScore(CommandMatch match, ParseResult parseResult) { float argValuesScore = 0, paramValuesScore = 0; if (match.Command.Parameters.Count > 0) { var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; argValuesScore = argValuesSum / match.Command.Parameters.Count; paramValuesScore = paramValuesSum / match.Command.Parameters.Count; } var totalArgsScore = (argValuesScore + paramValuesScore) / 2; return match.Command.Priority + totalArgsScore * 0.99f; } //Order the parse results by their score so that we choose the most likely result to execute var parseResults = parseResultsDict .OrderByDescending(x => CalculateScore(x.Key, x.Value)); var successfulParses = parseResults .Where(x => x.Value.IsSuccess) .ToArray(); if (successfulParses.Length == 0) { //All parses failed, return the one from the highest priority command, using score as a tie breaker var bestMatch = parseResults .FirstOrDefault(x => !x.Value.IsSuccess); await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false); return bestMatch.Value; } //If we get this far, at least one parse was successful. Execute the most likely overload. var chosenOverload = successfulParses[0]; var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution) await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); return result; } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { if (disposing) { _moduleLock?.Dispose(); } _isDisposed = true; } } void IDisposable.Dispose() { Dispose(true); } } }