mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Fixed some aliases and reworked namespaces
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
using NadekoBot.Common.Yml;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Humanizer;
|
||||
using Nadeko.Common;
|
||||
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
using System.Text;
|
||||
using Serilog;
|
||||
|
||||
namespace Nadeko.Common;
|
||||
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Channels;
|
||||
using Serilog;
|
||||
|
||||
namespace Nadeko.Common;
|
||||
|
||||
|
@@ -1,8 +1,6 @@
|
||||
#nullable disable
|
||||
using LinqToDB.Common;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
|
@@ -3,10 +3,10 @@
|
||||
public sealed class RpsGame
|
||||
{
|
||||
private static readonly NadekoRandom _rng = new NadekoRandom();
|
||||
|
||||
const decimal WIN_MULTI = 1.95m;
|
||||
const decimal DRAW_MULTI = 1m;
|
||||
const decimal LOSE_MULTI = 0m;
|
||||
|
||||
private const decimal WIN_MULTI = 1.95m;
|
||||
private const decimal DRAW_MULTI = 1m;
|
||||
private const decimal LOSE_MULTI = 0m;
|
||||
|
||||
public RpsGame()
|
||||
{
|
||||
|
@@ -1,5 +1,5 @@
|
||||
#nullable disable
|
||||
using Nadeko.Bot.Db.Models;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot;
|
||||
|
||||
|
@@ -3,5 +3,5 @@ namespace Nadeko.Common;
|
||||
|
||||
public interface IDiscordPermOverrideService
|
||||
{
|
||||
bool TryGetOverrides(ulong guildId, string commandName, out Nadeko.Bot.Db.GuildPerm? perm);
|
||||
bool TryGetOverrides(ulong guildId, string commandName, out NadekoBot.Db.GuildPerm? perm);
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
using Nadeko.Bot.Db.Models;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
|
@@ -1,6 +1,4 @@
|
||||
using Nadeko.Bot.Db.Models;
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
using OneOf;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
|
@@ -0,0 +1,80 @@
|
||||
#nullable enable
|
||||
|
||||
using Nadeko.Snake;
|
||||
|
||||
[DIIgnore]
|
||||
public sealed class BehaviorAdapter : ICustomBehavior
|
||||
{
|
||||
private readonly WeakReference<Snek> _snekWr;
|
||||
private readonly IMedusaStrings _strings;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly string _name;
|
||||
|
||||
public string Name => _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,9 @@
|
||||
using Nadeko.Snake;
|
||||
|
||||
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);
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Nadeko.Snake;
|
||||
|
||||
public sealed class DmContextAdapter : DmContext
|
||||
{
|
||||
public override IMedusaStrings Strings { get; }
|
||||
public override IDMChannel Channel { get; }
|
||||
public override IUserMessage Message { get; }
|
||||
public override ISelfUser Bot { 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;
|
||||
Bot = ctx.Client.CurrentUser;
|
||||
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
using Nadeko.Snake;
|
||||
|
||||
namespace Nadeko.Medusa.Adapters;
|
||||
|
||||
public class FilterAdapter : PreconditionAttribute
|
||||
{
|
||||
private readonly FilterAttribute _filterAttribute;
|
||||
private readonly IMedusaStrings _strings;
|
||||
|
||||
public FilterAdapter(FilterAttribute filterAttribute,
|
||||
IMedusaStrings strings)
|
||||
{
|
||||
_filterAttribute = filterAttribute;
|
||||
_strings = strings;
|
||||
}
|
||||
|
||||
public override async Task<PreconditionResult> CheckPermissionsAsync(
|
||||
ICommandContext context,
|
||||
CommandInfo command,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var medusaContext = ContextAdapterFactory.CreateNew(context,
|
||||
_strings,
|
||||
services);
|
||||
|
||||
var result = await _filterAttribute.CheckAsync(medusaContext);
|
||||
|
||||
if (!result)
|
||||
return PreconditionResult.FromError($"Precondition '{_filterAttribute.GetType().Name}' failed.");
|
||||
|
||||
return PreconditionResult.FromSuccess();
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Nadeko.Snake;
|
||||
|
||||
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 ISelfUser Bot { 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;
|
||||
Bot = ctx.Client.CurrentUser;
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
using Nadeko.Snake;
|
||||
|
||||
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/Common/CommandContextType.cs
Normal file
27
src/NadekoBot/_common/Medusa/Common/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);
|
||||
}
|
20
src/NadekoBot/_common/Medusa/Common/Config/MedusaConfig.cs
Normal file
20
src/NadekoBot/_common/Medusa/Common/Config/MedusaConfig.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
#nullable enable
|
||||
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();
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
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() ?? new List<string>();
|
||||
|
||||
public void AddLoadedMedusa(string name)
|
||||
{
|
||||
ModifyConfig(conf =>
|
||||
{
|
||||
if (conf.Loaded is null)
|
||||
conf.Loaded = new();
|
||||
|
||||
if(!conf.Loaded.Contains(name))
|
||||
conf.Loaded.Add(name);
|
||||
});
|
||||
}
|
||||
|
||||
public void RemoveLoadedMedusa(string name)
|
||||
{
|
||||
ModifyConfig(conf =>
|
||||
{
|
||||
if (conf.Loaded is null)
|
||||
conf.Loaded = new();
|
||||
|
||||
conf.Loaded.Remove(name);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace Nadeko.Medusa;
|
||||
|
||||
public class MedusaAssemblyLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _resolver;
|
||||
|
||||
public MedusaAssemblyLoadContext(string folderPath) : base(isCollectible: true)
|
||||
=> _resolver = new(folderPath);
|
||||
|
||||
// public Assembly MainAssembly { get; private set; }
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||
if (assemblyPath != null)
|
||||
{
|
||||
var assembly = LoadFromAssemblyPath(assemblyPath);
|
||||
LoadDependencies(assembly);
|
||||
return assembly;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void LoadDependencies(Assembly assembly)
|
||||
{
|
||||
foreach (var reference in assembly.GetReferencedAssemblies())
|
||||
{
|
||||
Load(reference);
|
||||
}
|
||||
}
|
||||
}
|
99
src/NadekoBot/_common/Medusa/Common/MedusaIoCKernelModule.cs
Normal file
99
src/NadekoBot/_common/Medusa/Common/MedusaIoCKernelModule.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Nadeko.Snake;
|
||||
using System.Reflection;
|
||||
using Ninject;
|
||||
using Ninject.Activation;
|
||||
using Ninject.Activation.Caching;
|
||||
using Ninject.Modules;
|
||||
using Ninject.Planning;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Nadeko.Medusa;
|
||||
|
||||
public sealed class MedusaNinjectModule : NinjectModule
|
||||
{
|
||||
public override string Name { get; }
|
||||
private volatile bool isLoaded = false;
|
||||
private readonly Dictionary<Type, Type[]> _types;
|
||||
|
||||
public MedusaNinjectModule(Assembly assembly, string name)
|
||||
{
|
||||
Name = name;
|
||||
_types = assembly.GetExportedTypes()
|
||||
.Where(t => t.IsClass)
|
||||
.Where(t => t.GetCustomAttribute<svcAttribute>() is not null)
|
||||
.ToDictionary(x => x,
|
||||
type => type.GetInterfaces().ToArray());
|
||||
}
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
if (isLoaded)
|
||||
return;
|
||||
|
||||
foreach (var (type, data) in _types)
|
||||
{
|
||||
var attribute = type.GetCustomAttribute<svcAttribute>()!;
|
||||
var scope = GetScope(attribute.Lifetime);
|
||||
|
||||
Bind(type)
|
||||
.ToSelf()
|
||||
.InScope(scope);
|
||||
|
||||
foreach (var inter in data)
|
||||
{
|
||||
Bind(inter)
|
||||
.ToMethod(x => x.Kernel.Get(type))
|
||||
.InScope(scope);
|
||||
}
|
||||
}
|
||||
|
||||
isLoaded = true;
|
||||
}
|
||||
|
||||
private Func<IContext, object?> GetScope(Lifetime lt)
|
||||
=> _ => lt switch
|
||||
{
|
||||
Lifetime.Singleton => this,
|
||||
Lifetime.Transient => null,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public override void Unload()
|
||||
{
|
||||
if (!isLoaded)
|
||||
return;
|
||||
|
||||
var planner = (RemovablePlanner)Kernel!.Components.Get<IPlanner>();
|
||||
var cache = Kernel.Components.Get<ICache>();
|
||||
foreach (var binding in this.Bindings)
|
||||
{
|
||||
Kernel.RemoveBinding(binding);
|
||||
}
|
||||
|
||||
foreach (var type in _types.SelectMany(x => x.Value).Concat(_types.Keys))
|
||||
{
|
||||
var binds = Kernel.GetBindings(type);
|
||||
|
||||
if (!binds.Any())
|
||||
{
|
||||
Unbind(type);
|
||||
|
||||
planner.RemovePlan(type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Bindings.Clear();
|
||||
|
||||
cache.Clear(this);
|
||||
_types.Clear();
|
||||
|
||||
// in case the library uses System.Text.Json
|
||||
var assembly = typeof(JsonSerializerOptions).Assembly;
|
||||
var updateHandlerType = assembly.GetType("System.Text.Json.JsonSerializerOptionsUpdateHandler");
|
||||
var clearCacheMethod = updateHandlerType?.GetMethod("ClearCache", BindingFlags.Static | BindingFlags.Public);
|
||||
clearCacheMethod?.Invoke(null, new object?[] { null });
|
||||
|
||||
isLoaded = false;
|
||||
}
|
||||
}
|
919
src/NadekoBot/_common/Medusa/Common/MedusaLoaderService.cs
Normal file
919
src/NadekoBot/_common/Medusa/Common/MedusaLoaderService.cs
Normal file
@@ -0,0 +1,919 @@
|
||||
using Discord.Commands.Builders;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Nadeko.Common.Medusa;
|
||||
using Nadeko.Medusa.Adapters;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using Ninject;
|
||||
using Ninject.Modules;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Nadeko.Snake;
|
||||
|
||||
namespace Nadeko.Medusa;
|
||||
|
||||
// ReSharper disable RedundantAssignment
|
||||
public sealed class MedusaLoaderService : IMedusaLoaderService, IReadyExecutor, INService
|
||||
{
|
||||
private readonly CommandService _cmdService;
|
||||
private readonly IBehaviorHandler _behHandler;
|
||||
private readonly IPubSub _pubSub;
|
||||
private readonly IMedusaConfigService _medusaConfig;
|
||||
private readonly IKernel _kernel;
|
||||
|
||||
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,
|
||||
IKernel kernel,
|
||||
IBehaviorHandler behHandler,
|
||||
IPubSub pubSub,
|
||||
IMedusaConfigService medusaConfig)
|
||||
{
|
||||
_cmdService = cmdService;
|
||||
_behHandler = behHandler;
|
||||
_pubSub = pubSub;
|
||||
_medusaConfig = medusaConfig;
|
||||
_kernel = kernel;
|
||||
|
||||
// has to be done this way to support this feature on sharded bots
|
||||
_pubSub.Sub(_loadKey, async name => await InternalLoadAsync(name));
|
||||
_pubSub.Sub(_unloadKey, async name => await InternalUnloadAsync(name));
|
||||
|
||||
_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, snekInfos.Instance.Prefix, 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);
|
||||
|
||||
await _lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (LoadAssemblyInternal(safeName,
|
||||
out var ctx,
|
||||
out var snekData,
|
||||
out var kernelModule,
|
||||
out var strings,
|
||||
out var typeReaders))
|
||||
{
|
||||
var moduleInfos = new List<ModuleInfo>();
|
||||
|
||||
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, kernelModule);
|
||||
moduleInfos.Add(module);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,
|
||||
"Error loading snek {SnekName}",
|
||||
point.Name);
|
||||
}
|
||||
}
|
||||
|
||||
var execs = GetExecsInternal(snekData, strings);
|
||||
await _behHandler.AddRangeAsync(execs);
|
||||
|
||||
_resolved[name] = new(LoadContext: ctx,
|
||||
ModuleInfos: moduleInfos.ToImmutableArray(),
|
||||
SnekInfos: snekData.ToImmutableArray(),
|
||||
strings,
|
||||
typeReaders,
|
||||
execs)
|
||||
{
|
||||
KernelModule = kernelModule
|
||||
};
|
||||
|
||||
|
||||
_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)
|
||||
{
|
||||
var behs = new List<ICustomBehavior>();
|
||||
foreach (var snek in snekData)
|
||||
{
|
||||
behs.Add(new BehaviorAdapter(new(snek.Instance), strings, _kernel));
|
||||
|
||||
foreach (var sub in snek.Subsneks)
|
||||
{
|
||||
behs.Add(new BehaviorAdapter(new(sub.Instance), strings, _kernel));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
[NotNullWhen(true)] out INinjectModule? ninjectModule,
|
||||
out IMedusaStrings strings,
|
||||
out Dictionary<Type, TypeReader> typeReaders)
|
||||
{
|
||||
ctxWr = null;
|
||||
snekData = null;
|
||||
|
||||
var path = Path.GetFullPath($"{BASE_DIR}/{safeName}/{safeName}.dll");
|
||||
var dir = Path.GetFullPath($"{BASE_DIR}/{safeName}");
|
||||
|
||||
if (!Directory.Exists(dir))
|
||||
throw new DirectoryNotFoundException($"Medusa folder not found: {dir}");
|
||||
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"Medusa dll not found: {path}");
|
||||
|
||||
strings = MedusaStrings.CreateDefault(dir);
|
||||
var ctx = new MedusaAssemblyLoadContext(Path.GetDirectoryName(path)!);
|
||||
var a = ctx.LoadFromAssemblyPath(Path.GetFullPath(path));
|
||||
ctx.LoadDependencies(a);
|
||||
|
||||
// load services
|
||||
ninjectModule = new MedusaNinjectModule(a, safeName);
|
||||
_kernel.Load(ninjectModule);
|
||||
|
||||
var sis = LoadSneksFromAssembly(safeName, a);
|
||||
typeReaders = LoadTypeReadersFromAssembly(a, strings);
|
||||
|
||||
// todo allow this
|
||||
if (sis.Count == 0)
|
||||
{
|
||||
_kernel.Unload(safeName);
|
||||
return false;
|
||||
}
|
||||
|
||||
ctxWr = new(ctx);
|
||||
snekData = sis;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly Type _paramParserType = typeof(ParamParser<>);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private Dictionary<Type, TypeReader> LoadTypeReadersFromAssembly(
|
||||
Assembly assembly,
|
||||
IMedusaStrings strings)
|
||||
{
|
||||
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(_kernel, parserType);
|
||||
|
||||
var targetType = parserType.BaseType!.GetGenericArguments()[0];
|
||||
var typeReaderInstance = (TypeReader)Activator.CreateInstance(
|
||||
typeof(ParamParserAdapter<>).MakeGenericType(targetType),
|
||||
args: new[] { parserObj, strings, _kernel })!;
|
||||
|
||||
typeReaders.Add(targetType, typeReaderInstance);
|
||||
}
|
||||
|
||||
return typeReaders;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private async Task<ModuleInfo> LoadModuleInternalAsync(
|
||||
string medusaName,
|
||||
SnekInfo snekInfo,
|
||||
IMedusaStrings strings,
|
||||
INinjectModule 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,
|
||||
INinjectModule kernelModule)
|
||||
=> mb =>
|
||||
{
|
||||
var m = mb.WithName(snekInfo.Name);
|
||||
|
||||
foreach (var f in snekInfo.Filters)
|
||||
{
|
||||
m.AddPrecondition(new FilterAdapter(f, strings));
|
||||
}
|
||||
|
||||
foreach (var cmd in snekInfo.Commands)
|
||||
{
|
||||
m.AddCommand(cmd.Aliases.First(),
|
||||
CreateCallback(cmd.ContextType,
|
||||
new(snekInfo),
|
||||
new(cmd),
|
||||
strings),
|
||||
CreateCommandFactory(medusaName, cmd, strings));
|
||||
}
|
||||
|
||||
foreach (var subInfo in snekInfo.Subsneks)
|
||||
m.AddModule(subInfo.Instance.Prefix, CreateModuleFactory(medusaName, subInfo, strings, kernelModule));
|
||||
};
|
||||
|
||||
private static readonly RequireContextAttribute _reqGuild = new RequireContextAttribute(ContextType.Guild);
|
||||
private static readonly RequireContextAttribute _reqDm = new RequireContextAttribute(ContextType.DM);
|
||||
|
||||
private Action<CommandBuilder> CreateCommandFactory(string medusaName, SnekCommandData cmd, IMedusaStrings strings)
|
||||
=> (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);
|
||||
|
||||
foreach (var f in cmd.Filters)
|
||||
cb.AddPrecondition(new FilterAdapter(f, strings));
|
||||
|
||||
foreach (var ubp in cmd.UserAndBotPerms)
|
||||
{
|
||||
if (ubp is user_permAttribute up)
|
||||
{
|
||||
if (up.GuildPerm is { } gp)
|
||||
cb.AddPrecondition(new UserPermAttribute(gp));
|
||||
else if (up.ChannelPerm is { } cp)
|
||||
cb.AddPrecondition(new UserPermAttribute(cp));
|
||||
}
|
||||
else if (ubp is bot_permAttribute bp)
|
||||
{
|
||||
if (bp.GuildPerm is { } gp)
|
||||
cb.AddPrecondition(new BotPermAttribute(gp));
|
||||
else if (bp.ChannelPerm is { } cp)
|
||||
cb.AddPrecondition(new BotPermAttribute(cp));
|
||||
}
|
||||
else if (ubp is bot_owner_onlyAttribute)
|
||||
{
|
||||
cb.AddPrecondition(new OwnerOnlyAttribute());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (paramData.IsOptional)
|
||||
pb.WithDefault(paramData.DefaultValue);
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> CreateCallback(
|
||||
CommandContextType contextType,
|
||||
WeakReference<SnekInfo> snekDataWr,
|
||||
WeakReference<SnekCommandData> snekCommandDataWr,
|
||||
IMedusaStrings strings)
|
||||
=> async (
|
||||
context,
|
||||
parameters,
|
||||
svcs,
|
||||
_) =>
|
||||
{
|
||||
if (!snekCommandDataWr.TryGetTarget(out var cmdData)
|
||||
|| !snekDataWr.TryGetTarget(out var snekData))
|
||||
{
|
||||
Log.Warning("Attempted to run an unloaded snek's command");
|
||||
return;
|
||||
}
|
||||
|
||||
var paramObjs = ParamObjs(contextType, cmdData, parameters, context, svcs, _kernel, 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;
|
||||
}
|
||||
};
|
||||
|
||||
[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)
|
||||
{
|
||||
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;
|
||||
var km = lsi.KernelModule;
|
||||
lsi.KernelModule = null!;
|
||||
|
||||
_kernel.Unload(km.Name);
|
||||
|
||||
if (km is IDisposable d)
|
||||
d.Dispose();
|
||||
|
||||
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 MedusaIoCKernelModule LoadMedusaServicesInternal(string name, Assembly a)
|
||||
// => new MedusaIoCKernelModule(name, a);
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public IReadOnlyCollection<SnekInfo> LoadSneksFromAssembly(string name, Assembly a)
|
||||
{
|
||||
// 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);
|
||||
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, parentData);
|
||||
}
|
||||
|
||||
return topModules.Values.ToArray();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private SnekInfo GetModuleData(Type type, SnekInfo? parentData = null)
|
||||
{
|
||||
var filters = type.GetCustomAttributes<FilterAttribute>(true)
|
||||
.ToArray();
|
||||
|
||||
var instance = (Snek)ActivatorUtilities.CreateInstance(_kernel, 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>(true).ToArray();
|
||||
var userAndBotPerms = method.GetCustomAttributes<MedusaPermAttribute>(true)
|
||||
.ToArray();
|
||||
var prio = method.GetCustomAttribute<prioAttribute>(true)?.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<leftoverAttribute>(true);
|
||||
var hasDefaultValue = pi.HasDefaultValue;
|
||||
var defaultValue = pi.DefaultValue;
|
||||
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, defaultValue, isLeftover, isParams));
|
||||
}
|
||||
|
||||
|
||||
var cmdAttribute = method.GetCustomAttribute<cmdAttribute>(true)!;
|
||||
var aliases = cmdAttribute.Aliases;
|
||||
if (aliases.Length == 0)
|
||||
aliases = new[] { method.Name.ToLowerInvariant() };
|
||||
|
||||
cmds.Add(new(
|
||||
aliases,
|
||||
method,
|
||||
instance,
|
||||
filters,
|
||||
userAndBotPerms,
|
||||
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}";
|
||||
}
|
10
src/NadekoBot/_common/Medusa/Common/Models/ParamData.cs
Normal file
10
src/NadekoBot/_common/Medusa/Common/Models/ParamData.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Nadeko.Medusa;
|
||||
|
||||
public sealed record ParamData(
|
||||
Type Type,
|
||||
string Name,
|
||||
bool IsOptional,
|
||||
object? DefaultValue,
|
||||
bool IsLeftover,
|
||||
bool IsParams
|
||||
);
|
17
src/NadekoBot/_common/Medusa/Common/Models/ResolvedMedusa.cs
Normal file
17
src/NadekoBot/_common/Medusa/Common/Models/ResolvedMedusa.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Nadeko.Snake;
|
||||
using Ninject.Modules;
|
||||
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 required INinjectModule KernelModule { get; set; }
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
using Nadeko.Snake;
|
||||
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,
|
||||
MedusaPermAttribute[] userAndBotPerms,
|
||||
CommandContextType contextType,
|
||||
IReadOnlyList<Type> injectedParams,
|
||||
IReadOnlyList<ParamData> parameters,
|
||||
CommandStrings strings,
|
||||
int priority)
|
||||
{
|
||||
Aliases = aliases;
|
||||
MethodInfo = methodInfo;
|
||||
Module = module;
|
||||
Filters = filters;
|
||||
UserAndBotPerms = userAndBotPerms;
|
||||
ContextType = contextType;
|
||||
InjectedParams = injectedParams;
|
||||
Parameters = parameters;
|
||||
Priority = priority;
|
||||
OptionalStrings = strings;
|
||||
}
|
||||
|
||||
public MedusaPermAttribute[] UserAndBotPerms { get; set; }
|
||||
|
||||
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; }
|
||||
}
|
13
src/NadekoBot/_common/Medusa/Common/Models/SnekData.cs
Normal file
13
src/NadekoBot/_common/Medusa/Common/Models/SnekData.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Nadeko.Snake;
|
||||
|
||||
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();
|
||||
}
|
122
src/NadekoBot/_common/Medusa/Common/RemovablePlanner.cs
Normal file
122
src/NadekoBot/_common/Medusa/Common/RemovablePlanner.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
//-------------------------------------------------------------------------------
|
||||
// <copyright file="Planner.cs" company="Ninject Project Contributors">
|
||||
// Copyright (c) 2007-2009, Enkari, Ltd.
|
||||
// Copyright (c) 2009-2011 Ninject Project Contributors
|
||||
// Authors: Nate Kohari (nate@enkari.com)
|
||||
// Remo Gloor (remo.gloor@gmail.com)
|
||||
//
|
||||
// Dual-licensed under the Apache License, Version 2.0, and the Microsoft Public License (Ms-PL).
|
||||
// you may not use this file except in compliance with one of the Licenses.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// or
|
||||
// http://www.microsoft.com/opensource/licenses.mspx
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// </copyright>
|
||||
//-------------------------------------------------------------------------------
|
||||
|
||||
// ReSharper disable all
|
||||
#pragma warning disable
|
||||
|
||||
namespace Ninject.Planning;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Ninject.Components;
|
||||
using Ninject.Infrastructure.Language;
|
||||
using Ninject.Planning.Strategies;
|
||||
|
||||
/// <summary>
|
||||
/// Generates plans for how to activate instances.
|
||||
/// </summary>
|
||||
public class RemovablePlanner : NinjectComponent, IPlanner
|
||||
{
|
||||
private readonly ReaderWriterLock plannerLock = new ReaderWriterLock();
|
||||
private readonly Dictionary<Type, IPlan> plans = new Dictionary<Type, IPlan>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RemovablePlanner"/> class.
|
||||
/// </summary>
|
||||
/// <param name="strategies">The strategies to execute during planning.</param>
|
||||
public RemovablePlanner(IEnumerable<IPlanningStrategy> strategies)
|
||||
{
|
||||
this.Strategies = strategies.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the strategies that contribute to the planning process.
|
||||
/// </summary>
|
||||
public IList<IPlanningStrategy> Strategies { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates an activation plan for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type for which a plan should be created.</param>
|
||||
/// <returns>The type's activation plan.</returns>
|
||||
public IPlan GetPlan(Type type)
|
||||
{
|
||||
this.plannerLock.AcquireReaderLock(Timeout.Infinite);
|
||||
try
|
||||
{
|
||||
IPlan plan;
|
||||
return this.plans.TryGetValue(type, out plan) ? plan : this.CreateNewPlan(type);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.plannerLock.ReleaseReaderLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty plan for the specified type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type for which a plan should be created.</param>
|
||||
/// <returns>The created plan.</returns>
|
||||
protected virtual IPlan CreateEmptyPlan(Type type)
|
||||
{
|
||||
return new Plan(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new plan for the specified type.
|
||||
/// This method requires an active reader lock!
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>The newly created plan.</returns>
|
||||
private IPlan CreateNewPlan(Type type)
|
||||
{
|
||||
var lockCooki = this.plannerLock.UpgradeToWriterLock(Timeout.Infinite);
|
||||
try
|
||||
{
|
||||
IPlan plan;
|
||||
if (this.plans.TryGetValue(type, out plan))
|
||||
{
|
||||
return plan;
|
||||
}
|
||||
|
||||
plan = this.CreateEmptyPlan(type);
|
||||
this.plans.Add(type, plan);
|
||||
this.Strategies.Map(s => s.Execute(plan));
|
||||
|
||||
return plan;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.plannerLock.DowngradeFromWriterLock(ref lockCooki);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemovePlan(Type type)
|
||||
{
|
||||
plans.Remove(type);
|
||||
plans.TrimExcess();
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
#nullable disable
|
||||
using System.Globalization;
|
||||
using NadekoBot.Common;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
@@ -48,15 +47,18 @@ public abstract class NadekoModule : ModuleBase
|
||||
string footer = null)
|
||||
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
|
||||
|
||||
//
|
||||
// public Task<IUserMessage> SendAsync(SmartText text, NadekoInteraction inter = null, IUserMessage replyTo = null)
|
||||
// => ctx.Channel.SendAsync(_eb, text, MsgType.Ok, inter, replyTo: replyTo);
|
||||
|
||||
// colored
|
||||
public Task<IUserMessage> SendErrorAsync(string text, NadekoInteraction inter = null)
|
||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Error, inter);
|
||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Error, inter, replyTo: ctx.Message);
|
||||
|
||||
public Task<IUserMessage> SendConfirmAsync(string text, NadekoInteraction inter = null)
|
||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Ok, inter);
|
||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Ok, inter, replyTo: ctx.Message);
|
||||
|
||||
public Task<IUserMessage> SendPendingAsync(string text, NadekoInteraction inter = null)
|
||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Pending, inter);
|
||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Pending, inter, replyTo: ctx.Message);
|
||||
|
||||
|
||||
// localized normal
|
||||
@@ -138,6 +140,9 @@ public abstract class NadekoModule : ModuleBase
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IUserMessage> EmbedAsync(IEmbedBuilder embed, string msg = "", IReadOnlyCollection<IEmbedBuilder> embeds = null)
|
||||
=> ctx.Channel.EmbedAsync(embed, msg, replyTo: ctx.Message);
|
||||
}
|
||||
|
||||
public abstract class NadekoModule<TService> : NadekoModule
|
||||
|
@@ -139,7 +139,7 @@ public static class ServiceCollectionExtensions
|
||||
|| c.IsAssignableTo(typeof(IExecPostCommand))
|
||||
|| c.IsAssignableTo(typeof(IExecNoCommand)))
|
||||
&& !c.HasAttribute<DIIgnoreAttribute>()
|
||||
#if GLOBAL_NADEK
|
||||
#if GLOBAL_NADEKO
|
||||
&& !c.HasAttribute<NoPublicBotAttribute>()
|
||||
#endif
|
||||
)
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Db.Models;
|
||||
using Nadeko.Bot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Services.Currency;
|
||||
|
||||
|
@@ -2,7 +2,7 @@ using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Services.Currency;
|
||||
using Nadeko.Bot.Db.Models;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
#nullable disable
|
||||
using Nadeko.Bot.Db.Models;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Services;
|
||||
|
||||
|
@@ -3,9 +3,7 @@ using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Db.Models;
|
||||
using Nadeko.Bot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Permissions.Services;
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using Nadeko.Bot.Db.Models;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
@@ -25,7 +25,7 @@ public class DiscordPermOverrideService : INService, IExecPreCommand, IDiscordPe
|
||||
.ToConcurrent();
|
||||
}
|
||||
|
||||
public bool TryGetOverrides(ulong guildId, string commandName, out Nadeko.Bot.Db.GuildPerm? perm)
|
||||
public bool TryGetOverrides(ulong guildId, string commandName, out NadekoBot.Db.GuildPerm? perm)
|
||||
{
|
||||
commandName = commandName.ToLowerInvariant();
|
||||
if (_overrides.TryGetValue((guildId, commandName), out var dpo))
|
||||
@@ -62,12 +62,12 @@ public class DiscordPermOverrideService : INService, IExecPreCommand, IDiscordPe
|
||||
.Add(over = new()
|
||||
{
|
||||
Command = commandName,
|
||||
Perm = (Nadeko.Bot.Db.GuildPerm)perm,
|
||||
Perm = (NadekoBot.Db.GuildPerm)perm,
|
||||
GuildId = guildId
|
||||
});
|
||||
}
|
||||
else
|
||||
over.Perm = (Nadeko.Bot.Db.GuildPerm)perm;
|
||||
over.Perm = (NadekoBot.Db.GuildPerm)perm;
|
||||
|
||||
_overrides[(guildId, commandName)] = over;
|
||||
|
||||
|
@@ -7,7 +7,7 @@ namespace NadekoBot.Services;
|
||||
|
||||
public sealed class StatsService : IStatsService, IReadyExecutor, INService
|
||||
{
|
||||
public const string BOT_VERSION = "5.0.0-alpha2";
|
||||
public const string BOT_VERSION = "5.0.0-beta1";
|
||||
|
||||
public string Author
|
||||
=> "Kwoth#2452";
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class MessageChannelExtensions
|
||||
@@ -10,18 +9,35 @@ public static class MessageChannelExtensions
|
||||
Embed? embed = null,
|
||||
IReadOnlyCollection<Embed>? embeds = null,
|
||||
bool sanitizeAll = false,
|
||||
MessageComponent? components = null)
|
||||
MessageComponent? components = null,
|
||||
IUserMessage? replyTo = null)
|
||||
{
|
||||
plainText = sanitizeAll
|
||||
? plainText?.SanitizeAllMentions() ?? ""
|
||||
: plainText?.SanitizeMentions() ?? "";
|
||||
|
||||
var msgReference = CreateMessageReference(channel, replyTo);
|
||||
return channel.SendMessageAsync(plainText,
|
||||
embed: embed,
|
||||
embeds: embeds is null
|
||||
? null
|
||||
: embeds as Embed[] ?? embeds.ToArray(),
|
||||
components: components);
|
||||
components: components,
|
||||
messageReference: msgReference);
|
||||
}
|
||||
|
||||
private static MessageReference? CreateMessageReference(IChannel source, IMessage? replyTo)
|
||||
{
|
||||
if (replyTo is null)
|
||||
return null;
|
||||
|
||||
if (replyTo.Channel.Id != source.Id)
|
||||
return null;
|
||||
|
||||
return new(replyTo.Id,
|
||||
replyTo.Channel.Id,
|
||||
(replyTo.Channel as ITextChannel)?.GuildId,
|
||||
failIfNotExists: false);
|
||||
}
|
||||
|
||||
public static async Task<IUserMessage> SendAsync(
|
||||
@@ -30,14 +46,16 @@ public static class MessageChannelExtensions
|
||||
NadekoInteraction? inter,
|
||||
Embed? embed = null,
|
||||
IReadOnlyCollection<Embed>? embeds = null,
|
||||
bool sanitizeAll = false)
|
||||
bool sanitizeAll = false,
|
||||
IUserMessage? replyTo = null)
|
||||
{
|
||||
var msg = await channel.SendAsync(plainText,
|
||||
embed,
|
||||
embeds,
|
||||
sanitizeAll,
|
||||
inter?.CreateComponent());
|
||||
|
||||
inter?.CreateComponent(),
|
||||
replyTo);
|
||||
|
||||
if (inter is not null)
|
||||
await inter.RunAsync(msg);
|
||||
|
||||
@@ -47,17 +65,22 @@ public static class MessageChannelExtensions
|
||||
public static Task<IUserMessage> SendAsync(
|
||||
this IMessageChannel channel,
|
||||
SmartText text,
|
||||
bool sanitizeAll = false)
|
||||
bool sanitizeAll = false,
|
||||
IUserMessage? replyTo = null)
|
||||
=> text switch
|
||||
{
|
||||
SmartEmbedText set => channel.SendAsync(set.PlainText,
|
||||
set.IsValid ? set.GetEmbed().Build() : null,
|
||||
sanitizeAll: sanitizeAll),
|
||||
sanitizeAll: sanitizeAll,
|
||||
replyTo: replyTo),
|
||||
SmartPlainText st => channel.SendAsync(st.Text,
|
||||
default(Embed),
|
||||
sanitizeAll: sanitizeAll),
|
||||
sanitizeAll: sanitizeAll,
|
||||
replyTo: replyTo),
|
||||
SmartEmbedTextArray arr => channel.SendAsync(arr.Content,
|
||||
embeds: arr.GetEmbedBuilders().Map(e => e.Build())),
|
||||
embeds: arr.GetEmbedBuilders().Map(e => e.Build()),
|
||||
sanitizeAll: sanitizeAll,
|
||||
replyTo: replyTo),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(text))
|
||||
};
|
||||
|
||||
@@ -66,18 +89,21 @@ public static class MessageChannelExtensions
|
||||
IEmbedBuilder? embed,
|
||||
string plainText = "",
|
||||
IReadOnlyCollection<IEmbedBuilder>? embeds = null,
|
||||
NadekoInteraction? inter = null)
|
||||
NadekoInteraction? inter = null,
|
||||
IUserMessage? replyTo = null)
|
||||
=> ch.SendAsync(plainText,
|
||||
inter,
|
||||
embed: embed?.Build(),
|
||||
embeds: embeds?.Map(x => x.Build()));
|
||||
|
||||
embeds: embeds?.Map(x => x.Build()),
|
||||
replyTo: replyTo);
|
||||
|
||||
public static Task<IUserMessage> SendAsync(
|
||||
this IMessageChannel ch,
|
||||
IEmbedBuilderService eb,
|
||||
string text,
|
||||
MsgType type,
|
||||
NadekoInteraction? inter = null)
|
||||
NadekoInteraction? inter = null,
|
||||
IUserMessage? replyTo = null)
|
||||
{
|
||||
var builder = eb.Create().WithDescription(text);
|
||||
|
||||
@@ -89,12 +115,12 @@ public static class MessageChannelExtensions
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type))
|
||||
});
|
||||
|
||||
return ch.EmbedAsync(builder, inter: inter);
|
||||
return ch.EmbedAsync(builder, inter: inter, replyTo: replyTo);
|
||||
}
|
||||
|
||||
|
||||
public static Task<IUserMessage> SendConfirmAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
|
||||
=> ch.SendAsync(eb, text, MsgType.Ok);
|
||||
|
||||
|
||||
public static Task<IUserMessage> SendAsync(
|
||||
this IMessageChannel ch,
|
||||
IEmbedBuilderService eb,
|
||||
@@ -121,12 +147,12 @@ public static class MessageChannelExtensions
|
||||
MsgType.Pending => embed.WithPendingColor(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type))
|
||||
};
|
||||
|
||||
|
||||
return ch.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
// embed title and optional footer overloads
|
||||
|
||||
|
||||
public static Task<IUserMessage> SendConfirmAsync(
|
||||
this IMessageChannel ch,
|
||||
IEmbedBuilderService eb,
|
||||
@@ -135,7 +161,7 @@ public static class MessageChannelExtensions
|
||||
string? url = null,
|
||||
string? footer = null)
|
||||
=> ch.SendAsync(eb, MsgType.Ok, title, text, url, footer);
|
||||
|
||||
|
||||
public static Task<IUserMessage> SendErrorAsync(
|
||||
this IMessageChannel ch,
|
||||
IEmbedBuilderService eb,
|
||||
@@ -144,7 +170,7 @@ public static class MessageChannelExtensions
|
||||
string? url = null,
|
||||
string? footer = null)
|
||||
=> ch.SendAsync(eb, MsgType.Error, title, text, url, footer);
|
||||
|
||||
|
||||
// regular send overloads
|
||||
public static Task<IUserMessage> SendErrorAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
|
||||
=> ch.SendAsync(eb, text, MsgType.Error);
|
||||
@@ -164,7 +190,7 @@ public static class MessageChannelExtensions
|
||||
|
||||
private const string BUTTON_LEFT = "BUTTON_LEFT";
|
||||
private const string BUTTON_RIGHT = "BUTTON_RIGHT";
|
||||
|
||||
|
||||
private static readonly IEmote _arrowLeft = Emote.Parse("<:x:1232256519844790302>");
|
||||
private static readonly IEmote _arrowRight = Emote.Parse("<:x:1232256515298295838>");
|
||||
|
||||
@@ -181,7 +207,7 @@ public static class MessageChannelExtensions
|
||||
totalElements,
|
||||
itemsPerPage,
|
||||
addPaginatedFooter);
|
||||
|
||||
|
||||
public static async Task SendPaginatedConfirmAsync<T>(
|
||||
this ICommandContext ctx,
|
||||
int currentPage,
|
||||
@@ -192,23 +218,24 @@ public static class MessageChannelExtensions
|
||||
bool addPaginatedFooter = true)
|
||||
{
|
||||
var lastPage = (totalElements - 1) / itemsPerPage;
|
||||
|
||||
|
||||
var embed = await pageFunc(currentPage);
|
||||
|
||||
if (addPaginatedFooter)
|
||||
embed.AddPaginatedFooter(currentPage, lastPage);
|
||||
|
||||
SimpleInteraction<T>? maybeInter = null;
|
||||
|
||||
async Task<ComponentBuilder> GetComponentBuilder()
|
||||
{
|
||||
var cb = new ComponentBuilder();
|
||||
|
||||
|
||||
cb.WithButton(new ButtonBuilder()
|
||||
.WithStyle(ButtonStyle.Primary)
|
||||
.WithCustomId(BUTTON_LEFT)
|
||||
.WithDisabled(lastPage == 0)
|
||||
.WithEmote(_arrowLeft)
|
||||
.WithDisabled(currentPage <= 0));
|
||||
.WithStyle(ButtonStyle.Primary)
|
||||
.WithCustomId(BUTTON_LEFT)
|
||||
.WithDisabled(lastPage == 0)
|
||||
.WithEmote(_arrowLeft)
|
||||
.WithDisabled(currentPage <= 0));
|
||||
|
||||
if (interFactory is not null)
|
||||
{
|
||||
@@ -219,10 +246,10 @@ public static class MessageChannelExtensions
|
||||
}
|
||||
|
||||
cb.WithButton(new ButtonBuilder()
|
||||
.WithStyle(ButtonStyle.Primary)
|
||||
.WithCustomId(BUTTON_RIGHT)
|
||||
.WithDisabled(lastPage == 0 || currentPage >= lastPage)
|
||||
.WithEmote(_arrowRight));
|
||||
.WithStyle(ButtonStyle.Primary)
|
||||
.WithCustomId(BUTTON_RIGHT)
|
||||
.WithDisabled(lastPage == 0 || currentPage >= lastPage)
|
||||
.WithEmote(_arrowRight));
|
||||
|
||||
return cb;
|
||||
}
|
||||
@@ -232,7 +259,7 @@ public static class MessageChannelExtensions
|
||||
var toSend = await pageFunc(currentPage);
|
||||
if (addPaginatedFooter)
|
||||
toSend.AddPaginatedFooter(currentPage, lastPage);
|
||||
|
||||
|
||||
var component = (await GetComponentBuilder()).Build();
|
||||
|
||||
await smc.ModifyOriginalResponseAsync(x =>
|
||||
@@ -241,9 +268,9 @@ public static class MessageChannelExtensions
|
||||
x.Components = component;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var component = (await GetComponentBuilder()).Build();
|
||||
var msg = await ctx.Channel.SendAsync(null, embed: embed.Build(), components: component);
|
||||
var msg = await ctx.Channel.SendAsync(null, embed: embed.Build(), components: component, replyTo: ctx.Message);
|
||||
|
||||
async Task OnInteractionAsync(SocketInteraction si)
|
||||
{
|
||||
@@ -271,7 +298,7 @@ public static class MessageChannelExtensions
|
||||
{
|
||||
if (currentPage >= lastPage)
|
||||
return;
|
||||
|
||||
|
||||
++currentPage;
|
||||
_ = UpdatePageAsync(smc);
|
||||
}
|
||||
@@ -297,14 +324,14 @@ public static class MessageChannelExtensions
|
||||
await Task.Delay(30_000);
|
||||
|
||||
client.InteractionCreated -= OnInteractionAsync;
|
||||
|
||||
|
||||
await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build());
|
||||
}
|
||||
|
||||
private static readonly Emoji _okEmoji = new Emoji("✅");
|
||||
private static readonly Emoji _warnEmoji = new Emoji("⚠️");
|
||||
private static readonly Emoji _errorEmoji = new Emoji("❌");
|
||||
|
||||
|
||||
public static Task ReactAsync(this ICommandContext ctx, MsgType type)
|
||||
{
|
||||
var emoji = type switch
|
||||
|
Reference in New Issue
Block a user