using System; using System.Linq; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; using Discord.Commands.Builders; namespace Discord.Commands { internal static class ModuleClassBuilder { private static readonly TypeInfo ModuleTypeInfo = typeof(IModuleBase).GetTypeInfo(); public static async Task> SearchAsync(Assembly assembly, CommandService service) { bool IsLoadableModule(TypeInfo info) { return info.DeclaredMethods.Any(x => x.GetCustomAttribute() != null) && info.GetCustomAttribute() == null; } var result = new List(); foreach (var typeInfo in assembly.DefinedTypes) { if (typeInfo.IsPublic || typeInfo.IsNestedPublic) { if (IsValidModuleDefinition(typeInfo) && !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) { result.Add(typeInfo); } } else if (IsLoadableModule(typeInfo)) { await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}.").ConfigureAwait(false); } } return result; } public static Task> BuildAsync(CommandService service, IServiceProvider services, params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services); public static async Task> BuildAsync(IEnumerable validTypes, CommandService service, IServiceProvider services) { /*if (!validTypes.Any()) throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); var builtTypes = new List(); var result = new Dictionary(); foreach (var typeInfo in topLevelGroups) { // TODO: This shouldn't be the case; may be safe to remove? if (result.ContainsKey(typeInfo.AsType())) continue; var module = new ModuleBuilder(service, null); BuildModule(module, typeInfo, service, services); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); builtTypes.Add(typeInfo); result[typeInfo.AsType()] = module.Build(service, services); } await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); return result; } private static void BuildSubTypes(ModuleBuilder builder, IEnumerable subTypes, List builtTypes, CommandService service, IServiceProvider services) { foreach (var typeInfo in subTypes) { if (!IsValidModuleDefinition(typeInfo)) continue; if (builtTypes.Contains(typeInfo)) continue; builder.AddModule((module) => { BuildModule(module, typeInfo, service, services); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); }); builtTypes.Add(typeInfo); } } private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service, IServiceProvider services) { var attributes = typeInfo.GetCustomAttributes(); builder.TypeInfo = typeInfo; foreach (var attribute in attributes) { switch (attribute) { case NameAttribute name: builder.Name = name.Text; break; case SummaryAttribute summary: builder.Summary = summary.Text; break; case RemarksAttribute remarks: builder.Remarks = remarks.Text; break; case AliasAttribute alias: builder.AddAliases(alias.Aliases); break; case GroupAttribute group: builder.Name = builder.Name ?? group.Prefix; builder.Group = group.Prefix; builder.AddAliases(group.Prefix); break; case PreconditionAttribute precondition: builder.AddPrecondition(precondition); break; default: builder.AddAttributes(attribute); break; } } //Check for unspecified info if (builder.Aliases.Count == 0) builder.AddAliases(""); if (builder.Name == null) builder.Name = typeInfo.Name; // Get all methods (including from inherited members), that are valid commands var validCommands = typeInfo.GetMethods().Where(IsValidCommandDefinition); foreach (var method in validCommands) { builder.AddCommand((command) => { BuildCommand(command, typeInfo, method, service, services); }); } } private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service, IServiceProvider serviceprovider) { var attributes = method.GetCustomAttributes(); foreach (var attribute in attributes) { switch (attribute) { case CommandAttribute command: builder.AddAliases(command.Text); builder.RunMode = command.RunMode; builder.Name = builder.Name ?? command.Text; builder.IgnoreExtraArgs = command.IgnoreExtraArgs ?? service._ignoreExtraArgs; break; case NameAttribute name: builder.Name = name.Text; break; case PriorityAttribute priority: builder.Priority = priority.Priority; break; case SummaryAttribute summary: builder.Summary = summary.Text; break; case RemarksAttribute remarks: builder.Remarks = remarks.Text; break; case AliasAttribute alias: builder.AddAliases(alias.Aliases); break; case PreconditionAttribute precondition: builder.AddPrecondition(precondition); break; default: builder.AddAttributes(attribute); break; } } if (builder.Name == null) builder.Name = method.Name; var parameters = method.GetParameters(); int pos = 0, count = parameters.Length; foreach (var paramInfo in parameters) { builder.AddParameter((parameter) => { BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider); }); } var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); async Task ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd) { var instance = createInstance(services); instance.SetContext(context); try { instance.BeforeExecute(cmd); var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); if (task is Task resultTask) { return await resultTask.ConfigureAwait(false); } else { await task.ConfigureAwait(false); return ExecuteResult.FromSuccess(); } } finally { instance.AfterExecute(cmd); (instance as IDisposable)?.Dispose(); } } builder.Callback = ExecuteCallback; } private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service, IServiceProvider services) { var attributes = paramInfo.GetCustomAttributes(); var paramType = paramInfo.ParameterType; builder.Name = paramInfo.Name; builder.IsOptional = paramInfo.IsOptional; builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; foreach (var attribute in attributes) { switch (attribute) { case SummaryAttribute summary: builder.Summary = summary.Text; break; case OverrideTypeReaderAttribute typeReader: builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader, services); break; case ParamArrayAttribute _: builder.IsMultiple = true; paramType = paramType.GetElementType(); break; case ParameterPreconditionAttribute precon: builder.AddPrecondition(precon); break; case NameAttribute name: builder.Name = name.Text; break; case RemainderAttribute _: if (position != count - 1) throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); builder.IsRemainder = true; break; default: builder.AddAttributes(attribute); break; } } builder.ParameterType = paramType; if (builder.TypeReader == null) { builder.TypeReader = service.GetDefaultTypeReader(paramType) ?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value; } } internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) { var readers = service.GetTypeReaders(paramType); TypeReader reader = null; if (readers != null) { if (readers.TryGetValue(typeReaderType, out reader)) return reader; } //We dont have a cached type reader, create one reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services); service.AddTypeReader(paramType, reader, false); return reader; } private static bool IsValidModuleDefinition(TypeInfo typeInfo) { return ModuleTypeInfo.IsAssignableFrom(typeInfo) && !typeInfo.IsAbstract && !typeInfo.ContainsGenericParameters; } private static bool IsValidCommandDefinition(MethodInfo methodInfo) { return methodInfo.IsDefined(typeof(CommandAttribute)) && (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && !methodInfo.IsStatic && !methodInfo.IsGenericMethod; } } }