Beispiel #1
0
        /// <inheritdoc/>
        public ICommandInstance InitializeCommand(ICommandInstanceDescriptor descriptor, CommandsOptions options)
        {
            // validate this is a correct command attribute type
            if (!(descriptor.Attribute is RegexCommandAttribute regexCommand))
            {
                throw new ArgumentException($"{this.GetType().Name} can only be used with {typeof(RegexCommandAttribute).Name} commands", nameof(descriptor.Attribute));
            }

            // if pattern starts with ^, replace it with \G
            // this will ensure it'll match start of the string when starting from index after prefix
            string pattern = regexCommand.Pattern;

            if (pattern.Length > 0 && pattern[0] == '^')
            {
                pattern = $@"\G{pattern.Substring(1)}";
            }

            // init instance
            return(new RegexCommandInstance(
                       pattern: pattern,
                       regexOptions: regexCommand.Options,
                       method: descriptor.Method,
                       requirements: descriptor.GetRequirements(),
                       prefixOverride: descriptor.GetPrefixOverride(),
                       prefixRequirementOverride: descriptor.GetPrefixRequirementOverride(),
                       caseSensitivityOverride: descriptor.GetCaseSensitivityOverride()));
        }
Beispiel #2
0
        /// <inheritdoc/>
        public ICommandInstance InitializeCommand(ICommandInstanceDescriptor descriptor, CommandsOptions options)
        {
            // validate this is a correct command attribute type
            if (!(descriptor.Attribute is CommandAttribute command))
            {
                throw new ArgumentException($"{this.GetType().Name} can only be used with {typeof(CommandAttribute).Name} commands", nameof(descriptor.Attribute));
            }

            // init instance
            return(new StandardCommandInstance(
                       text: command.Text,
                       method: descriptor.Method,
                       requirements: descriptor.GetRequirements(),
                       prefixOverride: descriptor.GetPrefixOverride(),
                       prefixRequirementOverride: descriptor.GetPrefixRequirementOverride(),
                       caseSensitivityOverride: descriptor.GetCaseSensitivityOverride()));
        }
        private async Task <ICommandResult> ExecuteAsyncInternal(ICommandContext context, CancellationToken cancellationToken = default)
        {
            if (!this._started)
            {
                throw new InvalidOperationException($"This {this.GetType().Name} is not started yet");
            }
            using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this._cts.Token))
            {
                IEnumerable <KeyValuePair <ICommandInstanceDescriptor, ICommandInstance> > commandsCopy;
                // copying might not be the fastest thing to do, but it'll ensure that commands won't be changed out of the lock
                // locking only copying to prevent hangs if user is not careful with their commands, while still preventing race conditions with StartAsync
                await this._lock.WaitAsync(cts.Token).ConfigureAwait(false);

                try
                {
                    // order commands by priority
                    // try to get from concrete Descriptor if possible, as it should be precached and avoid additional reflection and thus faster
                    commandsCopy = this._commands.OrderByDescending(kvp => (kvp.Key is CommandInstanceDescriptor cid) ? cid.Priority : kvp.Key.GetPriority());
                }
                finally
                {
                    this._lock.Release();
                }

                using (IServiceScope serviceScope = this._services.CreateScope())
                {
                    // Because scope won't have fallback services, and commands might need them, combine scope with fallback services
                    // TODO: think of a nicer way to solve fallback services problem - I don't like it as it is now
                    IServiceProvider services = serviceScope.ServiceProvider;
                    if (!object.ReferenceEquals(services, this._fallbackServices))
                    {
                        services = CombinedServiceProvider.Combine(serviceScope.ServiceProvider, this._fallbackServices);
                    }

                    foreach (KeyValuePair <ICommandInstanceDescriptor, ICommandInstance> commandKvp in commandsCopy)
                    {
                        ICommandInstanceDescriptor command  = commandKvp.Key;
                        ICommandInstance           instance = commandKvp.Value;
                        using (this._log.BeginCommandScope(context, command.GetHandlerType().Name, command.Method.Name))
                        {
                            ICommandsHandlerProviderResult handlerResult = null;
                            try
                            {
                                cts.Token.ThrowIfCancellationRequested();

                                // check if the command should run at all - if not, skip
                                ICommandResult matchResult = await instance.CheckMatchAsync(context, services, cts.Token).ConfigureAwait(false);

                                if (!matchResult.IsSuccess)
                                {
                                    continue;
                                }

                                // initialize handler
                                this._log?.LogTrace("Initializing handler handler {Handler} for command {Name}", command.GetHandlerType().Name, command.Method.Name);
                                handlerResult = this._handlerProvider.GetCommandHandler(command, services);
                                if (handlerResult?.HandlerInstance == null)
                                {
                                    this._log?.LogError("Retrieving handler {Handler} for command {Name} has failed, command execution aborting", command.GetHandlerType().Name, command.Method.Name);
                                    return(CommandExecutionResult.FromException(new ArgumentNullException(nameof(ICommandsHandlerProviderResult.HandlerInstance),
                                                                                                          $"Retrieving handler {command.GetHandlerType().Name} for command {command.Method.Name} has failed, command execution aborting")));
                                }
                                this._log?.LogTrace("Executing command {Name} from handler {Handler}", command.Method.Name, command.GetHandlerType().Name);

                                // execute the command
                                ICommandResult executeResult = await instance.ExecuteAsync(context, services, matchResult, handlerResult.HandlerInstance, cts.Token).ConfigureAwait(false);

                                if (executeResult.Exception != null)
                                {
                                    this._log?.LogError(executeResult.Exception, "Exception when executing command {Name} from handler {Handler}", command.Method.Name, command.GetHandlerType().Name);
                                    return(executeResult);
                                }
                                if (executeResult is IMessagesCommandResult messagesResult && messagesResult.Messages?.Any() == true)
                                {
                                    this._log?.LogTrace("Sending command results messages as a command response");
                                    await context.ReplyTextAsync(string.Join("\n", messagesResult.Messages), cts.Token).ConfigureAwait(false);
                                }
                                return(executeResult);
                            }
                            // special error case: operation canceled
                            // operation canceled is normal, so it shouldn't be logged as error
                            catch (OperationCanceledException ex)
                            {
                                this._log?.LogWarning("Execution of command {Name} from handler {Handler} was cancelled", command.Method.Name, command.GetHandlerType().Name);
                                return(CommandExecutionResult.FromException(ex));
                            }
                            // special error case: responding when silenced
                            // bots almost always respond to a command - but if they're silenced, an exception will be thrown
                            // this is normal - so it shouldn't be logged as error. Warning max
                            catch (MessageSendingException ex) when
                                (this.LogSilencedException(ex, context, "Unhandled Exception when executing command {Name} from handler {Handler} - likely due to being silenced or spam filtered", command.Method.Name, command.GetHandlerType().Name))
                            {
                                return(CommandExecutionResult.FromException(ex));
                            }
                            // normal error case
                            catch (Exception ex) when(ex.LogAsError(_log, "Unhandled Exception when executing command {Name} from handler {Handler}", command.Method.Name, command.GetHandlerType().Name))
                            {
                                return(CommandExecutionResult.FromException(ex));
                            }
                            finally
                            {
                                // if handler is allocated, not persistent and disposable, let's dispose it
                                if (handlerResult?.Descriptor?.Attribute?.IsPersistent != true && handlerResult?.HandlerInstance is IDisposable disposableHandler)
                                {
                                    try { disposableHandler?.Dispose(); } catch { }
                                }
                            }
                        }
                    }
                }
                return(CommandExecutionResult.Failure);
            }
        }
 /// <inheritdoc/>
 public bool Equals(ICommandInstanceDescriptor other)
 {
     return(other != null &&
            EqualityComparer <CommandAttributeBase> .Default.Equals(Attribute, other.Attribute) &&
            EqualityComparer <MethodInfo> .Default.Equals(Method, other.Method));
 }
 /// <summary>Gets command's prefix requirement override.</summary>
 /// <param name="descriptor">Command descriptor.</param>
 /// <returns>Prefix requirement value.</returns>
 public static PrefixRequirement?GetPrefixRequirementOverride(this ICommandInstanceDescriptor descriptor)
 => descriptor.Method.GetCustomAttribute <PrefixAttribute>(true)?.PrefixRequirementOverride ??
 descriptor.GetHandlerType().GetCustomAttribute <PrefixAttribute>(true)?.PrefixRequirementOverride;
 /// <summary>Gets command's pre-execute checks.</summary>
 /// <param name="descriptor">Command descriptor.</param>
 /// <returns>Enumerable on all pre-execute checks on the command's method and handler type.</returns>
 public static IEnumerable <CommandRequirementAttribute> GetRequirements(this ICommandInstanceDescriptor descriptor)
 => descriptor.Method.GetCustomAttributes <CommandRequirementAttribute>(true)
 .Union(descriptor.GetHandlerType().GetCustomAttributes <CommandRequirementAttribute>(true));
 /// <summary>Checks if the command is overriding default case sensitivity.</summary>
 /// <remarks>See <see cref="CaseSensitivityAttribute"/> for more information about command case senstivity.</remarks>
 /// <returns>True/false if command is overriding case sensitivity - true to be case sensitive, false to be case insensitive; null if not overriding.</returns>
 public static bool?GetCaseSensitivityOverride(this ICommandInstanceDescriptor descriptor)
 => descriptor.Method.GetCustomAttribute <CaseSensitivityAttribute>(true)?.CaseSensitive ??
 descriptor.GetHandlerType().GetCustomAttribute <CaseSensitivityAttribute>(true)?.CaseSensitive;
 /// <summary>Gets type of the command's handler.</summary>
 /// <param name="descriptor">Command descriptor.</param>
 /// <returns>Type of command's handler.</returns>
 public static Type GetHandlerType(this ICommandInstanceDescriptor descriptor)
 => descriptor.Method.DeclaringType;
 /// <summary>Gets command's priority.</summary>
 /// <remarks>See <see cref="PriorityAttribute"/> for more information about command priorities.</remarks>
 /// <param name="descriptor">Command descriptor.</param>
 /// <returns>Command's priority value.</returns>
 /// <seealso cref="PriorityAttribute"/>
 public static int GetPriority(this ICommandInstanceDescriptor descriptor)
 // on-method priority overwrites handler priority. Default is 0.
 => descriptor.Method.GetCustomAttribute <PriorityAttribute>()?.Priority ??
 GetHandlerType(descriptor).GetCustomAttribute <PriorityAttribute>()?.Priority ??
 0;
 /// <summary>Retrieves CommandHandler attribute for the command's handler.</summary>
 /// <param name="descriptor">Command descriptor.</param>
 /// <returns>CommandHandler attribute present on the command's handler type; null if not found.</returns>
 public static CommandsHandlerAttribute GetHandlerAttribute(this ICommandInstanceDescriptor descriptor)
 => GetHandlerType(descriptor).GetCustomAttribute <CommandsHandlerAttribute>(true);
        /// <inheritdoc/>
        public ICommandsHandlerProviderResult GetCommandHandler(ICommandInstanceDescriptor descriptor, IServiceProvider services)
        {
            Type handlerType = descriptor.GetHandlerType();
            CommandHandlerDescriptor handlerDescriptor = null;

            lock (_lock)
            {
                // check if persistent
                if (_persistentHandlers.TryGetValue(handlerType, out CommandsHandlerProviderResult handler))
                {
                    return(handler);
                }

                // keep resolved services to avoid recreating them in case of multiple constructors
                IDictionary <Type, object> resolvedServices = new Dictionary <Type, object>();

                // if no persistent handler was found, we need to create a new one - check if constructor is known yet
                if (!_knownConstructors.TryGetValue(handlerType, out ConstructorInfo handlerConstructor))
                {
                    // if descriptor not cached, create new one
                    // start with grabbing all constructors
                    IEnumerable <ConstructorInfo> allConstructors = handlerType
                                                                    .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

                    // check if any of the constructors are specifically designated to be used by Commands System
                    IEnumerable <ConstructorInfo> selectedConstructors = allConstructors
                                                                         .Select(ctor => (constructor: ctor, attribute: ctor.GetCustomAttribute <CommandsHandlerConstructorAttribute>(false)))
                                                                         .Where(ctor => ctor.attribute != null)
                                                                         .OrderByDescending(ctor => ctor.attribute.Priority)
                                                                         .ThenByDescending(ctor => ctor.constructor.GetParameters().Length)
                                                                         .Select(ctor => ctor.constructor);

                    // if no explicitly-attributed constructor found, grab all that are public
                    if (!selectedConstructors.Any())
                    {
                        selectedConstructors = allConstructors
                                               .Where(ctor => ctor.IsPublic)
                                               .OrderByDescending(ctor => ctor.GetParameters().Length);
                    }

                    // try to resolve dependencies for each constructor. First one that can be resolved wins
                    foreach (ConstructorInfo ctor in selectedConstructors)
                    {
                        if (TryCreateHandlerDescriptor(ctor, services, out handlerDescriptor, ref resolvedServices))
                        {
                            // cache found descriptor
                            _knownConstructors.Add(handlerType, ctor);
                            handlerConstructor = ctor;
                            break;
                        }
                    }
                    // throw if we didn't find any constructor we can resolve
                    if (handlerDescriptor == null)
                    {
                        throw new InvalidOperationException($"Cannot create descriptor for type {handlerType.FullName} - none of the constructors can have its dependencies resolved");
                    }
                }
                // if constructor is already known, just create a descriptor
                else
                {
                    TryCreateHandlerDescriptor(handlerConstructor, services, out handlerDescriptor, ref resolvedServices);
                }

                // now that we have a descriptor, let's create an instance
                handler = new CommandsHandlerProviderResult(handlerDescriptor, handlerDescriptor.CreateInstance());

                // if it's a persistent instance, store it
                if (handlerDescriptor.IsPersistent())
                {
                    _persistentHandlers.Add(handlerType, handler);
                }

                // finally, return the fresh handler
                return(handler);
            }
        }