diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0399bd2f3..a50121798 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: mcr.microsoft.com/dotnet/sdk:6.0 +image: mcr.microsoft.com/dotnet/sdk:7.0 stages: - build @@ -69,8 +69,8 @@ publish-windows: - if: "$CI_COMMIT_TAG" image: scottyhardy/docker-wine before_script: - - choco install dotnet-6.0-runtime --version=6.0.4 -y - - choco install dotnet-6.0-sdk --version=6.0.202 -y + - choco install dotnet-runtime --version=7.0.2 -y + - choco install dotnet-sdk --version=7.0.102 -y - choco install innosetup -y artifacts: paths: diff --git a/src/NadekoBot/Bot.cs b/src/NadekoBot/Bot.cs index 62e4a350d..4e1d07a15 100644 --- a/src/NadekoBot/Bot.cs +++ b/src/NadekoBot/Bot.cs @@ -1,13 +1,16 @@ #nullable disable +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using NadekoBot.Common.Configs; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Db; using NadekoBot.Modules.Utility; using NadekoBot.Services.Database.Models; +using Ninject; +using Ninject.Extensions.Conventions; +using Ninject.Infrastructure.Language; using System.Collections.Immutable; using System.Diagnostics; -using System.Net; using System.Reflection; using RunMode = Discord.Commands.RunMode; @@ -20,7 +23,7 @@ public sealed class Bot public DiscordSocketClient Client { get; } public ImmutableArray AllGuildConfigs { get; private set; } - private IServiceProvider Services { get; set; } + private IKernel Services { get; set; } public string Mention { get; private set; } public bool IsReady { get; private set; } @@ -51,9 +54,9 @@ public sealed class Bot 50; #endif - if(!_creds.UsePrivilegedIntents) + if (!_creds.UsePrivilegedIntents) Log.Warning("You are not using privileged intents. Some features will not work properly"); - + Client = new(new() { MessageCacheSize = messageCacheSize, @@ -99,67 +102,69 @@ public sealed class Bot AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); } - var svcs = new ServiceCollection().AddTransient(_ => _credsProvider.GetCreds()) // bot creds - .AddSingleton(_credsProvider) - .AddSingleton(_db) // database - .AddSingleton(Client) // discord socket client - .AddSingleton(_commandService) - // .AddSingleton(_interactionService) - .AddSingleton(this) - .AddSingleton() - .AddSingleton() - .AddConfigServices() - .AddConfigMigrators() - .AddMemoryCache() - // music - .AddMusic() - // cache - .AddCache(_creds); - + var kernel = new StandardKernel(); + + kernel.Bind().ToMethod(_ => _credsProvider.GetCreds()).InTransientScope(); + + kernel.Bind().ToConstant(_credsProvider).InSingletonScope(); + kernel.Bind().ToConstant(_db).InSingletonScope(); + kernel.Bind().ToConstant(Client).InSingletonScope(); + kernel.Bind().ToConstant(_commandService).InSingletonScope(); + kernel.Bind().ToConstant(this).InSingletonScope(); + + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().ToConstant(new MemoryCache(new MemoryCacheOptions())).InSingletonScope(); + + kernel.AddConfigServices() + .AddConfigMigrators() + .AddMusic() + .AddCache(_creds) + .AddHttpClients(); - svcs.AddHttpClient(); - svcs.AddHttpClient("memelist") - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler - { - AllowAutoRedirect = false - }); - - svcs.AddHttpClient("google:search") - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() - { - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate - }); if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1") - svcs.AddSingleton(); + { + kernel.Bind().To().InSingletonScope(); + } else { - svcs.AddSingleton() - .AddSingleton(x => x.GetRequiredService()) - .AddSingleton(x => x.GetRequiredService()); + kernel.Bind().To().InSingletonScope(); } - svcs.Scan(scan => scan.FromAssemblyOf() - .AddClasses(classes => classes.AssignableToAny( - // services - typeof(INService), - - // behaviours - typeof(IExecOnMessage), - typeof(IInputTransformer), - typeof(IExecPreCommand), - typeof(IExecPostCommand), - typeof(IExecNoCommand)) - .WithoutAttribute() + kernel.Bind(scan => + { + var classes = scan.FromThisAssembly() + .SelectAllClasses() + .Where(c => (c.IsAssignableTo(typeof(INService)) + || c.IsAssignableTo(typeof(IExecOnMessage)) + || c.IsAssignableTo(typeof(IInputTransformer)) + || c.IsAssignableTo(typeof(IExecPreCommand)) + || c.IsAssignableTo(typeof(IExecPostCommand)) + || c.IsAssignableTo(typeof(IExecNoCommand))) + && !c.HasAttribute() #if GLOBAL_NADEKO - .WithoutAttribute() + && !c.HasAttribute() #endif - ) - .AsSelfWithInterfaces() - .WithSingletonLifetime()); + ); + classes + .BindAllInterfaces() + .Configure(c => c.InSingletonScope()); + + classes.BindToSelf() + .Configure(c => c.InSingletonScope()); + }); + + kernel.Bind().ToConstant(kernel).InSingletonScope(); + + var services = kernel.GetServices(typeof(INService)); + foreach (var s in services) + { + Console.WriteLine(s.GetType().FullName); + } //initialize Services - Services = svcs.BuildServiceProvider(); + Services = kernel; Services.GetRequiredService().Initialize(); Services.GetRequiredService(); @@ -169,7 +174,7 @@ public sealed class Bot _ = LoadTypeReaders(typeof(Bot).Assembly); sw.Stop(); - Log.Information( "All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds); + Log.Information("All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds); } private void ApplyConfigMigrations() @@ -249,10 +254,10 @@ public sealed class Bot LoginErrorHandler.Handle(ex); Helpers.ReadErrorAndExit(4); } - + await clientReady.Task.ConfigureAwait(false); Client.Ready -= SetClientReady; - + Client.JoinedGuild += Client_JoinedGuild; Client.LeftGuild += Client_LeftGuild; @@ -286,7 +291,7 @@ public sealed class Bot { if (ShardId == 0) await _db.SetupAsync(); - + var sw = Stopwatch.StartNew(); await LoginAsync(_creds.Token); @@ -387,7 +392,7 @@ public sealed class Bot """); return Task.CompletedTask; } - + #if GLOBAL_NADEKO || DEBUG if (arg.Exception is not null) Log.Warning(arg.Exception, "{ErrorSource} | {ErrorMessage}", arg.Source, arg.Message); diff --git a/src/NadekoBot/Common/Medusa/MedusaIoCKernelModule.cs b/src/NadekoBot/Common/Medusa/MedusaIoCKernelModule.cs new file mode 100644 index 000000000..c4d969824 --- /dev/null +++ b/src/NadekoBot/Common/Medusa/MedusaIoCKernelModule.cs @@ -0,0 +1,50 @@ +using Ninject.Modules; +using Ninject.Extensions.Conventions; +using System.Reflection; + +namespace Nadeko.Medusa; + +public sealed class MedusaIoCKernelModule : NinjectModule +{ + private Assembly _a; + public override string Name { get; } + + public MedusaIoCKernelModule(string name, Assembly a) + { + Name = name; + _a = a; + } + + public override void Load() + { + // todo cehck for duplicate registrations with ninject.extensions.convention + Kernel.Bind(conf => + { + var transient = conf.From(_a) + .SelectAllClasses() + .WithAttribute(x => x.Lifetime == Lifetime.Transient); + + transient.BindAllInterfaces().Configure(x => x.InTransientScope()); + transient.BindToSelf().Configure(x => x.InTransientScope()); + + var singleton = conf.From(_a) + .SelectAllClasses() + .WithAttribute(x => x.Lifetime == Lifetime.Singleton); + + singleton.BindAllInterfaces().Configure(x => x.InSingletonScope()); + singleton.BindToSelf().Configure(x => x.InSingletonScope()); + }); + } + + public override void Unload() + { + // todo implement unload + // Kernel.Unbind(); + } + + public override void Dispose(bool disposing) + { + _a = null!; + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Medusa/MedusaLoaderService.cs b/src/NadekoBot/Common/Medusa/MedusaLoaderService.cs index 4fca69e72..2e25622ad 100644 --- a/src/NadekoBot/Common/Medusa/MedusaLoaderService.cs +++ b/src/NadekoBot/Common/Medusa/MedusaLoaderService.cs @@ -2,6 +2,8 @@ using Microsoft.Extensions.DependencyInjection; using Nadeko.Medusa.Adapters; using NadekoBot.Common.ModuleBehaviors; +using Ninject; +using Ninject.Modules; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -18,29 +20,31 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, private readonly IBehaviorHandler _behHandler; private readonly IPubSub _pubSub; private readonly IMedusaConfigService _medusaConfig; - + private readonly IKernel _kernel; + private readonly ConcurrentDictionary _resolved = new(); private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); private readonly TypedKey _loadKey = new("medusa:load"); private readonly TypedKey _unloadKey = new("medusa:unload"); - + private readonly TypedKey _stringsReload = new("medusa:reload_strings"); private const string BASE_DIR = "data/medusae"; - public MedusaLoaderService(CommandService cmdService, - IServiceProvider botServices, + public MedusaLoaderService( + CommandService cmdService, + IKernel kernel, IBehaviorHandler behHandler, IPubSub pubSub, IMedusaConfigService medusaConfig) { _cmdService = cmdService; - _botServices = botServices; _behHandler = behHandler; _pubSub = pubSub; _medusaConfig = medusaConfig; - + _kernel = kernel; + // has to be done this way to support this feature on sharded bots _pubSub.Sub(_loadKey, async name => await InternalLoadAsync(name)); _pubSub.Sub(_unloadKey, async name => await InternalUnloadAsync(name)); @@ -74,12 +78,13 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, { commands.Add(new SnekCommandStats(command.Aliases.First())); } - + sneks.Add(new SnekStats(snekInfos.Name, commands)); } toReturn.Add(new MedusaStats(name, resolvedData.Strings.GetDescription(culture), sneks)); } + return toReturn; } @@ -88,9 +93,9 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, foreach (var name in _medusaConfig.GetLoadedMedusae()) { var result = await InternalLoadAsync(name); - if(result != MedusaLoadResult.Success) + if (result != MedusaLoadResult.Success) Log.Warning("Unable to load '{MedusaName}' medusa", name); - else + else Log.Warning("Loaded medusa '{MedusaName}'", name); } } @@ -110,7 +115,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, return res; } - + [MethodImpl(MethodImplOptions.NoInlining)] public async Task UnloadMedusaAsync(string medusaName) { @@ -150,7 +155,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, resolved.Strings.Reload(); } } - + private async Task ReloadStringsInternal() { await _lock.WaitAsync(); @@ -179,13 +184,13 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, .Desc ?? string.Empty; } - + [MethodImpl(MethodImplOptions.NoInlining)] private async ValueTask InternalLoadAsync(string name) { if (_resolved.ContainsKey(name)) return MedusaLoadResult.AlreadyLoaded; - + var safeName = Uri.EscapeDataString(name); await _lock.WaitAsync(); @@ -194,7 +199,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, if (LoadAssemblyInternal(safeName, out var ctx, out var snekData, - out var services, + out var kernelModule, out var strings, out var typeReaders)) { @@ -213,7 +218,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, await sub.Instance.InitializeAsync(); } - var module = await LoadModuleInternalAsync(name, point, strings, services); + var module = await LoadModuleInternalAsync(name, point, strings, kernelModule); moduleInfos.Add(module); } catch (Exception ex) @@ -224,7 +229,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, } } - var execs = GetExecsInternal(snekData, strings, services); + var execs = GetExecsInternal(snekData, strings); await _behHandler.AddRangeAsync(execs); _resolved[name] = new(LoadContext: ctx, @@ -232,13 +237,10 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, SnekInfos: snekData.ToImmutableArray(), strings, typeReaders, - execs) - { - Services = services - }; + execs, + kernelModule); + - - services = null; _medusaConfig.AddLoadedMedusa(safeName); return MedusaLoadResult.Success; } @@ -261,20 +263,22 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, } [MethodImpl(MethodImplOptions.NoInlining)] - private IReadOnlyCollection GetExecsInternal(IReadOnlyCollection snekData, IMedusaStrings strings, IServiceProvider services) + private IReadOnlyCollection GetExecsInternal( + IReadOnlyCollection snekData, + IMedusaStrings strings) { var behs = new List(); foreach (var snek in snekData) { - behs.Add(new BehaviorAdapter(new(snek.Instance), strings, services)); + behs.Add(new BehaviorAdapter(new(snek.Instance), strings, _kernel)); foreach (var sub in snek.Subsneks) { - behs.Add(new BehaviorAdapter(new(sub.Instance), strings, services)); + behs.Add(new BehaviorAdapter(new(sub.Instance), strings, _kernel)); } } - + return behs; } @@ -290,10 +294,10 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, notAddedTypeReaders.Add(type); continue; } - + _cmdService.AddTypeReader(type, typeReader); } - + // remove the ones that were not added // to prevent them from being unloaded later // as they didn't come from this medusa @@ -308,56 +312,61 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, string safeName, [NotNullWhen(true)] out WeakReference? ctxWr, [NotNullWhen(true)] out IReadOnlyCollection? snekData, - out IServiceProvider services, + [NotNullWhen(true)] out INinjectModule? ninjectModule, out IMedusaStrings strings, out Dictionary typeReaders) { ctxWr = null; snekData = null; - + var path = $"{BASE_DIR}/{safeName}/{safeName}.dll"; strings = MedusaStrings.CreateDefault($"{BASE_DIR}/{safeName}"); var ctx = new MedusaAssemblyLoadContext(Path.GetDirectoryName(path)!); var a = ctx.LoadFromAssemblyPath(Path.GetFullPath(path)); - var sis = LoadSneksFromAssembly(a, out services); - typeReaders = LoadTypeReadersFromAssembly(a, strings, services); + + // load services + ninjectModule = new MedusaIoCKernelModule(safeName, a); + _kernel.Load(ninjectModule); + + var sis = LoadSneksFromAssembly(safeName, a); + typeReaders = LoadTypeReadersFromAssembly(a, strings); if (sis.Count == 0) { + _kernel.Unload(safeName); return false; } ctxWr = new(ctx); snekData = sis; - + return true; } private static readonly Type _paramParserType = typeof(ParamParser<>); - + [MethodImpl(MethodImplOptions.NoInlining)] private Dictionary LoadTypeReadersFromAssembly( Assembly assembly, - IMedusaStrings strings, - IServiceProvider services) + IMedusaStrings strings) { var paramParsers = assembly.GetExportedTypes() - .Where(x => x.IsClass - && !x.IsAbstract - && x.BaseType is not null - && x.BaseType.IsGenericType - && x.BaseType.GetGenericTypeDefinition() == _paramParserType); + .Where(x => x.IsClass + && !x.IsAbstract + && x.BaseType is not null + && x.BaseType.IsGenericType + && x.BaseType.GetGenericTypeDefinition() == _paramParserType); var typeReaders = new Dictionary(); foreach (var parserType in paramParsers) { - var parserObj = ActivatorUtilities.CreateInstance(services, parserType); + var parserObj = ActivatorUtilities.CreateInstance(_kernel, parserType); var targetType = parserType.BaseType!.GetGenericArguments()[0]; var typeReaderInstance = (TypeReader)Activator.CreateInstance( typeof(ParamParserAdapter<>).MakeGenericType(targetType), - args: new[] { parserObj, strings, services })!; - + args: new[] { parserObj, strings, _kernel })!; + typeReaders.Add(targetType, typeReaderInstance); } @@ -365,11 +374,15 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, } [MethodImpl(MethodImplOptions.NoInlining)] - private async Task LoadModuleInternalAsync(string medusaName, SnekInfo snekInfo, IMedusaStrings strings, IServiceProvider services) + private async Task LoadModuleInternalAsync( + string medusaName, + SnekInfo snekInfo, + IMedusaStrings strings, + INinjectModule services) { var module = await _cmdService.CreateModuleAsync(snekInfo.Instance.Prefix, CreateModuleFactory(medusaName, snekInfo, strings, services)); - + return module; } @@ -378,7 +391,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, string medusaName, SnekInfo snekInfo, IMedusaStrings strings, - IServiceProvider medusaServices) + INinjectModule kernelModule) => mb => { var m = mb.WithName(snekInfo.Name); @@ -394,17 +407,17 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, CreateCallback(cmd.ContextType, new(snekInfo), new(cmd), - new(medusaServices), strings), CreateCommandFactory(medusaName, cmd, strings)); } foreach (var subInfo in snekInfo.Subsneks) - m.AddModule(subInfo.Instance.Prefix, CreateModuleFactory(medusaName, subInfo, strings, medusaServices)); + m.AddModule(subInfo.Instance.Prefix, CreateModuleFactory(medusaName, subInfo, strings, kernelModule)); }; private static readonly RequireContextAttribute _reqGuild = new RequireContextAttribute(ContextType.Guild); private static readonly RequireContextAttribute _reqDm = new RequireContextAttribute(ContextType.DM); + private Action CreateCommandFactory(string medusaName, SnekCommandData cmd, IMedusaStrings strings) => (cb) => { @@ -414,7 +427,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, cb.AddPrecondition(_reqGuild); else if (cmd.ContextType == CommandContextType.Dm) cb.AddPrecondition(_reqDm); - + foreach (var f in cmd.Filters) cb.AddPrecondition(new FilterAdapter(f, strings)); @@ -441,12 +454,12 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, } cb.WithPriority(cmd.Priority); - + // using summary to save method name // method name is used to retrieve desc/usages cb.WithRemarks($"medusa///{medusaName}"); cb.WithSummary(cmd.MethodInfo.Name.ToLowerInvariant()); - + foreach (var param in cmd.Parameters) { cb.AddParameter(param.Name, param.Type, CreateParamFactory(param)); @@ -466,19 +479,21 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, CommandContextType contextType, WeakReference snekDataWr, WeakReference snekCommandDataWr, - WeakReference medusaServicesWr, IMedusaStrings strings) - => async (context, parameters, svcs, _) => + => async ( + context, + parameters, + svcs, + _) => { if (!snekCommandDataWr.TryGetTarget(out var cmdData) - || !snekDataWr.TryGetTarget(out var snekData) - || !medusaServicesWr.TryGetTarget(out var medusaServices)) + || !snekDataWr.TryGetTarget(out var snekData)) { Log.Warning("Attempted to run an unloaded snek's command"); return; } - - var paramObjs = ParamObjs(contextType, cmdData, parameters, context, svcs, medusaServices, strings); + + var paramObjs = ParamObjs(contextType, cmdData, parameters, context, svcs, _kernel, strings); try { @@ -502,9 +517,8 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, { paramObjs = null; cmdData = null; - + snekData = null; - medusaServices = null; } }; @@ -548,7 +562,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, for (var i = 0; i < parameters.Length; i++) paramObjs[startAt + i] = parameters[i]; - + return paramObjs; } @@ -569,17 +583,14 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, } await _behHandler.RemoveRangeAsync(lsi.Execs); - + await DisposeSnekInstances(lsi); var lc = lsi.LoadContext; - // removing this line will prevent assembly from being unloaded quickly - // as this local variable will be held for a long time potentially - // due to how async works - lsi.Services = null!; + // lsi.KernelModule = null!; lsi = null; - + _medusaConfig.RemoveLoadedMedusa(name); return UnloadInternal(lc) ? MedusaUnloadResult.Success @@ -610,7 +621,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, foreach (var sub in si.Subsneks) { await sub.Instance.DisposeAsync(); - } + } } catch (Exception ex) { @@ -635,10 +646,10 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, [MethodImpl(MethodImplOptions.NoInlining)] private void UnloadContext(WeakReference lsiLoadContext) { - if(lsiLoadContext.TryGetTarget(out var ctx)) + if (lsiLoadContext.TryGetTarget(out var ctx)) ctx.Unload(); } - + private void GcCleanup() { // cleanup @@ -653,25 +664,14 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, private static readonly Type _snekType = typeof(Snek); - [MethodImpl(MethodImplOptions.NoInlining)] - private IServiceProvider LoadMedusaServicesInternal(Assembly a) - => new ServiceCollection() - .Scan(x => x.FromAssemblies(a) - .AddClasses(static x => x.WithAttribute(x => x.Lifetime == Lifetime.Transient)) - .AsSelfWithInterfaces() - .WithTransientLifetime() - .AddClasses(static x => x.WithAttribute(x => x.Lifetime == Lifetime.Singleton)) - .AsSelfWithInterfaces() - .WithSingletonLifetime()) - .BuildServiceProvider(); + // [MethodImpl(MethodImplOptions.NoInlining)] + // private MedusaIoCKernelModule LoadMedusaServicesInternal(string name, Assembly a) + // => new MedusaIoCKernelModule(name, a); [MethodImpl(MethodImplOptions.NoInlining)] - public IReadOnlyCollection LoadSneksFromAssembly(Assembly a, out IServiceProvider services) + public IReadOnlyCollection LoadSneksFromAssembly(string name, Assembly a) { - var medusaServices = LoadMedusaServicesInternal(a); - services = new MedusaServiceProvider(_botServices, medusaServices); - // find all types in teh assembly var types = a.GetExportedTypes(); // snek is always a public non abstract class @@ -683,14 +683,14 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, .ToList(); var topModules = new Dictionary(); - + foreach (var cl in classes) { if (cl.DeclaringType is not null) continue; - + // get module data, and add it to the topModules dictionary - var module = GetModuleData(cl, services); + var module = GetModuleData(cl); topModules.Add(cl, module); } @@ -708,21 +708,21 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, dt.Name); continue; } - - GetModuleData(c, services, parentData); + + GetModuleData(c, parentData); } return topModules.Values.ToArray(); } - + [MethodImpl(MethodImplOptions.NoInlining)] - private SnekInfo GetModuleData(Type type, IServiceProvider services, SnekInfo? parentData = null) + private SnekInfo GetModuleData(Type type, SnekInfo? parentData = null) { var filters = type.GetCustomAttributes(true) .ToArray(); - - var instance = (Snek)ActivatorUtilities.CreateInstance(services, type); - + + var instance = (Snek)ActivatorUtilities.CreateInstance(_kernel, type); + var module = new SnekInfo(instance.Name, parentData, instance, @@ -744,7 +744,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, | BindingFlags.Public) .Where(static x => { - if(x.GetCustomAttribute(true) is null) + if (x.GetCustomAttribute(true) is null) return false; if (x.ReturnType.IsGenericType) @@ -752,14 +752,14 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, var genericType = x.ReturnType.GetGenericTypeDefinition(); if (genericType == typeof(Task<>)) return true; - + // if (genericType == typeof(ValueTask<>)) // return true; Log.Warning("Method {MethodName} has an invalid return type: {ReturnType}", x.Name, x.ReturnType); - + return false; } @@ -776,8 +776,8 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, return succ; }); - - + + var cmds = new List(); foreach (var method in methodInfos) { @@ -808,20 +808,22 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, if (isContext) { if (hasDefaultValue || leftoverAttribute != null || isParams) - throw new ArgumentException("IContext parameter cannot be optional, leftover, constant or params. " + GetErrorPath(method, pi)); + throw new ArgumentException( + "IContext parameter cannot be optional, leftover, constant or params. " + + GetErrorPath(method, pi)); if (paramCounter != 0) throw new ArgumentException($"IContext parameter has to be first. {GetErrorPath(method, pi)}"); canInject = true; - + if (paramType.IsAssignableTo(typeof(GuildContext))) cmdContext = CommandContextType.Guild; else if (paramType.IsAssignableTo(typeof(DmContext))) cmdContext = CommandContextType.Dm; else cmdContext = CommandContextType.Any; - + continue; } @@ -831,7 +833,7 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, throw new ArgumentException($"Parameters marked as [Injected] have to come after IContext"); canInject = true; - + diParams.Add(paramType); continue; } @@ -861,11 +863,11 @@ public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, } - var cmdAttribute = method.GetCustomAttribute(true)!; + var cmdAttribute = method.GetCustomAttribute(true)!; var aliases = cmdAttribute.Aliases; if (aliases.Length == 0) aliases = new[] { method.Name.ToLowerInvariant() }; - + cmds.Add(new( aliases, method, diff --git a/src/NadekoBot/Common/Medusa/MedusaServiceProvider.cs b/src/NadekoBot/Common/Medusa/MedusaServiceProvider.cs deleted file mode 100644 index e019ced9c..000000000 --- a/src/NadekoBot/Common/Medusa/MedusaServiceProvider.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Nadeko.Medusa; - -public class MedusaServiceProvider : IServiceProvider -{ - private readonly IServiceProvider _nadekoServices; - private readonly IServiceProvider _medusaServices; - - public MedusaServiceProvider(IServiceProvider nadekoServices, IServiceProvider medusaServices) - { - _nadekoServices = nadekoServices; - _medusaServices = medusaServices; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - public object? GetService(Type serviceType) - { - if (!serviceType.Assembly.IsCollectible) - return _nadekoServices.GetService(serviceType); - - return _medusaServices.GetService(serviceType); - } -} \ No newline at end of file diff --git a/src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs b/src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs index f645e8146..70f2fb978 100644 --- a/src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs +++ b/src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using Ninject.Modules; +using System.Collections.Immutable; namespace Nadeko.Medusa; @@ -8,7 +9,7 @@ public sealed record ResolvedMedusa( IImmutableList SnekInfos, IMedusaStrings Strings, Dictionary TypeReaders, - IReadOnlyCollection Execs) + IReadOnlyCollection Execs, + INinjectModule KernelModule) { - public IServiceProvider Services { get; set; } = null!; } \ No newline at end of file diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index b4a63f657..48f13266a 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -47,8 +47,14 @@ - + + + + + + + @@ -56,7 +62,6 @@ - diff --git a/src/NadekoBot/Services/Impl/BehaviorExecutor.cs b/src/NadekoBot/Services/Impl/BehaviorExecutor.cs index 717a17a21..ce97a8d5b 100644 --- a/src/NadekoBot/Services/Impl/BehaviorExecutor.cs +++ b/src/NadekoBot/Services/Impl/BehaviorExecutor.cs @@ -1,6 +1,7 @@ #nullable disable using Microsoft.Extensions.DependencyInjection; using NadekoBot.Common.ModuleBehaviors; +using Ninject; namespace NadekoBot.Services; diff --git a/src/NadekoBot/_Extensions/ReflectionExtensions.cs b/src/NadekoBot/_Extensions/ReflectionExtensions.cs new file mode 100644 index 000000000..c85aeec79 --- /dev/null +++ b/src/NadekoBot/_Extensions/ReflectionExtensions.cs @@ -0,0 +1,23 @@ +namespace NadekoBot.Extensions; + +public static class ReflectionExtensions +{ + public static bool IsAssignableToGenericType(this Type givenType, Type genericType) + { + var interfaceTypes = givenType.GetInterfaces(); + + foreach (var it in interfaceTypes) + { + if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) + return true; + } + + if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + return true; + + Type baseType = givenType.BaseType; + if (baseType == null) return false; + + return IsAssignableToGenericType(baseType, genericType); + } +} \ No newline at end of file diff --git a/src/NadekoBot/_Extensions/ServiceCollectionExtensions.cs b/src/NadekoBot/_Extensions/ServiceCollectionExtensions.cs index a54af943f..034911e71 100644 --- a/src/NadekoBot/_Extensions/ServiceCollectionExtensions.cs +++ b/src/NadekoBot/_Extensions/ServiceCollectionExtensions.cs @@ -1,76 +1,128 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NadekoBot.Modules.Music; using NadekoBot.Modules.Music.Resolvers; using NadekoBot.Modules.Music.Services; +using Ninject; +using Ninject.Extensions.Conventions; using StackExchange.Redis; +using System.Net; using System.Reflection; namespace NadekoBot.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddBotStringsServices(this IServiceCollection services, BotCacheImplemenation botCache) - => botCache == BotCacheImplemenation.Memory - ? services.AddSingleton() - .AddSingleton() - .AddSingleton() - : services.AddSingleton() - .AddSingleton() - .AddSingleton(); - - public static IServiceCollection AddConfigServices(this IServiceCollection services) + public static IKernel AddBotStringsServices(this IKernel kernel, BotCacheImplemenation botCache) { - services.Scan(x => x.FromCallingAssembly() - .AddClasses(f => f.AssignableTo(typeof(ConfigServiceBase<>))) - .AsSelfWithInterfaces()); + if (botCache == BotCacheImplemenation.Memory) + { + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + } + else + { + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + } - return services; + return kernel; } - public static IServiceCollection AddConfigMigrators(this IServiceCollection services) - => services.AddSealedSubclassesOf(typeof(IConfigMigrator)); - - public static IServiceCollection AddMusic(this IServiceCollection services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(svc => svc.GetRequiredService()); - - // consider using scrutor, because slightly different versions - // of this might be needed in several different places - public static IServiceCollection AddSealedSubclassesOf(this IServiceCollection services, Type baseType) + public static IKernel AddConfigServices(this IKernel kernel) { - var subTypes = Assembly.GetCallingAssembly() - .ExportedTypes.Where(type => type.IsSealed && baseType.IsAssignableFrom(type)); + kernel.Bind(x => + { + var configs = x.FromThisAssembly() + .SelectAllClasses() + .Where(f => f.IsAssignableToGenericType(typeof(ConfigServiceBase<>))); - foreach (var subType in subTypes) - services.AddSingleton(baseType, subType); + // todo check for duplicates + configs.BindToSelf() + .Configure(c => c.InSingletonScope()); + configs.BindAllInterfaces() + .Configure(c => c.InSingletonScope()); + }); - return services; + return kernel; } - public static IServiceCollection AddCache(this IServiceCollection services, IBotCredentials creds) + public static IKernel AddConfigMigrators(this IKernel kernel) + => kernel.AddSealedSubclassesOf(typeof(IConfigMigrator)); + + public static IKernel AddMusic(this IKernel kernel) + { + kernel.Bind() + .To() + .InSingletonScope(); + + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().ToSelf().InSingletonScope(); + + return kernel; + } + + public static IKernel AddSealedSubclassesOf(this IKernel kernel, Type baseType) + { + kernel.Bind(x => + { + var classes = x.FromThisAssembly() + .SelectAllClasses() + .Where(c => c.IsPublic && c.IsNested && baseType.IsAssignableFrom(baseType)); + + classes.BindAllInterfaces().Configure(x => x.InSingletonScope()); + classes.BindToSelf().Configure(x => x.InSingletonScope()); + }); + + return kernel; + } + + public static IKernel AddCache(this IKernel kernel, IBotCredentials creds) { if (creds.BotCache == BotCacheImplemenation.Redis) { var conf = ConfigurationOptions.Parse(creds.RedisOptions); - services.AddSingleton(ConnectionMultiplexer.Connect(conf)) - .AddSingleton() - .AddSingleton(); + kernel.Bind().ToConstant(ConnectionMultiplexer.Connect(conf)).InSingletonScope(); + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); } else { - services.AddSingleton() - .AddSingleton(); - + kernel.Bind().To().InSingletonScope(); + kernel.Bind().To().InSingletonScope(); } - return services + return kernel .AddBotStringsServices(creds.BotCache); } + + public static IKernel AddHttpClients(this IKernel kernel) + { + IServiceCollection svcs = new ServiceCollection(); + svcs.AddHttpClient(); + svcs.AddHttpClient("memelist") + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AllowAutoRedirect = false + }); + + svcs.AddHttpClient("google:search") + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }); + + var prov = svcs.BuildServiceProvider(); + kernel.Bind().ToMethod(_ => prov.GetRequiredService()); + kernel.Bind().ToMethod(_ => prov.GetRequiredService()); + + return kernel; + } } \ No newline at end of file diff --git a/src/NadekoBot/data/patron.yml b/src/NadekoBot/data/patron.yml index 6485e1cdb..40da7bba7 100644 --- a/src/NadekoBot/data/patron.yml +++ b/src/NadekoBot/data/patron.yml @@ -4,7 +4,7 @@ version: 2 isEnabled: false # List of patron only features and relevant quota data quotas: -# Dictionary of feature names with their respective limits. Set to null for unlimited + # Dictionary of feature names with their respective limits. Set to null for unlimited features: timely:extra_percent: V: 10