mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04: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