Updated Ellie.Marmalade
This commit is contained in:
parent
fb808dfc1d
commit
a645caf495
29 changed files with 917 additions and 3 deletions
13
EllieBot.sln
13
EllieBot.sln
|
@ -21,11 +21,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ayu.Discord.Voice", "src\ay
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Tests", "src\EllieBot.Tests\EllieBot.Tests.csproj", "{179DF3B3-AD32-4335-8231-9818338DF3A2}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Tests", "src\EllieBot.Tests\EllieBot.Tests.csproj", "{179DF3B3-AD32-4335-8231-9818338DF3A2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.Coordinator", "src\EllieBot.Coordinator\EllieBot.Coordinator.csproj", "{A631DDF0-3AD1-4CB9-8458-314B1320868A}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Coordinator", "src\EllieBot.Coordinator\EllieBot.Coordinator.csproj", "{A631DDF0-3AD1-4CB9-8458-314B1320868A}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.Generators", "src\EllieBot.Generators\EllieBot.Generators.csproj", "{CB1A5307-DD85-4795-8A8A-A25D36DADC51}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Generators", "src\EllieBot.Generators\EllieBot.Generators.csproj", "{CB1A5307-DD85-4795-8A8A-A25D36DADC51}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.VotesApi", "src\EllieBot.VotesApi\EllieBot.VotesApi.csproj", "{F1A77F56-71B0-430E-AE46-94CDD7D43874}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.VotesApi", "src\EllieBot.VotesApi\EllieBot.VotesApi.csproj", "{F1A77F56-71B0-430E-AE46-94CDD7D43874}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ellie.Marmalade", "src\Ellie.Marmalade\Ellie.Marmalade.csproj", "{76AC715D-12FF-4CBE-9585-A861139A2D0C}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
@ -57,6 +59,10 @@ Global
|
||||||
{F1A77F56-71B0-430E-AE46-94CDD7D43874}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F1A77F56-71B0-430E-AE46-94CDD7D43874}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{F1A77F56-71B0-430E-AE46-94CDD7D43874}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F1A77F56-71B0-430E-AE46-94CDD7D43874}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F1A77F56-71B0-430E-AE46-94CDD7D43874}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F1A77F56-71B0-430E-AE46-94CDD7D43874}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{76AC715D-12FF-4CBE-9585-A861139A2D0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{76AC715D-12FF-4CBE-9585-A861139A2D0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{76AC715D-12FF-4CBE-9585-A861139A2D0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{76AC715D-12FF-4CBE-9585-A861139A2D0C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -69,6 +75,7 @@ Global
|
||||||
{A631DDF0-3AD1-4CB9-8458-314B1320868A} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
{A631DDF0-3AD1-4CB9-8458-314B1320868A} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||||
{CB1A5307-DD85-4795-8A8A-A25D36DADC51} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
{CB1A5307-DD85-4795-8A8A-A25D36DADC51} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||||
{F1A77F56-71B0-430E-AE46-94CDD7D43874} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
{F1A77F56-71B0-430E-AE46-94CDD7D43874} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||||
|
{76AC715D-12FF-4CBE-9585-A861139A2D0C} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4}
|
SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4}
|
||||||
|
|
10
src/Ellie.Marmalade/Attributes/FilterAttribute.cs
Normal file
10
src/Ellie.Marmalade/Attributes/FilterAttribute.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
10
src/Ellie.Marmalade/Attributes/MarmaladePermAttribute.cs
Normal file
10
src/Ellie.Marmalade/Attributes/MarmaladePermAttribute.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used as a marker class for bot_perm and user_perm Attributes
|
||||||
|
/// Has no functionality.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class MarmaladePermAttribute : Attribute
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public sealed class bot_owner_onlyAttribute : MarmaladePermAttribute
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
22
src/Ellie.Marmalade/Attributes/bot_permAttribute.cs
Normal file
22
src/Ellie.Marmalade/Attributes/bot_permAttribute.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public sealed class bot_permAttribute : MarmaladePermAttribute
|
||||||
|
{
|
||||||
|
public GuildPermission? GuildPerm { get; }
|
||||||
|
public ChannelPermission? ChannelPerm { get; }
|
||||||
|
|
||||||
|
public bot_permAttribute(GuildPermission perm)
|
||||||
|
{
|
||||||
|
GuildPerm = perm;
|
||||||
|
ChannelPerm = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bot_permAttribute(ChannelPermission perm)
|
||||||
|
{
|
||||||
|
ChannelPerm = perm;
|
||||||
|
GuildPerm = null;
|
||||||
|
}
|
||||||
|
}
|
37
src/Ellie.Marmalade/Attributes/cmdAttribute.cs
Normal file
37
src/Ellie.Marmalade/Attributes/cmdAttribute.cs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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/Ellie.Marmalade/Attributes/injectAttribute.cs
Normal file
10
src/Ellie.Marmalade/Attributes/injectAttribute.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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/Ellie.Marmalade/Attributes/leftoverAttribute.cs
Normal file
10
src/Ellie.Marmalade/Attributes/leftoverAttribute.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the parameter to take
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter)]
|
||||||
|
public class leftoverAttribute : Attribute
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
20
src/Ellie.Marmalade/Attributes/prioAttribute.cs
Normal file
20
src/Ellie.Marmalade/Attributes/prioAttribute.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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/Ellie.Marmalade/Attributes/svcAttribute.cs
Normal file
23
src/Ellie.Marmalade/Attributes/svcAttribute.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
}
|
22
src/Ellie.Marmalade/Attributes/user_permAttribute.cs
Normal file
22
src/Ellie.Marmalade/Attributes/user_permAttribute.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public sealed class user_permAttribute : MarmaladePermAttribute
|
||||||
|
{
|
||||||
|
public GuildPermission? GuildPerm { get; }
|
||||||
|
public ChannelPermission? ChannelPerm { get; }
|
||||||
|
|
||||||
|
public user_permAttribute(GuildPermission perm)
|
||||||
|
{
|
||||||
|
GuildPerm = perm;
|
||||||
|
ChannelPerm = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public user_permAttribute(ChannelPermission perm)
|
||||||
|
{
|
||||||
|
ChannelPerm = perm;
|
||||||
|
GuildPerm = null;
|
||||||
|
}
|
||||||
|
}
|
143
src/Ellie.Marmalade/Canary.cs
Normal file
143
src/Ellie.Marmalade/Canary.cs
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
{
|
||||||
|
}
|
52
src/Ellie.Marmalade/Context/AnyContext.cs
Normal file
52
src/Ellie.Marmalade/Context/AnyContext.cs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
using Discord;
|
||||||
|
using EllieBot;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// Bot user
|
||||||
|
/// </summary>
|
||||||
|
public abstract ISelfUser Bot { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to strings used by this medusa
|
||||||
|
/// </summary>
|
||||||
|
public abstract IMarmaladeStrings 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/Ellie.Marmalade/Context/DmContext.cs
Normal file
11
src/Ellie.Marmalade/Context/DmContext.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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/Ellie.Marmalade/Context/GuildContext.cs
Normal file
12
src/Ellie.Marmalade/Context/GuildContext.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
}
|
24
src/Ellie.Marmalade/Ellie.Marmalade.csproj
Normal file
24
src/Ellie.Marmalade/Ellie.Marmalade.csproj
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>preview</LangVersion>
|
||||||
|
<EnablePreviewFeatures>true</EnablePreviewFeatures>
|
||||||
|
<RootNamespace>Ellie.Canary</RootNamespace>
|
||||||
|
|
||||||
|
<Authors>The EllieBot Devs</Authors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Discord.Net.Core" Version="3.104.0" />
|
||||||
|
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Version)' == '' ">
|
||||||
|
<Version>5.0.0</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
8
src/Ellie.Marmalade/EmbedColor.cs
Normal file
8
src/Ellie.Marmalade/EmbedColor.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace EllieBot;
|
||||||
|
|
||||||
|
public enum EmbedColor
|
||||||
|
{
|
||||||
|
Ok,
|
||||||
|
Pending,
|
||||||
|
Error
|
||||||
|
}
|
14
src/Ellie.Marmalade/Extensions/EmbedBuilderExtensions.cs
Normal file
14
src/Ellie.Marmalade/Extensions/EmbedBuilderExtensions.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
namespace EllieBot;
|
||||||
|
|
||||||
|
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/Ellie.Marmalade/Extensions/MarmaladeExtensions.cs
Normal file
66
src/Ellie.Marmalade/Extensions/MarmaladeExtensions.cs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
using Discord;
|
||||||
|
using Ellie.Canary;
|
||||||
|
|
||||||
|
namespace EllieBot;
|
||||||
|
|
||||||
|
public static class MarmaladeExtensions
|
||||||
|
{
|
||||||
|
public static Task<IUserMessage> EmbedAsync(this IMessageChannel ch, IEmbedBuilder embed, string msg = "")
|
||||||
|
=> ch.SendMessageAsync(msg,
|
||||||
|
embed: embed.Build(),
|
||||||
|
options: new()
|
||||||
|
{
|
||||||
|
RetryMode = RetryMode.Retry502
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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, args));
|
||||||
|
|
||||||
|
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, args)}");
|
||||||
|
|
||||||
|
public static Task<IUserMessage> ReplyPendingLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
||||||
|
=> ctx.SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key, args)}");
|
||||||
|
|
||||||
|
public static Task<IUserMessage> ReplyConfirmLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
|
||||||
|
=> ctx.SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {ctx.GetText(key, args)}");
|
||||||
|
}
|
18
src/Ellie.Marmalade/IEmbedBuilder.cs
Normal file
18
src/Ellie.Marmalade/IEmbedBuilder.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using Discord;
|
||||||
|
|
||||||
|
namespace EllieBot;
|
||||||
|
|
||||||
|
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);
|
||||||
|
IEmbedBuilder WithDiscordColor(Color color);
|
||||||
|
Embed Build();
|
||||||
|
IEmbedBuilder WithUrl(string url);
|
||||||
|
IEmbedBuilder WithImageUrl(string url);
|
||||||
|
IEmbedBuilder WithThumbnailUrl(string url);
|
||||||
|
}
|
16
src/Ellie.Marmalade/ParamParser/ParamParser.cs
Normal file
16
src/Ellie.Marmalade/ParamParser/ParamParser.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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/Ellie.Marmalade/ParamParser/ParseResult.cs
Normal file
48
src/Ellie.Marmalade/ParamParser/ParseResult.cs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
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/Ellie.Marmalade/README.md
Normal file
1
src/Ellie.Marmalade/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This is the library which is the base of any marmalade.
|
24
src/Ellie.Marmalade/Strings/CommandStrings.cs
Normal file
24
src/Ellie.Marmalade/Strings/CommandStrings.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
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/Ellie.Marmalade/Strings/IMarmaladeStrings.cs
Normal file
15
src/Ellie.Marmalade/Strings/IMarmaladeStrings.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines methods to retrieve and reload marmalade strings
|
||||||
|
/// </summary>
|
||||||
|
public interface IMarmaladeStrings
|
||||||
|
{
|
||||||
|
// 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/Ellie.Marmalade/Strings/IMarmaladeStringsProvider.cs
Normal file
28
src/Ellie.Marmalade/Strings/IMarmaladeStringsProvider.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by classes which provide localized strings in their own ways
|
||||||
|
/// </summary>
|
||||||
|
public interface IMarmaladeStringsProvider
|
||||||
|
{
|
||||||
|
/// <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/Ellie.Marmalade/Strings/LocalMarmaladeStringsProvider.cs
Normal file
40
src/Ellie.Marmalade/Strings/LocalMarmaladeStringsProvider.cs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
public class LocalMarmaladeStringsProvider : IMarmaladeStringsProvider
|
||||||
|
{
|
||||||
|
private readonly StringsLoader _source;
|
||||||
|
private IReadOnlyDictionary<string, IReadOnlyDictionary<string, string>> _responseStrings;
|
||||||
|
private IReadOnlyDictionary<string, IReadOnlyDictionary<string, CommandStrings>> _commandStrings;
|
||||||
|
|
||||||
|
public LocalMarmaladeStringsProvider(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/Ellie.Marmalade/Strings/MarmaladeStrings.cs
Normal file
79
src/Ellie.Marmalade/Strings/MarmaladeStrings.cs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
public class MarmaladeStrings : IMarmaladeStrings
|
||||||
|
{
|
||||||
|
/// <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 IMarmaladeStringsProvider _stringsProvider;
|
||||||
|
|
||||||
|
public MarmaladeStrings(IMarmaladeStringsProvider 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 marmalades",
|
||||||
|
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("marmalades.description", locale ?? _usCultureInfo);
|
||||||
|
|
||||||
|
public static MarmaladeStrings CreateDefault(string basePath)
|
||||||
|
=> new MarmaladeStrings(new LocalMarmaladeStringsProvider(new(basePath)));
|
||||||
|
|
||||||
|
public void Reload()
|
||||||
|
=> _stringsProvider.Reload();
|
||||||
|
}
|
137
src/Ellie.Marmalade/Strings/StringsLoader.cs
Normal file
137
src/Ellie.Marmalade/Strings/StringsLoader.cs
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Serilog;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace Ellie.Canary;
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
Loading…
Reference in a new issue