mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-03 16:24:27 -05:00 
			
		
		
		
	Medusa System Added
Read about the medusa system [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)
This commit is contained in:
		
							
								
								
									
										10
									
								
								src/Nadeko.Medusa/Attributes/FilterAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/Nadeko.Medusa/Attributes/FilterAttribute.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Overridden to implement custom checks which commands have to pass in order to be executed.
 | 
			
		||||
/// </summary>
 | 
			
		||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
 | 
			
		||||
public abstract class FilterAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
    public abstract ValueTask<bool> CheckAsync(AnyContext ctx);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								src/Nadeko.Medusa/Attributes/cmdAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/Nadeko.Medusa/Attributes/cmdAttribute.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Marks a method as a snek command
 | 
			
		||||
/// </summary>
 | 
			
		||||
[AttributeUsage(AttributeTargets.Method)]
 | 
			
		||||
public class cmdAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Command description. Avoid using, as cmds.yml is preferred
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string? desc { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Command args examples. Avoid using, as cmds.yml is preferred
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string[]? args { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Command aliases
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public string[] Aliases { get; }
 | 
			
		||||
 | 
			
		||||
    public cmdAttribute()
 | 
			
		||||
    {
 | 
			
		||||
        desc = null;
 | 
			
		||||
        args = null;
 | 
			
		||||
        Aliases = Array.Empty<string>();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public cmdAttribute(params string[] aliases)
 | 
			
		||||
    {
 | 
			
		||||
        Aliases = aliases;
 | 
			
		||||
        desc = null;
 | 
			
		||||
        args = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/Nadeko.Medusa/Attributes/injectAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/Nadeko.Medusa/Attributes/injectAttribute.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Marks services in command arguments for injection.
 | 
			
		||||
/// The injected services must come after the context and before any input parameters.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class injectAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/Nadeko.Medusa/Attributes/leftoverAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/Nadeko.Medusa/Attributes/leftoverAttribute.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Marks the parameter to take 
 | 
			
		||||
/// </summary>
 | 
			
		||||
[AttributeUsage(AttributeTargets.Parameter)]
 | 
			
		||||
public class leftoverAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								src/Nadeko.Medusa/Attributes/prioAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/Nadeko.Medusa/Attributes/prioAttribute.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Sets the priority of a command in case there are multiple commands with the same name but different parameters.
 | 
			
		||||
/// Higher value means higher priority.
 | 
			
		||||
/// </summary>
 | 
			
		||||
[AttributeUsage(AttributeTargets.Method)]
 | 
			
		||||
public class prioAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
    public int Priority { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Snek command priority
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="priority">Priority value. The higher the value, the higher the priority</param>
 | 
			
		||||
    public prioAttribute(int priority)
 | 
			
		||||
    {
 | 
			
		||||
        Priority = priority;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								src/Nadeko.Medusa/Attributes/svcAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/Nadeko.Medusa/Attributes/svcAttribute.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Marks the class as a service which can be used within the same Medusa
 | 
			
		||||
/// </summary>
 | 
			
		||||
[AttributeUsage(AttributeTargets.Class)]
 | 
			
		||||
public class svcAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
    public Lifetime Lifetime { get; }
 | 
			
		||||
    public svcAttribute(Lifetime lifetime)
 | 
			
		||||
    {
 | 
			
		||||
        Lifetime = lifetime;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Lifetime for <see cref="svcAttribute"/>
 | 
			
		||||
/// </summary>
 | 
			
		||||
public enum Lifetime
 | 
			
		||||
{
 | 
			
		||||
    Singleton,
 | 
			
		||||
    Transient
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								src/Nadeko.Medusa/Context/AnyContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/Nadeko.Medusa/Context/AnyContext.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Commands which take this class as a first parameter can be executed in both DMs and Servers 
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class AnyContext
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Channel from the which the command is invoked
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public abstract IMessageChannel Channel { get; }
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Message which triggered the command
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public abstract IUserMessage Message { get; }
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// The user who invoked the command
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public abstract IUser User { get; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Provides access to strings used by this medusa
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public abstract IMedusaStrings Strings { get; } 
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Gets a formatted localized string using a key and arguments which should be formatted in
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="key">The key of the string as specified in localization files</param>
 | 
			
		||||
    /// <param name="args">Arguments (if any) to format in</param>
 | 
			
		||||
    /// <returns>A formatted localized string</returns>
 | 
			
		||||
    public abstract string GetText(string key, object[]? args = null);
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Creates a context-aware <see cref="IEmbedBuilder"/> instance
 | 
			
		||||
    /// (future feature for guild-based embed colors)
 | 
			
		||||
    /// Any code dealing with embeds should use it for future-proofness
 | 
			
		||||
    /// instead of manually creating embedbuilder instances
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>A context-aware <see cref="IEmbedBuilder"/> instance </returns>
 | 
			
		||||
    public abstract IEmbedBuilder Embed();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								src/Nadeko.Medusa/Context/DmContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/Nadeko.Medusa/Context/DmContext.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Commands which take this type as the first parameter can only be executed in DMs
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class DmContext : AnyContext
 | 
			
		||||
{
 | 
			
		||||
    public abstract override IDMChannel Channel { get; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								src/Nadeko.Medusa/Context/GuildContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/Nadeko.Medusa/Context/GuildContext.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Commands which take this type as a first parameter can only be executed in a server
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class GuildContext : AnyContext
 | 
			
		||||
{
 | 
			
		||||
   public abstract override ITextChannel Channel { get; }
 | 
			
		||||
   public abstract IGuild Guild { get; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/Nadeko.Medusa/EmbedColor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/Nadeko.Medusa/EmbedColor.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
namespace NadekoBot;
 | 
			
		||||
 | 
			
		||||
public enum EmbedColor
 | 
			
		||||
{
 | 
			
		||||
    Ok,
 | 
			
		||||
    Pending,
 | 
			
		||||
    Error
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								src/Nadeko.Medusa/Extensions/EmbedBuilderExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/Nadeko.Medusa/Extensions/EmbedBuilderExtensions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
namespace NadekoBot;
 | 
			
		||||
 | 
			
		||||
public static class EmbedBuilderExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static IEmbedBuilder WithOkColor(this IEmbedBuilder eb)
 | 
			
		||||
        => eb.WithColor(EmbedColor.Ok);
 | 
			
		||||
    
 | 
			
		||||
    public static IEmbedBuilder WithPendingColor(this IEmbedBuilder eb)
 | 
			
		||||
        => eb.WithColor(EmbedColor.Pending);
 | 
			
		||||
    
 | 
			
		||||
    public static IEmbedBuilder WithErrorColor(this IEmbedBuilder eb)
 | 
			
		||||
        => eb.WithColor(EmbedColor.Error);
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/Nadeko.Medusa/Extensions/MedusaExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/Nadeko.Medusa/Extensions/MedusaExtensions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
using Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot;
 | 
			
		||||
 | 
			
		||||
public static class MedusaExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static Task<IUserMessage> EmbedAsync(this IMessageChannel ch, IEmbedBuilder embed, string msg = "")
 | 
			
		||||
        => ch.SendMessageAsync(msg,
 | 
			
		||||
            embed: embed.Build(),
 | 
			
		||||
            options: new()
 | 
			
		||||
            {
 | 
			
		||||
                RetryMode = RetryMode.AlwaysRetry
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
    // unlocalized
 | 
			
		||||
    public static Task<IUserMessage> SendConfirmAsync(this IMessageChannel ch, AnyContext ctx, string msg)
 | 
			
		||||
        => ch.EmbedAsync(ctx.Embed().WithOkColor().WithDescription(msg));
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> SendPendingAsync(this IMessageChannel ch, AnyContext ctx, string msg)
 | 
			
		||||
        => ch.EmbedAsync(ctx.Embed().WithPendingColor().WithDescription(msg));
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> SendErrorAsync(this IMessageChannel ch, AnyContext ctx, string msg)
 | 
			
		||||
        => ch.EmbedAsync(ctx.Embed().WithErrorColor().WithDescription(msg));
 | 
			
		||||
 | 
			
		||||
    // unlocalized
 | 
			
		||||
    public static Task<IUserMessage> SendConfirmAsync(this AnyContext ctx, string msg)
 | 
			
		||||
        => ctx.Channel.SendConfirmAsync(ctx, msg);
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> SendPendingAsync(this AnyContext ctx, string msg)
 | 
			
		||||
        => ctx.Channel.SendPendingAsync(ctx, msg);
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> SendErrorAsync(this AnyContext ctx, string msg)
 | 
			
		||||
        => ctx.Channel.SendErrorAsync(ctx, msg);
 | 
			
		||||
 | 
			
		||||
    // localized
 | 
			
		||||
    public static Task ConfirmAsync(this AnyContext ctx)
 | 
			
		||||
        => ctx.Message.AddReactionAsync(new Emoji("✅"));
 | 
			
		||||
 | 
			
		||||
    public static Task ErrorAsync(this AnyContext ctx)
 | 
			
		||||
        => ctx.Message.AddReactionAsync(new Emoji("❌"));
 | 
			
		||||
 | 
			
		||||
    public static Task WarningAsync(this AnyContext ctx)
 | 
			
		||||
        => ctx.Message.AddReactionAsync(new Emoji("⚠️"));
 | 
			
		||||
 | 
			
		||||
    public static Task WaitAsync(this AnyContext ctx)
 | 
			
		||||
        => ctx.Message.AddReactionAsync(new Emoji("🤔"));
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> ErrorLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
 | 
			
		||||
        => ctx.SendErrorAsync(ctx.GetText(key));
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> PendingLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
 | 
			
		||||
        => ctx.SendPendingAsync(ctx.GetText(key, args));
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> ConfirmLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
 | 
			
		||||
        => ctx.SendConfirmAsync(ctx.GetText(key, args));
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> ReplyErrorLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
 | 
			
		||||
        => ctx.SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key)}");
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> ReplyPendingLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
 | 
			
		||||
        => ctx.SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key)}");
 | 
			
		||||
 | 
			
		||||
    public static Task<IUserMessage> ReplyConfirmLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
 | 
			
		||||
        => ctx.SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key)}");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/Nadeko.Medusa/IEmbedBuilder.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/Nadeko.Medusa/IEmbedBuilder.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/Nadeko.Medusa/Nadeko.Medusa.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/Nadeko.Medusa/Nadeko.Medusa.csproj
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
			
		||||
 | 
			
		||||
    <PropertyGroup>
 | 
			
		||||
        <TargetFramework>net6.0</TargetFramework>
 | 
			
		||||
        <ImplicitUsings>enable</ImplicitUsings>
 | 
			
		||||
        <Nullable>enable</Nullable>
 | 
			
		||||
        <LangVersion>preview</LangVersion>
 | 
			
		||||
        <EnablePreviewFeatures>true</EnablePreviewFeatures>
 | 
			
		||||
        <RootNamespace>Nadeko.Snake</RootNamespace>
 | 
			
		||||
        
 | 
			
		||||
        <Authors>The NadekoBot Team</Authors>
 | 
			
		||||
        <Version>1.0.2</Version>
 | 
			
		||||
    </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
      <PackageReference Include="Discord.Net.Core" Version="3.5.0" />
 | 
			
		||||
      <PackageReference Include="Serilog" Version="2.10.0" />
 | 
			
		||||
      <PackageReference Include="YamlDotNet" Version="11.2.1" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
							
								
								
									
										16
									
								
								src/Nadeko.Medusa/ParamParser/ParamParser.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/Nadeko.Medusa/ParamParser/ParamParser.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Overridden to implement parsers for custom types 
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <typeparam name="T">Type into which to parse the input</typeparam>
 | 
			
		||||
public abstract class ParamParser<T>
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Overridden to implement parsing logic
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="ctx">Context</param>
 | 
			
		||||
    /// <param name="input">Input to parse</param>
 | 
			
		||||
    /// <returns>A <see cref="ParseResult{T}"/> with successful or failed status</returns>
 | 
			
		||||
    public abstract ValueTask<ParseResult<T>> TryParseAsync(AnyContext ctx, string input);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								src/Nadeko.Medusa/ParamParser/ParseResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/Nadeko.Medusa/ParamParser/ParseResult.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
public readonly struct ParseResult<T>
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Whether the parsing was successful
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public bool IsSuccess { get; private init; }
 | 
			
		||||
    
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Parsed value. It should only have value if <see cref="IsSuccess"/> is set to true
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public T? Data { get; private init;  }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Instantiate a **successful** parse result
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="data">Parsed value</param>
 | 
			
		||||
    public ParseResult(T data)
 | 
			
		||||
    {
 | 
			
		||||
        Data = data;
 | 
			
		||||
        IsSuccess = true;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Create a new <see cref="ParseResult{T}"/> with IsSuccess = false
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>A new <see cref="ParseResult{T}"/></returns>
 | 
			
		||||
    public static ParseResult<T> Fail()
 | 
			
		||||
        => new ParseResult<T>
 | 
			
		||||
        {
 | 
			
		||||
            IsSuccess = false,
 | 
			
		||||
            Data = default,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Create a new <see cref="ParseResult{T}"/> with IsSuccess = true
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="obj">Value of the parsed object</param>
 | 
			
		||||
    /// <returns>A new <see cref="ParseResult{T}"/></returns>
 | 
			
		||||
    public static ParseResult<T> Success(T obj)
 | 
			
		||||
        => new ParseResult<T>
 | 
			
		||||
        {
 | 
			
		||||
            IsSuccess = true,
 | 
			
		||||
            Data = obj,
 | 
			
		||||
        };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/Nadeko.Medusa/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/Nadeko.Medusa/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
This is the library which is the base of any medusa.
 | 
			
		||||
							
								
								
									
										143
									
								
								src/Nadeko.Medusa/Snek.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/Nadeko.Medusa/Snek.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
			
		||||
using Discord;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// The base class which will be loaded as a module into NadekoBot
 | 
			
		||||
/// Any user-defined snek has to inherit from this class.
 | 
			
		||||
/// Sneks get instantiated ONLY ONCE during the loading,
 | 
			
		||||
/// and any snek commands will be executed on the same instance.
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class Snek : IAsyncDisposable
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Name of the snek. Defaults to the lowercase class name
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual string Name
 | 
			
		||||
        => GetType().Name.ToLowerInvariant();
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// The prefix required before the command name. For example
 | 
			
		||||
    /// if you set this to 'test' then a command called 'cmd' will have to be invoked by using
 | 
			
		||||
    /// '.test cmd' instead of `.cmd` 
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public virtual string Prefix
 | 
			
		||||
        => string.Empty;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Executed once this snek has been instantiated and before any command is executed.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>A <see cref="ValueTask"/> representing completion</returns>
 | 
			
		||||
    public virtual ValueTask InitializeAsync()
 | 
			
		||||
        => default;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Override to cleanup any resources or references which might hold this snek in memory
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public virtual ValueTask DisposeAsync()
 | 
			
		||||
        => default;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// This method is called right after the message was received by the bot.
 | 
			
		||||
    /// You can use this method to make the bot conditionally ignore some messages and prevent further processing.
 | 
			
		||||
    /// <para>Execution order:</para>
 | 
			
		||||
    /// <para>
 | 
			
		||||
    /// *<see cref="ExecOnMessageAsync"/>* →
 | 
			
		||||
    /// <see cref="ExecInputTransformAsync"/> →
 | 
			
		||||
    /// <see cref="ExecPreCommandAsync"/> →
 | 
			
		||||
    /// <see cref="ExecPostCommandAsync"/> OR <see cref="ExecOnNoCommandAsync"/>
 | 
			
		||||
    /// </para>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guild">Guild in which the message was sent</param>
 | 
			
		||||
    /// <param name="msg">Message received by the bot</param>
 | 
			
		||||
    /// <returns>A <see cref="ValueTask"/> representing whether the message should be ignored and not processed further</returns>
 | 
			
		||||
    public virtual ValueTask<bool> ExecOnMessageAsync(IGuild? guild, IUserMessage msg)
 | 
			
		||||
        => default;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Override this method to modify input before the bot searches for any commands matching the input
 | 
			
		||||
    /// Executed after <see cref="ExecOnMessageAsync"/>
 | 
			
		||||
    /// This is useful if you want to reinterpret the message under some conditions
 | 
			
		||||
    /// <para>Execution order:</para>
 | 
			
		||||
    /// <para>
 | 
			
		||||
    /// <see cref="ExecOnMessageAsync"/> →
 | 
			
		||||
    /// *<see cref="ExecInputTransformAsync"/>* →
 | 
			
		||||
    /// <see cref="ExecPreCommandAsync"/> →
 | 
			
		||||
    /// <see cref="ExecPostCommandAsync"/> OR <see cref="ExecOnNoCommandAsync"/>
 | 
			
		||||
    /// </para> 
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guild">Guild in which the message was sent</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>A <see cref="ValueTask"/> representing new, potentially modified content</returns>
 | 
			
		||||
    public virtual ValueTask<string?> ExecInputTransformAsync(
 | 
			
		||||
        IGuild? guild,
 | 
			
		||||
        IMessageChannel channel,
 | 
			
		||||
        IUser user,
 | 
			
		||||
        string input
 | 
			
		||||
    )
 | 
			
		||||
        => default;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// This method is called after the command was found but not executed,
 | 
			
		||||
    /// and can be used to prevent the command's execution.
 | 
			
		||||
    /// The command information doesn't have to be from this snek as this method
 | 
			
		||||
    /// will be called when *any* command from any module or snek was found.
 | 
			
		||||
    /// You can choose to prevent the execution of the command by returning "true" value.
 | 
			
		||||
    /// <para>Execution order:</para>
 | 
			
		||||
    /// <para>
 | 
			
		||||
    /// <see cref="ExecOnMessageAsync"/> →
 | 
			
		||||
    /// <see cref="ExecInputTransformAsync"/> →
 | 
			
		||||
    /// *<see cref="ExecPreCommandAsync"/>* →
 | 
			
		||||
    /// <see cref="ExecPostCommandAsync"/> OR <see cref="ExecOnNoCommandAsync"/>
 | 
			
		||||
    /// </para>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="context">Command context</param>
 | 
			
		||||
    /// <param name="moduleName">Name of the snek or module from which the command originates</param>
 | 
			
		||||
    /// <param name="commandName">Name of the command which is about to be executed</param>
 | 
			
		||||
    /// <returns>A <see cref="ValueTask"/> representing whether the execution should be blocked</returns>
 | 
			
		||||
    public virtual ValueTask<bool> ExecPreCommandAsync(
 | 
			
		||||
        AnyContext context,
 | 
			
		||||
        string moduleName,
 | 
			
		||||
        string commandName
 | 
			
		||||
    )
 | 
			
		||||
        => default;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// This method is called after the command was succesfully executed.
 | 
			
		||||
    /// If this method was called, then <see cref="ExecOnNoCommandAsync"/> will not be executed
 | 
			
		||||
    /// <para>Execution order:</para>
 | 
			
		||||
    /// <para>
 | 
			
		||||
    /// <see cref="ExecOnMessageAsync"/> →
 | 
			
		||||
    /// <see cref="ExecInputTransformAsync"/> →
 | 
			
		||||
    /// <see cref="ExecPreCommandAsync"/> →
 | 
			
		||||
    /// *<see cref="ExecPostCommandAsync"/>* OR <see cref="ExecOnNoCommandAsync"/>
 | 
			
		||||
    /// </para>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>A <see cref="ValueTask"/> representing completion</returns>
 | 
			
		||||
    public virtual ValueTask ExecPostCommandAsync(AnyContext ctx, string moduleName, string commandName)
 | 
			
		||||
        => default;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// This method is called if no command was found for the input.
 | 
			
		||||
    /// Useful if you want to have games or features which take arbitrary input
 | 
			
		||||
    /// but ignore any messages which were blocked or caused a command execution
 | 
			
		||||
    /// If this method was called, then <see cref="ExecPostCommandAsync"/> will not be executed
 | 
			
		||||
    /// <para>Execution order:</para>
 | 
			
		||||
    /// <para>
 | 
			
		||||
    /// <see cref="ExecOnMessageAsync"/> →
 | 
			
		||||
    /// <see cref="ExecInputTransformAsync"/> →
 | 
			
		||||
    /// <see cref="ExecPreCommandAsync"/> →
 | 
			
		||||
    /// <see cref="ExecPostCommandAsync"/> OR *<see cref="ExecOnNoCommandAsync"/>*
 | 
			
		||||
    /// </para>
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>A <see cref="ValueTask"/> representing completion</returns>
 | 
			
		||||
    public virtual ValueTask ExecOnNoCommandAsync(IGuild? guild, IUserMessage msg)
 | 
			
		||||
        => default;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public readonly struct ExecResponse
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								src/Nadeko.Medusa/Strings/CommandStrings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/Nadeko.Medusa/Strings/CommandStrings.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
using YamlDotNet.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
public readonly struct CommandStrings
 | 
			
		||||
{
 | 
			
		||||
    public CommandStrings(string? desc, string[]? args)
 | 
			
		||||
    {
 | 
			
		||||
        Desc = desc;
 | 
			
		||||
        Args = args;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [YamlMember(Alias = "desc")]
 | 
			
		||||
    public string? Desc { get; init; }
 | 
			
		||||
    
 | 
			
		||||
    [YamlMember(Alias = "args")]
 | 
			
		||||
    public string[]? Args { get; init; }
 | 
			
		||||
 | 
			
		||||
    public void Deconstruct(out string? desc, out string[]? args)
 | 
			
		||||
    {
 | 
			
		||||
        desc = Desc;
 | 
			
		||||
        args = Args;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/Nadeko.Medusa/Strings/IMedusaStrings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/Nadeko.Medusa/Strings/IMedusaStrings.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     Defines methods to retrieve and reload medusa strings
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IMedusaStrings
 | 
			
		||||
{
 | 
			
		||||
    // string GetText(string key, ulong? guildId = null, params object[] data);
 | 
			
		||||
    string? GetText(string key, CultureInfo locale, params object[] data);
 | 
			
		||||
    void Reload();
 | 
			
		||||
    CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo);
 | 
			
		||||
    string? GetDescription(CultureInfo? locale);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/Nadeko.Medusa/Strings/IMedusaStringsProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/Nadeko.Medusa/Strings/IMedusaStringsProvider.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     Implemented by classes which provide localized strings in their own ways
 | 
			
		||||
/// </summary>
 | 
			
		||||
public interface IMedusaStringsProvider
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Gets localized string
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="localeName">Language name</param>
 | 
			
		||||
    /// <param name="key">String key</param>
 | 
			
		||||
    /// <returns>Localized string</returns>
 | 
			
		||||
    string? GetText(string localeName, string key);
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Reloads string cache
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    void Reload();
 | 
			
		||||
 | 
			
		||||
    // /// <summary>
 | 
			
		||||
    // ///     Gets command arg examples and description
 | 
			
		||||
    // /// </summary>
 | 
			
		||||
    // /// <param name="localeName">Language name</param>
 | 
			
		||||
    // /// <param name="commandName">Command name</param>
 | 
			
		||||
    // CommandStrings GetCommandStrings(string localeName, string commandName);
 | 
			
		||||
    CommandStrings? GetCommandStrings(string localeName, string commandName);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								src/Nadeko.Medusa/Strings/LocalMedusaStringsProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/Nadeko.Medusa/Strings/LocalMedusaStringsProvider.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
public class LocalMedusaStringsProvider : IMedusaStringsProvider
 | 
			
		||||
{
 | 
			
		||||
    private readonly StringsLoader _source;
 | 
			
		||||
    private IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> _responseStrings;
 | 
			
		||||
    private IReadOnlyDictionary<string, IReadOnlyDictionary<string, CommandStrings>> _commandStrings;
 | 
			
		||||
 | 
			
		||||
    public LocalMedusaStringsProvider(StringsLoader source)
 | 
			
		||||
    {
 | 
			
		||||
        _source = source;
 | 
			
		||||
        _responseStrings = _source.GetResponseStrings();
 | 
			
		||||
        _commandStrings = _source.GetCommandStrings();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void Reload()
 | 
			
		||||
    {
 | 
			
		||||
        _responseStrings = _source.GetResponseStrings();
 | 
			
		||||
        _commandStrings = _source.GetCommandStrings();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public string? GetText(string localeName, string key)
 | 
			
		||||
    {
 | 
			
		||||
        if (_responseStrings.TryGetValue(localeName.ToLowerInvariant(), out var langStrings)
 | 
			
		||||
            && langStrings.TryGetValue(key.ToLowerInvariant(), out var text))
 | 
			
		||||
            return text;
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public CommandStrings? GetCommandStrings(string localeName, string commandName)
 | 
			
		||||
    {
 | 
			
		||||
        if (_commandStrings.TryGetValue(localeName.ToLowerInvariant(), out var langStrings)
 | 
			
		||||
            && langStrings.TryGetValue(commandName.ToLowerInvariant(), out var strings))
 | 
			
		||||
            return strings;
 | 
			
		||||
    
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										79
									
								
								src/Nadeko.Medusa/Strings/MedusaStrings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/Nadeko.Medusa/Strings/MedusaStrings.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
public class MedusaStrings : IMedusaStrings
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    ///     Used as failsafe in case response key doesn't exist in the selected or default language.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    private readonly CultureInfo _usCultureInfo = new("en-US");
 | 
			
		||||
    
 | 
			
		||||
    private readonly IMedusaStringsProvider _stringsProvider;
 | 
			
		||||
 | 
			
		||||
    public MedusaStrings(IMedusaStringsProvider stringsProvider)
 | 
			
		||||
    {
 | 
			
		||||
        _stringsProvider = stringsProvider;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string? GetString(string key, CultureInfo cultureInfo)
 | 
			
		||||
        => _stringsProvider.GetText(cultureInfo.Name, key);
 | 
			
		||||
 | 
			
		||||
    public string? GetText(string key, CultureInfo cultureInfo)
 | 
			
		||||
        => GetString(key, cultureInfo)
 | 
			
		||||
           ?? GetString(key, _usCultureInfo);
 | 
			
		||||
 | 
			
		||||
    public string? GetText(string key, CultureInfo cultureInfo, params object[] data)
 | 
			
		||||
    {
 | 
			
		||||
        var text = GetText(key, cultureInfo);
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(text))
 | 
			
		||||
            return null;
 | 
			
		||||
        
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            return string.Format(text, data);
 | 
			
		||||
        }
 | 
			
		||||
        catch (FormatException)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning(" Key '{Key}' is not properly formatted in '{LanguageName}' response strings",
 | 
			
		||||
                key,
 | 
			
		||||
                cultureInfo.Name);
 | 
			
		||||
            
 | 
			
		||||
            return $"⚠️ Response string key '{key}' is not properly formatted. Please report this.\n\n{text}";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo)
 | 
			
		||||
    {
 | 
			
		||||
        var cmdStrings = _stringsProvider.GetCommandStrings(cultureInfo.Name, commandName);
 | 
			
		||||
        if (cmdStrings is null)
 | 
			
		||||
        {
 | 
			
		||||
            if (cultureInfo.Name == _usCultureInfo.Name)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Warning("'{CommandName}' doesn't exist in 'en-US' command strings for one of the medusae",
 | 
			
		||||
                    commandName);
 | 
			
		||||
 | 
			
		||||
                return new(null, null);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Log.Information("Missing '{CommandName}' command strings for the '{LocaleName}' locale",
 | 
			
		||||
                commandName,
 | 
			
		||||
                cultureInfo.Name);
 | 
			
		||||
            
 | 
			
		||||
            return GetCommandStrings(commandName, _usCultureInfo);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return cmdStrings.Value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string? GetDescription(CultureInfo? locale = null)
 | 
			
		||||
        => GetText("medusa.description", locale ?? _usCultureInfo);
 | 
			
		||||
 | 
			
		||||
    public static MedusaStrings CreateDefault(string basePath)
 | 
			
		||||
        => new MedusaStrings(new LocalMedusaStringsProvider(new(basePath)));
 | 
			
		||||
    
 | 
			
		||||
    public void Reload()
 | 
			
		||||
        => _stringsProvider.Reload();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										137
									
								
								src/Nadeko.Medusa/Strings/StringsLoader.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/Nadeko.Medusa/Strings/StringsLoader.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
using Serilog;
 | 
			
		||||
using YamlDotNet.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace Nadeko.Snake;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
///     Loads strings from the shortcut or localizable path
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class StringsLoader
 | 
			
		||||
{
 | 
			
		||||
    private readonly string _localizableResponsesPath;
 | 
			
		||||
    private readonly string _shortcutResponsesFile;
 | 
			
		||||
    
 | 
			
		||||
    private readonly string _localizableCommandsPath;
 | 
			
		||||
    private readonly string _shortcutCommandsFile;
 | 
			
		||||
 | 
			
		||||
    public StringsLoader(string basePath)
 | 
			
		||||
    {
 | 
			
		||||
        _localizableResponsesPath = Path.Join(basePath, "strings/res");
 | 
			
		||||
        _shortcutResponsesFile = Path.Join(basePath, "res.yml");
 | 
			
		||||
        
 | 
			
		||||
        _localizableCommandsPath = Path.Join(basePath, "strings/cmds");
 | 
			
		||||
        _shortcutCommandsFile = Path.Join(basePath, "cmds.yml");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyDictionary<string, IReadOnlyDictionary<string, CommandStrings>> GetCommandStrings()
 | 
			
		||||
    {
 | 
			
		||||
        var outputDict = new Dictionary<string, IReadOnlyDictionary<string, CommandStrings>>();
 | 
			
		||||
 | 
			
		||||
        if (File.Exists(_shortcutCommandsFile))
 | 
			
		||||
        {
 | 
			
		||||
            if (TryLoadCommandsFromFile(_shortcutCommandsFile, out var dict, out _))
 | 
			
		||||
            {
 | 
			
		||||
                outputDict["en-us"] = dict;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return outputDict;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (Directory.Exists(_localizableCommandsPath))
 | 
			
		||||
        {
 | 
			
		||||
            foreach (var cmdsFile in Directory.EnumerateFiles(_localizableCommandsPath))
 | 
			
		||||
            {
 | 
			
		||||
                if (TryLoadCommandsFromFile(cmdsFile, out var dict, out var locale) && locale is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    outputDict[locale.ToLowerInvariant()] = dict;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return outputDict;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    private static readonly IDeserializer _deserializer = new DeserializerBuilder().Build();
 | 
			
		||||
    private static bool TryLoadCommandsFromFile(string file,
 | 
			
		||||
        [NotNullWhen(true)] out IReadOnlyDictionary<string, CommandStrings>? strings,
 | 
			
		||||
        out string? localeName)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var text = File.ReadAllText(file);
 | 
			
		||||
            strings = _deserializer.Deserialize<Dictionary<string, CommandStrings>?>(text)
 | 
			
		||||
                      ?? new();
 | 
			
		||||
            localeName = GetLocaleName(file);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Error loading {FileName} command strings: {ErrorMessage}", file, ex.Message);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        strings = null;
 | 
			
		||||
        localeName = null;
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> GetResponseStrings()
 | 
			
		||||
    {
 | 
			
		||||
        var outputDict = new Dictionary<string, IReadOnlyDictionary<string, string>>();
 | 
			
		||||
 | 
			
		||||
        // try to load a shortcut file
 | 
			
		||||
        if (File.Exists(_shortcutResponsesFile))
 | 
			
		||||
        {
 | 
			
		||||
            if (TryLoadResponsesFromFile(_shortcutResponsesFile, out var dict, out _))
 | 
			
		||||
            {
 | 
			
		||||
                outputDict["en-us"] = dict;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return outputDict;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!Directory.Exists(_localizableResponsesPath))
 | 
			
		||||
            return outputDict;
 | 
			
		||||
        
 | 
			
		||||
        // if shortcut file doesn't exist, try to load localizable files
 | 
			
		||||
        foreach (var file in Directory.GetFiles(_localizableResponsesPath))
 | 
			
		||||
        {
 | 
			
		||||
            if (TryLoadResponsesFromFile(file, out var strings, out var localeName) && localeName is not null)
 | 
			
		||||
            {
 | 
			
		||||
                outputDict[localeName.ToLowerInvariant()] = strings;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return outputDict;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static bool TryLoadResponsesFromFile(string file,
 | 
			
		||||
        [NotNullWhen(true)] out IReadOnlyDictionary<string, string>? strings,
 | 
			
		||||
        out string? localeName)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            strings = _deserializer.Deserialize<Dictionary<string, string>?>(File.ReadAllText(file));
 | 
			
		||||
            if (strings is null)
 | 
			
		||||
            {
 | 
			
		||||
                localeName = null;
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            localeName = GetLocaleName(file).ToLowerInvariant();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Error loading {FileName} response strings: {ErrorMessage}", file, ex.Message);
 | 
			
		||||
            strings = null;
 | 
			
		||||
            localeName = null;
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static string GetLocaleName(string fileName)
 | 
			
		||||
        => Path.GetFileNameWithoutExtension(fileName);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								src/Nadeko.Medusa/pack-and-push.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/Nadeko.Medusa/pack-and-push.ps1
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
dotnet pack -o bin/Release/packed
 | 
			
		||||
dotnet nuget push bin/Release/packed/ --api-key $env:nadeko_myget_api_key --source https://www.myget.org/F/nadeko/api/v2/package
 | 
			
		||||
		Reference in New Issue
	
	Block a user