mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Medusa System Added
Read about the medusa system [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)
This commit is contained in:
		@@ -147,12 +147,14 @@ public sealed class Bot
 | 
			
		||||
                                      typeof(INService),
 | 
			
		||||
 | 
			
		||||
                                      // behaviours
 | 
			
		||||
                                      typeof(IEarlyBehavior),
 | 
			
		||||
                                      typeof(ILateBlocker),
 | 
			
		||||
                                      typeof(IExecOnMessage),
 | 
			
		||||
                                      typeof(IInputTransformer),
 | 
			
		||||
                                      typeof(ILateExecutor))
 | 
			
		||||
                                      typeof(IExecPreCommand),
 | 
			
		||||
                                      typeof(IExecPostCommand),
 | 
			
		||||
                                      typeof(IExecNoCommand))
 | 
			
		||||
                                                .WithoutAttribute<DontAddToIocContainerAttribute>()
 | 
			
		||||
#if GLOBAL_NADEKO
 | 
			
		||||
                    .WithoutAttribute<NoPublicBotAttribute>()
 | 
			
		||||
                                                .WithoutAttribute<NoPublicBotAttribute>()
 | 
			
		||||
#endif
 | 
			
		||||
                              )
 | 
			
		||||
                              .AsSelfWithInterfaces()
 | 
			
		||||
@@ -160,8 +162,7 @@ public sealed class Bot
 | 
			
		||||
 | 
			
		||||
        //initialize Services
 | 
			
		||||
        Services = svcs.BuildServiceProvider();
 | 
			
		||||
        var exec = Services.GetRequiredService<IBehaviourExecutor>();
 | 
			
		||||
        exec.Initialize();
 | 
			
		||||
        Services.GetRequiredService<IBehaviorHandler>().Initialize();
 | 
			
		||||
 | 
			
		||||
        if (Client.ShardId == 0)
 | 
			
		||||
            ApplyConfigMigrations();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot;
 | 
			
		||||
 | 
			
		||||
public interface IEmbedBuilder
 | 
			
		||||
{
 | 
			
		||||
    IEmbedBuilder WithDescription(string desc);
 | 
			
		||||
    IEmbedBuilder WithTitle(string title);
 | 
			
		||||
    IEmbedBuilder AddField(string title, object value, bool isInline = false);
 | 
			
		||||
    IEmbedBuilder WithFooter(string text, string iconUrl = null);
 | 
			
		||||
    IEmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null);
 | 
			
		||||
    IEmbedBuilder WithColor(EmbedColor color);
 | 
			
		||||
    Embed Build();
 | 
			
		||||
    IEmbedBuilder WithUrl(string url);
 | 
			
		||||
    IEmbedBuilder WithImageUrl(string url);
 | 
			
		||||
    IEmbedBuilder WithThumbnailUrl(string url);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum EmbedColor
 | 
			
		||||
{
 | 
			
		||||
    Ok,
 | 
			
		||||
    Pending,
 | 
			
		||||
    Error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										76
									
								
								src/NadekoBot/Common/Medusa/Adapters/BehaviorAdapter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/NadekoBot/Common/Medusa/Adapters/BehaviorAdapter.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
#nullable enable
 | 
			
		||||
 | 
			
		||||
[DontAddToIocContainer]
 | 
			
		||||
public sealed class BehaviorAdapter : ICustomBehavior
 | 
			
		||||
{
 | 
			
		||||
    private readonly WeakReference<Snek> _snekWr;
 | 
			
		||||
    private readonly IMedusaStrings _strings;
 | 
			
		||||
    private readonly IServiceProvider _services;
 | 
			
		||||
    private readonly string _name;
 | 
			
		||||
 | 
			
		||||
    // unused
 | 
			
		||||
    public int Priority
 | 
			
		||||
        => 0;
 | 
			
		||||
 | 
			
		||||
    public BehaviorAdapter(WeakReference<Snek> snekWr, IMedusaStrings strings, IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        _snekWr = snekWr;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _services = services;
 | 
			
		||||
 | 
			
		||||
        _name = snekWr.TryGetTarget(out var snek)
 | 
			
		||||
            ? $"snek/{snek.GetType().Name}"
 | 
			
		||||
            : "unknown";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_snekWr.TryGetTarget(out var snek))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        return await snek.ExecPreCommandAsync(ContextAdapterFactory.CreateNew(context, _strings, _services),
 | 
			
		||||
            moduleName,
 | 
			
		||||
            command.Name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ExecOnMessageAsync(IGuild? guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_snekWr.TryGetTarget(out var snek))
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        return await snek.ExecOnMessageAsync(guild, msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<string?> TransformInput(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        IMessageChannel channel,
 | 
			
		||||
        IUser user,
 | 
			
		||||
        string input)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_snekWr.TryGetTarget(out var snek))
 | 
			
		||||
            return null;
 | 
			
		||||
        
 | 
			
		||||
        return await snek.ExecInputTransformAsync(guild, channel, user, input);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task ExecOnNoCommandAsync(IGuild? guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_snekWr.TryGetTarget(out var snek))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        await snek.ExecOnNoCommandAsync(guild, msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async ValueTask ExecPostCommandAsync(ICommandContext context, string moduleName, string commandName)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_snekWr.TryGetTarget(out var snek))
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        await snek.ExecPostCommandAsync(ContextAdapterFactory.CreateNew(context, _strings, _services),
 | 
			
		||||
            moduleName,
 | 
			
		||||
            commandName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override string ToString()
 | 
			
		||||
        => _name;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
internal class ContextAdapterFactory
 | 
			
		||||
{
 | 
			
		||||
    public static AnyContext CreateNew(ICommandContext context, IMedusaStrings strings, IServiceProvider services)
 | 
			
		||||
        => context.Guild is null
 | 
			
		||||
            ? new DmContextAdapter(context, strings, services)
 | 
			
		||||
            : new GuildContextAdapter(context, strings, services);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								src/NadekoBot/Common/Medusa/Adapters/DmContextAdapter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/NadekoBot/Common/Medusa/Adapters/DmContextAdapter.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
public sealed class DmContextAdapter : DmContext
 | 
			
		||||
{
 | 
			
		||||
    public override IMedusaStrings Strings { get; }
 | 
			
		||||
    public override IDMChannel Channel { get; }
 | 
			
		||||
    public override IUserMessage Message { get; }
 | 
			
		||||
    public override IUser User
 | 
			
		||||
        => Message.Author;
 | 
			
		||||
    
 | 
			
		||||
    private readonly IServiceProvider _services;
 | 
			
		||||
    private readonly Lazy<IEmbedBuilderService> _ebs;
 | 
			
		||||
    private readonly Lazy<IBotStrings> _botStrings;
 | 
			
		||||
    private readonly Lazy<ILocalization> _localization;
 | 
			
		||||
 | 
			
		||||
    public DmContextAdapter(ICommandContext ctx, IMedusaStrings strings, IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        if (ctx is not { Channel: IDMChannel ch })
 | 
			
		||||
        {
 | 
			
		||||
            throw new ArgumentException("Can't use non-dm context to create DmContextAdapter", nameof(ctx));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Strings = strings;
 | 
			
		||||
 | 
			
		||||
        _services = services;
 | 
			
		||||
 | 
			
		||||
        Channel = ch;
 | 
			
		||||
        Message = ctx.Message;
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        _ebs = new(_services.GetRequiredService<IEmbedBuilderService>());
 | 
			
		||||
        _botStrings = new(_services.GetRequiredService<IBotStrings>);
 | 
			
		||||
        _localization = new(_services.GetRequiredService<ILocalization>());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override IEmbedBuilder Embed()
 | 
			
		||||
        => _ebs.Value.Create();
 | 
			
		||||
 | 
			
		||||
    public override string GetText(string key, object[]? args = null)
 | 
			
		||||
    {
 | 
			
		||||
        var cultureInfo = _localization.Value.GetCultureInfo(default(ulong?));
 | 
			
		||||
        var output = Strings.GetText(key, cultureInfo, args ?? Array.Empty<object>());
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(output))
 | 
			
		||||
            return output;
 | 
			
		||||
        
 | 
			
		||||
        return _botStrings.Value.GetText(key, cultureInfo, args);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								src/NadekoBot/Common/Medusa/Adapters/GuildContextAdapter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/NadekoBot/Common/Medusa/Adapters/GuildContextAdapter.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
public sealed class GuildContextAdapter : GuildContext
 | 
			
		||||
{
 | 
			
		||||
    private readonly IServiceProvider _services;
 | 
			
		||||
    private readonly ICommandContext _ctx;
 | 
			
		||||
    private readonly Lazy<IEmbedBuilderService> _ebs;
 | 
			
		||||
    private readonly Lazy<IBotStrings> _botStrings;
 | 
			
		||||
    private readonly Lazy<ILocalization> _localization;
 | 
			
		||||
    
 | 
			
		||||
    public override IMedusaStrings Strings { get; }
 | 
			
		||||
    public override IGuild Guild { get; }
 | 
			
		||||
    public override ITextChannel Channel { get; }
 | 
			
		||||
    public override IUserMessage Message
 | 
			
		||||
        => _ctx.Message;
 | 
			
		||||
 | 
			
		||||
    public override IGuildUser User { get; } 
 | 
			
		||||
 | 
			
		||||
    public override IEmbedBuilder Embed()
 | 
			
		||||
        => _ebs.Value.Create();
 | 
			
		||||
 | 
			
		||||
    public GuildContextAdapter(ICommandContext ctx, IMedusaStrings strings, IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        if (ctx.Guild is not IGuild guild || ctx.Channel is not ITextChannel channel)
 | 
			
		||||
        {
 | 
			
		||||
            throw new ArgumentException("Can't use non-guild context to create GuildContextAdapter", nameof(ctx));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Strings = strings;
 | 
			
		||||
        User = (IGuildUser)ctx.User;
 | 
			
		||||
 | 
			
		||||
        _services = services;
 | 
			
		||||
        _ebs = new(_services.GetRequiredService<IEmbedBuilderService>());
 | 
			
		||||
        _botStrings = new(_services.GetRequiredService<IBotStrings>);
 | 
			
		||||
        _localization = new(_services.GetRequiredService<ILocalization>());
 | 
			
		||||
 | 
			
		||||
        (_ctx, Guild, Channel) = (ctx, guild, channel);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override string GetText(string key, object[]? args = null)
 | 
			
		||||
    {
 | 
			
		||||
        args ??= Array.Empty<object>();
 | 
			
		||||
        
 | 
			
		||||
        var cultureInfo = _localization.Value.GetCultureInfo(_ctx.Guild.Id);
 | 
			
		||||
        var output = Strings.GetText(key, cultureInfo, args);
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(output))
 | 
			
		||||
            return output;
 | 
			
		||||
        
 | 
			
		||||
        return _botStrings.Value.GetText(key, cultureInfo, args);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/NadekoBot/Common/Medusa/Adapters/ParamParserAdapter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/NadekoBot/Common/Medusa/Adapters/ParamParserAdapter.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
public sealed class ParamParserAdapter<T> : TypeReader
 | 
			
		||||
{
 | 
			
		||||
    private readonly ParamParser<T> _parser;
 | 
			
		||||
    private readonly IMedusaStrings _strings;
 | 
			
		||||
    private readonly IServiceProvider _services;
 | 
			
		||||
 | 
			
		||||
    public ParamParserAdapter(ParamParser<T> parser,
 | 
			
		||||
        IMedusaStrings strings,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        _parser = parser;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _services = services;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        string input,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        var medusaContext = ContextAdapterFactory.CreateNew(context,
 | 
			
		||||
            _strings,
 | 
			
		||||
            _services);
 | 
			
		||||
        
 | 
			
		||||
        var result = await _parser.TryParseAsync(medusaContext, input);
 | 
			
		||||
        
 | 
			
		||||
        if(result.IsSuccess)
 | 
			
		||||
            return Discord.Commands.TypeReaderResult.FromSuccess(result.Data);
 | 
			
		||||
        
 | 
			
		||||
        return Discord.Commands.TypeReaderResult.FromError(CommandError.Unsuccessful, "Invalid input");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/NadekoBot/Common/Medusa/CommandContextType.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/NadekoBot/Common/Medusa/CommandContextType.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Enum specifying in which context the command can be executed
 | 
			
		||||
/// </summary>
 | 
			
		||||
public enum CommandContextType
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Command can only be executed in a guild
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Guild,
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Command can only be executed in DMs
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Dm,
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Command can be executed anywhere
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Any,
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Command can be executed anywhere, and it doesn't require context to be passed to it
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    Unspecified
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
public interface IMedusaConfigService
 | 
			
		||||
{
 | 
			
		||||
    IReadOnlyCollection<string> GetLoadedMedusae();
 | 
			
		||||
    void AddLoadedMedusa(string name);
 | 
			
		||||
    void RemoveLoadedMedusa(string name);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								src/NadekoBot/Common/Medusa/Config/MedusaConfig.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/NadekoBot/Common/Medusa/Config/MedusaConfig.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
using Cloneable;
 | 
			
		||||
using NadekoBot.Common.Yml;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
[Cloneable]
 | 
			
		||||
public sealed partial class MedusaConfig : ICloneable<MedusaConfig>
 | 
			
		||||
{
 | 
			
		||||
    [Comment(@"DO NOT CHANGE")]
 | 
			
		||||
    public int Version { get; set; } = 1;
 | 
			
		||||
    
 | 
			
		||||
    [Comment("List of medusae automatically loaded at startup")]
 | 
			
		||||
    public List<string> Loaded { get; set; }
 | 
			
		||||
 | 
			
		||||
    public MedusaConfig()
 | 
			
		||||
    {
 | 
			
		||||
        Loaded = new();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										43
									
								
								src/NadekoBot/Common/Medusa/Config/MedusaConfigService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/NadekoBot/Common/Medusa/Config/MedusaConfigService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
using NadekoBot.Common.Configs;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
public sealed class MedusaConfigService : ConfigServiceBase<MedusaConfig>, IMedusaConfigService
 | 
			
		||||
{
 | 
			
		||||
    private const string FILE_PATH = "data/medusae/medusa.yml";
 | 
			
		||||
    private static readonly TypedKey<MedusaConfig> _changeKey = new("config.medusa.updated");
 | 
			
		||||
 | 
			
		||||
    public override string Name
 | 
			
		||||
        => "medusa";
 | 
			
		||||
 | 
			
		||||
    public MedusaConfigService(
 | 
			
		||||
        IConfigSeria serializer,
 | 
			
		||||
        IPubSub pubSub)
 | 
			
		||||
        : base(FILE_PATH, serializer, pubSub, _changeKey)
 | 
			
		||||
    {   
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyCollection<string> GetLoadedMedusae()
 | 
			
		||||
        => Data.Loaded.ToList();
 | 
			
		||||
 | 
			
		||||
    public void AddLoadedMedusa(string name)
 | 
			
		||||
    {
 | 
			
		||||
        name = name.Trim().ToLowerInvariant();
 | 
			
		||||
        
 | 
			
		||||
        ModifyConfig(conf =>
 | 
			
		||||
        {
 | 
			
		||||
            if(!conf.Loaded.Contains(name))
 | 
			
		||||
                conf.Loaded.Add(name);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public void RemoveLoadedMedusa(string name)
 | 
			
		||||
    {
 | 
			
		||||
        name = name.Trim().ToLowerInvariant();
 | 
			
		||||
        
 | 
			
		||||
        ModifyConfig(conf =>
 | 
			
		||||
        {
 | 
			
		||||
            conf.Loaded.Remove(name);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								src/NadekoBot/Common/Medusa/IMedusaLoaderService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/NadekoBot/Common/Medusa/IMedusaLoaderService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
public interface IMedusaLoaderService
 | 
			
		||||
{
 | 
			
		||||
    Task<MedusaLoadResult> LoadMedusaAsync(string medusaName);
 | 
			
		||||
    Task<MedusaUnloadResult> UnloadMedusaAsync(string medusaName);
 | 
			
		||||
    string GetCommandDescription(string medusaName, string commandName, CultureInfo culture);
 | 
			
		||||
    string[] GetCommandExampleArgs(string medusaName, string commandName, CultureInfo culture);
 | 
			
		||||
    Task ReloadStrings();
 | 
			
		||||
    IReadOnlyCollection<string> GetAllMedusae();
 | 
			
		||||
    IReadOnlyCollection<MedusaStats> GetLoadedMedusae(CultureInfo? cultureInfo = null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public sealed record MedusaStats(string Name,
 | 
			
		||||
    string? Description,
 | 
			
		||||
    IReadOnlyCollection<SnekStats> Sneks);
 | 
			
		||||
    
 | 
			
		||||
public sealed record SnekStats(string Name, 
 | 
			
		||||
    IReadOnlyCollection<SnekCommandStats> Commands);
 | 
			
		||||
 | 
			
		||||
public sealed record SnekCommandStats(string Name);
 | 
			
		||||
							
								
								
									
										36
									
								
								src/NadekoBot/Common/Medusa/MedusaAssemblyLoadContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/NadekoBot/Common/Medusa/MedusaAssemblyLoadContext.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Runtime.Loader;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
public sealed class MedusaAssemblyLoadContext : AssemblyLoadContext
 | 
			
		||||
{
 | 
			
		||||
    private readonly AssemblyDependencyResolver _depResolver;
 | 
			
		||||
 | 
			
		||||
    public MedusaAssemblyLoadContext(string pluginPath) : base(isCollectible: true)
 | 
			
		||||
    {
 | 
			
		||||
        _depResolver = new(pluginPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override Assembly? Load(AssemblyName assemblyName)
 | 
			
		||||
    {
 | 
			
		||||
        var assemblyPath = _depResolver.ResolveAssemblyToPath(assemblyName);
 | 
			
		||||
        if (assemblyPath != null)
 | 
			
		||||
        {
 | 
			
		||||
            return LoadFromAssemblyPath(assemblyPath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
 | 
			
		||||
    {
 | 
			
		||||
        var libraryPath = _depResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
 | 
			
		||||
        if (libraryPath != null)
 | 
			
		||||
        {
 | 
			
		||||
            return LoadUnmanagedDllFromPath(libraryPath);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return IntPtr.Zero;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										880
									
								
								src/NadekoBot/Common/Medusa/MedusaLoaderService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										880
									
								
								src/NadekoBot/Common/Medusa/MedusaLoaderService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,880 @@
 | 
			
		||||
using Discord.Commands.Builders;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using System.Runtime.CompilerServices;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
// ReSharper disable RedundantAssignment
 | 
			
		||||
public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly CommandService _cmdService;
 | 
			
		||||
    private readonly IServiceProvider _botServices;
 | 
			
		||||
    private readonly IBehaviorHandler _behHandler;
 | 
			
		||||
    private readonly IPubSub _pubSub;
 | 
			
		||||
    private readonly IMedusaConfigService _medusaConfig;
 | 
			
		||||
    
 | 
			
		||||
    private readonly ConcurrentDictionary<string, ResolvedMedusa> _resolved = new();
 | 
			
		||||
    private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
 | 
			
		||||
 | 
			
		||||
    private readonly TypedKey<string> _loadKey = new("medusa:load");
 | 
			
		||||
    private readonly TypedKey<string> _unloadKey = new("medusa:unload");
 | 
			
		||||
    
 | 
			
		||||
    private readonly TypedKey<bool> _stringsReload = new("medusa:reload_strings");
 | 
			
		||||
 | 
			
		||||
    private const string BASE_DIR = "data/medusae";
 | 
			
		||||
 | 
			
		||||
    public MedusaLoaderService(CommandService cmdService,
 | 
			
		||||
        IServiceProvider botServices,
 | 
			
		||||
        IBehaviorHandler behHandler,
 | 
			
		||||
        IPubSub pubSub,
 | 
			
		||||
        IMedusaConfigService medusaConfig)
 | 
			
		||||
    {
 | 
			
		||||
        _cmdService = cmdService;
 | 
			
		||||
        _botServices = botServices;
 | 
			
		||||
        _behHandler = behHandler;
 | 
			
		||||
        _pubSub = pubSub;
 | 
			
		||||
        _medusaConfig = medusaConfig;
 | 
			
		||||
        
 | 
			
		||||
        // 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));
 | 
			
		||||
 | 
			
		||||
        _pubSub.Sub(_stringsReload, async _ => await ReloadStringsInternal());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyCollection<string> GetAllMedusae()
 | 
			
		||||
    {
 | 
			
		||||
        if (!Directory.Exists(BASE_DIR))
 | 
			
		||||
            return Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
        return Directory.GetDirectories(BASE_DIR)
 | 
			
		||||
                        .Select(x => Path.GetRelativePath(BASE_DIR, x))
 | 
			
		||||
                        .ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    public IReadOnlyCollection<MedusaStats> GetLoadedMedusae(CultureInfo? culture)
 | 
			
		||||
    {
 | 
			
		||||
        var toReturn = new List<MedusaStats>(_resolved.Count);
 | 
			
		||||
        foreach (var (name, resolvedData) in _resolved)
 | 
			
		||||
        {
 | 
			
		||||
            var sneks = new List<SnekStats>(resolvedData.SnekInfos.Count);
 | 
			
		||||
 | 
			
		||||
            foreach (var snekInfos in resolvedData.SnekInfos.Concat(resolvedData.SnekInfos.SelectMany(x => x.Subsneks)))
 | 
			
		||||
            {
 | 
			
		||||
                var commands = new List<SnekCommandStats>();
 | 
			
		||||
 | 
			
		||||
                foreach (var command in snekInfos.Commands)
 | 
			
		||||
                {
 | 
			
		||||
                    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var name in _medusaConfig.GetLoadedMedusae())
 | 
			
		||||
        {
 | 
			
		||||
            var result = await InternalLoadAsync(name);
 | 
			
		||||
            if(result != MedusaLoadResult.Success)
 | 
			
		||||
                Log.Warning("Unable to load '{MedusaName}' medusa", name);
 | 
			
		||||
            else 
 | 
			
		||||
                Log.Warning("Loaded medusa '{MedusaName}'", name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    public async Task<MedusaLoadResult> LoadMedusaAsync(string medusaName)
 | 
			
		||||
    {
 | 
			
		||||
        // try loading on this shard first to see if it works
 | 
			
		||||
        var res = await InternalLoadAsync(medusaName);
 | 
			
		||||
        if (res == MedusaLoadResult.Success)
 | 
			
		||||
        {
 | 
			
		||||
            // if it does publish it so that other shards can load the medusa too
 | 
			
		||||
            // this method will be ran twice on this shard but it doesn't matter as 
 | 
			
		||||
            // the second attempt will be ignored
 | 
			
		||||
            await _pubSub.Pub(_loadKey, medusaName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return res;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    public async Task<MedusaUnloadResult> UnloadMedusaAsync(string medusaName)
 | 
			
		||||
    {
 | 
			
		||||
        var res = await InternalUnloadAsync(medusaName);
 | 
			
		||||
        if (res == MedusaUnloadResult.Success)
 | 
			
		||||
        {
 | 
			
		||||
            await _pubSub.Pub(_unloadKey, medusaName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return res;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    public string[] GetCommandExampleArgs(string medusaName, string commandName, CultureInfo culture)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_resolved.TryGetValue(medusaName, out var data))
 | 
			
		||||
            return Array.Empty<string>();
 | 
			
		||||
 | 
			
		||||
        return data.Strings.GetCommandStrings(commandName, culture).Args
 | 
			
		||||
               ?? data.SnekInfos
 | 
			
		||||
                      .SelectMany(x => x.Commands)
 | 
			
		||||
                      .FirstOrDefault(x => x.Aliases.Any(alias
 | 
			
		||||
                          => alias.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)))
 | 
			
		||||
                      ?.OptionalStrings
 | 
			
		||||
                      .Args
 | 
			
		||||
               ?? new[] { string.Empty };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task ReloadStrings()
 | 
			
		||||
        => _pubSub.Pub(_stringsReload, true);
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private void ReloadStringsSync()
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var resolved in _resolved.Values)
 | 
			
		||||
        {
 | 
			
		||||
            resolved.Strings.Reload();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private async Task ReloadStringsInternal()
 | 
			
		||||
    {
 | 
			
		||||
        await _lock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            ReloadStringsSync();
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _lock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    public string GetCommandDescription(string medusaName, string commandName, CultureInfo culture)
 | 
			
		||||
    {
 | 
			
		||||
        if (!_resolved.TryGetValue(medusaName, out var data))
 | 
			
		||||
            return string.Empty;
 | 
			
		||||
 | 
			
		||||
        return data.Strings.GetCommandStrings(commandName, culture).Desc
 | 
			
		||||
               ?? data.SnekInfos
 | 
			
		||||
                      .SelectMany(x => x.Commands)
 | 
			
		||||
                      .FirstOrDefault(x => x.Aliases.Any(alias
 | 
			
		||||
                          => alias.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)))
 | 
			
		||||
                      ?.OptionalStrings
 | 
			
		||||
                      .Desc
 | 
			
		||||
               ?? string.Empty;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private async ValueTask<MedusaLoadResult> InternalLoadAsync(string name)
 | 
			
		||||
    {
 | 
			
		||||
        if (_resolved.ContainsKey(name))
 | 
			
		||||
            return MedusaLoadResult.AlreadyLoaded;
 | 
			
		||||
        
 | 
			
		||||
        var safeName = Uri.EscapeDataString(name);
 | 
			
		||||
        name = name.ToLowerInvariant();
 | 
			
		||||
 | 
			
		||||
        await _lock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var success = LoadAssemblyInternal(safeName,
 | 
			
		||||
                out var ctx,
 | 
			
		||||
                out var snekData,
 | 
			
		||||
                out var services,
 | 
			
		||||
                out var strings,
 | 
			
		||||
                out var typeReaders);
 | 
			
		||||
 | 
			
		||||
            if (success)
 | 
			
		||||
            {
 | 
			
		||||
                var moduleInfos = new List<ModuleInfo>();
 | 
			
		||||
 | 
			
		||||
                // todo uncomment
 | 
			
		||||
                LoadTypeReadersInternal(typeReaders);
 | 
			
		||||
 | 
			
		||||
                foreach (var point in snekData)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        // initialize snek and subsneks
 | 
			
		||||
                        await point.Instance.InitializeAsync();
 | 
			
		||||
                        foreach (var sub in point.Subsneks)
 | 
			
		||||
                        {
 | 
			
		||||
                            await sub.Instance.InitializeAsync();
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        var module = await LoadModuleInternalAsync(name, point, strings, services);
 | 
			
		||||
                        moduleInfos.Add(module);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Warning(ex,
 | 
			
		||||
                            "Error loading snek {SnekName}",
 | 
			
		||||
                            point.Name);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                var execs = GetExecsInternal(snekData, strings, services);
 | 
			
		||||
                await _behHandler.AddRangeAsync(execs);
 | 
			
		||||
 | 
			
		||||
                _resolved[name] = new(LoadContext: ctx,
 | 
			
		||||
                    ModuleInfos: moduleInfos.ToImmutableArray(),
 | 
			
		||||
                    SnekInfos: snekData.ToImmutableArray(),
 | 
			
		||||
                    strings,
 | 
			
		||||
                    typeReaders,
 | 
			
		||||
                    execs)
 | 
			
		||||
                {
 | 
			
		||||
                    Services = services
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                
 | 
			
		||||
                services = null;
 | 
			
		||||
                _medusaConfig.AddLoadedMedusa(safeName);
 | 
			
		||||
                return MedusaLoadResult.Success;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return MedusaLoadResult.Empty;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex) when (ex is FileNotFoundException or BadImageFormatException)
 | 
			
		||||
        {
 | 
			
		||||
            return MedusaLoadResult.NotFound;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "An error occurred loading a medusa");
 | 
			
		||||
            return MedusaLoadResult.UnknownError;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _lock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private IReadOnlyCollection<ICustomBehavior> GetExecsInternal(IReadOnlyCollection<SnekInfo> snekData, IMedusaStrings strings, IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        var behs = new List<ICustomBehavior>();
 | 
			
		||||
        foreach (var snek in snekData)
 | 
			
		||||
        {
 | 
			
		||||
            behs.Add(new BehaviorAdapter(new(snek.Instance), strings, services));
 | 
			
		||||
 | 
			
		||||
            foreach (var sub in snek.Subsneks)
 | 
			
		||||
            {
 | 
			
		||||
                behs.Add(new BehaviorAdapter(new(sub.Instance), strings, services));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        return behs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private void LoadTypeReadersInternal(Dictionary<Type, TypeReader> typeReaders)
 | 
			
		||||
    {
 | 
			
		||||
        var notAddedTypeReaders = new List<Type>();
 | 
			
		||||
        foreach (var (type, typeReader) in typeReaders)
 | 
			
		||||
        {
 | 
			
		||||
            // if type reader for this type already exists, it will not be replaced
 | 
			
		||||
            if (_cmdService.TypeReaders.Contains(type))
 | 
			
		||||
            {
 | 
			
		||||
                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
 | 
			
		||||
        foreach (var toRemove in notAddedTypeReaders)
 | 
			
		||||
        {
 | 
			
		||||
            typeReaders.Remove(toRemove);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private bool LoadAssemblyInternal(
 | 
			
		||||
        string safeName,
 | 
			
		||||
        [NotNullWhen(true)] out WeakReference<MedusaAssemblyLoadContext>? ctxWr,
 | 
			
		||||
        [NotNullWhen(true)] out IReadOnlyCollection<SnekInfo>? snekData,
 | 
			
		||||
        out IServiceProvider services,
 | 
			
		||||
        out IMedusaStrings strings,
 | 
			
		||||
        out Dictionary<Type, TypeReader> 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);
 | 
			
		||||
 | 
			
		||||
        if (sis.Count == 0)
 | 
			
		||||
        {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ctxWr = new(ctx);
 | 
			
		||||
        snekData = sis;
 | 
			
		||||
        
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static readonly Type _paramParserType = typeof(ParamParser<>);
 | 
			
		||||
    
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private Dictionary<Type, TypeReader> LoadTypeReadersFromAssembly(
 | 
			
		||||
        Assembly assembly,
 | 
			
		||||
        IMedusaStrings strings,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        var paramParsers = assembly.GetExportedTypes()
 | 
			
		||||
                .Where(x => x.IsClass
 | 
			
		||||
                            && !x.IsAbstract
 | 
			
		||||
                            && x.BaseType is not null
 | 
			
		||||
                            && x.BaseType.IsGenericType
 | 
			
		||||
                            && x.BaseType.GetGenericTypeDefinition() == _paramParserType);
 | 
			
		||||
 | 
			
		||||
        var typeReaders = new Dictionary<Type, TypeReader>();
 | 
			
		||||
        foreach (var parserType in paramParsers)
 | 
			
		||||
        {
 | 
			
		||||
            var parserObj = ActivatorUtilities.CreateInstance(services, parserType);
 | 
			
		||||
 | 
			
		||||
            var targetType = parserType.BaseType!.GetGenericArguments()[0];
 | 
			
		||||
            var typeReaderInstance = (TypeReader)Activator.CreateInstance(
 | 
			
		||||
                typeof(ParamParserAdapter<>).MakeGenericType(targetType),
 | 
			
		||||
                args: new[] { parserObj, strings, services })!;
 | 
			
		||||
            
 | 
			
		||||
            typeReaders.Add(targetType, typeReaderInstance);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return typeReaders;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private async Task<ModuleInfo> LoadModuleInternalAsync(string medusaName, SnekInfo snekInfo, IMedusaStrings strings, IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        var module = await _cmdService.CreateModuleAsync(snekInfo.Instance.Prefix,
 | 
			
		||||
            CreateModuleFactory(medusaName, snekInfo, strings, services));
 | 
			
		||||
        
 | 
			
		||||
        return module;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private Action<ModuleBuilder> CreateModuleFactory(
 | 
			
		||||
        string medusaName,
 | 
			
		||||
        SnekInfo snekInfo,
 | 
			
		||||
        IMedusaStrings strings,
 | 
			
		||||
        IServiceProvider medusaServices)
 | 
			
		||||
        => mb =>
 | 
			
		||||
        {
 | 
			
		||||
            var m = mb.WithName(snekInfo.Name);
 | 
			
		||||
 | 
			
		||||
            foreach (var cmd in snekInfo.Commands)
 | 
			
		||||
            {
 | 
			
		||||
                m.AddCommand(cmd.Aliases.First(),
 | 
			
		||||
                    CreateCallback(cmd.ContextType,
 | 
			
		||||
                        new(snekInfo),
 | 
			
		||||
                        new(cmd),
 | 
			
		||||
                        new(medusaServices),
 | 
			
		||||
                        strings),
 | 
			
		||||
                    CreateCommandFactory(medusaName, cmd));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var subInfo in snekInfo.Subsneks)
 | 
			
		||||
                m.AddModule(subInfo.Instance.Prefix, CreateModuleFactory(medusaName, subInfo, strings, medusaServices));
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    private static readonly RequireContextAttribute _reqGuild = new RequireContextAttribute(ContextType.Guild);
 | 
			
		||||
    private static readonly RequireContextAttribute _reqDm = new RequireContextAttribute(ContextType.DM);
 | 
			
		||||
    private Action<CommandBuilder> CreateCommandFactory(string medusaName, SnekCommandData cmd)
 | 
			
		||||
        => (cb) =>
 | 
			
		||||
        {
 | 
			
		||||
            cb.AddAliases(cmd.Aliases.Skip(1).ToArray());
 | 
			
		||||
 | 
			
		||||
            if (cmd.ContextType == CommandContextType.Guild)
 | 
			
		||||
                cb.AddPrecondition(_reqGuild);
 | 
			
		||||
            else if (cmd.ContextType == CommandContextType.Dm)
 | 
			
		||||
                cb.AddPrecondition(_reqDm);
 | 
			
		||||
 | 
			
		||||
            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));
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    private Action<ParameterBuilder> CreateParamFactory(ParamData paramData)
 | 
			
		||||
        => (pb) =>
 | 
			
		||||
        {
 | 
			
		||||
            pb.WithIsMultiple(paramData.IsParams)
 | 
			
		||||
              .WithIsOptional(paramData.IsOptional)
 | 
			
		||||
              .WithIsRemainder(paramData.IsLeftover);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> CreateCallback(
 | 
			
		||||
        CommandContextType contextType,
 | 
			
		||||
        WeakReference<SnekInfo> snekDataWr,
 | 
			
		||||
        WeakReference<SnekCommandData> snekCommandDataWr,
 | 
			
		||||
        WeakReference<IServiceProvider> medusaServicesWr,
 | 
			
		||||
        IMedusaStrings strings)
 | 
			
		||||
        => async (context, parameters, svcs, _) =>
 | 
			
		||||
        {
 | 
			
		||||
            if (!snekCommandDataWr.TryGetTarget(out var cmdData)
 | 
			
		||||
                || !snekDataWr.TryGetTarget(out var snekData)
 | 
			
		||||
                || !medusaServicesWr.TryGetTarget(out var medusaServices))
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Attempted to run an unloaded snek's command");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
                
 | 
			
		||||
            var paramObjs = ParamObjs(contextType, cmdData, parameters, context, svcs, medusaServices, strings);
 | 
			
		||||
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var methodInfo = cmdData.MethodInfo;
 | 
			
		||||
                if (methodInfo.ReturnType == typeof(Task)
 | 
			
		||||
                    || (methodInfo.ReturnType.IsGenericType
 | 
			
		||||
                        && methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)))
 | 
			
		||||
                {
 | 
			
		||||
                    await (Task)methodInfo.Invoke(snekData.Instance, paramObjs)!;
 | 
			
		||||
                }
 | 
			
		||||
                else if (methodInfo.ReturnType == typeof(ValueTask))
 | 
			
		||||
                {
 | 
			
		||||
                    await ((ValueTask)methodInfo.Invoke(snekData.Instance, paramObjs)!).AsTask();
 | 
			
		||||
                }
 | 
			
		||||
                else // if (methodInfo.ReturnType == typeof(void))
 | 
			
		||||
                {
 | 
			
		||||
                    methodInfo.Invoke(snekData.Instance, paramObjs);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
                paramObjs = null;
 | 
			
		||||
                cmdData = null;
 | 
			
		||||
                
 | 
			
		||||
                snekData = null;
 | 
			
		||||
                medusaServices = null;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private static object[] ParamObjs(
 | 
			
		||||
        CommandContextType contextType,
 | 
			
		||||
        SnekCommandData cmdData,
 | 
			
		||||
        object[] parameters,
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        IServiceProvider svcs,
 | 
			
		||||
        IServiceProvider svcProvider,
 | 
			
		||||
        IMedusaStrings strings)
 | 
			
		||||
    {
 | 
			
		||||
        var extraParams = contextType == CommandContextType.Unspecified ? 0 : 1;
 | 
			
		||||
        extraParams += cmdData.InjectedParams.Count;
 | 
			
		||||
 | 
			
		||||
        var paramObjs = new object[parameters.Length + extraParams];
 | 
			
		||||
 | 
			
		||||
        var startAt = 0;
 | 
			
		||||
        if (contextType != CommandContextType.Unspecified)
 | 
			
		||||
        {
 | 
			
		||||
            paramObjs[0] = ContextAdapterFactory.CreateNew(context, strings, svcs);
 | 
			
		||||
 | 
			
		||||
            startAt = 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < cmdData.InjectedParams.Count; i++)
 | 
			
		||||
        {
 | 
			
		||||
            var svc = svcProvider.GetService(cmdData.InjectedParams[i]);
 | 
			
		||||
            if (svc is null)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException($"Cannot inject a service of type {cmdData.InjectedParams[i]}");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            paramObjs[i + startAt] = svc;
 | 
			
		||||
 | 
			
		||||
            svc = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        startAt += cmdData.InjectedParams.Count;
 | 
			
		||||
 | 
			
		||||
        for (var i = 0; i < parameters.Length; i++)
 | 
			
		||||
            paramObjs[startAt + i] = parameters[i];
 | 
			
		||||
        
 | 
			
		||||
        return paramObjs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private async Task<MedusaUnloadResult> InternalUnloadAsync(string name)
 | 
			
		||||
    {
 | 
			
		||||
        name = name.ToLowerInvariant();
 | 
			
		||||
        if (!_resolved.Remove(name, out var lsi))
 | 
			
		||||
            return MedusaUnloadResult.NotLoaded;
 | 
			
		||||
 | 
			
		||||
        await _lock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            UnloadTypeReaders(lsi.TypeReaders);
 | 
			
		||||
 | 
			
		||||
            foreach (var mi in lsi.ModuleInfos)
 | 
			
		||||
            {
 | 
			
		||||
                await _cmdService.RemoveModuleAsync(mi);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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 = null;
 | 
			
		||||
            
 | 
			
		||||
            _medusaConfig.RemoveLoadedMedusa(name);
 | 
			
		||||
            return UnloadInternal(lc)
 | 
			
		||||
                ? MedusaUnloadResult.Success
 | 
			
		||||
                : MedusaUnloadResult.PossiblyUnable;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _lock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void UnloadTypeReaders(Dictionary<Type, TypeReader> valueTypeReaders)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var tr in valueTypeReaders)
 | 
			
		||||
        {
 | 
			
		||||
            _cmdService.TryRemoveTypeReader(tr.Key, false, out _);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private async Task DisposeSnekInstances(ResolvedMedusa medusa)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var si in medusa.SnekInfos)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await si.Instance.DisposeAsync();
 | 
			
		||||
                foreach (var sub in si.Subsneks)
 | 
			
		||||
                {
 | 
			
		||||
                    await sub.Instance.DisposeAsync();
 | 
			
		||||
                }  
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning(ex,
 | 
			
		||||
                    "Failed cleanup of Snek {SnekName}. This medusa might not unload correctly",
 | 
			
		||||
                    si.Instance.Name);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // medusae = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private bool UnloadInternal(WeakReference<MedusaAssemblyLoadContext> lsi)
 | 
			
		||||
    {
 | 
			
		||||
        UnloadContext(lsi);
 | 
			
		||||
        GcCleanup();
 | 
			
		||||
 | 
			
		||||
        return !lsi.TryGetTarget(out _);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private void UnloadContext(WeakReference<MedusaAssemblyLoadContext> lsiLoadContext)
 | 
			
		||||
    {
 | 
			
		||||
        if(lsiLoadContext.TryGetTarget(out var ctx))
 | 
			
		||||
            ctx.Unload();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private void GcCleanup()
 | 
			
		||||
    {
 | 
			
		||||
        // cleanup
 | 
			
		||||
        for (var i = 0; i < 10; i++)
 | 
			
		||||
        {
 | 
			
		||||
            GC.Collect();
 | 
			
		||||
            GC.WaitForPendingFinalizers();
 | 
			
		||||
            GC.WaitForFullGCComplete();
 | 
			
		||||
            GC.Collect();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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<svcAttribute>(x => x.Lifetime == Lifetime.Transient))
 | 
			
		||||
                       .AsSelfWithInterfaces()
 | 
			
		||||
                       .WithTransientLifetime()
 | 
			
		||||
                       .AddClasses(static x => x.WithAttribute<svcAttribute>(x => x.Lifetime == Lifetime.Singleton))
 | 
			
		||||
                       .AsSelfWithInterfaces()
 | 
			
		||||
                       .WithSingletonLifetime())
 | 
			
		||||
           .BuildServiceProvider();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    public IReadOnlyCollection<SnekInfo> LoadSneksFromAssembly(Assembly a, out IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        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
 | 
			
		||||
        var classes = types.Where(static x => x.IsClass
 | 
			
		||||
                                              && (x.IsNestedPublic || x.IsPublic)
 | 
			
		||||
                                              && !x.IsAbstract
 | 
			
		||||
                                              && x.BaseType == _snekType
 | 
			
		||||
                                              && (x.DeclaringType is null || x.DeclaringType.IsAssignableTo(_snekType)))
 | 
			
		||||
                           .ToList();
 | 
			
		||||
 | 
			
		||||
        var topModules = new Dictionary<Type, SnekInfo>();
 | 
			
		||||
        
 | 
			
		||||
        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);
 | 
			
		||||
            topModules.Add(cl, module);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach (var c in classes)
 | 
			
		||||
        {
 | 
			
		||||
            if (c.DeclaringType is not Type dt)
 | 
			
		||||
                continue;
 | 
			
		||||
 | 
			
		||||
            // if there is no top level module which this module is a child of
 | 
			
		||||
            // just print a warning and skip it
 | 
			
		||||
            if (!topModules.TryGetValue(dt, out var parentData))
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("Can't load submodule {SubName} because parent module {Name} does not exist",
 | 
			
		||||
                    c.Name,
 | 
			
		||||
                    dt.Name);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            GetModuleData(c, services, parentData);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return topModules.Values.ToArray();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private SnekInfo GetModuleData(Type type, IServiceProvider services, SnekInfo? parentData = null)
 | 
			
		||||
    {
 | 
			
		||||
        var filters = type.GetCustomAttributes<FilterAttribute>(true)
 | 
			
		||||
                          .ToArray();
 | 
			
		||||
        
 | 
			
		||||
        var instance = (Snek)ActivatorUtilities.CreateInstance(services, type);
 | 
			
		||||
        
 | 
			
		||||
        var module = new SnekInfo(instance.Name,
 | 
			
		||||
            parentData,
 | 
			
		||||
            instance,
 | 
			
		||||
            GetCommands(instance, type),
 | 
			
		||||
            filters);
 | 
			
		||||
 | 
			
		||||
        if (parentData is not null)
 | 
			
		||||
            parentData.Subsneks.Add(module);
 | 
			
		||||
 | 
			
		||||
        return module;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private IReadOnlyCollection<SnekCommandData> GetCommands(Snek instance, Type type)
 | 
			
		||||
    {
 | 
			
		||||
        var methodInfos = type
 | 
			
		||||
                          .GetMethods(BindingFlags.Instance
 | 
			
		||||
                                      | BindingFlags.DeclaredOnly
 | 
			
		||||
                                      | BindingFlags.Public)
 | 
			
		||||
                          .Where(static x =>
 | 
			
		||||
                          {
 | 
			
		||||
                              if(x.GetCustomAttribute<cmdAttribute>(true) is null)
 | 
			
		||||
                                  return false;
 | 
			
		||||
 | 
			
		||||
                              if (x.ReturnType.IsGenericType)
 | 
			
		||||
                              {
 | 
			
		||||
                                  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;
 | 
			
		||||
                              }
 | 
			
		||||
 | 
			
		||||
                              var succ = x.ReturnType == typeof(Task)
 | 
			
		||||
                                         || x.ReturnType == typeof(ValueTask)
 | 
			
		||||
                                         || x.ReturnType == typeof(void);
 | 
			
		||||
 | 
			
		||||
                              if (!succ)
 | 
			
		||||
                              {
 | 
			
		||||
                                  Log.Warning("Method {MethodName} has an invalid return type: {ReturnType}",
 | 
			
		||||
                                      x.Name,
 | 
			
		||||
                                      x.ReturnType);
 | 
			
		||||
                              }
 | 
			
		||||
 | 
			
		||||
                              return succ;
 | 
			
		||||
                          });
 | 
			
		||||
        
 | 
			
		||||
        
 | 
			
		||||
        var cmds = new List<SnekCommandData>();
 | 
			
		||||
        foreach (var method in methodInfos)
 | 
			
		||||
        {
 | 
			
		||||
            var filters = method.GetCustomAttributes<FilterAttribute>().ToArray();
 | 
			
		||||
            var prio = method.GetCustomAttribute<prioAttribute>()?.Priority ?? 0;
 | 
			
		||||
 | 
			
		||||
            var paramInfos = method.GetParameters();
 | 
			
		||||
            var cmdParams = new List<ParamData>();
 | 
			
		||||
            var diParams = new List<Type>();
 | 
			
		||||
            var cmdContext = CommandContextType.Unspecified;
 | 
			
		||||
            var canInject = false;
 | 
			
		||||
            for (var paramCounter = 0; paramCounter < paramInfos.Length; paramCounter++)
 | 
			
		||||
            {
 | 
			
		||||
                var pi = paramInfos[paramCounter];
 | 
			
		||||
 | 
			
		||||
                var paramName = pi.Name ?? "unnamed";
 | 
			
		||||
                var isContext = paramCounter == 0 && pi.ParameterType.IsAssignableTo(typeof(AnyContext));
 | 
			
		||||
 | 
			
		||||
                var leftoverAttribute = pi.GetCustomAttribute<Nadeko.Snake.leftoverAttribute>(true);
 | 
			
		||||
                var hasDefaultValue = pi.HasDefaultValue;
 | 
			
		||||
                var isLeftover = leftoverAttribute != null;
 | 
			
		||||
                var isParams = pi.GetCustomAttribute<ParamArrayAttribute>() is not null;
 | 
			
		||||
                var paramType = pi.ParameterType;
 | 
			
		||||
                var isInjected = pi.GetCustomAttribute<injectAttribute>(true) is not null;
 | 
			
		||||
 | 
			
		||||
                if (isContext)
 | 
			
		||||
                {
 | 
			
		||||
                    if (hasDefaultValue || leftoverAttribute != null || isParams)
 | 
			
		||||
                        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;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (isInjected)
 | 
			
		||||
                {
 | 
			
		||||
                    if (!canInject && paramCounter != 0)
 | 
			
		||||
                        throw new ArgumentException($"Parameters marked as [Injected] have to come after IContext");
 | 
			
		||||
 | 
			
		||||
                    canInject = true;
 | 
			
		||||
                    
 | 
			
		||||
                    diParams.Add(paramType);
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                canInject = false;
 | 
			
		||||
 | 
			
		||||
                if (isParams)
 | 
			
		||||
                {
 | 
			
		||||
                    if (hasDefaultValue)
 | 
			
		||||
                        throw new NotSupportedException("Params can't have const values at the moment. "
 | 
			
		||||
                                                        + GetErrorPath(method, pi));
 | 
			
		||||
                    // if it's params, it means it's an array, and i only need a parser for the actual type,
 | 
			
		||||
                    // as the parser will run on each array element, it can't be null
 | 
			
		||||
                    paramType = paramType.GetElementType()!;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // leftover can only be the last parameter.
 | 
			
		||||
                if (isLeftover && paramCounter != paramInfos.Length - 1)
 | 
			
		||||
                {
 | 
			
		||||
                    var path = GetErrorPath(method, pi);
 | 
			
		||||
                    Log.Error("Only one parameter can be marked [Leftover] and it has to be the last one. {Path} ",
 | 
			
		||||
                        path);
 | 
			
		||||
                    throw new ArgumentException("Leftover attribute error.");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                cmdParams.Add(new ParamData(paramType, paramName, hasDefaultValue, isLeftover, isParams));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            var cmdAttribute = method.GetCustomAttribute<cmdAttribute>()!; 
 | 
			
		||||
            var aliases = cmdAttribute.Aliases;
 | 
			
		||||
            if (aliases.Length == 0)
 | 
			
		||||
                aliases = new[] { method.Name.ToLowerInvariant() };
 | 
			
		||||
            
 | 
			
		||||
            cmds.Add(new(
 | 
			
		||||
                aliases,
 | 
			
		||||
                method,
 | 
			
		||||
                instance,
 | 
			
		||||
                filters,
 | 
			
		||||
                cmdContext,
 | 
			
		||||
                diParams,
 | 
			
		||||
                cmdParams,
 | 
			
		||||
                new(cmdAttribute.desc, cmdAttribute.args),
 | 
			
		||||
                prio
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return cmds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [MethodImpl(MethodImplOptions.NoInlining)]
 | 
			
		||||
    private string GetErrorPath(MethodInfo m, System.Reflection.ParameterInfo pi)
 | 
			
		||||
        => $@"Module: {m.DeclaringType?.Name} 
 | 
			
		||||
Command: {m.Name}
 | 
			
		||||
ParamName: {pi.Name}
 | 
			
		||||
ParamType: {pi.ParameterType.Name}";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum MedusaLoadResult
 | 
			
		||||
{
 | 
			
		||||
    Success,
 | 
			
		||||
    NotFound,
 | 
			
		||||
    AlreadyLoaded,
 | 
			
		||||
    Empty,
 | 
			
		||||
    UnknownError,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum MedusaUnloadResult
 | 
			
		||||
{
 | 
			
		||||
    Success,
 | 
			
		||||
    NotLoaded,
 | 
			
		||||
    PossiblyUnable,
 | 
			
		||||
    NotFound,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								src/NadekoBot/Common/Medusa/MedusaServiceProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/NadekoBot/Common/Medusa/MedusaServiceProvider.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/NadekoBot/Common/Medusa/Models/ParamData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/NadekoBot/Common/Medusa/Models/ParamData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
public sealed record ParamData(
 | 
			
		||||
    Type Type,
 | 
			
		||||
    string Name,
 | 
			
		||||
    bool IsOptional,
 | 
			
		||||
    bool IsLeftover,
 | 
			
		||||
    bool IsParams
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										14
									
								
								src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/NadekoBot/Common/Medusa/Models/ResolvedMedusa.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
public sealed record ResolvedMedusa(
 | 
			
		||||
    WeakReference<MedusaAssemblyLoadContext> LoadContext,
 | 
			
		||||
    IImmutableList<ModuleInfo> ModuleInfos,
 | 
			
		||||
    IImmutableList<SnekInfo> SnekInfos,
 | 
			
		||||
    IMedusaStrings Strings,
 | 
			
		||||
    Dictionary<Type, TypeReader> TypeReaders,
 | 
			
		||||
    IReadOnlyCollection<ICustomBehavior> Execs)
 | 
			
		||||
{
 | 
			
		||||
    public IServiceProvider Services { get; set; } = null!;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/NadekoBot/Common/Medusa/Models/SnekCommandData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/NadekoBot/Common/Medusa/Models/SnekCommandData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
using Microsoft.VisualBasic;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using CommandStrings = Nadeko.Snake.CommandStrings;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
public sealed class SnekCommandData
 | 
			
		||||
{
 | 
			
		||||
    public SnekCommandData(
 | 
			
		||||
        IReadOnlyCollection<string> aliases,
 | 
			
		||||
        MethodInfo methodInfo,
 | 
			
		||||
        Snek module,
 | 
			
		||||
        FilterAttribute[] filters,
 | 
			
		||||
        CommandContextType contextType,
 | 
			
		||||
        IReadOnlyList<Type> injectedParams,
 | 
			
		||||
        IReadOnlyList<ParamData> parameters,
 | 
			
		||||
        CommandStrings strings,
 | 
			
		||||
        int priority)
 | 
			
		||||
    {
 | 
			
		||||
        Aliases = aliases;
 | 
			
		||||
        MethodInfo = methodInfo;
 | 
			
		||||
        Module = module;
 | 
			
		||||
        Filters = filters;
 | 
			
		||||
        ContextType = contextType;
 | 
			
		||||
        InjectedParams = injectedParams;
 | 
			
		||||
        Parameters = parameters;
 | 
			
		||||
        Priority = priority;
 | 
			
		||||
        OptionalStrings = strings;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public CommandStrings OptionalStrings { get; set; }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyCollection<string> Aliases { get; }
 | 
			
		||||
    public MethodInfo MethodInfo { get; set; }
 | 
			
		||||
    public Snek Module { get; set; }
 | 
			
		||||
    public FilterAttribute[] Filters { get; set; }
 | 
			
		||||
    public CommandContextType ContextType { get; }
 | 
			
		||||
    public IReadOnlyList<Type> InjectedParams { get; }
 | 
			
		||||
    public IReadOnlyList<ParamData> Parameters { get; }
 | 
			
		||||
    public int Priority { get; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/NadekoBot/Common/Medusa/Models/SnekData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/NadekoBot/Common/Medusa/Models/SnekData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
namespace Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
public sealed record SnekInfo(
 | 
			
		||||
    string Name,
 | 
			
		||||
    SnekInfo? Parent,
 | 
			
		||||
    Snek Instance,
 | 
			
		||||
    IReadOnlyCollection<SnekCommandData> Commands,
 | 
			
		||||
    IReadOnlyCollection<FilterAttribute> Filters)
 | 
			
		||||
{
 | 
			
		||||
    public List<SnekInfo> Subsneks { get; set; } = new();
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     Implemented by modules which block execution before anything is executed
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IEarlyBehavior
 | 
			
		||||
{
 | 
			
		||||
    int Priority { get; }
 | 
			
		||||
    Task<bool> RunBehavior(IGuild guild, IUserMessage msg);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								src/NadekoBot/Common/ModuleBehaviors/IExecNoCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/NadekoBot/Common/ModuleBehaviors/IExecNoCommand.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     Executed if no command was found for this message
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IExecNoCommand
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Executed at the end of the lifecycle if no command was found
 | 
			
		||||
    /// <see cref="IExecOnMessage"/> →
 | 
			
		||||
    /// <see cref="IInputTransformer"/> →
 | 
			
		||||
    /// <see cref="IExecPreCommand"/> →
 | 
			
		||||
    /// [<see cref="IExecPostCommand"/> | *<see cref="IExecNoCommand"/>*]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guild"></param>
 | 
			
		||||
    /// <param name="msg"></param>
 | 
			
		||||
    /// <returns>A task representing completion</returns>
 | 
			
		||||
    Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/NadekoBot/Common/ModuleBehaviors/IExecOnMessage.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/NadekoBot/Common/ModuleBehaviors/IExecOnMessage.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     Implemented by modules to handle non-bot messages received
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IExecOnMessage
 | 
			
		||||
{
 | 
			
		||||
    int Priority { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Ran after a non-bot message was received
 | 
			
		||||
    /// *<see cref="IExecOnMessage"/>* →
 | 
			
		||||
    /// <see cref="IInputTransformer"/> →
 | 
			
		||||
    /// <see cref="IExecPreCommand"/> →
 | 
			
		||||
    /// [<see cref="IExecPostCommand"/> | <see cref="IExecNoCommand"/>]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guild">Guild where the message was sent</param>
 | 
			
		||||
    /// <param name="msg">The message that was received</param>
 | 
			
		||||
    /// <returns>Whether further processing of this message should be blocked</returns>
 | 
			
		||||
    Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/NadekoBot/Common/ModuleBehaviors/IExecPostCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/NadekoBot/Common/ModuleBehaviors/IExecPostCommand.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// This interface's method is executed after the command successfully finished execution.
 | 
			
		||||
/// ***There is no support for this method in NadekoBot services.***
 | 
			
		||||
/// It is only meant to be used in medusa system
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IExecPostCommand
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Executed after a command was successfully executed
 | 
			
		||||
    /// <see cref="IExecOnMessage"/> →
 | 
			
		||||
    /// <see cref="IInputTransformer"/> →
 | 
			
		||||
    /// <see cref="IExecPreCommand"/> →
 | 
			
		||||
    /// [*<see cref="IExecPostCommand"/>* | <see cref="IExecNoCommand"/>]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="ctx">Command context</param>
 | 
			
		||||
    /// <param name="moduleName">Module name</param>
 | 
			
		||||
    /// <param name="commandName">Command name</param>
 | 
			
		||||
    /// <returns>A task representing completion</returns>
 | 
			
		||||
    ValueTask ExecPostCommandAsync(ICommandContext ctx, string moduleName, string commandName);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/NadekoBot/Common/ModuleBehaviors/IExecPreCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/NadekoBot/Common/ModuleBehaviors/IExecPreCommand.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// This interface's method is executed after a command was found but before it was executed.
 | 
			
		||||
/// Able to block further processing of a command
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IExecPreCommand
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// <para>
 | 
			
		||||
    /// Ran after a command was found but before execution.
 | 
			
		||||
    /// </para>
 | 
			
		||||
    /// <see cref="IExecOnMessage"/> →
 | 
			
		||||
    /// <see cref="IInputTransformer"/> →
 | 
			
		||||
    /// *<see cref="IExecPreCommand"/>* →
 | 
			
		||||
    /// [<see cref="IExecPostCommand"/> | <see cref="IExecNoCommand"/>]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="context">Command context</param>
 | 
			
		||||
    /// <param name="moduleName">Name of the module</param>
 | 
			
		||||
    /// <param name="command">Command info</param>
 | 
			
		||||
    /// <returns>Whether further processing of the command is blocked</returns>
 | 
			
		||||
    Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,23 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Implemented by services which may transform input before a command is searched for
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IInputTransformer
 | 
			
		||||
{
 | 
			
		||||
    Task<string> TransformInput(
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Ran after a non-bot message was received
 | 
			
		||||
    /// <see cref="IExecOnMessage"/> ->
 | 
			
		||||
    /// *<see cref="IInputTransformer"/>* ->
 | 
			
		||||
    /// <see cref="IExecPreCommand"/> ->
 | 
			
		||||
    /// [<see cref="IExecPostCommand"/> OR <see cref="IExecNoCommand"/>]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guild">Guild</param>
 | 
			
		||||
    /// <param name="channel">Channel in which the message was sent</param>
 | 
			
		||||
    /// <param name="user">User who sent the message</param>
 | 
			
		||||
    /// <param name="input">Content of the message</param>
 | 
			
		||||
    /// <returns>New input, if any, otherwise null</returns>
 | 
			
		||||
    Task<string?> TransformInput(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        IMessageChannel channel,
 | 
			
		||||
        IUser user,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
public interface ILateBlocker
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; }
 | 
			
		||||
 | 
			
		||||
    Task<bool> TryBlockLate(ICommandContext context, string moduleName, CommandInfo command);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     Last thing to be executed, won't stop further executions
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface ILateExecutor
 | 
			
		||||
{
 | 
			
		||||
    Task LateExecute(IGuild guild, IUserMessage msg);
 | 
			
		||||
}
 | 
			
		||||
@@ -18,4 +18,13 @@ public sealed class NoPublicBotAttribute : PreconditionAttribute
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromSuccess());
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Classed marked with this attribute will not be added to the service provider 
 | 
			
		||||
/// </summary>
 | 
			
		||||
[AttributeUsage(AttributeTargets.Class)]
 | 
			
		||||
public class DontAddToIocContainerAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.TypeReaders;
 | 
			
		||||
 | 
			
		||||
[MeansImplicitUse(ImplicitUseTargetFlags.Default | ImplicitUseTargetFlags.WithInheritors)]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								src/NadekoBot/Directory.Build.props
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/NadekoBot/Directory.Build.props
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
<Project>
 | 
			
		||||
    <ItemDefinitionGroup>
 | 
			
		||||
        <ProjectReference>
 | 
			
		||||
            <PrivateAssets>all</PrivateAssets>
 | 
			
		||||
        </ProjectReference>
 | 
			
		||||
    </ItemDefinitionGroup>
 | 
			
		||||
</Project>
 | 
			
		||||
@@ -10,6 +10,7 @@ global using NadekoBot.Services;
 | 
			
		||||
global using NadekoBot.Common;
 | 
			
		||||
global using NadekoBot.Common.Attributes;
 | 
			
		||||
global using NadekoBot.Extensions;
 | 
			
		||||
global using Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
// discord
 | 
			
		||||
global using Discord;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ using System.Threading.Channels;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class ImageOnlyChannelService : IEarlyBehavior
 | 
			
		||||
public sealed class ImageOnlyChannelService : IExecOnMessage
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; } = 0;
 | 
			
		||||
    private readonly IMemoryCache _ticketCache;
 | 
			
		||||
@@ -93,7 +93,7 @@ public sealed class ImageOnlyChannelService : IEarlyBehavior
 | 
			
		||||
        return newState;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunBehavior(IGuild guild, IUserMessage msg)
 | 
			
		||||
    public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (msg.Channel is not ITextChannel tch)
 | 
			
		||||
            return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public class DiscordPermOverrideService : INService, ILateBlocker
 | 
			
		||||
public class DiscordPermOverrideService : INService, IExecPreCommand
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; } = int.MaxValue;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
@@ -118,7 +118,7 @@ public class DiscordPermOverrideService : INService, ILateBlocker
 | 
			
		||||
                        .ToListAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> TryBlockLate(ICommandContext context, string moduleName, CommandInfo command)
 | 
			
		||||
    public async Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
 | 
			
		||||
    {
 | 
			
		||||
        if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && perm is not null)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Nadeko.Medusa;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
@@ -19,13 +20,19 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly IBotStrings _strings;
 | 
			
		||||
        private readonly IMedusaLoaderService _medusaLoader;
 | 
			
		||||
        private readonly ICoordinator _coord;
 | 
			
		||||
 | 
			
		||||
        public SelfCommands(DiscordSocketClient client, IBotStrings strings, ICoordinator coord)
 | 
			
		||||
        public SelfCommands(
 | 
			
		||||
            DiscordSocketClient client,
 | 
			
		||||
            IBotStrings strings,
 | 
			
		||||
            ICoordinator coord,
 | 
			
		||||
            IMedusaLoaderService medusaLoader)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _strings = strings;
 | 
			
		||||
            _coord = coord;
 | 
			
		||||
            _medusaLoader = medusaLoader;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
@@ -506,6 +513,7 @@ public partial class Administration
 | 
			
		||||
        public async partial Task StringsReload()
 | 
			
		||||
        {
 | 
			
		||||
            _strings.Reload();
 | 
			
		||||
            await _medusaLoader.ReloadStrings();
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.bot_strings_reloaded);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ using System.Collections.Immutable;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly CommandHandler _cmdHandler;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
@@ -206,7 +206,7 @@ public sealed class SelfService : ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
        => _pubSub.Pub(_guildLeaveKey, guildStr);
 | 
			
		||||
 | 
			
		||||
    // forwards dms
 | 
			
		||||
    public async Task LateExecute(IGuild guild, IUserMessage msg)
 | 
			
		||||
    public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        var bs = _bss.Data;
 | 
			
		||||
        if (msg.Channel is IDMChannel && bs.ForwardMessages && ownerChannels.Any())
 | 
			
		||||
 
 | 
			
		||||
@@ -913,7 +913,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithTitle("♻️ " + GetText(logChannel.Guild, strs.user_unbanned))
 | 
			
		||||
                               .WithDescription(usr.ToString())
 | 
			
		||||
                               .WithDescription(usr.ToString()!)
 | 
			
		||||
                               .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                               .WithFooter(CurrentTime(guild));
 | 
			
		||||
 | 
			
		||||
@@ -948,7 +948,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                var embed = _eb.Create()
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithTitle("🚫 " + GetText(logChannel.Guild, strs.user_banned))
 | 
			
		||||
                               .WithDescription(usr.ToString())
 | 
			
		||||
                               .WithDescription(usr.ToString()!)
 | 
			
		||||
                               .AddField("Id", usr.Id.ToString())
 | 
			
		||||
                               .WithFooter(CurrentTime(guild));
 | 
			
		||||
 | 
			
		||||
@@ -999,7 +999,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                               .WithOkColor()
 | 
			
		||||
                               .WithTitle("🗑 "
 | 
			
		||||
                                          + GetText(logChannel.Guild, strs.msg_del(((ITextChannel)msg.Channel).Name)))
 | 
			
		||||
                               .WithDescription(msg.Author.ToString())
 | 
			
		||||
                               .WithDescription(msg.Author.ToString()!)
 | 
			
		||||
                               .AddField(GetText(logChannel.Guild, strs.content),
 | 
			
		||||
                                   string.IsNullOrWhiteSpace(resolvedMessage) ? "-" : resolvedMessage)
 | 
			
		||||
                               .AddField("Id", msg.Id.ToString())
 | 
			
		||||
@@ -1060,7 +1060,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
 | 
			
		||||
                               .WithTitle("📝 "
 | 
			
		||||
                                          + GetText(logChannel.Guild,
 | 
			
		||||
                                              strs.msg_update(((ITextChannel)after.Channel).Name)))
 | 
			
		||||
                               .WithDescription(after.Author.ToString())
 | 
			
		||||
                               .WithDescription(after.Author.ToString()!)
 | 
			
		||||
                               .AddField(GetText(logChannel.Guild, strs.old_msg),
 | 
			
		||||
                                   string.IsNullOrWhiteSpace(before.Content)
 | 
			
		||||
                                       ? "-"
 | 
			
		||||
 
 | 
			
		||||
@@ -577,7 +577,7 @@ public partial class Administration
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async partial Task Unban([Leftover] string user)
 | 
			
		||||
        {
 | 
			
		||||
            var bans = await ctx.Guild.GetBansAsync();
 | 
			
		||||
            var bans = await ctx.Guild.GetBansAsync().FlattenAsync();
 | 
			
		||||
 | 
			
		||||
            var bun = bans.FirstOrDefault(x => x.User.ToString()!.ToLowerInvariant() == user.ToLowerInvariant());
 | 
			
		||||
 | 
			
		||||
@@ -596,9 +596,7 @@ public partial class Administration
 | 
			
		||||
        [BotPerm(GuildPerm.BanMembers)]
 | 
			
		||||
        public async partial Task Unban(ulong userId)
 | 
			
		||||
        {
 | 
			
		||||
            var bans = await ctx.Guild.GetBansAsync();
 | 
			
		||||
 | 
			
		||||
            var bun = bans.FirstOrDefault(x => x.User.Id == userId);
 | 
			
		||||
            var bun = await ctx.Guild.GetBanAsync(userId);
 | 
			
		||||
 | 
			
		||||
            if (bun is null)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ using YamlDotNet.Serialization.NamingConventions;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.NadekoExpressions;
 | 
			
		||||
 | 
			
		||||
public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
 | 
			
		||||
{
 | 
			
		||||
    private const string MENTION_PH = "%bot.mention%";
 | 
			
		||||
 | 
			
		||||
@@ -220,7 +220,7 @@ public sealed class NadekoExpressionsService : IEarlyBehavior, IReadyExecutor
 | 
			
		||||
        return result[_rng.Next(0, result.Count)];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunBehavior(IGuild guild, IUserMessage msg)
 | 
			
		||||
    public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        // maybe this message is an expression
 | 
			
		||||
        var expr = TryGetExpression(msg);
 | 
			
		||||
 
 | 
			
		||||
@@ -93,7 +93,7 @@ public class GameStatusEvent : ICurrencyEvent
 | 
			
		||||
                    });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Log.Information("Awarded {Count} users {Amount} currency.{Remaining}",
 | 
			
		||||
            Log.Information("Game status event awarded {Count} users {Amount} currency.{Remaining}",
 | 
			
		||||
                toAward.Count,
 | 
			
		||||
                _amount,
 | 
			
		||||
                _isPotLimited ? $" {PotSize} left." : "");
 | 
			
		||||
 
 | 
			
		||||
@@ -86,7 +86,7 @@ public class ReactionEvent : ICurrencyEvent
 | 
			
		||||
                    });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Log.Information("Awarded {Count} users {Amount} currency.{Remaining}",
 | 
			
		||||
            Log.Information("Reaction Event awarded {Count} users {Amount} currency.{Remaining}",
 | 
			
		||||
                toAward.Count,
 | 
			
		||||
                _amount,
 | 
			
		||||
                _isPotLimited ? $" {PotSize} left." : "");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using SixLabors.Fonts;
 | 
			
		||||
@@ -12,7 +13,7 @@ using Image = SixLabors.ImageSharp.Image;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
 | 
			
		||||
public class PlantPickService : INService
 | 
			
		||||
public class PlantPickService : INService, IExecNoCommand
 | 
			
		||||
{
 | 
			
		||||
    //channelId/last generation
 | 
			
		||||
    public ConcurrentDictionary<ulong, DateTime> LastGenerations { get; } = new();
 | 
			
		||||
@@ -49,8 +50,7 @@ public class PlantPickService : INService
 | 
			
		||||
        _rng = new();
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _gss = gss;
 | 
			
		||||
 | 
			
		||||
        cmd.OnMessageNoTrigger += PotentialFlowerGeneration;
 | 
			
		||||
        
 | 
			
		||||
        using var uow = db.GetDbContext();
 | 
			
		||||
        var guildIds = client.Guilds.Select(x => x.Id).ToList();
 | 
			
		||||
        var configs = uow.Set<GuildConfig>()
 | 
			
		||||
@@ -62,6 +62,9 @@ public class PlantPickService : INService
 | 
			
		||||
        _generationChannels = new(configs.SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
        => PotentialFlowerGeneration(msg);
 | 
			
		||||
 | 
			
		||||
    private string GetText(ulong gid, LocStr str)
 | 
			
		||||
        => _strings.GetText(str, gid);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Services;
 | 
			
		||||
 | 
			
		||||
public class ChatterBotService : IEarlyBehavior
 | 
			
		||||
public class ChatterBotService : IExecOnMessage
 | 
			
		||||
{
 | 
			
		||||
    public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
 | 
			
		||||
 | 
			
		||||
@@ -95,7 +95,7 @@ public class ChatterBotService : IEarlyBehavior
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunBehavior(IGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is not SocketGuild sg)
 | 
			
		||||
            return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Hangman;
 | 
			
		||||
 | 
			
		||||
public sealed class HangmanService : IHangmanService, ILateExecutor
 | 
			
		||||
public sealed class HangmanService : IHangmanService, IExecNoCommand
 | 
			
		||||
{
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, HangmanGame> _hangmanGames = new();
 | 
			
		||||
    private readonly IHangmanSource _source;
 | 
			
		||||
@@ -64,7 +64,7 @@ public sealed class HangmanService : IHangmanService, ILateExecutor
 | 
			
		||||
    public IReadOnlyCollection<string> GetHangmanTypes()
 | 
			
		||||
        => _source.GetCategories();
 | 
			
		||||
 | 
			
		||||
    public async Task LateExecute(IGuild guild, IUserMessage msg)
 | 
			
		||||
    public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (_hangmanGames.ContainsKey(msg.Channel.Id))
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Services;
 | 
			
		||||
 | 
			
		||||
public class PollService : IEarlyBehavior
 | 
			
		||||
public class PollService : IExecOnMessage
 | 
			
		||||
{
 | 
			
		||||
    public ConcurrentDictionary<ulong, PollRunner> ActivePolls { get; } = new();
 | 
			
		||||
 | 
			
		||||
@@ -103,7 +103,7 @@ public class PollService : IEarlyBehavior
 | 
			
		||||
        catch { }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunBehavior(IGuild guild, IUserMessage msg)
 | 
			
		||||
    public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Amazon.S3;
 | 
			
		||||
using Nadeko.Medusa;
 | 
			
		||||
using NadekoBot.Modules.Help.Common;
 | 
			
		||||
using NadekoBot.Modules.Help.Services;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
@@ -23,6 +24,7 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
    private readonly IBotStrings _strings;
 | 
			
		||||
 | 
			
		||||
    private readonly AsyncLazy<ulong> _lazyClientId;
 | 
			
		||||
    private readonly IMedusaLoaderService _medusae;
 | 
			
		||||
 | 
			
		||||
    public Help(
 | 
			
		||||
        GlobalPermissionService perms,
 | 
			
		||||
@@ -30,7 +32,8 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
        BotConfigService bss,
 | 
			
		||||
        IServiceProvider services,
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        IBotStrings strings)
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        IMedusaLoaderService medusae)
 | 
			
		||||
    {
 | 
			
		||||
        _cmds = cmds;
 | 
			
		||||
        _bss = bss;
 | 
			
		||||
@@ -38,6 +41,7 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
        _services = services;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _medusae = medusae;
 | 
			
		||||
 | 
			
		||||
        _lazyClientId = new(async () => (await _client.GetApplicationInfoAsync()).Id);
 | 
			
		||||
    }
 | 
			
		||||
@@ -329,8 +333,8 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
                                         return new CommandJsonObject
 | 
			
		||||
                                         {
 | 
			
		||||
                                             Aliases = com.Aliases.Select(alias => prefix + alias).ToArray(),
 | 
			
		||||
                                             Description = com.RealSummary(_strings, ctx.Guild?.Id, prefix),
 | 
			
		||||
                                             Usage = com.RealRemarksArr(_strings, ctx.Guild?.Id, prefix),
 | 
			
		||||
                                             Description = com.RealSummary(_strings, _medusae, Culture, prefix),
 | 
			
		||||
                                             Usage = com.RealRemarksArr(_strings, _medusae, Culture, prefix),
 | 
			
		||||
                                             Submodule = com.Module.Name,
 | 
			
		||||
                                             Module = com.Module.GetTopLevelModule().Name,
 | 
			
		||||
                                             Options = optHelpStr,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,40 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using CommandLine;
 | 
			
		||||
using Nadeko.Medusa;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Help.Services;
 | 
			
		||||
 | 
			
		||||
public class HelpService : ILateExecutor, INService
 | 
			
		||||
public class HelpService : IExecNoCommand, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly CommandHandler _ch;
 | 
			
		||||
    private readonly IBotStrings _strings;
 | 
			
		||||
    private readonly DiscordPermOverrideService _dpos;
 | 
			
		||||
    private readonly BotConfigService _bss;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
    private readonly ILocalization _loc;
 | 
			
		||||
    private readonly IMedusaLoaderService _medusae;
 | 
			
		||||
 | 
			
		||||
    public HelpService(
 | 
			
		||||
        CommandHandler ch,
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        DiscordPermOverrideService dpos,
 | 
			
		||||
        BotConfigService bss,
 | 
			
		||||
        IEmbedBuilderService eb)
 | 
			
		||||
        IEmbedBuilderService eb,
 | 
			
		||||
        ILocalization loc,
 | 
			
		||||
        IMedusaLoaderService medusae)
 | 
			
		||||
    {
 | 
			
		||||
        _ch = ch;
 | 
			
		||||
        _strings = strings;
 | 
			
		||||
        _dpos = dpos;
 | 
			
		||||
        _bss = bss;
 | 
			
		||||
        _eb = eb;
 | 
			
		||||
        _loc = loc;
 | 
			
		||||
        _medusae = medusae;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task LateExecute(IGuild guild, IUserMessage msg)
 | 
			
		||||
    public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        var settings = _bss.Data;
 | 
			
		||||
        if (guild is null)
 | 
			
		||||
@@ -57,13 +64,16 @@ public class HelpService : ILateExecutor, INService
 | 
			
		||||
    public IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild)
 | 
			
		||||
    {
 | 
			
		||||
        var prefix = _ch.GetPrefix(guild);
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        var str = $"**`{prefix + com.Aliases.First()}`**";
 | 
			
		||||
        var alias = com.Aliases.Skip(1).FirstOrDefault();
 | 
			
		||||
        if (alias is not null)
 | 
			
		||||
            str += $" **/ `{prefix + alias}`**";
 | 
			
		||||
 | 
			
		||||
        var em = _eb.Create().AddField(str, $"{com.RealSummary(_strings, guild?.Id, prefix)}", true);
 | 
			
		||||
        var culture = _loc.GetCultureInfo(guild);
 | 
			
		||||
        
 | 
			
		||||
        var em = _eb.Create()
 | 
			
		||||
                    .AddField(str, $"{com.RealSummary(_strings, _medusae, culture,  prefix)}", true);
 | 
			
		||||
 | 
			
		||||
        _dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides);
 | 
			
		||||
        var reqs = GetCommandRequirements(com, overrides);
 | 
			
		||||
@@ -72,7 +82,7 @@ public class HelpService : ILateExecutor, INService
 | 
			
		||||
 | 
			
		||||
        em.AddField(_strings.GetText(strs.usage),
 | 
			
		||||
              string.Join("\n",
 | 
			
		||||
                  Array.ConvertAll(com.RealRemarksArr(_strings, guild?.Id, prefix), arg => Format.Code(arg))))
 | 
			
		||||
                  Array.ConvertAll(com.RealRemarksArr(_strings,_medusae, culture, prefix), arg => Format.Code(arg))))
 | 
			
		||||
          .WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild))
 | 
			
		||||
          .WithOkColor();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										200
									
								
								src/NadekoBot/Modules/Medusae/Medusa.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								src/NadekoBot/Modules/Medusae/Medusa.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,200 @@
 | 
			
		||||
using Nadeko.Medusa;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules;
 | 
			
		||||
 | 
			
		||||
[OwnerOnly]
 | 
			
		||||
public partial class Medusa : NadekoModule<IMedusaLoaderService>
 | 
			
		||||
{
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [OwnerOnly]
 | 
			
		||||
    public async partial Task MedusaLoad(string? name = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(name))
 | 
			
		||||
        {
 | 
			
		||||
            var loaded = _service.GetLoadedMedusae()
 | 
			
		||||
                                 .Select(x => x.Name)
 | 
			
		||||
                                 .ToHashSet();
 | 
			
		||||
            
 | 
			
		||||
            var unloaded = _service.GetAllMedusae()
 | 
			
		||||
                    .Where(x => !loaded.Contains(x))
 | 
			
		||||
                    .Select(x => Format.Code(x.ToString()))
 | 
			
		||||
                    .ToArray();
 | 
			
		||||
 | 
			
		||||
            if (unloaded.Length == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.no_medusa_available);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.SendPaginatedConfirmAsync(0,
 | 
			
		||||
                page =>
 | 
			
		||||
                {
 | 
			
		||||
                    return _eb.Create(ctx)
 | 
			
		||||
                              .WithOkColor()
 | 
			
		||||
                              .WithTitle(GetText(strs.list_of_unloaded))
 | 
			
		||||
                              .WithDescription(unloaded.Skip(10 * page).Take(10).Join('\n'));
 | 
			
		||||
                },
 | 
			
		||||
                unloaded.Length,
 | 
			
		||||
                10);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var res = await _service.LoadMedusaAsync(name);
 | 
			
		||||
        if (res == MedusaLoadResult.Success)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.medusa_loaded(Format.Code(name)));
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var locStr = res switch
 | 
			
		||||
            {
 | 
			
		||||
                MedusaLoadResult.Empty => strs.medusa_empty,
 | 
			
		||||
                MedusaLoadResult.AlreadyLoaded => strs.medusa_already_loaded(Format.Code(name)),
 | 
			
		||||
                MedusaLoadResult.NotFound => strs.medusa_invalid_not_found,
 | 
			
		||||
                MedusaLoadResult.UnknownError => strs.error_occured,
 | 
			
		||||
                _ => strs.error_occured
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await ReplyErrorLocalizedAsync(locStr);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [OwnerOnly]
 | 
			
		||||
    public async partial Task MedusaUnload(string? name = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(name))
 | 
			
		||||
        {
 | 
			
		||||
            var loaded = _service.GetLoadedMedusae();
 | 
			
		||||
            if (loaded.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyPendingLocalizedAsync(strs.no_medusa_loaded);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(_eb.Create(ctx)
 | 
			
		||||
                                            .WithOkColor()
 | 
			
		||||
                                            .WithTitle(GetText(strs.loaded_medusae))
 | 
			
		||||
                                            .WithDescription(loaded.Select(x => x.Name)
 | 
			
		||||
                                                                   .Join("\n")));
 | 
			
		||||
            
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        var res = await _service.UnloadMedusaAsync(name);
 | 
			
		||||
        if (res == MedusaUnloadResult.Success)
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.medusa_unloaded(Format.Code(name)));
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var locStr = res switch
 | 
			
		||||
            {
 | 
			
		||||
                MedusaUnloadResult.NotLoaded => strs.medusa_not_loaded,
 | 
			
		||||
                MedusaUnloadResult.PossiblyUnable => strs.medusa_possibly_cant_unload,
 | 
			
		||||
                _ => strs.error_occured
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await ReplyErrorLocalizedAsync(locStr);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [OwnerOnly]
 | 
			
		||||
    public async partial Task MedusaList()
 | 
			
		||||
    {
 | 
			
		||||
        var all = _service.GetAllMedusae();
 | 
			
		||||
 | 
			
		||||
        if (all.Count == 0)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyPendingLocalizedAsync(strs.no_medusa_available);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        var loaded = _service.GetLoadedMedusae()
 | 
			
		||||
                             .Select(x => x.Name)
 | 
			
		||||
                             .ToHashSet();
 | 
			
		||||
 | 
			
		||||
        var output = all
 | 
			
		||||
            .Select(m =>
 | 
			
		||||
            {
 | 
			
		||||
                var emoji = loaded.Contains(m) ? "`✅`" : "`🔴`";
 | 
			
		||||
                return $"{emoji} `{m}`";
 | 
			
		||||
            })
 | 
			
		||||
            .ToArray();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        await ctx.SendPaginatedConfirmAsync(0,
 | 
			
		||||
            page => _eb.Create(ctx)
 | 
			
		||||
                       .WithOkColor()
 | 
			
		||||
                       .WithTitle(GetText(strs.list_of_medusae))
 | 
			
		||||
                       .WithDescription(output.Skip(page * 10).Take(10).Join('\n')),
 | 
			
		||||
            output.Length,
 | 
			
		||||
            10);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [OwnerOnly]
 | 
			
		||||
    public async partial Task MedusaInfo(string? name = null)
 | 
			
		||||
    {
 | 
			
		||||
        var medusae = _service.GetLoadedMedusae();
 | 
			
		||||
 | 
			
		||||
        if (name is not null)
 | 
			
		||||
        {
 | 
			
		||||
            var found = medusae.FirstOrDefault(x => string.Equals(x.Name,
 | 
			
		||||
                name,
 | 
			
		||||
                StringComparison.InvariantCultureIgnoreCase));
 | 
			
		||||
            
 | 
			
		||||
            if (found is null)
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.medusa_name_not_found);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var cmdCount = found.Sneks.Sum(x => x.Commands.Count);
 | 
			
		||||
            var cmdNames = found.Sneks
 | 
			
		||||
                                .SelectMany(x => x.Commands)
 | 
			
		||||
                                   .Select(x => Format.Code(x.Name))
 | 
			
		||||
                                   .Join(" | ");
 | 
			
		||||
 | 
			
		||||
            var eb = _eb.Create(ctx)
 | 
			
		||||
                        .WithOkColor()
 | 
			
		||||
                        .WithAuthor(GetText(strs.medusa_info))
 | 
			
		||||
                        .WithTitle(found.Name)
 | 
			
		||||
                        .WithDescription(found.Description)
 | 
			
		||||
                        .AddField(GetText(strs.sneks_count(found.Sneks.Count)),
 | 
			
		||||
                            found.Sneks.Count == 0
 | 
			
		||||
                                ? "-"
 | 
			
		||||
                                : found.Sneks.Select(x => x.Name).Join('\n'),
 | 
			
		||||
                            true)
 | 
			
		||||
                        .AddField(GetText(strs.commands_count(cmdCount)),
 | 
			
		||||
                            string.IsNullOrWhiteSpace(cmdNames)
 | 
			
		||||
                                ? "-"
 | 
			
		||||
                                : cmdNames,
 | 
			
		||||
                            true);
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(eb);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (medusae.Count == 0)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyPendingLocalizedAsync(strs.no_medusa_loaded);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        await ctx.SendPaginatedConfirmAsync(0,
 | 
			
		||||
            page =>
 | 
			
		||||
            {
 | 
			
		||||
                var eb = _eb.Create(ctx)
 | 
			
		||||
                            .WithOkColor();
 | 
			
		||||
 | 
			
		||||
                foreach (var medusa in medusae.Skip(page * 9).Take(9))
 | 
			
		||||
                {
 | 
			
		||||
                    eb.AddField(medusa.Name,
 | 
			
		||||
                        $@"`Sneks:` {medusa.Sneks.Count}
 | 
			
		||||
`Commands:` {medusa.Sneks.Sum(x => x.Commands.Count)}
 | 
			
		||||
--
 | 
			
		||||
{medusa.Description}");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return eb;
 | 
			
		||||
            }, medusae.Count, 9);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class BlacklistService : IEarlyBehavior
 | 
			
		||||
public sealed class BlacklistService : IExecOnMessage
 | 
			
		||||
{
 | 
			
		||||
    public int Priority
 | 
			
		||||
        => int.MaxValue;
 | 
			
		||||
@@ -34,7 +34,7 @@ public sealed class BlacklistService : IEarlyBehavior
 | 
			
		||||
        return default;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<bool> RunBehavior(IGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    public Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var bl in blacklist)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public class CmdCdService : ILateBlocker, INService
 | 
			
		||||
public class CmdCdService : IExecPreCommand, INService
 | 
			
		||||
{
 | 
			
		||||
    public ConcurrentDictionary<ulong, ConcurrentHashSet<CommandCooldown>> CommandCooldowns { get; }
 | 
			
		||||
    public ConcurrentDictionary<ulong, ConcurrentHashSet<ActiveCooldown>> ActiveCooldowns { get; } = new();
 | 
			
		||||
@@ -51,7 +51,7 @@ public class CmdCdService : ILateBlocker, INService
 | 
			
		||||
        return Task.FromResult(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<bool> TryBlockLate(ICommandContext ctx, string moduleName, CommandInfo command)
 | 
			
		||||
    public Task<bool> ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command)
 | 
			
		||||
    {
 | 
			
		||||
        var guild = ctx.Guild;
 | 
			
		||||
        var user = ctx.User;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class FilterService : IEarlyBehavior
 | 
			
		||||
public sealed class FilterService : IExecOnMessage
 | 
			
		||||
{
 | 
			
		||||
    public ConcurrentHashSet<ulong> InviteFilteringChannels { get; }
 | 
			
		||||
    public ConcurrentHashSet<ulong> InviteFilteringServers { get; }
 | 
			
		||||
@@ -69,7 +69,7 @@ public sealed class FilterService : IEarlyBehavior
 | 
			
		||||
                if (guild is null || newMsg is not IUserMessage usrMsg)
 | 
			
		||||
                    return Task.CompletedTask;
 | 
			
		||||
 | 
			
		||||
                return RunBehavior(guild, usrMsg);
 | 
			
		||||
                return ExecOnMessageAsync(guild, usrMsg);
 | 
			
		||||
            });
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        };
 | 
			
		||||
@@ -110,7 +110,7 @@ public sealed class FilterService : IEarlyBehavior
 | 
			
		||||
        return words;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunBehavior(IGuild guild, IUserMessage msg)
 | 
			
		||||
    public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (msg.Author is not IGuildUser gu || gu.GuildPermissions.Administrator)
 | 
			
		||||
            return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public class GlobalPermissionService : ILateBlocker, INService
 | 
			
		||||
public class GlobalPermissionService : IExecPreCommand, INService
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; } = 0;
 | 
			
		||||
 | 
			
		||||
@@ -19,7 +19,7 @@ public class GlobalPermissionService : ILateBlocker, INService
 | 
			
		||||
        => _bss = bss;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public Task<bool> TryBlockLate(ICommandContext ctx, string moduleName, CommandInfo command)
 | 
			
		||||
    public Task<bool> ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command)
 | 
			
		||||
    {
 | 
			
		||||
        var settings = _bss.Data;
 | 
			
		||||
        var commandName = command.Name.ToLowerInvariant();
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
 | 
			
		||||
public class PermissionService : ILateBlocker, INService
 | 
			
		||||
public class PermissionService : IExecPreCommand, INService
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; } = 0;
 | 
			
		||||
 | 
			
		||||
@@ -94,7 +94,7 @@ public class PermissionService : ILateBlocker, INService
 | 
			
		||||
                return old;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> TryBlockLate(ICommandContext ctx, string moduleName, CommandInfo command)
 | 
			
		||||
    public async Task<bool> ExecPreCommandAsync(ICommandContext ctx, string moduleName, CommandInfo command)
 | 
			
		||||
    {
 | 
			
		||||
        var guild = ctx.Guild;
 | 
			
		||||
        var msg = ctx.Message;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ using System.Net;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyExecutor, INService
 | 
			
		||||
public sealed class TranslateService : ITranslateService, IExecNoCommand, IReadyExecutor, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IGoogleApiService _google;
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
@@ -50,7 +50,7 @@ public sealed class TranslateService : ITranslateService, ILateExecutor, IReadyE
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public async Task LateExecute(IGuild guild, IUserMessage msg)
 | 
			
		||||
    public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(msg.Content))
 | 
			
		||||
            return;
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ public class CommandMapService : IInputTransformer, INService
 | 
			
		||||
        string input)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is null || string.IsNullOrWhiteSpace(input))
 | 
			
		||||
            return input;
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        if (AliasMaps.TryGetValue(guild.Id, out var maps))
 | 
			
		||||
        {
 | 
			
		||||
@@ -86,6 +86,6 @@ public class CommandMapService : IInputTransformer, INService
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return input;
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -8,7 +8,7 @@ public partial class Utility
 | 
			
		||||
        private readonly IEnumerable<IConfigService> _settingServices;
 | 
			
		||||
 | 
			
		||||
        public ConfigCommands(IEnumerable<IConfigService> settingServices)
 | 
			
		||||
            => _settingServices = settingServices;
 | 
			
		||||
            => _settingServices = settingServices.Where(x => x.Name != "medusa");
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [OwnerOnly]
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ using Image = SixLabors.ImageSharp.Image;
 | 
			
		||||
namespace NadekoBot.Modules.Xp.Services;
 | 
			
		||||
 | 
			
		||||
// todo improve xp with linqtodb
 | 
			
		||||
public class XpService : INService, IReadyExecutor
 | 
			
		||||
public class XpService : INService, IReadyExecutor, IExecNoCommand
 | 
			
		||||
{
 | 
			
		||||
    public const int XP_REQUIRED_LVL_1 = 36;
 | 
			
		||||
 | 
			
		||||
@@ -109,8 +109,6 @@ public class XpService : INService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
        _excludedServers = new(allGuildConfigs.Where(x => x.XpSettings.ServerExcluded).Select(x => x.GuildId));
 | 
			
		||||
 | 
			
		||||
        _cmd.OnMessageNoTrigger += Cmd_OnMessageNoTrigger;
 | 
			
		||||
 | 
			
		||||
#if !GLOBAL_NADEKO
 | 
			
		||||
        _client.UserVoiceStateUpdated += Client_OnUserVoiceStateUpdated;
 | 
			
		||||
 | 
			
		||||
@@ -552,7 +550,7 @@ public class XpService : INService, IReadyExecutor
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Task Cmd_OnMessageNoTrigger(IUserMessage arg)
 | 
			
		||||
    public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage arg)
 | 
			
		||||
    {
 | 
			
		||||
        if (arg.Author is not SocketGuildUser user || user.IsBot)
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
 
 | 
			
		||||
@@ -19,14 +19,16 @@
 | 
			
		||||
    </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
        <PackageReference Include="AngleSharp" Version="0.16.1" />
 | 
			
		||||
        <PackageReference Include="AWSSDK.S3" Version="3.7.7.21" />
 | 
			
		||||
        <PackageReference Include="AngleSharp" Version="0.16.1">
 | 
			
		||||
            <PrivateAssets>all</PrivateAssets>
 | 
			
		||||
        </PackageReference>
 | 
			
		||||
        <PackageReference Include="AWSSDK.S3" Version="3.7.8.4" />
 | 
			
		||||
        <PackageReference Include="CodeHollow.FeedReader" Version="1.2.4" />
 | 
			
		||||
        <PackageReference Include="CommandLineParser" Version="2.8.0" />
 | 
			
		||||
        <PackageReference Include="Discord.Net" Version="3.4.1" />
 | 
			
		||||
        <PackageReference Include="Discord.Net" Version="3.5.0" />
 | 
			
		||||
        <PackageReference Include="CoreCLR-NCalc" Version="2.2.92" />
 | 
			
		||||
        <PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
 | 
			
		||||
        <PackageReference Include="Google.Apis.YouTube.v3" Version="1.55.0.2449" />
 | 
			
		||||
        <PackageReference Include="Google.Apis.YouTube.v3" Version="1.56.0.2617" />
 | 
			
		||||
        <PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084" />
 | 
			
		||||
        <PackageReference Include="Google.Protobuf" Version="3.19.4" />
 | 
			
		||||
        <PackageReference Include="Grpc.Net.ClientFactory" Version="2.44.0" />
 | 
			
		||||
@@ -39,7 +41,7 @@
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
 | 
			
		||||
        <PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.1.0" />
 | 
			
		||||
        <PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
 | 
			
		||||
@@ -47,14 +49,18 @@
 | 
			
		||||
        <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
 | 
			
		||||
        <PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
 | 
			
		||||
        <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
 | 
			
		||||
        <PackageReference Include="Scrutor" Version="4.0.0" />
 | 
			
		||||
        <PackageReference Include="Scrutor" Version="4.1.0" />
 | 
			
		||||
        <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
 | 
			
		||||
        <PackageReference Include="Serilog.Sinks.Seq" Version="5.1.1" />
 | 
			
		||||
        <PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
 | 
			
		||||
        <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0010" />
 | 
			
		||||
        <PackageReference Include="StackExchange.Redis" Version="2.5.43" />
 | 
			
		||||
        <PackageReference Include="YamlDotNet" Version="11.2.1" />
 | 
			
		||||
        <PackageReference Include="Humanizer" Version="2.14.1" />
 | 
			
		||||
        
 | 
			
		||||
        <PackageReference Include="Humanizer" Version="2.14.1">
 | 
			
		||||
            <PrivateAssets>all</PrivateAssets>
 | 
			
		||||
        </PackageReference>
 | 
			
		||||
        
 | 
			
		||||
        <PackageReference Include="JetBrains.Annotations" Version="2021.3.0" />
 | 
			
		||||
        
 | 
			
		||||
        <!-- Db-related packages -->
 | 
			
		||||
@@ -86,11 +92,11 @@
 | 
			
		||||
<!--        </PackageReference>-->
 | 
			
		||||
        
 | 
			
		||||
        <PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
 | 
			
		||||
        
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
        <ProjectReference Include="..\ayu\Ayu.Discord.Voice\Ayu.Discord.Voice.csproj" />
 | 
			
		||||
        <ProjectReference Include="..\Nadeko.Medusa\Nadeko.Medusa.csproj" />
 | 
			
		||||
        <ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,6 @@ public class CommandHandler : INService, IReadyExecutor
 | 
			
		||||
 | 
			
		||||
    public event Func<IUserMessage, CommandInfo, Task> CommandExecuted = delegate { return Task.CompletedTask; };
 | 
			
		||||
    public event Func<CommandInfo, ITextChannel, string, Task> CommandErrored = delegate { return Task.CompletedTask; };
 | 
			
		||||
    public event Func<IUserMessage, Task> OnMessageNoTrigger = delegate { return Task.CompletedTask; };
 | 
			
		||||
 | 
			
		||||
    //userid/msg count
 | 
			
		||||
    public ConcurrentDictionary<ulong, uint> UserMessagesSent { get; } = new();
 | 
			
		||||
@@ -27,7 +26,7 @@ public class CommandHandler : INService, IReadyExecutor
 | 
			
		||||
    private readonly CommandService _commandService;
 | 
			
		||||
    private readonly BotConfigService _bss;
 | 
			
		||||
    private readonly Bot _bot;
 | 
			
		||||
    private readonly IBehaviourExecutor _behaviourExecutor;
 | 
			
		||||
    private readonly IBehaviorHandler _behaviorHandler;
 | 
			
		||||
    private readonly IServiceProvider _services;
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<ulong, string> _prefixes;
 | 
			
		||||
@@ -41,7 +40,7 @@ public class CommandHandler : INService, IReadyExecutor
 | 
			
		||||
        CommandService commandService,
 | 
			
		||||
        BotConfigService bss,
 | 
			
		||||
        Bot bot,
 | 
			
		||||
        IBehaviourExecutor behaviourExecutor,
 | 
			
		||||
        IBehaviorHandler behaviorHandler,
 | 
			
		||||
        // InteractionService interactions,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
@@ -49,7 +48,7 @@ public class CommandHandler : INService, IReadyExecutor
 | 
			
		||||
        _commandService = commandService;
 | 
			
		||||
        _bss = bss;
 | 
			
		||||
        _bot = bot;
 | 
			
		||||
        _behaviourExecutor = behaviourExecutor;
 | 
			
		||||
        _behaviorHandler = behaviorHandler;
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _services = services;
 | 
			
		||||
        // _interactions = interactions;
 | 
			
		||||
@@ -243,44 +242,53 @@ public class CommandHandler : INService, IReadyExecutor
 | 
			
		||||
    {
 | 
			
		||||
        var startTime = Environment.TickCount;
 | 
			
		||||
 | 
			
		||||
        var blocked = await _behaviourExecutor.RunEarlyBehavioursAsync(guild, usrMsg);
 | 
			
		||||
        var blocked = await _behaviorHandler.RunExecOnMessageAsync(guild, usrMsg);
 | 
			
		||||
        if (blocked)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var blockTime = Environment.TickCount - startTime;
 | 
			
		||||
 | 
			
		||||
        var messageContent = await _behaviourExecutor.RunInputTransformersAsync(guild, usrMsg);
 | 
			
		||||
        var messageContent = await _behaviorHandler.RunInputTransformersAsync(guild, usrMsg);
 | 
			
		||||
 | 
			
		||||
        var prefix = GetPrefix(guild?.Id);
 | 
			
		||||
        var isPrefixCommand = messageContent.StartsWith(".prefix", StringComparison.InvariantCultureIgnoreCase);
 | 
			
		||||
        // execute the command and measure the time it took
 | 
			
		||||
        if (messageContent.StartsWith(prefix, StringComparison.InvariantCulture) || isPrefixCommand)
 | 
			
		||||
        {
 | 
			
		||||
            var (success, error, info) = await ExecuteCommandAsync(new(_client, usrMsg),
 | 
			
		||||
            var context = new CommandContext(_client, usrMsg);
 | 
			
		||||
            var (success, error, info) = await ExecuteCommandAsync(context,
 | 
			
		||||
                messageContent,
 | 
			
		||||
                isPrefixCommand ? 1 : prefix.Length,
 | 
			
		||||
                _services,
 | 
			
		||||
                MultiMatchHandling.Best);
 | 
			
		||||
            startTime = Environment.TickCount - startTime;
 | 
			
		||||
 | 
			
		||||
            if (success)
 | 
			
		||||
            // if a command is found
 | 
			
		||||
            if (info is not null)
 | 
			
		||||
            {
 | 
			
		||||
                await LogSuccessfulExecution(usrMsg, channel as ITextChannel, blockTime, startTime);
 | 
			
		||||
                await CommandExecuted(usrMsg, info);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
                // if it successfully executed
 | 
			
		||||
                if (success)
 | 
			
		||||
                {
 | 
			
		||||
                    await LogSuccessfulExecution(usrMsg, channel as ITextChannel, blockTime, startTime);
 | 
			
		||||
                    await CommandExecuted(usrMsg, info);
 | 
			
		||||
                    await _behaviorHandler.RunPostCommandAsync(context, info.Module.GetTopLevelModule().Name, info);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            if (error is not null)
 | 
			
		||||
            {
 | 
			
		||||
                LogErroredExecution(error, usrMsg, channel as ITextChannel, blockTime, startTime);
 | 
			
		||||
                if (guild is not null)
 | 
			
		||||
                    await CommandErrored(info, channel as ITextChannel, error);
 | 
			
		||||
                // if it errored
 | 
			
		||||
                if (error is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    LogErroredExecution(error, usrMsg, channel as ITextChannel, blockTime, startTime);
 | 
			
		||||
                    
 | 
			
		||||
                    if (guild is not null)
 | 
			
		||||
                        await CommandErrored(info, channel as ITextChannel, error);
 | 
			
		||||
                    
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
            await OnMessageNoTrigger(usrMsg);
 | 
			
		||||
 | 
			
		||||
        await _behaviourExecutor.RunLateExecutorsAsync(guild, usrMsg);
 | 
			
		||||
        await _behaviorHandler.RunOnNoCommandAsync(guild, usrMsg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommandAsync(
 | 
			
		||||
@@ -384,7 +392,7 @@ public class CommandHandler : INService, IReadyExecutor
 | 
			
		||||
            return (false, null, cmd);
 | 
			
		||||
        //return SearchResult.FromError(CommandError.Exception, "You are on a global cooldown.");
 | 
			
		||||
 | 
			
		||||
        var blocked = await _behaviourExecutor.RunLateBlockersAsync(context, cmd);
 | 
			
		||||
        var blocked = await _behaviorHandler.RunPreCommandAsync(context, cmd);
 | 
			
		||||
        if (blocked)
 | 
			
		||||
            return (false, null, cmd);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,17 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public interface IBehaviourExecutor
 | 
			
		||||
public interface IBehaviorHandler
 | 
			
		||||
{
 | 
			
		||||
    public Task<bool> RunEarlyBehavioursAsync(SocketGuild guild, IUserMessage usrMsg);
 | 
			
		||||
    public Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg);
 | 
			
		||||
    Task<bool> RunLateBlockersAsync(ICommandContext context, CommandInfo cmd);
 | 
			
		||||
    Task RunLateExecutorsAsync(SocketGuild guild, IUserMessage usrMsg);
 | 
			
		||||
 | 
			
		||||
    public void Initialize();
 | 
			
		||||
    Task<bool> AddAsync(ICustomBehavior behavior);
 | 
			
		||||
    Task AddRangeAsync(IEnumerable<ICustomBehavior> behavior);
 | 
			
		||||
    Task<bool> RemoveAsync(ICustomBehavior behavior);
 | 
			
		||||
    Task RemoveRangeAsync(IEnumerable<ICustomBehavior> behs);
 | 
			
		||||
    
 | 
			
		||||
    Task<bool> RunExecOnMessageAsync(SocketGuild guild, IUserMessage usrMsg);
 | 
			
		||||
    Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg);
 | 
			
		||||
    Task<bool> RunPreCommandAsync(ICommandContext context, CommandInfo cmd);
 | 
			
		||||
    ValueTask RunPostCommandAsync(ICommandContext ctx, string moduleName, CommandInfo cmd);
 | 
			
		||||
    Task RunOnNoCommandAsync(SocketGuild guild, IUserMessage usrMsg);
 | 
			
		||||
    void Initialize();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								src/NadekoBot/Services/ICustomBehavior.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/NadekoBot/Services/ICustomBehavior.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public interface ICustomBehavior
 | 
			
		||||
    : IExecOnMessage,
 | 
			
		||||
        IInputTransformer,
 | 
			
		||||
        IExecPreCommand,
 | 
			
		||||
        IExecNoCommand,
 | 
			
		||||
        IExecPostCommand
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -4,82 +4,299 @@ using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class BehaviorExecutor : IBehaviourExecutor, INService
 | 
			
		||||
// should be renamed to handler as it's not only executing
 | 
			
		||||
public sealed class BehaviorHandler : IBehaviorHandler, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IServiceProvider _services;
 | 
			
		||||
    private IEnumerable<ILateExecutor> lateExecutors;
 | 
			
		||||
    private IEnumerable<ILateBlocker> lateBlockers;
 | 
			
		||||
    private IEnumerable<IEarlyBehavior> earlyBehaviors;
 | 
			
		||||
    private IEnumerable<IInputTransformer> transformers;
 | 
			
		||||
    
 | 
			
		||||
    private IReadOnlyCollection<IExecNoCommand> noCommandExecs;
 | 
			
		||||
    private IReadOnlyCollection<IExecPreCommand> preCommandExecs;
 | 
			
		||||
    private IReadOnlyCollection<IExecOnMessage> onMessageExecs;
 | 
			
		||||
    private IReadOnlyCollection<IInputTransformer> inputTransformers;
 | 
			
		||||
 | 
			
		||||
    public BehaviorExecutor(IServiceProvider services)
 | 
			
		||||
        => _services = services;
 | 
			
		||||
    private readonly SemaphoreSlim _customLock = new(1, 1);
 | 
			
		||||
    private readonly List<ICustomBehavior> _customExecs = new();
 | 
			
		||||
 | 
			
		||||
    public BehaviorHandler(IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        _services = services;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Initialize()
 | 
			
		||||
    {
 | 
			
		||||
        lateExecutors = _services.GetServices<ILateExecutor>();
 | 
			
		||||
        lateBlockers = _services.GetServices<ILateBlocker>();
 | 
			
		||||
        earlyBehaviors = _services.GetServices<IEarlyBehavior>().OrderByDescending(x => x.Priority);
 | 
			
		||||
        transformers = _services.GetServices<IInputTransformer>();
 | 
			
		||||
        noCommandExecs = _services.GetServices<IExecNoCommand>().ToArray();
 | 
			
		||||
        preCommandExecs = _services.GetServices<IExecPreCommand>().ToArray();
 | 
			
		||||
        onMessageExecs = _services.GetServices<IExecOnMessage>().OrderByDescending(x => x.Priority).ToArray();
 | 
			
		||||
        inputTransformers = _services.GetServices<IInputTransformer>().ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunEarlyBehavioursAsync(SocketGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    #region Add/Remove
 | 
			
		||||
 | 
			
		||||
    public async Task AddRangeAsync(IEnumerable<ICustomBehavior> execs)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var beh in earlyBehaviors)
 | 
			
		||||
        await _customLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (await beh.RunBehavior(guild, usrMsg))
 | 
			
		||||
            foreach (var exe in execs)
 | 
			
		||||
            {
 | 
			
		||||
                if (_customExecs.Contains(exe))
 | 
			
		||||
                    continue;
 | 
			
		||||
 | 
			
		||||
                _customExecs.Add(exe);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _customLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<bool> AddAsync(ICustomBehavior behavior)
 | 
			
		||||
    {
 | 
			
		||||
        await _customLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (_customExecs.Contains(behavior))
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            _customExecs.Add(behavior);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _customLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<bool> RemoveAsync(ICustomBehavior behavior)
 | 
			
		||||
    {
 | 
			
		||||
        await _customLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return _customExecs.Remove(behavior);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _customLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task RemoveRangeAsync(IEnumerable<ICustomBehavior> behs)
 | 
			
		||||
    {
 | 
			
		||||
        await _customLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            foreach(var beh in behs)
 | 
			
		||||
                _customExecs.Remove(beh);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _customLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
    
 | 
			
		||||
    #region Running
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunExecOnMessageAsync(SocketGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        async Task<bool> Exec<T>(IReadOnlyCollection<T> execs)
 | 
			
		||||
            where T : IExecOnMessage
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var exec in execs)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (await exec.ExecOnMessageAsync(guild, usrMsg))
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Information("{TypeName} blocked message g:{GuildId} u:{UserId} c:{ChannelId} msg:{Message}",
 | 
			
		||||
                            GetExecName(exec),
 | 
			
		||||
                            guild?.Id,
 | 
			
		||||
                            usrMsg.Author.Id,
 | 
			
		||||
                            usrMsg.Channel.Id,
 | 
			
		||||
                            usrMsg.Content?.TrimTo(10));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Error(ex,
 | 
			
		||||
                        "An error occurred in {TypeName} late blocker: {ErrorMessage}",
 | 
			
		||||
                        GetExecName(exec),
 | 
			
		||||
                        ex.Message);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (await Exec(onMessageExecs))
 | 
			
		||||
        {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await _customLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (await Exec(_customExecs))
 | 
			
		||||
                return true;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _customLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string GetExecName(object exec)
 | 
			
		||||
        => exec is BehaviorAdapter ba
 | 
			
		||||
            ? ba.ToString()
 | 
			
		||||
            : exec.GetType().Name;
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunPreCommandAsync(ICommandContext ctx, CommandInfo cmd)
 | 
			
		||||
    {
 | 
			
		||||
        async Task<bool> Exec<T>(IReadOnlyCollection<T> execs) where T: IExecPreCommand
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var exec in execs)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    if (await exec.ExecPreCommandAsync(ctx, cmd.Module.GetTopLevelModule().Name, cmd))
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Information("{TypeName} Pre-Command blocked [{User}] Command: [{Command}]",
 | 
			
		||||
                            GetExecName(exec),
 | 
			
		||||
                            ctx.User,
 | 
			
		||||
                            cmd.Aliases[0]);
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Error(ex,
 | 
			
		||||
                        "An error occurred in {TypeName} PreCommand: {ErrorMessage}",
 | 
			
		||||
                        GetExecName(exec),
 | 
			
		||||
                        ex.Message);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (await Exec(preCommandExecs))
 | 
			
		||||
            return true;
 | 
			
		||||
 | 
			
		||||
        await _customLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (await Exec(_customExecs))
 | 
			
		||||
                return true;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _customLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task RunOnNoCommandAsync(SocketGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        async Task Exec<T>(IReadOnlyCollection<T> execs) where T : IExecNoCommand
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var exec in execs)
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    await exec.ExecOnNoCommandAsync(guild, usrMsg);
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Error(ex,
 | 
			
		||||
                        "An error occurred in {TypeName} OnNoCommand: {ErrorMessage}",
 | 
			
		||||
                        GetExecName(exec),
 | 
			
		||||
                        ex.Message);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await Exec(noCommandExecs);
 | 
			
		||||
        
 | 
			
		||||
        await _customLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await Exec(_customExecs);
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _customLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        var messageContent = usrMsg.Content;
 | 
			
		||||
        foreach (var exec in transformers)
 | 
			
		||||
        async Task<string> Exec<T>(IReadOnlyCollection<T> execs, string content)
 | 
			
		||||
            where T : IInputTransformer
 | 
			
		||||
        {
 | 
			
		||||
            string newContent;
 | 
			
		||||
            if ((newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, messageContent))
 | 
			
		||||
                != messageContent.ToLowerInvariant())
 | 
			
		||||
            foreach (var exec in execs)
 | 
			
		||||
            {
 | 
			
		||||
                messageContent = newContent;
 | 
			
		||||
                break;
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, content);
 | 
			
		||||
                    if (newContent is not null)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Information("{ExecName} transformed content {OldContent} -> {NewContent}",
 | 
			
		||||
                            GetExecName(exec),
 | 
			
		||||
                            content,
 | 
			
		||||
                            newContent);
 | 
			
		||||
                        return newContent;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning(ex, "An error occured during InputTransform handling: {ErrorMessage}", ex.Message);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return messageContent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> RunLateBlockersAsync(ICommandContext ctx, CommandInfo cmd)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var exec in lateBlockers)
 | 
			
		||||
        var newContent = await Exec(inputTransformers, usrMsg.Content);
 | 
			
		||||
        if (newContent is not null)
 | 
			
		||||
            return newContent;
 | 
			
		||||
        
 | 
			
		||||
        await _customLock.WaitAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (await exec.TryBlockLate(ctx, cmd.Module.GetTopLevelModule().Name, cmd))
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("Late blocking User [{User}] Command: [{Command}] in [{Module}]",
 | 
			
		||||
                    ctx.User,
 | 
			
		||||
                    cmd.Aliases[0],
 | 
			
		||||
                    exec.GetType().Name);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            newContent = await Exec(_customExecs, usrMsg.Content);
 | 
			
		||||
            if (newContent is not null)
 | 
			
		||||
                return newContent;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            _customLock.Release();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
        return usrMsg.Content;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task RunLateExecutorsAsync(SocketGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    public async ValueTask RunPostCommandAsync(ICommandContext ctx, string moduleName, CommandInfo cmd)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (var exec in lateExecutors)
 | 
			
		||||
        foreach (var exec in _customExecs)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await exec.LateExecute(guild, usrMsg);
 | 
			
		||||
                await exec.ExecPostCommandAsync(ctx, moduleName, cmd.Name);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Error in {TypeName} late executor: {ErrorMessage}", exec.GetType().Name, ex.Message);
 | 
			
		||||
                Log.Warning(ex,
 | 
			
		||||
                    "An error occured during PostCommand handling in {ExecName}: {ErrorMessage}",
 | 
			
		||||
                    GetExecName(exec),
 | 
			
		||||
                    ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #endregion
 | 
			
		||||
}
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class StatsService : IStatsService, IReadyExecutor, INService
 | 
			
		||||
{
 | 
			
		||||
    public const string BOT_VERSION = "4.0.6";
 | 
			
		||||
    public const string BOT_VERSION = "4.1.0";
 | 
			
		||||
 | 
			
		||||
    public string Author
 | 
			
		||||
        => "Kwoth#2452";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
using Humanizer.Localisation;
 | 
			
		||||
using Nadeko.Medusa;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Net.Http.Headers;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
@@ -12,7 +13,7 @@ public static class Extensions
 | 
			
		||||
        new(@"^(https?|ftp)://(?<path>[^\s/$.?#].[^\s]*)$", RegexOptions.Compiled);
 | 
			
		||||
 | 
			
		||||
    public static IEmbedBuilder WithAuthor(this IEmbedBuilder eb, IUser author)
 | 
			
		||||
        => eb.WithAuthor(author.ToString(), author.RealAvatarUrl().ToString());
 | 
			
		||||
        => eb.WithAuthor(author.ToString()!, author.RealAvatarUrl().ToString());
 | 
			
		||||
 | 
			
		||||
    public static Task EditAsync(this IUserMessage msg, SmartText text)
 | 
			
		||||
        => text switch
 | 
			
		||||
@@ -71,17 +72,53 @@ public static class Extensions
 | 
			
		||||
    public static string RealSummary(
 | 
			
		||||
        this CommandInfo cmd,
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        ulong? guildId,
 | 
			
		||||
        IMedusaLoaderService medusae,
 | 
			
		||||
        CultureInfo culture,
 | 
			
		||||
        string prefix)
 | 
			
		||||
        => string.Format(strings.GetCommandStrings(cmd.Summary, guildId).Desc, prefix);
 | 
			
		||||
    {
 | 
			
		||||
        string description;
 | 
			
		||||
        if (cmd.Remarks?.StartsWith("medusa///") ?? false)
 | 
			
		||||
        {
 | 
			
		||||
            // command method name is kept in Summary
 | 
			
		||||
            // medusa///<medusa-name-here> is kept in remarks
 | 
			
		||||
            // this way I can find the name of the medusa, and then name of the command for which
 | 
			
		||||
            // the description should be loaded
 | 
			
		||||
            var medusaName = cmd.Remarks.Split("///")[1];
 | 
			
		||||
            description = medusae.GetCommandDescription(medusaName, cmd.Summary, culture);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            description = strings.GetCommandStrings(cmd.Summary, culture).Desc;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return string.Format(description, prefix);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static string[] RealRemarksArr(
 | 
			
		||||
        this CommandInfo cmd,
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        ulong? guildId,
 | 
			
		||||
        IMedusaLoaderService medusae,
 | 
			
		||||
        CultureInfo culture,
 | 
			
		||||
        string prefix)
 | 
			
		||||
        => Array.ConvertAll(strings.GetCommandStrings(cmd.Summary, guildId).Args,
 | 
			
		||||
    {
 | 
			
		||||
        string[] args;
 | 
			
		||||
        if (cmd.Remarks?.StartsWith("medusa///") ?? false)
 | 
			
		||||
        {
 | 
			
		||||
            // command method name is kept in Summary
 | 
			
		||||
            // medusa///<medusa-name-here> is kept in remarks
 | 
			
		||||
            // this way I can find the name of the medusa,
 | 
			
		||||
            // and command for which data should be loaded
 | 
			
		||||
            var medusaName = cmd.Remarks.Split("///")[1];
 | 
			
		||||
            args = medusae.GetCommandExampleArgs(medusaName, cmd.Summary, culture);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            args = strings.GetCommandStrings(cmd.Summary, culture).Args;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return Array.ConvertAll(args,
 | 
			
		||||
            arg => GetFullUsage(cmd.Name, arg, prefix));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static string GetFullUsage(string commandName, string args, string prefix)
 | 
			
		||||
        => $"{prefix}{commandName} {string.Format(args, prefix)}".TrimEnd();
 | 
			
		||||
 
 | 
			
		||||
@@ -20,16 +20,20 @@ public static class ServiceCollectionExtensions
 | 
			
		||||
 | 
			
		||||
    public static IServiceCollection AddConfigServices(this IServiceCollection services)
 | 
			
		||||
    {
 | 
			
		||||
        var baseType = typeof(ConfigServiceBase<>);
 | 
			
		||||
 | 
			
		||||
        foreach (var type in Assembly.GetCallingAssembly().ExportedTypes.Where(x => x.IsSealed))
 | 
			
		||||
        {
 | 
			
		||||
            if (type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == baseType)
 | 
			
		||||
            {
 | 
			
		||||
                services.AddSingleton(type);
 | 
			
		||||
                services.AddSingleton(x => (IConfigService)x.GetRequiredService(type));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        services.Scan(x => x.FromCallingAssembly()
 | 
			
		||||
                            .AddClasses(f => f.AssignableTo(typeof(ConfigServiceBase<>)))
 | 
			
		||||
                            .AsSelfWithInterfaces());
 | 
			
		||||
        
 | 
			
		||||
        // var baseType = typeof(ConfigServiceBase<>);
 | 
			
		||||
        //
 | 
			
		||||
        // foreach (var type in Assembly.GetCallingAssembly().ExportedTypes.Where(x => x.IsSealed))
 | 
			
		||||
        // {
 | 
			
		||||
        //     if (type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == baseType)
 | 
			
		||||
        //     {
 | 
			
		||||
        //         services.AddSingleton(type);
 | 
			
		||||
        //         services.AddSingleton(x => (IConfigService)x.GetRequiredService(type));
 | 
			
		||||
        //     }
 | 
			
		||||
        // }
 | 
			
		||||
 | 
			
		||||
        return services;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1270,4 +1270,16 @@ exprsexport:
 | 
			
		||||
deleteemptyservers:
 | 
			
		||||
  - deleteemptyservers
 | 
			
		||||
stock:
 | 
			
		||||
  - stock
 | 
			
		||||
  - stock
 | 
			
		||||
medusaload:
 | 
			
		||||
  - medusaload
 | 
			
		||||
  - meload
 | 
			
		||||
medusaunload:
 | 
			
		||||
  - medusaunload
 | 
			
		||||
  - meunload
 | 
			
		||||
medusalist:
 | 
			
		||||
  - medusalist
 | 
			
		||||
  - melist
 | 
			
		||||
medusainfo:
 | 
			
		||||
  - medusainfo
 | 
			
		||||
  - meinfo
 | 
			
		||||
							
								
								
									
										5
									
								
								src/NadekoBot/data/medusae/medusa.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/NadekoBot/data/medusae/medusa.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# DO NOT CHANGE
 | 
			
		||||
version: 1
 | 
			
		||||
# List of medusae automatically loaded at startup
 | 
			
		||||
loaded:
 | 
			
		||||
  - uwu
 | 
			
		||||
@@ -2151,3 +2151,34 @@ deleteemptyservers:
 | 
			
		||||
  desc: "Deletes all servers in which the bot is the only member."
 | 
			
		||||
  args:
 | 
			
		||||
    - ""
 | 
			
		||||
medusaload:
 | 
			
		||||
  desc: |- 
 | 
			
		||||
    Loads a medusa with the specified name from the data/medusae/ folder.
 | 
			
		||||
    Provide no name to see the list of loadable medusae.
 | 
			
		||||
    Read about the medusa system [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)
 | 
			
		||||
  args:
 | 
			
		||||
    - "mycoolmedusa"
 | 
			
		||||
    - ""
 | 
			
		||||
medusaunload:
 | 
			
		||||
  desc: |-
 | 
			
		||||
    Unloads the previously loaded medusa.
 | 
			
		||||
    Provide no name to see the list of unloadable medusae. 
 | 
			
		||||
    Read about the medusa system [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)
 | 
			
		||||
  args:
 | 
			
		||||
    - "mycoolmedusa"
 | 
			
		||||
    - ""
 | 
			
		||||
medusainfo:
 | 
			
		||||
  desc: |-
 | 
			
		||||
    Shows information about the specified medusa such as the author, name, description, list of sneks, number of commands etc.
 | 
			
		||||
    Provide no name to see the basic information about all loaded medusae. 
 | 
			
		||||
    Read about the medusa system [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)
 | 
			
		||||
  args:
 | 
			
		||||
    - "mycoolmedusa"
 | 
			
		||||
    - ""
 | 
			
		||||
medusalist: 
 | 
			
		||||
  desc: |-
 | 
			
		||||
    Lists all loaded and unloaded medusae.
 | 
			
		||||
    Read about the medusa system [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)
 | 
			
		||||
  args:
 | 
			
		||||
    - ""
 | 
			
		||||
  
 | 
			
		||||
@@ -974,5 +974,21 @@
 | 
			
		||||
  "deleted_x_servers": "Deleted {0} servers.",
 | 
			
		||||
  "curtr_gift": "Gift from {0} [{1}]",
 | 
			
		||||
  "curtr_award": "Awarded by bot owner {0} [{1}]",  
 | 
			
		||||
  "curtr_take": "Taken by bot owner {0} [{1}]"  
 | 
			
		||||
  "curtr_take": "Taken by bot owner {0} [{1}]",
 | 
			
		||||
  "list_of_medusae": "List of Medusae",
 | 
			
		||||
  "list_of_unloaded": "List of Available Medusae",
 | 
			
		||||
  "medusa_name_not_found": "Medusa with that name doesn't exist or isn't loaded.",
 | 
			
		||||
  "medusa_info": "Medusa Information",
 | 
			
		||||
  "sneks_count": "Sneks ({0})",
 | 
			
		||||
  "commands_count": "Commands ({0})",
 | 
			
		||||
  "no_medusa_loaded": "There are no loaded medusae.",
 | 
			
		||||
  "no_medusa_available": "No medusa available.",
 | 
			
		||||
  "loaded_medusae": "Loaded Medusae",
 | 
			
		||||
  "medusa_not_loaded": "Medusa with that name is not loaded.",
 | 
			
		||||
  "medusa_possibly_cant_unload": "Medusa is probably not fully unloaded. Please restart the bot if issues arise.",
 | 
			
		||||
  "medusa_loaded": "Medusa {0} has been loaded.",
 | 
			
		||||
  "medusa_unloaded": "Medusa {0} has been unloaded.",
 | 
			
		||||
  "medusa_empty": "Medusa wasn't loaded as it didn't contain any Sneks.",
 | 
			
		||||
  "medusa_already_loaded": "Medusa {0} is already loaded",
 | 
			
		||||
  "medusa_invalid_not_found": "Medusa with that name wasn't found or the file was invalid"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user