Merge pull request 'Massive system rewrite.' (#1) from dev into main
Reviewed-on: Emotions-stuff/Ellie#1
56
EllieBot.sln
|
@ -11,13 +11,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
Dockerfile = Dockerfile
|
||||
LICENSE = LICENSE
|
||||
README.md = README.md
|
||||
TODO.md = TODO.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot", "src\EllieBot\EllieBot.csproj", "{BCB21472-84D2-4B63-B5DD-31E6A3EC9791}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ayu", "ayu", "{872A4C63-833C-4AE0-91AB-3CE348D3E6F8}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Tests", "src\EllieBot.Tests\EllieBot.Tests.csproj", "{179DF3B3-AD32-4335-8231-9818338DF3A2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ayu.Discord.Voice", "src\ayu\Ayu.Discord.Voice\Ayu.Discord.Voice.csproj", "{5AD2EFFB-7774-49B2-A791-3BAC4DAEE067}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Coordinator", "src\EllieBot.Coordinator\EllieBot.Coordinator.csproj", "{A631DDF0-3AD1-4CB9-8458-314B1320868A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Generators", "src\EllieBot.Generators\EllieBot.Generators.csproj", "{CB1A5307-DD85-4795-8A8A-A25D36DADC51}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.VotesApi", "src\EllieBot.VotesApi\EllieBot.VotesApi.csproj", "{F1A77F56-71B0-430E-AE46-94CDD7D43874}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Marmalade", "src\Ellie.Marmalade\Ellie.Marmalade.csproj", "{76AC715D-12FF-4CBE-9585-A861139A2D0C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Common", "src\Ellie.Common\Ellie.Common.csproj", "{5C1B88B0-B881-4E20-8382-4DDE275F8642}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Econ", "src\Ellie.Econ\Ellie.Econ.csproj", "{A73A6399-50E1-4362-BE29-86C2C88CF05A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
@ -29,18 +40,47 @@ Global
|
|||
{BCB21472-84D2-4B63-B5DD-31E6A3EC9791}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BCB21472-84D2-4B63-B5DD-31E6A3EC9791}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BCB21472-84D2-4B63-B5DD-31E6A3EC9791}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5AD2EFFB-7774-49B2-A791-3BAC4DAEE067}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5AD2EFFB-7774-49B2-A791-3BAC4DAEE067}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5AD2EFFB-7774-49B2-A791-3BAC4DAEE067}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5AD2EFFB-7774-49B2-A791-3BAC4DAEE067}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{179DF3B3-AD32-4335-8231-9818338DF3A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{179DF3B3-AD32-4335-8231-9818338DF3A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{179DF3B3-AD32-4335-8231-9818338DF3A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{179DF3B3-AD32-4335-8231-9818338DF3A2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A631DDF0-3AD1-4CB9-8458-314B1320868A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A631DDF0-3AD1-4CB9-8458-314B1320868A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A631DDF0-3AD1-4CB9-8458-314B1320868A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A631DDF0-3AD1-4CB9-8458-314B1320868A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CB1A5307-DD85-4795-8A8A-A25D36DADC51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CB1A5307-DD85-4795-8A8A-A25D36DADC51}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB1A5307-DD85-4795-8A8A-A25D36DADC51}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CB1A5307-DD85-4795-8A8A-A25D36DADC51}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F1A77F56-71B0-430E-AE46-94CDD7D43874}.Debug|Any CPU.ActiveCfg = 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.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
|
||||
{5C1B88B0-B881-4E20-8382-4DDE275F8642}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5C1B88B0-B881-4E20-8382-4DDE275F8642}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5C1B88B0-B881-4E20-8382-4DDE275F8642}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5C1B88B0-B881-4E20-8382-4DDE275F8642}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A73A6399-50E1-4362-BE29-86C2C88CF05A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A73A6399-50E1-4362-BE29-86C2C88CF05A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A73A6399-50E1-4362-BE29-86C2C88CF05A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A73A6399-50E1-4362-BE29-86C2C88CF05A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{BCB21472-84D2-4B63-B5DD-31E6A3EC9791} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||
{872A4C63-833C-4AE0-91AB-3CE348D3E6F8} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||
{5AD2EFFB-7774-49B2-A791-3BAC4DAEE067} = {872A4C63-833C-4AE0-91AB-3CE348D3E6F8}
|
||||
{179DF3B3-AD32-4335-8231-9818338DF3A2} = {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}
|
||||
{F1A77F56-71B0-430E-AE46-94CDD7D43874} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||
{76AC715D-12FF-4CBE-9585-A861139A2D0C} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||
{5C1B88B0-B881-4E20-8382-4DDE275F8642} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||
{A73A6399-50E1-4362-BE29-86C2C88CF05A} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4}
|
||||
|
|
8
TODO.md
|
@ -1,3 +1,9 @@
|
|||
# List of things to do
|
||||
|
||||
- Finish the full system rewrite
|
||||
- Finish the Ellie.Marmalade project
|
||||
- Finish the EllieBot.Tests project
|
||||
- Finish the EllieBot project
|
||||
- Finish the EllieBot.Coordinator project
|
||||
- Finish the EllieBot.Generators project
|
||||
- Finish the EllieBot.Voice project
|
||||
- Finish the EllieBot.VotesApi project
|
10
src/Ellie.Marmalade/Attributes/FilterAttribute.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -0,0 +1,10 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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.Marmalade;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class bot_owner_onlyAttribute : MarmaladePermAttribute
|
||||
{
|
||||
|
||||
}
|
22
src/Ellie.Marmalade/Attributes/bot_permAttribute.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using Discord;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
[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
|
@ -0,0 +1,37 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -0,0 +1,10 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -0,0 +1,10 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <summary>
|
||||
/// Marks the parameter to take
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
public class leftoverAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
20
src/Ellie.Marmalade/Attributes/prioAttribute.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -0,0 +1,23 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -0,0 +1,22 @@
|
|||
using Discord;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
[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
|
@ -0,0 +1,143 @@
|
|||
using Discord;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <summary>
|
||||
/// The base class which will be loaded as a module into EllieBot
|
||||
/// Any user-defined canary has to inherit from this class.
|
||||
/// Canaries get instantiated ONLY ONCE during the loading,
|
||||
/// and any canary commands will be executed on the same instance.
|
||||
/// </summary>
|
||||
public abstract class Canary : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the canary. 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 canary 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 canary 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 canary as this method
|
||||
/// will be called when *any* command from any module or canary 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 canary 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
|
@ -0,0 +1,52 @@
|
|||
using Discord;
|
||||
using EllieBot;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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 marmalade
|
||||
/// </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
|
@ -0,0 +1,11 @@
|
|||
using Discord;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -0,0 +1,12 @@
|
|||
using Discord;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -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.Marmalade</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
|
@ -0,0 +1,8 @@
|
|||
namespace EllieBot;
|
||||
|
||||
public enum EmbedColor
|
||||
{
|
||||
Ok,
|
||||
Pending,
|
||||
Error
|
||||
}
|
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
|
@ -0,0 +1,66 @@
|
|||
using Discord;
|
||||
using Ellie.Marmalade;
|
||||
|
||||
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
|
@ -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
|
@ -0,0 +1,16 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -0,0 +1,48 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
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
|
@ -0,0 +1 @@
|
|||
This is the library which is the base of any marmalade.
|
24
src/Ellie.Marmalade/Strings/CommandStrings.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
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
|
@ -0,0 +1,15 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -0,0 +1,28 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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
|
@ -0,0 +1,40 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
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
|
@ -0,0 +1,79 @@
|
|||
using System.Globalization;
|
||||
using Serilog;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
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
|
@ -0,0 +1,137 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Serilog;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <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);
|
||||
}
|
47
src/EllieBot.Coordinator/CoordStartup.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace EllieBot.Coordinator
|
||||
{
|
||||
public class CoordStartup
|
||||
{
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public CoordStartup(IConfiguration config)
|
||||
=> Configuration = config;
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddGrpc();
|
||||
services.AddSingleton<CoordinatorRunner>();
|
||||
services.AddSingleton<IHostedService, CoordinatorRunner>(
|
||||
serviceProvider => serviceProvider.GetRequiredService<CoordinatorRunner>());
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapGrpcService<CoordinatorService>();
|
||||
|
||||
endpoints.MapGet("/",
|
||||
async context =>
|
||||
{
|
||||
await context.Response.WriteAsync(
|
||||
"Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
19
src/EllieBot.Coordinator/EllieBot.Coordinator.csproj
Normal file
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos\coordinator.proto" GrpcServices="Server" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.47.0" />
|
||||
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
43
src/EllieBot.Coordinator/LogSetup.cs
Normal file
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
|
||||
namespace EllieBot.Services
|
||||
{
|
||||
public static class LogSetup
|
||||
{
|
||||
public static void SetupLogger(object source)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||
.MinimumLevel.Override("System", LogEventLevel.Information)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.File("coord.log", LogEventLevel.Information,
|
||||
rollOnFileSizeLimit: true,
|
||||
fileSizeLimitBytes: 10_000_000)
|
||||
.WriteTo.Console(LogEventLevel.Information,
|
||||
theme: GetTheme(),
|
||||
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}")
|
||||
.Enrich.WithProperty("LogSource", source)
|
||||
.CreateLogger();
|
||||
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
}
|
||||
|
||||
private static ConsoleTheme GetTheme()
|
||||
{
|
||||
if (Environment.OSVersion.Platform == PlatformID.Unix)
|
||||
return AnsiConsoleTheme.Code;
|
||||
|
||||
|
||||
#if DEBUG
|
||||
return AnsiConsoleTheme.Code;
|
||||
#else
|
||||
return ConsoleTheme.None;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
20
src/EllieBot.Coordinator/Program.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using EllieBot.Coordinator;
|
||||
using EllieBot.Services;
|
||||
using Serilog;
|
||||
|
||||
// Additional configuration is required to successfully run gRPC on macOS.
|
||||
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
|
||||
static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<CoordStartup>();
|
||||
});
|
||||
|
||||
LogSetup.SetupLogger("coord");
|
||||
Log.Information("Starting coordinator... Pid: {ProcessId}", Environment.ProcessId);
|
||||
|
||||
CreateHostBuilder(args).Build().Run();
|
13
src/EllieBot.Coordinator/Properties/launchSettings.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"profiles": {
|
||||
"EllieBot.Coordinator": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:3442;https://localhost:3443",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
127
src/EllieBot.Coordinator/Protos/coordinator.proto
Normal file
|
@ -0,0 +1,127 @@
|
|||
syntax = "proto3";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
option csharp_namespace = "EllieBot.Coordinator";
|
||||
|
||||
package elliebot;
|
||||
|
||||
service Coordinator {
|
||||
// sends update to coordinator to let it know that the shard is alive
|
||||
rpc Heartbeat(HeartbeatRequest) returns (HeartbeatReply);
|
||||
// restarts a shard given the id
|
||||
rpc RestartShard(RestartShardRequest) returns (RestartShardReply);
|
||||
// reshards given the new number of shards
|
||||
rpc Reshard(ReshardRequest) returns (ReshardReply);
|
||||
// Reload config
|
||||
rpc Reload(ReloadRequest) returns (ReloadReply);
|
||||
// Gets status of a single shard
|
||||
rpc GetStatus(GetStatusRequest) returns (GetStatusReply);
|
||||
// Get status of all shards
|
||||
rpc GetAllStatuses(GetAllStatusesRequest) returns (GetAllStatusesReply);
|
||||
// Restarts all shards. Queues them to be restarted at a normal rate. Setting Nuke to true will kill all shards right
|
||||
// away
|
||||
rpc RestartAllShards(RestartAllRequest) returns (RestartAllReply);
|
||||
|
||||
// kill coordinator (and all shards as a consequence)
|
||||
rpc Die(DieRequest) returns (DieReply);
|
||||
|
||||
rpc SetConfigText(SetConfigTextRequest) returns (SetConfigTextReply);
|
||||
|
||||
rpc GetConfigText(GetConfigTextRequest) returns (GetConfigTextReply);
|
||||
}
|
||||
|
||||
enum ConnState {
|
||||
Disconnected = 0;
|
||||
Connecting = 1;
|
||||
Connected = 2;
|
||||
}
|
||||
|
||||
message HeartbeatRequest {
|
||||
int32 shardId = 1;
|
||||
int32 guildCount = 2;
|
||||
ConnState state = 3;
|
||||
}
|
||||
|
||||
message HeartbeatReply {
|
||||
bool gracefulImminent = 1;
|
||||
}
|
||||
|
||||
message RestartShardRequest {
|
||||
int32 shardId = 1;
|
||||
// should it be queued for restart, set false to kill it and restart immediately with priority
|
||||
bool queue = 2;
|
||||
}
|
||||
|
||||
message RestartShardReply {
|
||||
|
||||
}
|
||||
|
||||
message ReshardRequest {
|
||||
int32 shards = 1;
|
||||
}
|
||||
|
||||
message ReshardReply {
|
||||
|
||||
}
|
||||
|
||||
message ReloadRequest {
|
||||
|
||||
}
|
||||
|
||||
message ReloadReply {
|
||||
|
||||
}
|
||||
|
||||
message GetStatusRequest {
|
||||
int32 shardId = 1;
|
||||
}
|
||||
|
||||
message GetStatusReply {
|
||||
int32 shardId = 1;
|
||||
ConnState state = 2;
|
||||
int32 guildCount = 3;
|
||||
google.protobuf.Timestamp lastUpdate = 4;
|
||||
bool scheduledForRestart = 5;
|
||||
google.protobuf.Timestamp startedAt = 6;
|
||||
}
|
||||
|
||||
message GetAllStatusesRequest {
|
||||
|
||||
}
|
||||
|
||||
message GetAllStatusesReply {
|
||||
repeated GetStatusReply Statuses = 1;
|
||||
}
|
||||
|
||||
message RestartAllRequest {
|
||||
bool nuke = 1;
|
||||
}
|
||||
|
||||
message RestartAllReply {
|
||||
|
||||
}
|
||||
|
||||
message DieRequest {
|
||||
bool graceful = 1;
|
||||
}
|
||||
|
||||
message DieReply {
|
||||
|
||||
}
|
||||
|
||||
message GetConfigTextRequest {
|
||||
|
||||
}
|
||||
|
||||
message GetConfigTextReply {
|
||||
string configYml = 1;
|
||||
}
|
||||
|
||||
message SetConfigTextRequest {
|
||||
string configYml = 1;
|
||||
}
|
||||
|
||||
message SetConfigTextReply {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
}
|
11
src/EllieBot.Coordinator/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Coordinator project
|
||||
|
||||
Grpc-based coordinator useful for sharded EllieBot. Its purpose is controlling the lifetime and checking status of the shards it creates.
|
||||
|
||||
### Supports
|
||||
|
||||
- Checking status
|
||||
- Individual shard restarts
|
||||
- Full shard restarts
|
||||
- Graceful coordinator restarts (restart/update coordinator without killing shards)
|
||||
- Kill/Stop
|
457
src/EllieBot.Coordinator/Services/CoordinatorRunner.cs
Normal file
|
@ -0,0 +1,457 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Serilog;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace EllieBot.Coordinator
|
||||
{
|
||||
public sealed class CoordinatorRunner : BackgroundService
|
||||
{
|
||||
private const string CONFIG_PATH = "coord.yml";
|
||||
|
||||
private const string GRACEFUL_STATE_PATH = "graceful.json";
|
||||
private const string GRACEFUL_STATE_BACKUP_PATH = "graceful_old.json";
|
||||
|
||||
private readonly Serializer _serializer;
|
||||
private readonly Deserializer _deserializer;
|
||||
|
||||
private Config _config;
|
||||
private ShardStatus[] _shardStatuses;
|
||||
|
||||
private readonly object locker = new object();
|
||||
private readonly Random _rng;
|
||||
private bool _gracefulImminent;
|
||||
|
||||
public CoordinatorRunner()
|
||||
{
|
||||
_serializer = new();
|
||||
_deserializer = new();
|
||||
_config = LoadConfig();
|
||||
_rng = new Random();
|
||||
|
||||
if (!TryRestoreOldState())
|
||||
InitAll();
|
||||
}
|
||||
|
||||
private Config LoadConfig()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
return _deserializer.Deserialize<Config>(File.ReadAllText(CONFIG_PATH));
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveConfig(in Config config)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
var output = _serializer.Serialize(config);
|
||||
File.WriteAllText(CONFIG_PATH, output);
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadConfig()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
var oldConfig = _config;
|
||||
var newConfig = LoadConfig();
|
||||
if (oldConfig.TotalShards != newConfig.TotalShards)
|
||||
{
|
||||
KillAll();
|
||||
}
|
||||
_config = newConfig;
|
||||
if (oldConfig.TotalShards != newConfig.TotalShards)
|
||||
{
|
||||
InitAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
// Log.Information("Executing");
|
||||
|
||||
bool first = true;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool hadAction = false;
|
||||
lock (locker)
|
||||
{
|
||||
var shardIds = Enumerable.Range(0, 1) // shard 0 is always first
|
||||
.Append((int)((1173494918812024863 >> 22) % _config.TotalShards)) // then ellie server shard
|
||||
.Concat(Enumerable.Range(1, _config.TotalShards - 1)
|
||||
.OrderBy(_ => _rng.Next())) // then all other shards in a random order
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (first)
|
||||
{
|
||||
// Log.Information("Startup order: {StartupOrder}",string.Join(' ', shardIds));
|
||||
first = false;
|
||||
}
|
||||
|
||||
foreach (var shardId in shardIds)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
var status = _shardStatuses[shardId];
|
||||
|
||||
if (status.ShouldRestart)
|
||||
{
|
||||
Log.Warning("Shard {ShardId} is restarting (scheduled)...", shardId);
|
||||
hadAction = true;
|
||||
StartShard(shardId);
|
||||
break;
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow - status.LastUpdate >
|
||||
TimeSpan.FromSeconds(_config.UnresponsiveSec))
|
||||
{
|
||||
Log.Warning("Shard {ShardId} is restarting (unresponsive)...", shardId);
|
||||
hadAction = true;
|
||||
StartShard(shardId);
|
||||
break;
|
||||
}
|
||||
|
||||
if (status.StateCounter > 8 && status.State != ConnState.Connected)
|
||||
{
|
||||
Log.Warning("Shard {ShardId} is restarting (stuck)...", shardId);
|
||||
hadAction = true;
|
||||
StartShard(shardId);
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (status.Process is null or { HasExited: true })
|
||||
{
|
||||
Log.Warning("Shard {ShardId} is starting (process)...", shardId);
|
||||
hadAction = true;
|
||||
StartShard(shardId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
Log.Warning("Process for shard {ShardId} is bugged... ", shardId);
|
||||
hadAction = true;
|
||||
StartShard(shardId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hadAction)
|
||||
{
|
||||
await Task.Delay(_config.RecheckIntervalMs, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error in coordinator: {Message}", ex.Message);
|
||||
}
|
||||
|
||||
await Task.Delay(5000, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartShard(int shardId)
|
||||
{
|
||||
var status = _shardStatuses[shardId];
|
||||
try
|
||||
{
|
||||
status.Process?.Kill(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
try
|
||||
{
|
||||
status.Process?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var proc = StartShardProcess(shardId);
|
||||
_shardStatuses[shardId] = status with
|
||||
{
|
||||
Process = proc,
|
||||
LastUpdate = DateTime.UtcNow,
|
||||
State = ConnState.Disconnected,
|
||||
ShouldRestart = false,
|
||||
StateCounter = 0,
|
||||
};
|
||||
}
|
||||
|
||||
private Process StartShardProcess(int shardId)
|
||||
=> Process.Start(new ProcessStartInfo()
|
||||
{
|
||||
FileName = _config.ShardStartCommand,
|
||||
Arguments = string.Format(_config.ShardStartArgs,
|
||||
shardId,
|
||||
_config.TotalShards),
|
||||
EnvironmentVariables =
|
||||
{
|
||||
{"ELLIEBOT_IS_COORDINATED", "1"}
|
||||
}
|
||||
// CreateNoWindow = true,
|
||||
// UseShellExecute = false,
|
||||
});
|
||||
|
||||
public bool Heartbeat(int shardId, int guildCount, ConnState state)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (shardId >= _shardStatuses.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(shardId));
|
||||
|
||||
var status = _shardStatuses[shardId];
|
||||
status = _shardStatuses[shardId] = status with
|
||||
{
|
||||
GuildCount = guildCount,
|
||||
State = state,
|
||||
LastUpdate = DateTime.UtcNow,
|
||||
StateCounter = status.State == state
|
||||
? status.StateCounter + 1
|
||||
: 1
|
||||
};
|
||||
if (status.StateCounter > 1 && status.State == ConnState.Disconnected)
|
||||
{
|
||||
Log.Warning("Shard {ShardId} is in DISCONNECTED state! ({StateCounter})",
|
||||
status.ShardId,
|
||||
status.StateCounter);
|
||||
}
|
||||
|
||||
return _gracefulImminent;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetShardCount(int totalShards)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
SaveConfig(new Config(
|
||||
totalShards,
|
||||
_config.RecheckIntervalMs,
|
||||
_config.ShardStartCommand,
|
||||
_config.ShardStartArgs,
|
||||
_config.UnresponsiveSec));
|
||||
}
|
||||
}
|
||||
|
||||
public void RestartShard(int shardId, bool queue)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (shardId >= _shardStatuses.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(shardId));
|
||||
|
||||
_shardStatuses[shardId] = _shardStatuses[shardId] with
|
||||
{
|
||||
ShouldRestart = true,
|
||||
StateCounter = 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void RestartAll(bool nuke)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (nuke)
|
||||
{
|
||||
KillAll();
|
||||
}
|
||||
|
||||
QueueAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void KillAll()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
for (var shardId = 0; shardId < _shardStatuses.Length; shardId++)
|
||||
{
|
||||
var status = _shardStatuses[shardId];
|
||||
if (status.Process is Process p)
|
||||
{
|
||||
try { p.Kill(); } catch { }
|
||||
try { p.Dispose(); } catch { }
|
||||
_shardStatuses[shardId] = status with
|
||||
{
|
||||
Process = null,
|
||||
ShouldRestart = true,
|
||||
LastUpdate = DateTime.UtcNow,
|
||||
State = ConnState.Disconnected,
|
||||
StateCounter = 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveState()
|
||||
{
|
||||
var coordState = new CoordState()
|
||||
{
|
||||
StatusObjects = _shardStatuses
|
||||
.Select(x => new JsonStatusObject()
|
||||
{
|
||||
Pid = x.Process?.Id,
|
||||
ConnectionState = x.State,
|
||||
GuildCount = x.GuildCount,
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
var jsonState = JsonSerializer.Serialize(coordState, new JsonSerializerOptions()
|
||||
{
|
||||
WriteIndented = true,
|
||||
});
|
||||
File.WriteAllText(GRACEFUL_STATE_PATH, jsonState);
|
||||
}
|
||||
private bool TryRestoreOldState()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (!File.Exists(GRACEFUL_STATE_PATH))
|
||||
return false;
|
||||
|
||||
Log.Information("Restoring old coordinator state...");
|
||||
|
||||
CoordState savedState;
|
||||
try
|
||||
{
|
||||
savedState = JsonSerializer.Deserialize<CoordState>(File.ReadAllText(GRACEFUL_STATE_PATH));
|
||||
|
||||
if (savedState is null)
|
||||
throw new Exception("Old state is null?!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error deserializing old state: {Message}", ex.Message);
|
||||
File.Move(GRACEFUL_STATE_PATH, GRACEFUL_STATE_BACKUP_PATH, overwrite: true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (savedState.StatusObjects.Count != _config.TotalShards)
|
||||
{
|
||||
Log.Error("Unable to restore old state because shard count doesn't match");
|
||||
File.Move(GRACEFUL_STATE_PATH, GRACEFUL_STATE_BACKUP_PATH, overwrite: true);
|
||||
return false;
|
||||
}
|
||||
|
||||
_shardStatuses = new ShardStatus[_config.TotalShards];
|
||||
|
||||
for (int shardId = 0; shardId < _shardStatuses.Length; shardId++)
|
||||
{
|
||||
var statusObj = savedState.StatusObjects[shardId];
|
||||
Process p = null;
|
||||
if (statusObj.Pid is { } pid)
|
||||
{
|
||||
try
|
||||
{
|
||||
p = Process.GetProcessById(pid);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Process for shard {ShardId} is not runnning", shardId);
|
||||
}
|
||||
}
|
||||
|
||||
_shardStatuses[shardId] = new(
|
||||
shardId,
|
||||
DateTime.UtcNow,
|
||||
statusObj.GuildCount,
|
||||
statusObj.ConnectionState,
|
||||
p is null,
|
||||
p);
|
||||
}
|
||||
|
||||
File.Move(GRACEFUL_STATE_PATH, GRACEFUL_STATE_BACKUP_PATH, overwrite: true);
|
||||
Log.Information("Old state restored!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitAll()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
_shardStatuses = new ShardStatus[_config.TotalShards];
|
||||
for (var shardId = 0; shardId < _shardStatuses.Length; shardId++)
|
||||
{
|
||||
_shardStatuses[shardId] = new ShardStatus(shardId, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueAll()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
for (var shardId = 0; shardId < _shardStatuses.Length; shardId++)
|
||||
{
|
||||
_shardStatuses[shardId] = _shardStatuses[shardId] with
|
||||
{
|
||||
ShouldRestart = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ShardStatus GetShardStatus(int shardId)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
if (shardId >= _shardStatuses.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(shardId));
|
||||
|
||||
return _shardStatuses[shardId];
|
||||
}
|
||||
}
|
||||
|
||||
public List<ShardStatus> GetAllStatuses()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
var toReturn = new List<ShardStatus>(_shardStatuses.Length);
|
||||
toReturn.AddRange(_shardStatuses);
|
||||
return toReturn;
|
||||
}
|
||||
}
|
||||
|
||||
public void PrepareGracefulShutdown()
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
_gracefulImminent = true;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetConfigText()
|
||||
=> File.ReadAllText(CONFIG_PATH);
|
||||
|
||||
public void SetConfigText(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
throw new ArgumentNullException(nameof(text), "coord.yml can't be empty");
|
||||
var config = _deserializer.Deserialize<Config>(text);
|
||||
SaveConfig(in config);
|
||||
ReloadConfig();
|
||||
}
|
||||
}
|
||||
}
|
144
src/EllieBot.Coordinator/Services/CoordinatorService.cs
Normal file
|
@ -0,0 +1,144 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
|
||||
namespace EllieBot.Coordinator
|
||||
{
|
||||
public sealed class CoordinatorService : Coordinator.CoordinatorBase
|
||||
{
|
||||
private readonly CoordinatorRunner _runner;
|
||||
|
||||
public CoordinatorService(CoordinatorRunner runner)
|
||||
=> _runner = runner;
|
||||
|
||||
public override Task<HeartbeatReply> Heartbeat(HeartbeatRequest request, ServerCallContext context)
|
||||
{
|
||||
var gracefulImminent = _runner.Heartbeat(request.ShardId, request.GuildCount, request.State);
|
||||
return Task.FromResult(new HeartbeatReply()
|
||||
{
|
||||
GracefulImminent = gracefulImminent
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<ReshardReply> Reshard(ReshardRequest request, ServerCallContext context)
|
||||
{
|
||||
_runner.SetShardCount(request.Shards);
|
||||
return Task.FromResult(new ReshardReply());
|
||||
}
|
||||
|
||||
public override Task<RestartShardReply> RestartShard(RestartShardRequest request, ServerCallContext context)
|
||||
{
|
||||
_runner.RestartShard(request.ShardId, request.Queue);
|
||||
return Task.FromResult(new RestartShardReply());
|
||||
}
|
||||
|
||||
public override Task<ReloadReply> Reload(ReloadRequest request, ServerCallContext context)
|
||||
{
|
||||
_runner.ReloadConfig();
|
||||
return Task.FromResult(new ReloadReply());
|
||||
}
|
||||
|
||||
public override Task<GetStatusReply> GetStatus(GetStatusRequest request, ServerCallContext context)
|
||||
{
|
||||
var status = _runner.GetShardStatus(request.ShardId);
|
||||
|
||||
|
||||
return Task.FromResult(StatusToStatusReply(status));
|
||||
}
|
||||
|
||||
public override Task<GetAllStatusesReply> GetAllStatuses(GetAllStatusesRequest request,
|
||||
ServerCallContext context)
|
||||
{
|
||||
var statuses = _runner
|
||||
.GetAllStatuses();
|
||||
|
||||
var reply = new GetAllStatusesReply();
|
||||
foreach (var status in statuses)
|
||||
reply.Statuses.Add(StatusToStatusReply(status));
|
||||
|
||||
return Task.FromResult(reply);
|
||||
}
|
||||
|
||||
private static GetStatusReply StatusToStatusReply(ShardStatus status)
|
||||
{
|
||||
DateTime startTime;
|
||||
try
|
||||
{
|
||||
startTime = status.Process is null or { HasExited: true }
|
||||
? DateTime.MinValue.ToUniversalTime()
|
||||
: status.Process.StartTime.ToUniversalTime();
|
||||
}
|
||||
catch
|
||||
{
|
||||
startTime = DateTime.MinValue.ToUniversalTime();
|
||||
}
|
||||
|
||||
var reply = new GetStatusReply()
|
||||
{
|
||||
State = status.State,
|
||||
GuildCount = status.GuildCount,
|
||||
ShardId = status.ShardId,
|
||||
LastUpdate = Timestamp.FromDateTime(status.LastUpdate),
|
||||
ScheduledForRestart = status.ShouldRestart,
|
||||
StartedAt = Timestamp.FromDateTime(startTime)
|
||||
};
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
public override Task<RestartAllReply> RestartAllShards(RestartAllRequest request, ServerCallContext context)
|
||||
{
|
||||
_runner.RestartAll(request.Nuke);
|
||||
return Task.FromResult(new RestartAllReply());
|
||||
}
|
||||
|
||||
public override async Task<DieReply> Die(DieRequest request, ServerCallContext context)
|
||||
{
|
||||
if (request.Graceful)
|
||||
{
|
||||
_runner.PrepareGracefulShutdown();
|
||||
await Task.Delay(10_000);
|
||||
}
|
||||
|
||||
_runner.SaveState();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(250);
|
||||
Environment.Exit(0);
|
||||
});
|
||||
|
||||
return new DieReply();
|
||||
}
|
||||
|
||||
public override Task<SetConfigTextReply> SetConfigText(SetConfigTextRequest request, ServerCallContext context)
|
||||
{
|
||||
var error = string.Empty;
|
||||
var success = true;
|
||||
try
|
||||
{
|
||||
_runner.SetConfigText(request.ConfigYml);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
success = false;
|
||||
}
|
||||
|
||||
return Task.FromResult<SetConfigTextReply>(new(new()
|
||||
{
|
||||
Success = success,
|
||||
Error = error
|
||||
}));
|
||||
}
|
||||
|
||||
public override Task<GetConfigTextReply> GetConfigText(GetConfigTextRequest request, ServerCallContext context)
|
||||
{
|
||||
var text = _runner.GetConfigText();
|
||||
return Task.FromResult(new GetConfigTextReply()
|
||||
{
|
||||
ConfigYml = text,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
21
src/EllieBot.Coordinator/Shared/Config.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace EllieBot.Coordinator
|
||||
{
|
||||
public readonly struct Config
|
||||
{
|
||||
public int TotalShards { get; init; }
|
||||
public int RecheckIntervalMs { get; init; }
|
||||
public string ShardStartCommand { get; init; }
|
||||
public string ShardStartArgs { get; init; }
|
||||
public double UnresponsiveSec { get; init; }
|
||||
|
||||
public Config(int totalShards, int recheckIntervalMs, string shardStartCommand, string shardStartArgs, double unresponsiveSec)
|
||||
{
|
||||
TotalShards = totalShards;
|
||||
RecheckIntervalMs = recheckIntervalMs;
|
||||
ShardStartCommand = shardStartCommand;
|
||||
ShardStartArgs = shardStartArgs;
|
||||
UnresponsiveSec = unresponsiveSec;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
9
src/EllieBot.Coordinator/Shared/CoordState.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace EllieBot.Coordinator
|
||||
{
|
||||
public class CoordState
|
||||
{
|
||||
public List<JsonStatusObject> StatusObjects { get; init; }
|
||||
}
|
||||
}
|
9
src/EllieBot.Coordinator/Shared/JsonStatusObject.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace EllieBot.Coordinator
|
||||
{
|
||||
public class JsonStatusObject
|
||||
{
|
||||
public int? Pid { get; init; }
|
||||
public int GuildCount { get; init; }
|
||||
public ConnState ConnectionState { get; init; }
|
||||
}
|
||||
}
|
15
src/EllieBot.Coordinator/Shared/ShardStatus.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace EllieBot.Coordinator
|
||||
{
|
||||
public sealed record ShardStatus(
|
||||
int ShardId,
|
||||
DateTime LastUpdate,
|
||||
int GuildCount = 0,
|
||||
ConnState State = ConnState.Disconnected,
|
||||
bool ShouldRestart = false,
|
||||
Process Process = null,
|
||||
int StateCounter = 0
|
||||
);
|
||||
}
|
9
src/EllieBot.Coordinator/appsettings.Development.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
20
src/EllieBot.Coordinator/appsettings.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Kestrel": {
|
||||
"EndpointDefaults": {
|
||||
"Protocols": "Http2"
|
||||
},
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Url": "http://localhost:3442"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
src/EllieBot.Coordinator/coord.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
# total number of shards
|
||||
TotalShards: 3
|
||||
# How often do shards ping their state back to the coordinator
|
||||
RecheckIntervalMs: 5000
|
||||
# Command to run the shard
|
||||
ShardStartCommand: dotnet
|
||||
# Arguments to run the shard
|
||||
# {0} = shard id
|
||||
# {1} = total number of shards
|
||||
ShardStartArgs: run -p "..\EllieBot\EllieBot.csproj" --no-build -- {0} {1}
|
||||
# How long does it take for the shard to be forcefully restarted once it stops reporting its state
|
||||
UnresponsiveSec: 30
|
254
src/EllieBot.Generators/Cloneable/CloneableGenerator.cs
Normal file
|
@ -0,0 +1,254 @@
|
|||
// Code temporarily yeeted from
|
||||
// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs
|
||||
// because of NRT issue
|
||||
#nullable enable
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Cloneable
|
||||
{
|
||||
[Generator]
|
||||
public class CloneableGenerator : ISourceGenerator
|
||||
{
|
||||
private const string PREVENT_DEEP_COPY_KEY_STRING = "PreventDeepCopy";
|
||||
private const string EXPLICIT_DECLARATION_KEY_STRING = "ExplicitDeclaration";
|
||||
|
||||
private const string CLONEABLE_NAMESPACE = "Cloneable";
|
||||
private const string CLONEABLE_ATTRIBUTE_STRING = "CloneableAttribute";
|
||||
private const string CLONE_ATTRIBUTE_STRING = "CloneAttribute";
|
||||
private const string IGNORE_CLONE_ATTRIBUTE_STRING = "IgnoreCloneAttribute";
|
||||
|
||||
private const string CLONEABLE_ATTRIBUTE_TEXT = @"// <AutoGenerated/>
|
||||
using System;
|
||||
|
||||
namespace " + CLONEABLE_NAMESPACE + @"
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = true, AllowMultiple = false)]
|
||||
public sealed class " + CLONEABLE_ATTRIBUTE_STRING + @" : Attribute
|
||||
{
|
||||
public " + CLONEABLE_ATTRIBUTE_STRING + @"()
|
||||
{
|
||||
}
|
||||
|
||||
public bool " + EXPLICIT_DECLARATION_KEY_STRING + @" { get; set; }
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
private const string CLONE_PROPERTY_ATTRIBUTE_TEXT = @"// <AutoGenerated/>
|
||||
using System;
|
||||
|
||||
namespace " + CLONEABLE_NAMESPACE + @"
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
|
||||
public sealed class " + CLONE_ATTRIBUTE_STRING + @" : Attribute
|
||||
{
|
||||
public " + CLONE_ATTRIBUTE_STRING + @"()
|
||||
{
|
||||
}
|
||||
|
||||
public bool " + PREVENT_DEEP_COPY_KEY_STRING + @" { get; set; }
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
private const string IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT = @"// <AutoGenerated/>
|
||||
using System;
|
||||
|
||||
namespace " + CLONEABLE_NAMESPACE + @"
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
|
||||
public sealed class " + IGNORE_CLONE_ATTRIBUTE_STRING + @" : Attribute
|
||||
{
|
||||
public " + IGNORE_CLONE_ATTRIBUTE_STRING + @"()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
private INamedTypeSymbol? _cloneableAttribute;
|
||||
private INamedTypeSymbol? _ignoreCloneAttribute;
|
||||
private INamedTypeSymbol? _cloneAttribute;
|
||||
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
=> context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
|
||||
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
InjectCloneableAttributes(context);
|
||||
GenerateCloneMethods(context);
|
||||
}
|
||||
|
||||
private void GenerateCloneMethods(GeneratorExecutionContext context)
|
||||
{
|
||||
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
|
||||
return;
|
||||
|
||||
Compilation compilation = GetCompilation(context);
|
||||
|
||||
InitAttributes(compilation);
|
||||
|
||||
var classSymbols = GetClassSymbols(compilation, receiver);
|
||||
foreach (var classSymbol in classSymbols)
|
||||
{
|
||||
if (!classSymbol.TryGetAttribute(_cloneableAttribute!, out var attributes))
|
||||
continue;
|
||||
|
||||
var attribute = attributes.Single();
|
||||
var isExplicit = (bool?)attribute.NamedArguments.FirstOrDefault(e => e.Key.Equals(EXPLICIT_DECLARATION_KEY_STRING)).Value.Value ?? false;
|
||||
context.AddSource($"{classSymbol.Name}_cloneable.g.cs", SourceText.From(CreateCloneableCode(classSymbol, isExplicit), Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
private void InitAttributes(Compilation compilation)
|
||||
{
|
||||
_cloneableAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{CLONEABLE_ATTRIBUTE_STRING}")!;
|
||||
_cloneAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{CLONE_ATTRIBUTE_STRING}")!;
|
||||
_ignoreCloneAttribute = compilation.GetTypeByMetadataName($"{CLONEABLE_NAMESPACE}.{IGNORE_CLONE_ATTRIBUTE_STRING}")!;
|
||||
}
|
||||
|
||||
private static Compilation GetCompilation(GeneratorExecutionContext context)
|
||||
{
|
||||
var options = context.Compilation.SyntaxTrees.First().Options as CSharpParseOptions;
|
||||
|
||||
var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(CLONEABLE_ATTRIBUTE_TEXT, Encoding.UTF8), options)).
|
||||
AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8), options)).
|
||||
AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8), options));
|
||||
return compilation;
|
||||
}
|
||||
|
||||
private string CreateCloneableCode(INamedTypeSymbol classSymbol, bool isExplicit)
|
||||
{
|
||||
string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
|
||||
var fieldAssignmentsCode = GenerateFieldAssignmentsCode(classSymbol, isExplicit).ToList();
|
||||
var fieldAssignmentsCodeSafe = fieldAssignmentsCode.Select(x =>
|
||||
{
|
||||
if (x.isCloneable)
|
||||
return x.line + "Safe(referenceChain)";
|
||||
return x.line;
|
||||
});
|
||||
var fieldAssignmentsCodeFast = fieldAssignmentsCode.Select(x =>
|
||||
{
|
||||
if (x.isCloneable)
|
||||
return x.line + "()";
|
||||
return x.line;
|
||||
});
|
||||
|
||||
return $@"using System.Collections.Generic;
|
||||
|
||||
namespace {namespaceName}
|
||||
{{
|
||||
{GetAccessModifier(classSymbol)} partial class {classSymbol.Name}
|
||||
{{
|
||||
/// <summary>
|
||||
/// Creates a copy of {classSymbol.Name} with NO circular reference checking. This method should be used if performance matters.
|
||||
///
|
||||
/// <exception cref=""StackOverflowException"">Will occur on any object that has circular references in the hierarchy.</exception>
|
||||
/// </summary>
|
||||
public {classSymbol.Name} Clone()
|
||||
{{
|
||||
return new {classSymbol.Name}
|
||||
{{
|
||||
{string.Join(",\n", fieldAssignmentsCodeFast)}
|
||||
}};
|
||||
}}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of {classSymbol.Name} with circular reference checking. If a circular reference was detected, only a reference of the leaf object is passed instead of cloning it.
|
||||
/// </summary>
|
||||
/// <param name=""referenceChain"">Should only be provided if specific objects should not be cloned but passed by reference instead.</param>
|
||||
public {classSymbol.Name} CloneSafe(Stack<object> referenceChain = null)
|
||||
{{
|
||||
if(referenceChain?.Contains(this) == true)
|
||||
return this;
|
||||
referenceChain ??= new Stack<object>();
|
||||
referenceChain.Push(this);
|
||||
var result = new {classSymbol.Name}
|
||||
{{
|
||||
{string.Join($",\n", fieldAssignmentsCodeSafe)}
|
||||
}};
|
||||
referenceChain.Pop();
|
||||
return result;
|
||||
}}
|
||||
}}
|
||||
}}";
|
||||
}
|
||||
|
||||
private IEnumerable<(string line, bool isCloneable)> GenerateFieldAssignmentsCode(INamedTypeSymbol classSymbol, bool isExplicit)
|
||||
{
|
||||
var fieldNames = GetCloneableProperties(classSymbol, isExplicit);
|
||||
|
||||
var fieldAssignments = fieldNames.Select(field => IsFieldCloneable(field, classSymbol))
|
||||
.OrderBy(x => x.isCloneable)
|
||||
.Select(x => (GenerateAssignmentCode(x.item.Name, x.isCloneable), x.isCloneable));
|
||||
return fieldAssignments;
|
||||
}
|
||||
|
||||
private string GenerateAssignmentCode(string name, bool isCloneable)
|
||||
{
|
||||
if (isCloneable)
|
||||
{
|
||||
return $@" {name} = this.{name}?.Clone";
|
||||
}
|
||||
|
||||
return $@" {name} = this.{name}";
|
||||
}
|
||||
|
||||
private (IPropertySymbol item, bool isCloneable) IsFieldCloneable(IPropertySymbol x, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(x.Type, classSymbol))
|
||||
{
|
||||
return (x, false);
|
||||
}
|
||||
|
||||
if (!x.Type.TryGetAttribute(_cloneableAttribute!, out var attributes))
|
||||
{
|
||||
return (x, false);
|
||||
}
|
||||
|
||||
var preventDeepCopy = (bool?)attributes.Single().NamedArguments.FirstOrDefault(e => e.Key.Equals(PREVENT_DEEP_COPY_KEY_STRING)).Value.Value ?? false;
|
||||
return (item: x, !preventDeepCopy);
|
||||
}
|
||||
|
||||
private string GetAccessModifier(INamedTypeSymbol classSymbol)
|
||||
=> classSymbol.DeclaredAccessibility.ToString().ToLowerInvariant();
|
||||
|
||||
private IEnumerable<IPropertySymbol> GetCloneableProperties(ITypeSymbol classSymbol, bool isExplicit)
|
||||
{
|
||||
var targetSymbolMembers = classSymbol.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(x => x.SetMethod is not null &&
|
||||
x.CanBeReferencedByName);
|
||||
if (isExplicit)
|
||||
{
|
||||
return targetSymbolMembers.Where(x => x.HasAttribute(_cloneAttribute!));
|
||||
}
|
||||
else
|
||||
{
|
||||
return targetSymbolMembers.Where(x => !x.HasAttribute(_ignoreCloneAttribute!));
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<INamedTypeSymbol> GetClassSymbols(Compilation compilation, SyntaxReceiver receiver)
|
||||
=> receiver.CandidateClasses.Select(clazz => GetClassSymbol(compilation, clazz));
|
||||
|
||||
private static INamedTypeSymbol GetClassSymbol(Compilation compilation, ClassDeclarationSyntax clazz)
|
||||
{
|
||||
var model = compilation.GetSemanticModel(clazz.SyntaxTree);
|
||||
var classSymbol = model.GetDeclaredSymbol(clazz)!;
|
||||
return classSymbol;
|
||||
}
|
||||
|
||||
private static void InjectCloneableAttributes(GeneratorExecutionContext context)
|
||||
{
|
||||
context.AddSource(CLONEABLE_ATTRIBUTE_STRING, SourceText.From(CLONEABLE_ATTRIBUTE_TEXT, Encoding.UTF8));
|
||||
context.AddSource(CLONE_ATTRIBUTE_STRING, SourceText.From(CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8));
|
||||
context.AddSource(IGNORE_CLONE_ATTRIBUTE_STRING, SourceText.From(IGNORE_CLONE_PROPERTY_ATTRIBUTE_TEXT, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
}
|
24
src/EllieBot.Generators/Cloneable/SymbolExtensions.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Code temporarily yeeted from
|
||||
// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs
|
||||
// because of NRT issue
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Cloneable
|
||||
{
|
||||
internal static class SymbolExtensions
|
||||
{
|
||||
public static bool TryGetAttribute(this ISymbol symbol, INamedTypeSymbol attributeType,
|
||||
out IEnumerable<AttributeData> attributes)
|
||||
{
|
||||
attributes = symbol.GetAttributes()
|
||||
.Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType));
|
||||
return attributes.Any();
|
||||
}
|
||||
|
||||
public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeType)
|
||||
=> symbol.GetAttributes()
|
||||
.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType));
|
||||
}
|
||||
}
|
27
src/EllieBot.Generators/Cloneable/SyntaxReceiver.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Code temporarily yeeted from
|
||||
// https://github.com/mostmand/Cloneable/blob/master/Cloneable/CloneableGenerator.cs
|
||||
// because of NRT issue
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Cloneable
|
||||
{
|
||||
internal class SyntaxReceiver : ISyntaxReceiver
|
||||
{
|
||||
public IList<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();
|
||||
|
||||
/// <summary>
|
||||
/// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
|
||||
/// </summary>
|
||||
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
|
||||
{
|
||||
// any field with at least one attribute is a candidate for being cloneable
|
||||
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&
|
||||
classDeclarationSyntax.AttributeLists.Count > 0)
|
||||
{
|
||||
CandidateClasses.Add(classDeclarationSyntax);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
336
src/EllieBot.Generators/Command/CommandAttributesGenerator.cs
Normal file
|
@ -0,0 +1,336 @@
|
|||
// #nullable enable
|
||||
// using System;
|
||||
// using System.CodeDom.Compiler;
|
||||
// using System.Collections.Generic;
|
||||
// using System.Collections.Immutable;
|
||||
// using System.Collections.ObjectModel;
|
||||
// using System.Diagnostics;
|
||||
// using System.IO;
|
||||
// using System.Linq;
|
||||
// using System.Text;
|
||||
// using System.Threading;
|
||||
// using Microsoft.CodeAnalysis;
|
||||
// using Microsoft.CodeAnalysis.CSharp;
|
||||
// using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
// using Microsoft.CodeAnalysis.Text;
|
||||
//
|
||||
// namespace EllieBot.Generators.Command;
|
||||
//
|
||||
// [Generator]
|
||||
// public class CommandAttributesGenerator : IIncrementalGenerator
|
||||
// {
|
||||
// public const string ATTRIBUTE = @"// <AutoGenerated />
|
||||
//
|
||||
// namespace EllieBot.Common;
|
||||
//
|
||||
// [System.AttributeUsage(System.AttributeTargets.Method)]
|
||||
// public class CmdAttribute : System.Attribute
|
||||
// {
|
||||
//
|
||||
// }";
|
||||
//
|
||||
// public class MethodModel
|
||||
// {
|
||||
// public string? Namespace { get; }
|
||||
// public IReadOnlyCollection<string> Classes { get; }
|
||||
// public string ReturnType { get; }
|
||||
// public string MethodName { get; }
|
||||
// public IEnumerable<string> Params { get; }
|
||||
//
|
||||
// public MethodModel(string? ns, IReadOnlyCollection<string> classes, string returnType, string methodName, IEnumerable<string> @params)
|
||||
// {
|
||||
// Namespace = ns;
|
||||
// Classes = classes;
|
||||
// ReturnType = returnType;
|
||||
// MethodName = methodName;
|
||||
// Params = @params;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class FileModel
|
||||
// {
|
||||
// public string? Namespace { get; }
|
||||
// public IReadOnlyCollection<string> ClassHierarchy { get; }
|
||||
// public IReadOnlyCollection<MethodModel> Methods { get; }
|
||||
//
|
||||
// public FileModel(string? ns, IReadOnlyCollection<string> classHierarchy, IReadOnlyCollection<MethodModel> methods)
|
||||
// {
|
||||
// Namespace = ns;
|
||||
// ClassHierarchy = classHierarchy;
|
||||
// Methods = methods;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
// {
|
||||
// // #if DEBUG
|
||||
// // if (!Debugger.IsAttached)
|
||||
// // Debugger.Launch();
|
||||
// // // SpinWait.SpinUntil(() => Debugger.IsAttached);
|
||||
// // #endif
|
||||
// context.RegisterPostInitializationOutput(static ctx => ctx.AddSource(
|
||||
// "CmdAttribute.g.cs",
|
||||
// SourceText.From(ATTRIBUTE, Encoding.UTF8)));
|
||||
//
|
||||
// var methods = context.SyntaxProvider
|
||||
// .CreateSyntaxProvider(
|
||||
// static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
|
||||
// static (ctx, cancel) => Transform(ctx, cancel))
|
||||
// .Where(static m => m is not null)
|
||||
// .Where(static m => m?.ChildTokens().Any(static x => x.IsKind(SyntaxKind.PublicKeyword)) ?? false);
|
||||
//
|
||||
// var compilationMethods = context.CompilationProvider.Combine(methods.Collect());
|
||||
//
|
||||
// context.RegisterSourceOutput(compilationMethods,
|
||||
// static (ctx, tuple) => RegisterAction(in ctx, tuple.Left, in tuple.Right));
|
||||
// }
|
||||
//
|
||||
// private static void RegisterAction(in SourceProductionContext ctx,
|
||||
// Compilation comp,
|
||||
// in ImmutableArray<MethodDeclarationSyntax?> methods)
|
||||
// {
|
||||
// if (methods is { IsDefaultOrEmpty: true })
|
||||
// return;
|
||||
//
|
||||
// var models = GetModels(comp, methods, ctx.CancellationToken);
|
||||
//
|
||||
// foreach (var model in models)
|
||||
// {
|
||||
// var name = $"{model.Namespace}.{string.Join(".", model.ClassHierarchy)}.g.cs";
|
||||
// try
|
||||
// {
|
||||
// var source = GetSourceText(model);
|
||||
// ctx.AddSource(name, SourceText.From(source, Encoding.UTF8));
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"Error writing source file {name}\n" + ex);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private static string GetSourceText(FileModel model)
|
||||
// {
|
||||
// using var sw = new StringWriter();
|
||||
// using var tw = new IndentedTextWriter(sw);
|
||||
//
|
||||
// tw.WriteLine("// <AutoGenerated />");
|
||||
// tw.WriteLine("#pragma warning disable CS1066");
|
||||
//
|
||||
// if (model.Namespace is not null)
|
||||
// {
|
||||
// tw.WriteLine($"namespace {model.Namespace};");
|
||||
// tw.WriteLine();
|
||||
// }
|
||||
//
|
||||
// foreach (var className in model.ClassHierarchy)
|
||||
// {
|
||||
// tw.WriteLine($"public partial class {className}");
|
||||
// tw.WriteLine("{");
|
||||
// tw.Indent ++;
|
||||
// }
|
||||
//
|
||||
// foreach (var method in model.Methods)
|
||||
// {
|
||||
// tw.WriteLine("[EllieCommand]");
|
||||
// tw.WriteLine("[EllieDescription]");
|
||||
// tw.WriteLine("[Aliases]");
|
||||
// tw.WriteLine($"public partial {method.ReturnType} {method.MethodName}({string.Join(", ", method.Params)});");
|
||||
// }
|
||||
//
|
||||
// foreach (var _ in model.ClassHierarchy)
|
||||
// {
|
||||
// tw.Indent --;
|
||||
// tw.WriteLine("}");
|
||||
// }
|
||||
//
|
||||
// tw.Flush();
|
||||
// return sw.ToString();
|
||||
// }
|
||||
//
|
||||
// private static IReadOnlyCollection<FileModel> GetModels(Compilation compilation,
|
||||
// in ImmutableArray<MethodDeclarationSyntax?> inputMethods,
|
||||
// CancellationToken cancel)
|
||||
// {
|
||||
// var models = new List<FileModel>();
|
||||
//
|
||||
// var methods = inputMethods
|
||||
// .Where(static x => x is not null)
|
||||
// .Distinct();
|
||||
//
|
||||
// var methodModels = methods
|
||||
// .Select(x => MethodDeclarationToMethodModel(compilation, x!))
|
||||
// .Where(static x => x is not null)
|
||||
// .Cast<MethodModel>();
|
||||
//
|
||||
// var groups = methodModels
|
||||
// .GroupBy(static x => $"{x.Namespace}.{string.Join(".", x.Classes)}");
|
||||
//
|
||||
// foreach (var group in groups)
|
||||
// {
|
||||
// if (cancel.IsCancellationRequested)
|
||||
// return new Collection<FileModel>();
|
||||
//
|
||||
// if (group is null)
|
||||
// continue;
|
||||
//
|
||||
// var elems = group.ToList();
|
||||
// if (elems.Count is 0)
|
||||
// continue;
|
||||
//
|
||||
// var model = new FileModel(
|
||||
// methods: elems,
|
||||
// ns: elems[0].Namespace,
|
||||
// classHierarchy: elems![0].Classes
|
||||
// );
|
||||
//
|
||||
// models.Add(model);
|
||||
// }
|
||||
//
|
||||
//
|
||||
// return models;
|
||||
// }
|
||||
//
|
||||
// private static MethodModel? MethodDeclarationToMethodModel(Compilation comp, MethodDeclarationSyntax decl)
|
||||
// {
|
||||
// // SpinWait.SpinUntil(static () => Debugger.IsAttached);
|
||||
//
|
||||
// SemanticModel semanticModel;
|
||||
// try
|
||||
// {
|
||||
// semanticModel = comp.GetSemanticModel(decl.SyntaxTree);
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
// // for some reason this method can throw "Not part of this compilation" argument exception
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// var methodModel = new MethodModel(
|
||||
// @params: decl.ParameterList.Parameters
|
||||
// .Where(p => p.Type is not null)
|
||||
// .Select(p =>
|
||||
// {
|
||||
// var prefix = p.Modifiers.Any(static x => x.IsKind(SyntaxKind.ParamsKeyword))
|
||||
// ? "params "
|
||||
// : string.Empty;
|
||||
//
|
||||
// var type = semanticModel
|
||||
// .GetTypeInfo(p.Type!)
|
||||
// .Type
|
||||
// ?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
//
|
||||
//
|
||||
// var name = p.Identifier.Text;
|
||||
//
|
||||
// var suffix = string.Empty;
|
||||
// if (p.Default is not null)
|
||||
// {
|
||||
// if (p.Default.Value is LiteralExpressionSyntax)
|
||||
// {
|
||||
// suffix = " = " + p.Default.Value;
|
||||
// }
|
||||
// else if (p.Default.Value is MemberAccessExpressionSyntax maes)
|
||||
// {
|
||||
// var maesSemModel = comp.GetSemanticModel(maes.SyntaxTree);
|
||||
// var sym = maesSemModel.GetSymbolInfo(maes.Name);
|
||||
// if (sym.Symbol is null)
|
||||
// {
|
||||
// suffix = " = " + p.Default.Value;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// suffix = " = " + sym.Symbol.ToDisplayString();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return $"{prefix}{type} {name}{suffix}";
|
||||
// })
|
||||
// .ToList(),
|
||||
// methodName: decl.Identifier.Text,
|
||||
// returnType: decl.ReturnType.ToString(),
|
||||
// ns: GetNamespace(decl),
|
||||
// classes: GetClasses(decl)
|
||||
// );
|
||||
//
|
||||
// return methodModel;
|
||||
// }
|
||||
//
|
||||
// //https://github.com/andrewlock/NetEscapades.EnumGenerators/blob/main/src/NetEscapades.EnumGenerators/EnumGenerator.cs
|
||||
// static string? GetNamespace(MethodDeclarationSyntax declarationSyntax)
|
||||
// {
|
||||
// // determine the namespace the class is declared in, if any
|
||||
// string? nameSpace = null;
|
||||
// var parentOfInterest = declarationSyntax.Parent;
|
||||
// while (parentOfInterest is not null)
|
||||
// {
|
||||
// parentOfInterest = parentOfInterest.Parent;
|
||||
//
|
||||
// if (parentOfInterest is BaseNamespaceDeclarationSyntax ns)
|
||||
// {
|
||||
// nameSpace = ns.Name.ToString();
|
||||
// while (true)
|
||||
// {
|
||||
// if (ns.Parent is not NamespaceDeclarationSyntax parent)
|
||||
// {
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// ns = parent;
|
||||
// nameSpace = $"{ns.Name}.{nameSpace}";
|
||||
// }
|
||||
//
|
||||
// return nameSpace;
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// return nameSpace;
|
||||
// }
|
||||
//
|
||||
// static IReadOnlyCollection<string> GetClasses(MethodDeclarationSyntax declarationSyntax)
|
||||
// {
|
||||
// // determine the namespace the class is declared in, if any
|
||||
// var classes = new LinkedList<string>();
|
||||
// var parentOfInterest = declarationSyntax.Parent;
|
||||
// while (parentOfInterest is not null)
|
||||
// {
|
||||
// if (parentOfInterest is ClassDeclarationSyntax cds)
|
||||
// {
|
||||
// classes.AddFirst(cds.Identifier.ToString());
|
||||
// }
|
||||
//
|
||||
// parentOfInterest = parentOfInterest.Parent;
|
||||
// }
|
||||
//
|
||||
// Debug.WriteLine($"Method {declarationSyntax.Identifier.Text} has {classes.Count} classes");
|
||||
//
|
||||
// return classes;
|
||||
// }
|
||||
//
|
||||
// private static MethodDeclarationSyntax? Transform(GeneratorSyntaxContext ctx, CancellationToken cancel)
|
||||
// {
|
||||
// var methodDecl = ctx.Node as MethodDeclarationSyntax;
|
||||
// if (methodDecl is null)
|
||||
// return default;
|
||||
//
|
||||
// foreach (var attListSyntax in methodDecl.AttributeLists)
|
||||
// {
|
||||
// foreach (var attSyntax in attListSyntax.Attributes)
|
||||
// {
|
||||
// if (cancel.IsCancellationRequested)
|
||||
// return default;
|
||||
//
|
||||
// var symbol = ctx.SemanticModel.GetSymbolInfo(attSyntax).Symbol;
|
||||
// if (symbol is not IMethodSymbol attSymbol)
|
||||
// continue;
|
||||
//
|
||||
// if (attSymbol.ContainingType.ToDisplayString() == "EllieBot.Common.CmdAttribute")
|
||||
// return methodDecl;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return default;
|
||||
// }
|
||||
// }
|
25
src/EllieBot.Generators/EllieBot.Generators.csproj
Normal file
|
@ -0,0 +1,25 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" GeneratePathProperty="true" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="GetDependencyTargetPaths">
|
||||
<ItemGroup>
|
||||
<TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
144
src/EllieBot.Generators/LocalizedStringsGenerator.cs
Normal file
|
@ -0,0 +1,144 @@
|
|||
#nullable enable
|
||||
using System;
|
||||
using System.CodeDom.Compiler;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace EllieBot.Generators
|
||||
{
|
||||
internal readonly struct TranslationPair
|
||||
{
|
||||
public string Name { get; }
|
||||
public string Value { get; }
|
||||
|
||||
public TranslationPair(string name, string value)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Generator]
|
||||
public class LocalizedStringsGenerator : ISourceGenerator
|
||||
{
|
||||
private const string LOC_STR_SOURCE = @"namespace EllieBot
|
||||
{
|
||||
public readonly struct LocStr
|
||||
{
|
||||
public readonly string Key;
|
||||
public readonly object[] Params;
|
||||
|
||||
public LocStr(string key, params object[] data)
|
||||
{
|
||||
Key = key;
|
||||
Params = data;
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
var file = context.AdditionalFiles.First(x => x.Path.EndsWith("responses.en-US.json"));
|
||||
|
||||
var fields = GetFields(file.GetText()?.ToString());
|
||||
|
||||
using (var stringWriter = new StringWriter())
|
||||
using (var sw = new IndentedTextWriter(stringWriter))
|
||||
{
|
||||
sw.WriteLine("namespace EllieBot;");
|
||||
sw.WriteLine();
|
||||
|
||||
sw.WriteLine("public static class strs");
|
||||
sw.WriteLine("{");
|
||||
sw.Indent++;
|
||||
|
||||
var typedParamStrings = new List<string>(10);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
var matches = Regex.Matches(field.Value, @"{(?<num>\d)[}:]");
|
||||
var max = 0;
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
max = Math.Max(max, int.Parse(match.Groups["num"].Value) + 1);
|
||||
}
|
||||
|
||||
typedParamStrings.Clear();
|
||||
var typeParams = new string[max];
|
||||
var passedParamString = string.Empty;
|
||||
for (var i = 0; i < max; i++)
|
||||
{
|
||||
typedParamStrings.Add($"in T{i} p{i}");
|
||||
passedParamString += $", p{i}";
|
||||
typeParams[i] = $"T{i}";
|
||||
}
|
||||
|
||||
var sig = string.Empty;
|
||||
var typeParamStr = string.Empty;
|
||||
if (max > 0)
|
||||
{
|
||||
sig = $"({string.Join(", ", typedParamStrings)})";
|
||||
typeParamStr = $"<{string.Join(", ", typeParams)}>";
|
||||
}
|
||||
|
||||
sw.WriteLine("public static LocStr {0}{1}{2} => new LocStr(\"{3}\"{4});",
|
||||
field.Name,
|
||||
typeParamStr,
|
||||
sig,
|
||||
field.Name,
|
||||
passedParamString);
|
||||
}
|
||||
|
||||
sw.Indent--;
|
||||
sw.WriteLine("}");
|
||||
|
||||
|
||||
sw.Flush();
|
||||
context.AddSource("strs.g.cs", stringWriter.ToString());
|
||||
}
|
||||
|
||||
context.AddSource("LocStr.g.cs", LOC_STR_SOURCE);
|
||||
}
|
||||
|
||||
private List<TranslationPair> GetFields(string? dataText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dataText))
|
||||
return new();
|
||||
|
||||
Dictionary<string, string> data;
|
||||
try
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<Dictionary<string, string>>(dataText!);
|
||||
if (output is null)
|
||||
return new();
|
||||
|
||||
data = output;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Debug.WriteLine("Failed parsing responses file.");
|
||||
return new();
|
||||
}
|
||||
|
||||
var list = new List<TranslationPair>();
|
||||
foreach (var entry in data)
|
||||
{
|
||||
list.Add(new(
|
||||
entry.Key,
|
||||
entry.Value
|
||||
));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
24
src/EllieBot.Generators/README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
## Generators
|
||||
|
||||
Project which contains source generators required for EllieBot project
|
||||
|
||||
---
|
||||
### 1) Localized Strings Generator
|
||||
|
||||
-- Why --
|
||||
Type safe response strings access, and enforces correct usage of response strings.
|
||||
|
||||
-- How it works --
|
||||
Creates a file "strs.cs" containing a class called "strs" in "EllieBot" namespace.
|
||||
|
||||
Loads "data/strings/responses.en-US.json" and creates a property or a function for each key in the responses json file based on whether the value has string format placeholders or not.
|
||||
|
||||
- If a value has no placeholders, it creates a property in the strs class which returns an instance of a LocStr struct containing only the key and no replacement parameters
|
||||
|
||||
- If a value has placeholders, it creates a function with the same number of arguments as the number of placeholders, and passes those arguments to the LocStr instance
|
||||
|
||||
-- How to use --
|
||||
1. Add a new key to responses.en-US.json "greet_me": "Hello, {0}"
|
||||
2. You now have access to a function strs.greet_me(obj p1)
|
||||
3. Using "GetText(strs.greet_me("Me"))" will return "Hello, Me"
|
||||
|
25
src/EllieBot.VotesApi/.dockerignore
Normal file
|
@ -0,0 +1,25 @@
|
|||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
1
src/EllieBot.VotesApi/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
store/
|
41
src/EllieBot.VotesApi/Common/AuthHandler.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EllieBot.VotesApi
|
||||
{
|
||||
public class AuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public const string SchemeName = "AUTHORIZATION_SCHEME";
|
||||
public const string DiscordsClaim = "DISCORDS_CLAIM";
|
||||
public const string TopggClaim = "TOPGG_CLAIM";
|
||||
|
||||
private readonly IConfiguration _conf;
|
||||
|
||||
public AuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
IConfiguration conf)
|
||||
: base(options, logger, encoder, clock)
|
||||
=> _conf = conf;
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var claims = new List<Claim>();
|
||||
|
||||
if (_conf[ConfKeys.DISCORDS_KEY].Trim() == Request.Headers["Authorization"].ToString().Trim())
|
||||
claims.Add(new(DiscordsClaim, "true"));
|
||||
|
||||
if (_conf[ConfKeys.TOPGG_KEY] == Request.Headers["Authorization"].ToString().Trim())
|
||||
claims.Add(new Claim(TopggClaim, "true"));
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(new(new(new ClaimsIdentity(claims)), SchemeName)));
|
||||
}
|
||||
}
|
||||
}
|
8
src/EllieBot.VotesApi/Common/ConfKeys.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace EllieBot.VotesApi
|
||||
{
|
||||
public static class ConfKeys
|
||||
{
|
||||
public const string DISCORDS_KEY = "DiscordsKey";
|
||||
public const string TOPGG_KEY = "TopGGKey";
|
||||
}
|
||||
}
|
26
src/EllieBot.VotesApi/Common/DiscordsVoteWebhookModel.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
namespace EllieBot.VotesApi
|
||||
{
|
||||
public class DiscordsVoteWebhookModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the user who voted
|
||||
/// </summary>
|
||||
public string User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the bot which recieved the vote
|
||||
/// </summary>
|
||||
public string Bot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains totalVotes, votesMonth, votes24, hasVoted - a list of IDs of users who have voted this month, and
|
||||
/// Voted24 - a list of IDs of users who have voted today
|
||||
/// </summary>
|
||||
public string Votes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of event, whether it is a vote event or test event
|
||||
/// </summary>
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
8
src/EllieBot.VotesApi/Common/Policies.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace EllieBot.VotesApi
|
||||
{
|
||||
public static class Policies
|
||||
{
|
||||
public const string DiscordsAuth = "DiscordsAuth";
|
||||
public const string TopggAuth = "TopggAuth";
|
||||
}
|
||||
}
|
30
src/EllieBot.VotesApi/Common/TopggVoteWebhookModel.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
namespace EllieBot.VotesApi
|
||||
{
|
||||
public class TopggVoteWebhookModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Discord ID of the bot that received a vote.
|
||||
/// </summary>
|
||||
public string Bot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Discord ID of the user who voted.
|
||||
/// </summary>
|
||||
public string User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the vote (should always be "upvote" except when using the test button it's "test").
|
||||
/// </summary>
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the weekend multiplier is in effect, meaning users votes count as two.
|
||||
/// </summary>
|
||||
public bool Weekend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Query string params found on the /bot/:ID/vote page. Example: ?a=1&b=2.
|
||||
/// </summary>
|
||||
public string Query { get; set; }
|
||||
}
|
||||
}
|
33
src/EllieBot.VotesApi/Controllers/DiscordsController.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.VotesApi.Services;
|
||||
|
||||
namespace EllieBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class DiscordsController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<DiscordsController> _logger;
|
||||
private readonly IVotesCache _cache;
|
||||
|
||||
public DiscordsController(ILogger<DiscordsController> logger, IVotesCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
[HttpGet("new")]
|
||||
[Authorize(Policy = Policies.DiscordsAuth)]
|
||||
public async Task<IEnumerable<Vote>> New()
|
||||
{
|
||||
var votes = await _cache.GetNewDiscordsVotesAsync();
|
||||
if (votes.Count > 0)
|
||||
_logger.LogInformation("Sending {NewDiscordsVotes} new discords votes", votes.Count);
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
34
src/EllieBot.VotesApi/Controllers/TopGgController.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.VotesApi.Services;
|
||||
|
||||
namespace EllieBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class TopGgController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<TopGgController> _logger;
|
||||
private readonly IVotesCache _cache;
|
||||
|
||||
public TopGgController(ILogger<TopGgController> logger, IVotesCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
[HttpGet("new")]
|
||||
[Authorize(Policy = Policies.TopggAuth)]
|
||||
public async Task<IEnumerable<Vote>> New()
|
||||
{
|
||||
var votes = await _cache.GetNewTopGgVotesAsync();
|
||||
if (votes.Count > 0)
|
||||
_logger.LogInformation("Sending {NewTopggVotes} new topgg votes", votes.Count);
|
||||
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
48
src/EllieBot.VotesApi/Controllers/WebhookController.cs
Normal file
|
@ -0,0 +1,48 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.VotesApi.Services;
|
||||
|
||||
namespace EllieBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class WebhookController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<WebhookController> _logger;
|
||||
private readonly IVotesCache _votesCache;
|
||||
|
||||
public WebhookController(ILogger<WebhookController> logger, IVotesCache votesCache)
|
||||
{
|
||||
_logger = logger;
|
||||
_votesCache = votesCache;
|
||||
}
|
||||
|
||||
[HttpPost("/discordswebhook")]
|
||||
[Authorize(Policy = Policies.DiscordsAuth)]
|
||||
public async Task<IActionResult> DiscordsWebhook([FromBody] DiscordsVoteWebhookModel data)
|
||||
{
|
||||
|
||||
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
data.User,
|
||||
data.Bot,
|
||||
"discords.com");
|
||||
|
||||
await _votesCache.AddNewDiscordsVote(data.User);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("/topggwebhook")]
|
||||
[Authorize(Policy = Policies.TopggAuth)]
|
||||
public async Task<IActionResult> TopggWebhook([FromBody] TopggVoteWebhookModel data)
|
||||
{
|
||||
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
data.User,
|
||||
data.Bot,
|
||||
"top.gg");
|
||||
|
||||
await _votesCache.AddNewTopggVote(data.User);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
20
src/EllieBot.VotesApi/Dockerfile
Normal file
|
@ -0,0 +1,20 @@
|
|||
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["src/EllieBot.VotesApi/EllieBot.VotesApi.csproj", "EllieBot.VotesApi/"]
|
||||
RUN dotnet restore "src/EllieBot.VotesApi/EllieBot.VotesApi.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/EllieBot.VotesApi"
|
||||
RUN dotnet build "EllieBot.VotesApi.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "EllieBot.VotesApi.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "EllieBot.VotesApi.dll"]
|
13
src/EllieBot.VotesApi/EllieBot.VotesApi.csproj
Normal file
|
@ -0,0 +1,13 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
9
src/EllieBot.VotesApi/Program.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using EllieBot.VotesApi;
|
||||
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
|
||||
static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
|
31
src/EllieBot.VotesApi/Properties/launchSettings.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:16451",
|
||||
"sslPort": 44323
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"EllieBot.VotesApi": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": "true",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
src/EllieBot.VotesApi/README.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
## Votes Api
|
||||
|
||||
This api is used if you want your bot to be able to reward users who vote for it on discords.com or top.gg
|
||||
|
||||
#### [GET] `/discords/new`
|
||||
Get the discords votes received after previous call to this endpoint.
|
||||
Input full url of this endpoint in your creds.yml file under Discords url field.
|
||||
For example "https://api.my.cool.bot/discords/new"
|
||||
#### [GET] `/topgg/new`
|
||||
Get the topgg votes received after previous call to this endpoint.
|
||||
Input full url of this endpoint in your creds.yml file under Topgg url field.
|
||||
For example "https://api.my.cool.bot/topgg/new"
|
||||
|
||||
#### [POST] `/discordswebhook`
|
||||
Input this endpoint as the webhook on discords.com bot edit page
|
||||
model: https://docs.botsfordiscord.com/methods/receiving-votes
|
||||
For example "https://api.my.cool.bot/topggwebhook"
|
||||
#### [POST] `/topggwebhook`
|
||||
Input this endpoint as the webhook https://top.gg/bot/:your-bot-id/webhooks (replace :your-bot-id with your bot's id)
|
||||
model: https://docs.top.gg/resources/webhooks/#schema
|
||||
For example "https://api.my.cool.bot/discordswebhook"
|
||||
|
||||
Input your super-secret header value in appsettings.json's DiscordsKey and TopGGKey fields
|
||||
They must match your DiscordsKey and TopGG key respectively, as well as your secrets in the discords.com and top.gg webhook setup pages
|
||||
|
||||
Full Example:
|
||||
|
||||
⚠ Change TopggKey and DiscordsKey to a secure long string
|
||||
⚠ You can use https://www.random.org/strings/?num=1&len=20&digits=on&upperalpha=on&loweralpha=on&unique=on&format=html&rnd=new to generate it
|
||||
|
||||
`creds.yml`
|
||||
```yml
|
||||
votes:
|
||||
TopggServiceUrl: "https://api.my.cool.bot/topgg"
|
||||
TopggKey: "my_topgg_key"
|
||||
DiscordsServiceUrl: "https://api.my.cool.bot/discords"
|
||||
DiscordsKey: "my_discords_key"
|
||||
```
|
||||
|
||||
`appsettings.json`
|
||||
```json
|
||||
...
|
||||
"DiscordsKey": "my_discords_key",
|
||||
"TopGGKey": "my_topgg_key",
|
||||
...
|
||||
```
|
100
src/EllieBot.VotesApi/Services/FileVotesCache.cs
Normal file
|
@ -0,0 +1,100 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace EllieBot.VotesApi.Services
|
||||
{
|
||||
public class FileVotesCache : IVotesCache
|
||||
{
|
||||
// private const string STATS_FILE = "store/stats.json";
|
||||
private const string TOPGG_FILE = "store/topgg.json";
|
||||
private const string DISCORDS_FILE = "store/discords.json";
|
||||
|
||||
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
|
||||
|
||||
public FileVotesCache()
|
||||
{
|
||||
if (!Directory.Exists("store"))
|
||||
Directory.CreateDirectory("store");
|
||||
|
||||
if (!File.Exists(TOPGG_FILE))
|
||||
File.WriteAllText(TOPGG_FILE, "[]");
|
||||
|
||||
if (!File.Exists(DISCORDS_FILE))
|
||||
File.WriteAllText(DISCORDS_FILE, "[]");
|
||||
}
|
||||
|
||||
public ITask AddNewTopggVote(string userId)
|
||||
=> AddNewVote(TOPGG_FILE, userId);
|
||||
|
||||
public ITask AddNewDiscordsVote(string userId)
|
||||
=> AddNewVote(DISCORDS_FILE, userId);
|
||||
|
||||
private async ITask AddNewVote(string file, string userId)
|
||||
{
|
||||
await _locker.WaitAsync();
|
||||
try
|
||||
{
|
||||
var votes = await GetVotesAsync(file);
|
||||
votes.Add(userId);
|
||||
await File.WriteAllTextAsync(file, JsonSerializer.Serialize(votes));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async ITask<IList<Vote>> GetNewTopGgVotesAsync()
|
||||
{
|
||||
var votes = await EvictTopggVotes();
|
||||
return votes;
|
||||
}
|
||||
|
||||
public async ITask<IList<Vote>> GetNewDiscordsVotesAsync()
|
||||
{
|
||||
var votes = await EvictDiscordsVotes();
|
||||
return votes;
|
||||
}
|
||||
|
||||
private ITask<List<Vote>> EvictTopggVotes()
|
||||
=> EvictVotes(TOPGG_FILE);
|
||||
|
||||
private ITask<List<Vote>> EvictDiscordsVotes()
|
||||
=> EvictVotes(DISCORDS_FILE);
|
||||
|
||||
private async ITask<List<Vote>> EvictVotes(string file)
|
||||
{
|
||||
await _locker.WaitAsync();
|
||||
try
|
||||
{
|
||||
|
||||
var ids = await GetVotesAsync(file);
|
||||
await File.WriteAllTextAsync(file, "[]");
|
||||
|
||||
return ids?
|
||||
.Select(x => (Ok: ulong.TryParse(x, out var r), Id: r))
|
||||
.Where(x => x.Ok)
|
||||
.Select(x => new Vote
|
||||
{
|
||||
UserId = x.Id
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async ITask<IList<string>> GetVotesAsync(string file)
|
||||
{
|
||||
await using var fs = File.Open(file, FileMode.Open);
|
||||
var votes = await JsonSerializer.DeserializeAsync<List<string>>(fs);
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
13
src/EllieBot.VotesApi/Services/IVotesCache.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using System.Collections.Generic;
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace EllieBot.VotesApi.Services
|
||||
{
|
||||
public interface IVotesCache
|
||||
{
|
||||
ITask<IList<Vote>> GetNewTopGgVotesAsync();
|
||||
ITask<IList<Vote>> GetNewDiscordsVotesAsync();
|
||||
ITask AddNewTopggVote(string userId);
|
||||
ITask AddNewDiscordsVote(string userId);
|
||||
}
|
||||
}
|
68
src/EllieBot.VotesApi/Startup.cs
Normal file
|
@ -0,0 +1,68 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using EllieBot.VotesApi.Services;
|
||||
|
||||
namespace EllieBot.VotesApi
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public Startup(IConfiguration configuration)
|
||||
=> Configuration = configuration;
|
||||
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers();
|
||||
services.AddSingleton<IVotesCache, FileVotesCache>();
|
||||
services.AddSwaggerGen(static c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "EllieBot.VotesApi", Version = "v1" });
|
||||
});
|
||||
|
||||
services
|
||||
.AddAuthentication(opts =>
|
||||
{
|
||||
opts.DefaultScheme = AuthHandler.SchemeName;
|
||||
opts.AddScheme<AuthHandler>(AuthHandler.SchemeName, AuthHandler.SchemeName);
|
||||
});
|
||||
|
||||
services
|
||||
.AddAuthorization(static opts =>
|
||||
{
|
||||
opts.DefaultPolicy = new AuthorizationPolicyBuilder(AuthHandler.SchemeName)
|
||||
.RequireAssertion(static _ => false)
|
||||
.Build();
|
||||
opts.AddPolicy(Policies.DiscordsAuth, static policy => policy.RequireClaim(AuthHandler.DiscordsClaim));
|
||||
opts.AddPolicy(Policies.TopggAuth, static policy => policy.RequireClaim(AuthHandler.TopggClaim));
|
||||
});
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(static c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "EllieBot.VotesApi v1"));
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(static endpoints => { endpoints.MapControllers(); });
|
||||
}
|
||||
}
|
||||
}
|
7
src/EllieBot.VotesApi/WeatherForecast.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace EllieBot.VotesApi
|
||||
{
|
||||
public class Vote
|
||||
{
|
||||
public ulong UserId { get; set; }
|
||||
}
|
||||
}
|
9
src/EllieBot.VotesApi/appsettings.Development.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
12
src/EllieBot.VotesApi/appsettings.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"DiscordsKey": "my_discords_key",
|
||||
"TopGGKey": "my_topgg_key",
|
||||
"AllowedHosts": "*"
|
||||
}
|
359
src/EllieBot/.editorconfig
Normal file
|
@ -0,0 +1,359 @@
|
|||
root = true
|
||||
# Remove the line below if you want to inherit .editorconfig settings from higher directories
|
||||
|
||||
[obj/**]
|
||||
generated_code = true
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
|
||||
|
||||
#### Core EditorConfig Options ####
|
||||
|
||||
# Indentation and spacing
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
|
||||
# New line preferences
|
||||
end_of_line = crlf
|
||||
insert_final_newline = false
|
||||
|
||||
#### .NET Coding Conventions ####
|
||||
|
||||
# Organize usings
|
||||
dotnet_separate_import_directive_groups = false
|
||||
dotnet_sort_system_directives_first = false
|
||||
|
||||
# this. and Me. preferences
|
||||
dotnet_style_qualification_for_event = false
|
||||
dotnet_style_qualification_for_field = false
|
||||
dotnet_style_qualification_for_method = false
|
||||
dotnet_style_qualification_for_property = false
|
||||
|
||||
# Language keywords vs BCL types preferences
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# Parentheses preferences
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
|
||||
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
|
||||
|
||||
# Modifier preferences
|
||||
dotnet_style_require_accessibility_modifiers = always:error
|
||||
|
||||
# Expression-level preferences
|
||||
dotnet_style_coalesce_expression = true
|
||||
dotnet_style_collection_initializer = true
|
||||
dotnet_style_explicit_tuple_names = true
|
||||
dotnet_style_namespace_match_folder = true
|
||||
dotnet_style_null_propagation = true
|
||||
dotnet_style_object_initializer = true
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
dotnet_style_prefer_auto_properties = true:warning
|
||||
dotnet_style_prefer_compound_assignment = true
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_return = false:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true
|
||||
dotnet_style_prefer_inferred_tuple_names = true
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||
dotnet_style_prefer_simplified_interpolation = true
|
||||
|
||||
# Field preferences
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
|
||||
# Parameter preferences
|
||||
dotnet_code_quality_unused_parameters = all:warning
|
||||
|
||||
#### C# Coding Conventions ####
|
||||
|
||||
# var preferences
|
||||
csharp_style_var_elsewhere = true
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_accessors = true:suggestion
|
||||
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_indexers = true:suggestion
|
||||
csharp_style_expression_bodied_lambdas = true:suggestion
|
||||
csharp_style_expression_bodied_local_functions = true:suggestion
|
||||
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_properties = true:suggestion
|
||||
|
||||
# Pattern matching preferences
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:error
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:error
|
||||
csharp_style_prefer_not_pattern = true:error
|
||||
csharp_style_prefer_pattern_matching = true:suggestion
|
||||
csharp_style_prefer_switch_expression = true
|
||||
|
||||
# Null-checking preferences
|
||||
csharp_style_conditional_delegate_call = true:error
|
||||
|
||||
# Modifier preferences
|
||||
csharp_prefer_static_local_function = true
|
||||
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
|
||||
|
||||
# Code-block preferences
|
||||
csharp_prefer_braces = when_multiline:warning
|
||||
csharp_prefer_simple_using_statement = true
|
||||
|
||||
# Expression-level preferences
|
||||
csharp_prefer_simple_default_expression = true
|
||||
csharp_style_deconstructed_variable_declaration = true
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:error
|
||||
csharp_style_inlined_variable_declaration = true:warning
|
||||
csharp_style_pattern_local_over_anonymous_function = true
|
||||
csharp_style_prefer_index_operator = true
|
||||
csharp_style_prefer_range_operator = true
|
||||
csharp_style_throw_expression = true:error
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:warning
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable
|
||||
|
||||
# 'using' directive preferences
|
||||
csharp_using_directive_placement = outside_namespace:error
|
||||
|
||||
# Enforce file-scoped namespaces
|
||||
csharp_style_namespace_declarations = file_scoped:error
|
||||
|
||||
# New line preferences
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
|
||||
csharp_style_allow_embedded_statements_on_same_line_experimental = false
|
||||
|
||||
#### C# Formatting Rules ####
|
||||
|
||||
# New line preferences
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_indent_switch_labels = true
|
||||
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = false
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# Wrapping preferences
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = false
|
||||
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.private_readonly_field.symbols = private_readonly_field
|
||||
dotnet_naming_rule.private_readonly_field.style = begins_with_underscore
|
||||
dotnet_naming_rule.private_readonly_field.severity = warning
|
||||
|
||||
dotnet_naming_rule.private_field.symbols = private_field
|
||||
dotnet_naming_rule.private_field.style = camel_case
|
||||
dotnet_naming_rule.private_field.severity = warning
|
||||
|
||||
dotnet_naming_rule.const_fields.symbols = const_fields
|
||||
dotnet_naming_rule.const_fields.style = all_upper
|
||||
dotnet_naming_rule.const_fields.severity = warning
|
||||
|
||||
# dotnet_naming_rule.class_should_be_pascal_case.severity = error
|
||||
# dotnet_naming_rule.class_should_be_pascal_case.symbols = class
|
||||
# dotnet_naming_rule.class_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.symbols = struct
|
||||
dotnet_naming_rule.struct_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = error
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
# dotnet_naming_rule.types_should_be_pascal_case.severity = error
|
||||
# dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
# dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# dotnet_naming_rule.enum_should_be_pascal_case.severity = error
|
||||
# dotnet_naming_rule.enum_should_be_pascal_case.symbols = enum
|
||||
# dotnet_naming_rule.enum_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# dotnet_naming_rule.property_should_be_pascal_case.severity = error
|
||||
# dotnet_naming_rule.property_should_be_pascal_case.symbols = property
|
||||
# dotnet_naming_rule.property_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.method_should_be_pascal_case.severity = error
|
||||
dotnet_naming_rule.method_should_be_pascal_case.symbols = method
|
||||
dotnet_naming_rule.method_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.severity = error
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.symbols = async_method
|
||||
dotnet_naming_rule.async_method_should_be_ends_with_async.style = ends_with_async
|
||||
|
||||
# dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = error
|
||||
# dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
# dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.severity = error
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.symbols = local_variable
|
||||
dotnet_naming_rule.local_variable_should_be_camel_case.style = camel_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.const_fields.required_modifiers = const
|
||||
dotnet_naming_symbols.const_fields.applicable_kinds = field
|
||||
|
||||
dotnet_naming_symbols.class.applicable_kinds = class
|
||||
dotnet_naming_symbols.class.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.class.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.struct.applicable_kinds = struct
|
||||
dotnet_naming_symbols.struct.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.struct.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.enum.applicable_kinds = enum
|
||||
dotnet_naming_symbols.enum.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.enum.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.method.applicable_kinds = method
|
||||
dotnet_naming_symbols.method.applicable_accessibilities = public
|
||||
dotnet_naming_symbols.method.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.property.applicable_kinds = property
|
||||
dotnet_naming_symbols.property.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.property.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.private_readonly_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_readonly_field.applicable_accessibilities = private, protected
|
||||
dotnet_naming_symbols.private_readonly_field.required_modifiers = readonly
|
||||
|
||||
dotnet_naming_symbols.private_field.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_field.applicable_accessibilities = private, protected
|
||||
dotnet_naming_symbols.private_field.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.async_method.applicable_kinds = method, local_function
|
||||
dotnet_naming_symbols.async_method.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.async_method.required_modifiers = async
|
||||
|
||||
dotnet_naming_symbols.local_variable.applicable_kinds = parameter, local
|
||||
dotnet_naming_symbols.local_variable.applicable_accessibilities = local
|
||||
dotnet_naming_symbols.local_variable.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
|
||||
dotnet_naming_style.all_upper.capitalization = all_upper
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.begins_with_underscore.required_prefix = _
|
||||
dotnet_naming_style.begins_with_underscore.required_suffix =
|
||||
dotnet_naming_style.begins_with_underscore.word_separator =
|
||||
dotnet_naming_style.begins_with_underscore.capitalization = camel_case
|
||||
|
||||
dotnet_naming_style.ends_with_async.required_prefix =
|
||||
# dotnet_naming_style.ends_with_async.required_suffix = Async
|
||||
dotnet_naming_style.ends_with_async.word_separator =
|
||||
dotnet_naming_style.ends_with_async.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.camel_case.required_prefix =
|
||||
dotnet_naming_style.camel_case.required_suffix =
|
||||
dotnet_naming_style.camel_case.word_separator =
|
||||
dotnet_naming_style.camel_case.capitalization = camel_case
|
||||
|
||||
# CA1822: Mark members as static
|
||||
dotnet_diagnostic.ca1822.severity = suggestion
|
||||
|
||||
# IDE0004: Cast is redundant
|
||||
dotnet_diagnostic.ide0004.severity = warning
|
||||
|
||||
# IDE0058: Expression value is never used
|
||||
dotnet_diagnostic.ide0058.severity = none
|
||||
|
||||
# # IDE0011: Add braces to 'if'/'else' statement
|
||||
# dotnet_diagnostic.ide0011.severity = none
|
||||
|
||||
resharper_wrap_after_invocation_lpar = false
|
||||
resharper_wrap_before_invocation_rpar = false
|
||||
|
||||
# ReSharper properties
|
||||
resharper_align_multiline_calls_chain = true
|
||||
resharper_csharp_wrap_after_declaration_lpar = true
|
||||
resharper_csharp_wrap_after_invocation_lpar = false
|
||||
resharper_csharp_wrap_before_binary_opsign = true
|
||||
resharper_csharp_wrap_before_invocation_rpar = false
|
||||
resharper_csharp_wrap_parameters_style = chop_if_long
|
||||
resharper_force_chop_compound_if_expression = false
|
||||
resharper_keep_existing_linebreaks = true
|
||||
resharper_keep_user_linebreaks = true
|
||||
resharper_max_formal_parameters_on_line = 3
|
||||
resharper_place_simple_embedded_statement_on_same_line = false
|
||||
resharper_wrap_chained_binary_expressions = chop_if_long
|
||||
resharper_wrap_chained_binary_patterns = chop_if_long
|
||||
resharper_wrap_chained_method_calls = chop_if_long
|
||||
resharper_wrap_object_and_collection_initializer_style = chop_always
|
||||
|
||||
resharper_csharp_wrap_before_first_type_parameter_constraint = true
|
||||
resharper_csharp_place_type_constraints_on_same_line = false
|
||||
resharper_csharp_wrap_before_extends_colon = true
|
||||
resharper_csharp_place_constructor_initializer_on_same_line = false
|
||||
resharper_force_attribute_style = separate
|
||||
resharper_csharp_braces_for_ifelse = required_for_multiline_statement
|
||||
resharper_csharp_braces_for_foreach = required_for_multiline
|
||||
resharper_csharp_braces_for_while = required_for_multiline
|
||||
resharper_csharp_braces_for_for = required_for_multiline
|
||||
resharper_arrange_redundant_parentheses_highlighting = hint
|
||||
|
||||
# IDE0011: Add braces
|
||||
dotnet_diagnostic.IDE0011.severity = warning
|
404
src/EllieBot/Bot.cs
Normal file
|
@ -0,0 +1,404 @@
|
|||
#nullable disable
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieBot.Common.Configs;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db;
|
||||
using EllieBot.Modules.Utility;
|
||||
using EllieBot.Services.Database.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using RunMode = Discord.Commands.RunMode;
|
||||
|
||||
namespace EllieBot;
|
||||
|
||||
public sealed class Bot
|
||||
{
|
||||
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
|
||||
|
||||
public DiscordSocketClient Client { get; }
|
||||
public ImmutableArray<GuildConfig> AllGuildConfigs { get; private set; }
|
||||
|
||||
private IServiceProvider Services { get; set; }
|
||||
|
||||
public string Mention { get; private set; }
|
||||
public bool IsReady { get; private set; }
|
||||
public int ShardId { get; set; }
|
||||
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly CommandService _commandService;
|
||||
private readonly DbService _db;
|
||||
|
||||
private readonly IBotCredsProvider _credsProvider;
|
||||
// private readonly InteractionService _interactionService;
|
||||
|
||||
public Bot(int shardId, int? totalShards, string credPath = null)
|
||||
{
|
||||
if (shardId < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(shardId));
|
||||
|
||||
ShardId = shardId;
|
||||
_credsProvider = new BotCredsProvider(totalShards, credPath);
|
||||
_creds = _credsProvider.GetCreds();
|
||||
|
||||
_db = new(_credsProvider);
|
||||
|
||||
var messageCacheSize =
|
||||
#if GLOBAL_ELLIE
|
||||
0;
|
||||
#else
|
||||
50;
|
||||
#endif
|
||||
|
||||
if(!_creds.UsePrivilegedIntents)
|
||||
Log.Warning("You are not using privileged intents. Some features will not work properly");
|
||||
|
||||
Client = new(new()
|
||||
{
|
||||
MessageCacheSize = messageCacheSize,
|
||||
LogLevel = LogSeverity.Warning,
|
||||
ConnectionTimeout = int.MaxValue,
|
||||
TotalShards = _creds.TotalShards,
|
||||
ShardId = shardId,
|
||||
AlwaysDownloadUsers = false,
|
||||
AlwaysResolveStickers = false,
|
||||
AlwaysDownloadDefaultStickers = false,
|
||||
GatewayIntents = _creds.UsePrivilegedIntents
|
||||
? GatewayIntents.All
|
||||
: GatewayIntents.AllUnprivileged,
|
||||
LogGatewayIntentWarnings = false,
|
||||
FormatUsersInBidirectionalUnicode = false,
|
||||
DefaultRetryMode = RetryMode.Retry502
|
||||
});
|
||||
|
||||
_commandService = new(new()
|
||||
{
|
||||
CaseSensitiveCommands = false,
|
||||
DefaultRunMode = RunMode.Sync,
|
||||
});
|
||||
|
||||
// _interactionService = new(Client.Rest);
|
||||
|
||||
Client.Log += Client_Log;
|
||||
}
|
||||
|
||||
|
||||
public List<ulong> GetCurrentGuildIds()
|
||||
=> Client.Guilds.Select(x => x.Id).ToList();
|
||||
|
||||
private void AddServices()
|
||||
{
|
||||
var startingGuildIdList = GetCurrentGuildIds();
|
||||
var sw = Stopwatch.StartNew();
|
||||
var bot = Client.CurrentUser;
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
uow.EnsureUserCreated(bot.Id, bot.Username, bot.Discriminator, bot.AvatarId);
|
||||
AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray();
|
||||
}
|
||||
|
||||
var svcs = new ServiceCollection().AddTransient(_ => _credsProvider.GetCreds()) // bot creds
|
||||
.AddSingleton(_credsProvider)
|
||||
.AddSingleton(_db) // database
|
||||
.AddSingleton(Client) // discord socket client
|
||||
.AddSingleton(_commandService)
|
||||
// .AddSingleton(_interactionService)
|
||||
.AddSingleton(this)
|
||||
.AddSingleton<ISeria, JsonSeria>()
|
||||
.AddSingleton<IConfigSeria, YamlSeria>()
|
||||
.AddConfigServices()
|
||||
.AddConfigMigrators()
|
||||
.AddMemoryCache()
|
||||
// music
|
||||
.AddMusic()
|
||||
// cache
|
||||
.AddCache(_creds);
|
||||
|
||||
|
||||
svcs.AddHttpClient();
|
||||
svcs.AddHttpClient("memelist")
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = false
|
||||
});
|
||||
|
||||
svcs.AddHttpClient("google:search")
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
|
||||
});
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ELLIE_IS_COORDINATED") != "1")
|
||||
svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
|
||||
else
|
||||
{
|
||||
svcs.AddSingleton<RemoteGrpcCoordinator>()
|
||||
.AddSingleton<ICoordinator>(x => x.GetRequiredService<RemoteGrpcCoordinator>())
|
||||
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RemoteGrpcCoordinator>());
|
||||
}
|
||||
|
||||
svcs.Scan(scan => scan.FromAssemblyOf<IReadyExecutor>()
|
||||
.AddClasses(classes => classes.AssignableToAny(
|
||||
// services
|
||||
typeof(IEService),
|
||||
|
||||
// behaviours
|
||||
typeof(IExecOnMessage),
|
||||
typeof(IInputTransformer),
|
||||
typeof(IExecPreCommand),
|
||||
typeof(IExecPostCommand),
|
||||
typeof(IExecNoCommand))
|
||||
.WithoutAttribute<DontAddToIocContainerAttribute>()
|
||||
#if GLOBAL_ELLIE
|
||||
.WithoutAttribute<NoPublicBotAttribute>()
|
||||
#endif
|
||||
)
|
||||
.AsSelfWithInterfaces()
|
||||
.WithSingletonLifetime());
|
||||
|
||||
//initialize Services
|
||||
Services = svcs.BuildServiceProvider();
|
||||
Services.GetRequiredService<IBehaviorHandler>().Initialize();
|
||||
Services.GetRequiredService<CurrencyRewardService>();
|
||||
|
||||
if (Client.ShardId == 0)
|
||||
ApplyConfigMigrations();
|
||||
|
||||
_ = LoadTypeReaders(typeof(Bot).Assembly);
|
||||
|
||||
sw.Stop();
|
||||
Log.Information( "All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
|
||||
}
|
||||
|
||||
private void ApplyConfigMigrations()
|
||||
{
|
||||
// execute all migrators
|
||||
var migrators = Services.GetServices<IConfigMigrator>();
|
||||
foreach (var migrator in migrators)
|
||||
migrator.EnsureMigrated();
|
||||
}
|
||||
|
||||
private IEnumerable<object> LoadTypeReaders(Assembly assembly)
|
||||
{
|
||||
Type[] allTypes;
|
||||
try
|
||||
{
|
||||
allTypes = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
Log.Warning(ex.LoaderExceptions[0], "Error getting types");
|
||||
return Enumerable.Empty<object>();
|
||||
}
|
||||
|
||||
var filteredTypes = allTypes.Where(x => x.IsSubclassOf(typeof(TypeReader))
|
||||
&& x.BaseType?.GetGenericArguments().Length > 0
|
||||
&& !x.IsAbstract);
|
||||
|
||||
var toReturn = new List<object>();
|
||||
foreach (var ft in filteredTypes)
|
||||
{
|
||||
var x = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft);
|
||||
var baseType = ft.BaseType;
|
||||
if (baseType is null)
|
||||
continue;
|
||||
var typeArgs = baseType.GetGenericArguments();
|
||||
_commandService.AddTypeReader(typeArgs[0], x);
|
||||
toReturn.Add(x);
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
private async Task LoginAsync(string token)
|
||||
{
|
||||
var clientReady = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
async Task SetClientReady()
|
||||
{
|
||||
clientReady.TrySetResult(true);
|
||||
try
|
||||
{
|
||||
foreach (var chan in await Client.GetDMChannelsAsync())
|
||||
await chan.CloseAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
//connect
|
||||
Log.Information("Shard {ShardId} logging in ...", Client.ShardId);
|
||||
try
|
||||
{
|
||||
Client.Ready += SetClientReady;
|
||||
|
||||
await Client.LoginAsync(TokenType.Bot, token);
|
||||
await Client.StartAsync();
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
LoginErrorHandler.Handle(ex);
|
||||
Helpers.ReadErrorAndExit(3);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoginErrorHandler.Handle(ex);
|
||||
Helpers.ReadErrorAndExit(4);
|
||||
}
|
||||
|
||||
await clientReady.Task.ConfigureAwait(false);
|
||||
Client.Ready -= SetClientReady;
|
||||
|
||||
Client.JoinedGuild += Client_JoinedGuild;
|
||||
Client.LeftGuild += Client_LeftGuild;
|
||||
|
||||
// _ = Client.SetStatusAsync(UserStatus.Online);
|
||||
Log.Information("Shard {ShardId} logged in", Client.ShardId);
|
||||
}
|
||||
|
||||
private Task Client_LeftGuild(SocketGuild arg)
|
||||
{
|
||||
Log.Information("Left server: {GuildName} [{GuildId}]", arg?.Name, arg?.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task Client_JoinedGuild(SocketGuild arg)
|
||||
{
|
||||
Log.Information("Joined server: {GuildName} [{GuildId}]", arg.Name, arg.Id);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
GuildConfig gc;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
gc = uow.GuildConfigsForId(arg.Id, null);
|
||||
}
|
||||
|
||||
await JoinedGuild.Invoke(gc);
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
if (ShardId == 0)
|
||||
await _db.SetupAsync();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
await LoginAsync(_creds.Token);
|
||||
|
||||
Mention = Client.CurrentUser.Mention;
|
||||
Log.Information("Shard {ShardId} loading services...", Client.ShardId);
|
||||
try
|
||||
{
|
||||
AddServices();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error adding services");
|
||||
Helpers.ReadErrorAndExit(9);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, sw.Elapsed.TotalSeconds);
|
||||
var commandHandler = Services.GetRequiredService<CommandHandler>();
|
||||
|
||||
// start handling messages received in commandhandler
|
||||
await commandHandler.StartHandling();
|
||||
|
||||
await _commandService.AddModulesAsync(typeof(Bot).Assembly, Services);
|
||||
// await _interactionService.AddModulesAsync(typeof(Bot).Assembly, Services);
|
||||
IsReady = true;
|
||||
|
||||
await EnsureBotOwnershipAsync();
|
||||
_ = Task.Run(ExecuteReadySubscriptions);
|
||||
Log.Information("Shard {ShardId} ready", Client.ShardId);
|
||||
}
|
||||
|
||||
private async ValueTask EnsureBotOwnershipAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_creds.OwnerIds.Count != 0)
|
||||
return;
|
||||
|
||||
Log.Information("Initializing Owner Id...");
|
||||
var info = await Client.GetApplicationInfoAsync();
|
||||
_credsProvider.ModifyCredsFile(x => x.OwnerIds = new[] { info.Owner.Id });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning("Getting application info failed: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private Task ExecuteReadySubscriptions()
|
||||
{
|
||||
var readyExecutors = Services.GetServices<IReadyExecutor>();
|
||||
var tasks = readyExecutors.Select(async toExec =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await toExec.OnReadyAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"Failed running OnReadyAsync method on {Type} type: {Message}",
|
||||
toExec.GetType().Name,
|
||||
ex.Message);
|
||||
}
|
||||
});
|
||||
|
||||
return tasks.WhenAll();
|
||||
}
|
||||
|
||||
private Task Client_Log(LogMessage arg)
|
||||
{
|
||||
if (arg.Message?.Contains("unknown dispatch", StringComparison.InvariantCultureIgnoreCase) ?? false)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (arg.Exception is { InnerException: WebSocketClosedException { CloseCode: 4014 } })
|
||||
{
|
||||
Log.Error(@"
|
||||
Login failed.
|
||||
|
||||
*** Please enable privileged intents ***
|
||||
|
||||
Certain Ellie features require Discord's privileged gateway intents.
|
||||
These include greeting and goodbye messages, as well as creating the Owner message channels for DM forwarding.
|
||||
|
||||
How to enable privileged intents:
|
||||
1. Head over to the Discord Developer Portal https://discord.com/developers/applications/
|
||||
2. Select your Application.
|
||||
3. Click on `Bot` in the left side navigation panel, and scroll down to the intents section.
|
||||
4. Enable all intents.
|
||||
5. Restart your bot.
|
||||
|
||||
Read this only if your bot is in 100 or more servers:
|
||||
|
||||
You'll need to apply to use the intents with Discord, but for small selfhosts, all that is required is enabling the intents in the developer portal.
|
||||
Yes, this is a new thing from Discord, as of October 2020. No, there's nothing we can do about it. Yes, we're aware it worked before.
|
||||
While waiting for your bot to be accepted, you can change the 'usePrivilegedIntents' inside your creds.yml to 'false', although this will break many of the ellie's features");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#if GLOBAL_ELLIE || DEBUG
|
||||
if (arg.Exception is not null)
|
||||
Log.Warning(arg.Exception, "{ErrorSource} | {ErrorMessage}", arg.Source, arg.Message);
|
||||
else
|
||||
Log.Warning("{ErrorSource} | {ErrorMessage}", arg.Source, arg.Message);
|
||||
#endif
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task RunAndBlockAsync()
|
||||
{
|
||||
await RunAsync();
|
||||
await Task.Delay(-1);
|
||||
}
|
||||
}
|
7
src/EllieBot/Directory.Build.props
Normal file
|
@ -0,0 +1,7 @@
|
|||
<Project>
|
||||
<ItemDefinitionGroup>
|
||||
<ProjectReference>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</ProjectReference>
|
||||
</ItemDefinitionGroup>
|
||||
</Project>
|
|
@ -1,10 +1,142 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<EnablePreviewFeatures>true</EnablePreviewFeatures>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
<OutputType>exe</OutputType>
|
||||
<ApplicationIcon>ellie_icon.ico</ApplicationIcon>
|
||||
|
||||
<!-- Analysis/Warnings -->
|
||||
<!-- <AnalysisMode>Recommended</AnalysisMode>-->
|
||||
<!-- <AnalysisModeGlobalization>None</AnalysisModeGlobalization>-->
|
||||
<NoWarn>CS1066</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="0.17.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<Publish>True</Publish>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AWSSDK.S3" Version="3.7.9.25" />
|
||||
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.4" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="CsvHelper" Version="28.0.1" />
|
||||
<PackageReference Include="Discord.Net" Version="3.203.0" />
|
||||
<PackageReference Include="CoreCLR-NCalc" Version="2.2.110" />
|
||||
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
|
||||
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.62.1.3205" />
|
||||
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.21.2" />
|
||||
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.47.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.47.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Html2Markdown" Version="5.0.2.561" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.2.0" />
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
|
||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NonBlocking" Version="2.1.0" />
|
||||
<PackageReference Include="OneOf" Version="3.0.223" />
|
||||
<PackageReference Include="Scrutor" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="5.1.1" />
|
||||
<PackageReference Include="SharpToken" Version="1.2.14" />
|
||||
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.7" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14" />
|
||||
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.6.48" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
|
||||
<PackageReference Include="Humanizer" Version="2.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<Publish>True</Publish>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
|
||||
|
||||
<!-- Db-related packages -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="linq2db.EntityFrameworkCore" Version="6.8.0" />
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.1" />
|
||||
|
||||
<!-- Used by stream notifications -->
|
||||
<PackageReference Include="TwitchLib.Api" Version="3.4.1" />
|
||||
|
||||
<!-- Uncomment to check for disposable issues -->
|
||||
<!-- <PackageReference Include="IDisposableAnalyzers" Version="4.0.2">-->
|
||||
<!-- <PrivateAssets>all</PrivateAssets>-->
|
||||
<!-- <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
|
||||
<!-- </PackageReference>-->
|
||||
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ellie.Common\Ellie.Common.csproj" />
|
||||
<ProjectReference Include="..\Ellie.Econ\Ellie.Econ.csproj" />
|
||||
<ProjectReference Include="..\Ellie.Marmalade\Ellie.Marmalade.csproj" />
|
||||
<ProjectReference Include="..\EllieBot.Generators\EllieBot.Generators.csproj" OutputItemType="Analyzer" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="data\strings\responses\responses.en-US.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Protobuf Include="..\EllieBot.Coordinator\Protos\coordinator.proto" GrpcServices="Client">
|
||||
<Link>Protos\coordinator.proto</Link>
|
||||
</Protobuf>
|
||||
<None Update="data\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ellie_icon.ico;libopus.so;libsodium.so;libsodium.dll;opus.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="creds.yml;creds_example.yml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Version)' == '' ">
|
||||
<VersionPrefix Condition=" '$(VersionPrefix)' == '' ">4.0.0</VersionPrefix>
|
||||
<Version Condition=" '$(VersionSuffix)' != '' ">$(VersionPrefix).$(VersionSuffix)</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">$(VersionPrefix)</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalEllie' ">
|
||||
<!-- Define trace doesn't seem to affect the build at all so I had to remove $(DefineConstants)-->
|
||||
<DefineTrace>false</DefineTrace>
|
||||
<DefineConstants>GLOBAL_ELLIE</DefineConstants>
|
||||
<NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>portable</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
31
src/EllieBot/GlobalUsings.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
// global using System.Collections.Concurrent
|
||||
global using NonBlocking;
|
||||
|
||||
// packages
|
||||
global using Serilog;
|
||||
global using Humanizer;
|
||||
|
||||
// elliebot
|
||||
global using EllieBot;
|
||||
global using EllieBot.Services;
|
||||
global using Ellie.Common; // new project
|
||||
global using EllieBot.Common; // old + elliebot specific things
|
||||
global using EllieBot.Common.Attributes;
|
||||
global using EllieBot.Extensions;
|
||||
global using Ellie.Marmalade;
|
||||
|
||||
// discord
|
||||
global using Discord;
|
||||
global using Discord.Commands;
|
||||
global using Discord.Net;
|
||||
global using Discord.WebSocket;
|
||||
|
||||
// aliases
|
||||
global using GuildPerm = Discord.GuildPermission;
|
||||
global using ChannelPerm = Discord.ChannelPermission;
|
||||
global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute;
|
||||
global using LeftoverAttribute = Discord.Commands.RemainderAttribute;
|
||||
global using TypeReaderResult = EllieBot.Common.TypeReaders.TypeReaderResult;
|
||||
|
||||
// non-essential
|
||||
global using JetBrains.Annotations;
|
115
src/EllieBot/creds_example.yml
Normal file
|
@ -0,0 +1,115 @@
|
|||
# DO NOT CHANGE
|
||||
version: 7
|
||||
# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/
|
||||
token: ""
|
||||
# List of Ids of the users who have bot owner permissions
|
||||
# **DO NOT ADD PEOPLE YOU DON'T TRUST**
|
||||
ownerIds: []
|
||||
# Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted
|
||||
usePrivilegedIntents: true
|
||||
# The number of shards that the bot will be running on.
|
||||
# Leave at 1 if you don't know what you're doing.
|
||||
#
|
||||
# note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
|
||||
# Also, in that case you should be using EllieBot.Coordinator to start the bot, and it will correctly override this value.
|
||||
totalShards: 1
|
||||
# Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
|
||||
# Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
|
||||
# Used only for Youtube Data Api (at the moment).
|
||||
googleApiKey: ""
|
||||
# Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
|
||||
# Enable SafeSearch
|
||||
# Remove all Sites to Search
|
||||
# Enable Search the entire web
|
||||
# Copy the 'Search Engine ID' to the SearchId field
|
||||
#
|
||||
# Do all steps again but enable image search for the ImageSearchId
|
||||
google:
|
||||
searchId:
|
||||
imageSearchId:
|
||||
# Settings for voting system for discordbots. Meant for use on global Ellie.
|
||||
votes:
|
||||
# top.gg votes service url
|
||||
# This is the url of your instance of the EllieBot.Votes api
|
||||
# Example: https://votes.my.cool.bot.com
|
||||
topggServiceUrl: ""
|
||||
# Authorization header value sent to the TopGG service url with each request
|
||||
# This should be equivalent to the TopggKey in your EllieBot.Votes api appsettings.json file
|
||||
topggKey: ""
|
||||
# discords.com votes service url
|
||||
# This is the url of your instance of the EllieBot.Votes api
|
||||
# Example: https://votes.my.cool.bot.com
|
||||
discordsServiceUrl: ""
|
||||
# Authorization header value sent to the Discords service url with each request
|
||||
# This should be equivalent to the DiscordsKey in your EllieBot.Votes api appsettings.json file
|
||||
discordsKey: ""
|
||||
# Patreon auto reward system settings.
|
||||
# go to https://www.patreon.com/portal -> my clients -> create client
|
||||
patreon:
|
||||
clientId:
|
||||
accessToken: ""
|
||||
refreshToken: ""
|
||||
clientSecret: ""
|
||||
# Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type "prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);" in the console. (ctrl + shift + i)
|
||||
campaignId: ""
|
||||
# Api key for sending stats to DiscordBotList.
|
||||
botListToken: ""
|
||||
# Official cleverbot api key.
|
||||
cleverbotApiKey: ""
|
||||
# Official GPT-3 api key.
|
||||
gpt3ApiKey: ""
|
||||
# Which cache implementation should bot use.
|
||||
# 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
|
||||
# 'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
|
||||
botCache: Memory
|
||||
# Redis connection string. Don't change if you don't know what you're doing.
|
||||
# Only used if botCache is set to 'redis'
|
||||
redisOptions: localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=
|
||||
# Database options. Don't change if you don't know what you're doing. Leave null for default values
|
||||
db:
|
||||
# Database type. "sqlite", "mysql" and "postgresql" are supported.
|
||||
# Default is "sqlite"
|
||||
type: sqlite
|
||||
# Database connection string.
|
||||
# You MUST change this if you're not using "sqlite" type.
|
||||
# Default is "Data Source=data/EllieBot.db"
|
||||
# Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=ellie"
|
||||
# Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=ellie;"
|
||||
connectionString: Data Source=data/EllieBot.db
|
||||
# Address and port of the coordinator endpoint. Leave empty for default.
|
||||
# Change only if you've changed the coordinator address or port.
|
||||
coordinatorUrl: http://localhost:3442
|
||||
# Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)
|
||||
rapidApiKey:
|
||||
# https://locationiq.com api key (register and you will receive the token in the email).
|
||||
# Used only for .time command.
|
||||
locationIqApiKey:
|
||||
# https://timezonedb.com api key (register and you will receive the token in the email).
|
||||
# Used only for .time command
|
||||
timezoneDbApiKey:
|
||||
# https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
|
||||
# Used for cryptocurrency related commands.
|
||||
coinmarketcapApiKey:
|
||||
# Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api
|
||||
osuApiKey:
|
||||
# Optional Trovo client id.
|
||||
# You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
|
||||
trovoClientId:
|
||||
# Obtain by creating an application at https://dev.twitch.tv/console/apps
|
||||
twitchClientId:
|
||||
# Obtain by creating an application at https://dev.twitch.tv/console/apps
|
||||
twitchClientSecret:
|
||||
# Command and args which will be used to restart the bot.
|
||||
# Only used if bot is executed directly (NOT through the coordinator)
|
||||
# placeholders:
|
||||
# {0} -> shard id
|
||||
# {1} -> total shards
|
||||
# Linux default
|
||||
# cmd: dotnet
|
||||
# args: "EllieBot.dll -- {0}"
|
||||
# Windows default
|
||||
# cmd: EllieBot.exe
|
||||
# args: "{0}"
|
||||
restartCommand:
|
||||
cmd:
|
||||
args:
|
1391
src/EllieBot/data/aliases.yml
Normal file
91
src/EllieBot/data/bot.yml
Normal file
|
@ -0,0 +1,91 @@
|
|||
# DO NOT CHANGE
|
||||
version: 5
|
||||
# Most commands, when executed, have a small colored line
|
||||
# next to the response. The color depends whether the command
|
||||
# is completed, errored or in progress (pending)
|
||||
# Color settings below are for the color of those lines.
|
||||
# To get color's hex, you can go here https://htmlcolorcodes.com/
|
||||
# and copy the hex code fo your selected color (marked as #)
|
||||
color:
|
||||
# Color used for embed responses when command successfully executes
|
||||
ok: 00e584
|
||||
# Color used for embed responses when command has an error
|
||||
error: ee281f
|
||||
# Color used for embed responses while command is doing work or is in progress
|
||||
pending: faa61a
|
||||
# Default bot language. It has to be in the list of supported languages (.langli)
|
||||
defaultLocale: en-US
|
||||
# Style in which executed commands will show up in the console.
|
||||
# Allowed values: Simple, Normal, None
|
||||
consoleOutputType: Normal
|
||||
# Whether the bot will check for new releases every hour
|
||||
checkForUpdates: true
|
||||
# Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?
|
||||
forwardMessages: false
|
||||
# Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
|
||||
# or all owners? (this might cause the bot to lag if there's a lot of owners specified)
|
||||
forwardToAllOwners: false
|
||||
# Any messages sent by users in Bot's DM to be forwarded to the specified channel.
|
||||
# This option will only work when ForwardToAllOwners is set to false
|
||||
forwardToChannel:
|
||||
# When a user DMs the bot with a message which is not a command
|
||||
# they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot.
|
||||
# Supports embeds. How it looks: https://puu.sh/B0BLV.png
|
||||
dmHelpText: |-
|
||||
{"description": "Type `%prefix%h` for help."}
|
||||
# Only users who send a DM to the bot containing one of the specified words will get a DmHelpText response.
|
||||
# Case insensitive.
|
||||
# Leave empty to reply with DmHelpText to every DM.
|
||||
dmHelpTextKeywords:
|
||||
- help
|
||||
- commands
|
||||
- cmds
|
||||
- module
|
||||
- can you do
|
||||
# This is the response for the .h command
|
||||
helpText: |-
|
||||
{
|
||||
"title": "To invite me to your server, use this link",
|
||||
"description": "https://discordapp.com/oauth2/authorize?client_id={0}&scope=bot&permissions=66186303",
|
||||
"color": 53380,
|
||||
"thumbnail": "https://cdn.elliebot.net/Ellie.png",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Useful help commands",
|
||||
"value": "`%bot.prefix%modules` Lists all bot modules.
|
||||
`%prefix%h CommandName` Shows some help about a specific command.
|
||||
`%prefix%commands ModuleName` Lists all commands in a module.",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "List of all Commands",
|
||||
"value": "https://commands.elliebot.net/",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "Ellie Support Server",
|
||||
"value": "https://discord.gg/etQdZxSyEH",
|
||||
"inline": true
|
||||
}
|
||||
]
|
||||
}
|
||||
# List of modules and commands completely blocked on the bot
|
||||
blocked:
|
||||
commands: []
|
||||
modules: []
|
||||
# Which string will be used to recognize the commands
|
||||
prefix: .
|
||||
# Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
|
||||
# 1st user who joins will get greeted immediately
|
||||
# If more users join within the next 5 seconds, they will be greeted in groups of 5.
|
||||
# This will cause %user.mention% and other placeholders to be replaced with multiple users.
|
||||
# Keep in mind this might break some of your embeds - for example if you have %user.avatar% in the thumbnail,
|
||||
# it will become invalid, as it will resolve to a list of avatars of grouped users.
|
||||
# note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some
|
||||
# servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited,
|
||||
# and (slightly) reduce the greet spam in those servers.
|
||||
groupGreets: false
|
||||
# Whether the bot will rotate through all specified statuses.
|
||||
# This setting can be changed via .ropl command.
|
||||
# See RotatingStatuses submodule in Administration.
|
||||
rotateStatuses: false
|
BIN
src/EllieBot/data/fonts/NotoSans-Bold.ttf
Normal file
BIN
src/EllieBot/data/fonts/Symbola-10.24.ttf
Normal file
BIN
src/EllieBot/data/fonts/Uni Sans.ttf
Normal file
BIN
src/EllieBot/data/fonts/dotty.ttf
Normal file
261
src/EllieBot/data/gambling.yml
Normal file
|
@ -0,0 +1,261 @@
|
|||
# DO NOT CHANGE
|
||||
version: 6
|
||||
# Currency settings
|
||||
currency:
|
||||
# What is the emoji/character which represents the currency
|
||||
sign: "💵"
|
||||
# What is the name of the currency
|
||||
name: Ellie Cash
|
||||
# For how long will the transactions be kept in the database (curtrs)
|
||||
# Set 0 to disable cleanup (keep transactions forever)
|
||||
transactionsLifetime: 0
|
||||
# Minimum amount users can bet (>=0)
|
||||
minBet: 0
|
||||
# Maximum amount users can bet
|
||||
# Set 0 for unlimited
|
||||
maxBet: 0
|
||||
# Settings for betflip command
|
||||
betFlip:
|
||||
# Bet multiplier if user guesses correctly
|
||||
multiplier: 1.95
|
||||
# Settings for betroll command
|
||||
betRoll:
|
||||
# When betroll is played, user will roll a number 0-100.
|
||||
# This setting will describe which multiplier is used for when the roll is higher than the given number.
|
||||
# Doesn't have to be ordered.
|
||||
pairs:
|
||||
- whenAbove: 99
|
||||
multiplyBy: 10
|
||||
- whenAbove: 90
|
||||
multiplyBy: 4
|
||||
- whenAbove: 66
|
||||
multiplyBy: 2
|
||||
# Automatic currency generation settings.
|
||||
generation:
|
||||
# when currency is generated, should it also have a random password
|
||||
# associated with it which users have to type after the .pick command
|
||||
# in order to get it
|
||||
hasPassword: true
|
||||
# Every message sent has a certain % chance to generate the currency
|
||||
# specify the percentage here (1 being 100%, 0 being 0% - for example
|
||||
# default is 0.02, which is 2%
|
||||
chance: 0.02
|
||||
# How many seconds have to pass for the next message to have a chance to spawn currency
|
||||
genCooldown: 10
|
||||
# Minimum amount of currency that can spawn
|
||||
minAmount: 1
|
||||
# Maximum amount of currency that can spawn.
|
||||
# Set to the same value as MinAmount to always spawn the same amount
|
||||
maxAmount: 1
|
||||
# Settings for timely command
|
||||
# (letting people claim X amount of currency every Y hours)
|
||||
timely:
|
||||
# How much currency will the users get every time they run .timely command
|
||||
# setting to 0 or less will disable this feature
|
||||
amount: 120
|
||||
# How often (in hours) can users claim currency with .timely command
|
||||
# setting to 0 or less will disable this feature
|
||||
cooldown: 12
|
||||
# How much will each user's owned currency decay over time.
|
||||
decay:
|
||||
# Percentage of user's current currency which will be deducted every 24h.
|
||||
# 0 - 1 (1 is 100%, 0.5 50%, 0 disabled)
|
||||
percent: 0
|
||||
# Maximum amount of user's currency that can decay at each interval. 0 for unlimited.
|
||||
maxDecay: 0
|
||||
# Only users who have more than this amount will have their currency decay.
|
||||
minThreshold: 99
|
||||
# How often, in hours, does the decay run. Default is 24 hours
|
||||
hourInterval: 24
|
||||
# Settings for LuckyLadder command
|
||||
luckyLadder:
|
||||
# Self-Explanatory. Has to have 8 values, otherwise the command won't work.
|
||||
multipliers:
|
||||
- 2.4
|
||||
- 1.7
|
||||
- 1.5
|
||||
- 1.2
|
||||
- 0.5
|
||||
- 0.3
|
||||
- 0.2
|
||||
- 0.1
|
||||
# Settings related to waifus
|
||||
waifu:
|
||||
# Minimum price a waifu can have
|
||||
minPrice: 50
|
||||
multipliers:
|
||||
# Multiplier for waifureset. Default 150.
|
||||
# Formula (at the time of writing this):
|
||||
# price = (waifu_price * 1.25f) + ((number_of_divorces + changes_of_heart + 2) * WaifuReset) rounded up
|
||||
waifuReset: 150
|
||||
# The minimum amount of currency that you have to pay
|
||||
# in order to buy a waifu who doesn't have a crush on you.
|
||||
# Default is 1.1
|
||||
# Example: If a waifu is worth 100, you will have to pay at least 100 * NormalClaim currency to claim her.
|
||||
# (100 * 1.1 = 110)
|
||||
normalClaim: 1.1
|
||||
# The minimum amount of currency that you have to pay
|
||||
# in order to buy a waifu that has a crush on you.
|
||||
# Default is 0.88
|
||||
# Example: If a waifu is worth 100, you will have to pay at least 100 * CrushClaim currency to claim her.
|
||||
# (100 * 0.88 = 88)
|
||||
crushClaim: 0.88
|
||||
# When divorcing a waifu, her new value will be her current value multiplied by this number.
|
||||
# Default 0.75 (meaning will lose 25% of her value)
|
||||
divorceNewValue: 0.75
|
||||
# All gift prices will be multiplied by this number.
|
||||
# Default 1 (meaning no effect)
|
||||
allGiftPrices: 1.0
|
||||
# What percentage of the value of the gift will a waifu gain when she's gifted.
|
||||
# Default 0.95 (meaning 95%)
|
||||
# Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095)
|
||||
giftEffect: 0.95
|
||||
# What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'.
|
||||
# Default 0.5 (meaning 50%)
|
||||
# Example: If a waifu is worth 1000, and she receives a negative gift worth 100, her new value will be 950)
|
||||
negativeGiftEffect: 0.50
|
||||
# Settings for periodic waifu price decay.
|
||||
# Waifu price decays only if the waifu has no claimer.
|
||||
decay:
|
||||
# Percentage (0 - 100) of the waifu value to reduce.
|
||||
# Set 0 to disable
|
||||
# For example if a waifu has a price of 500$, setting this value to 10 would reduce the waifu value by 10% (50$)
|
||||
percent: 0
|
||||
# How often to decay waifu values, in hours
|
||||
hourInterval: 24
|
||||
# Minimum waifu price required for the decay to be applied.
|
||||
# For example if this value is set to 300, any waifu with the price 300 or less will not experience decay.
|
||||
minPrice: 300
|
||||
# List of items available for gifting.
|
||||
# If negative is true, gift will instead reduce waifu value.
|
||||
items:
|
||||
- itemEmoji: "🥔"
|
||||
price: 5
|
||||
name: Potato
|
||||
- itemEmoji: "🍪"
|
||||
price: 10
|
||||
name: Cookie
|
||||
- itemEmoji: "🥖"
|
||||
price: 20
|
||||
name: Bread
|
||||
- itemEmoji: "🍭"
|
||||
price: 30
|
||||
name: Lollipop
|
||||
- itemEmoji: "🌹"
|
||||
price: 50
|
||||
name: Rose
|
||||
- itemEmoji: "🍺"
|
||||
price: 70
|
||||
name: Beer
|
||||
- itemEmoji: "🌮"
|
||||
price: 85
|
||||
name: Taco
|
||||
- itemEmoji: "💌"
|
||||
price: 100
|
||||
name: LoveLetter
|
||||
- itemEmoji: "🥛"
|
||||
price: 125
|
||||
name: Milk
|
||||
- itemEmoji: "🍕"
|
||||
price: 150
|
||||
name: Pizza
|
||||
- itemEmoji: "🍫"
|
||||
price: 200
|
||||
name: Chocolate
|
||||
- itemEmoji: "🍦"
|
||||
price: 250
|
||||
name: Icecream
|
||||
- itemEmoji: "🍣"
|
||||
price: 300
|
||||
name: Sushi
|
||||
- itemEmoji: "🍚"
|
||||
price: 400
|
||||
name: Rice
|
||||
- itemEmoji: "🍉"
|
||||
price: 500
|
||||
name: Watermelon
|
||||
- itemEmoji: "🍱"
|
||||
price: 600
|
||||
name: Bento
|
||||
- itemEmoji: "🎟"
|
||||
price: 800
|
||||
name: MovieTicket
|
||||
- itemEmoji: "🍰"
|
||||
price: 1000
|
||||
name: Cake
|
||||
- itemEmoji: "📔"
|
||||
price: 1500
|
||||
name: Book
|
||||
- itemEmoji: "🐱"
|
||||
price: 2000
|
||||
name: Cat
|
||||
- itemEmoji: "🐶"
|
||||
price: 2001
|
||||
name: Dog
|
||||
- itemEmoji: "🐼"
|
||||
price: 2500
|
||||
name: Panda
|
||||
- itemEmoji: "💄"
|
||||
price: 3000
|
||||
name: Lipstick
|
||||
- itemEmoji: "👛"
|
||||
price: 3500
|
||||
name: Purse
|
||||
- itemEmoji: "📱"
|
||||
price: 4000
|
||||
name: iPhone
|
||||
- itemEmoji: "👗"
|
||||
price: 4500
|
||||
name: Dress
|
||||
- itemEmoji: "💻"
|
||||
price: 5000
|
||||
name: Laptop
|
||||
- itemEmoji: "🎻"
|
||||
price: 7500
|
||||
name: Violin
|
||||
- itemEmoji: "🎹"
|
||||
price: 8000
|
||||
name: Piano
|
||||
- itemEmoji: "🚗"
|
||||
price: 9000
|
||||
name: Car
|
||||
- itemEmoji: "💍"
|
||||
price: 10000
|
||||
name: Ring
|
||||
- itemEmoji: "🛳"
|
||||
price: 12000
|
||||
name: Ship
|
||||
- itemEmoji: "🏠"
|
||||
price: 15000
|
||||
name: House
|
||||
- itemEmoji: "🚁"
|
||||
price: 20000
|
||||
name: Helicopter
|
||||
- itemEmoji: "🚀"
|
||||
price: 30000
|
||||
name: Spaceship
|
||||
- itemEmoji: "🌕"
|
||||
price: 50000
|
||||
name: Moon
|
||||
- itemEmoji: "🥀"
|
||||
price: 100
|
||||
name: WiltedRose
|
||||
negative: true
|
||||
- itemEmoji: ✂️
|
||||
price: 1000
|
||||
name: Haircut
|
||||
negative: true
|
||||
- itemEmoji: "🧻"
|
||||
price: 10000
|
||||
name: ToiletPaper
|
||||
negative: true
|
||||
# Amount of currency selfhosters will get PER pledged dollar CENT.
|
||||
# 1 = 100 currency per $. Used almost exclusively on public ellie.
|
||||
patreonCurrencyPerCent: 1
|
||||
# Currency reward per vote.
|
||||
# This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting
|
||||
voteReward: 100
|
||||
# Slot config
|
||||
slots:
|
||||
# Hex value of the color which the numbers on the slot image will have.
|
||||
currencyFontColor: ff0000
|
276
src/EllieBot/data/hangman/animals.yml
Normal file
|
@ -0,0 +1,276 @@
|
|||
- word: Alligator
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Alligator.jpg
|
||||
- word: Alpaca
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Alpaca.jpg
|
||||
- word: Anaconda
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Anaconda.jpg
|
||||
- word: Ant
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Ant.jpg
|
||||
- word: Antelope
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Antelope.jpg
|
||||
- word: Ape
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Ape.jpg
|
||||
- word: Armadillo
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Armadillo.jpg
|
||||
- word: Baboon
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Baboon.jpg
|
||||
- word: Badger
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Badger.jpg
|
||||
- word: Bald Eagle
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Bald Eagle.jpg
|
||||
- word: Barracuda
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Barracuda.jpg
|
||||
- word: Bat
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Bat.jpg
|
||||
- word: Bear
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Bear.jpg
|
||||
- word: Beaver
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Beaver.jpg
|
||||
- word: Bedbug
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Bedbug.jpg
|
||||
- word: Bee
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Bee.jpg
|
||||
- word: Beetle
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Beetle.jpg
|
||||
- word: Bird
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Bird.jpg
|
||||
- word: Bison
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Bison.jpg
|
||||
- word: Puma
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Puma.jpg
|
||||
- word: Black Widow
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Black Widow.jpg
|
||||
- word: Blue Jay
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Blue Jay.jpg
|
||||
- word: Blue Whale
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Blue Whale.jpg
|
||||
- word: Bobcat
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Bobcat.jpg
|
||||
- word: Buffalo
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Buffalo.jpg
|
||||
- word: Butterfly
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Butterfly.jpg
|
||||
- word: Buzzard
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Buzzard.jpg
|
||||
- word: Camel
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Camel.jpg
|
||||
- word: Carp
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Carp.jpg
|
||||
- word: Cat
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Cat.jpg
|
||||
- word: Caterpillar
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Caterpillar.jpg
|
||||
- word: Catfish
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Catfish.jpg
|
||||
- word: Cheetah
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Cheetah.jpg
|
||||
- word: Chicken
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Chicken.jpg
|
||||
- word: Chimpanzee
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Chimpanzee.jpg
|
||||
- word: Chipmunk
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Chipmunk.jpg
|
||||
- word: Cobra
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Cobra.jpg
|
||||
- word: Cod
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Cod.jpg
|
||||
- word: Condor
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Condor.jpg
|
||||
- word: Cougar
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Cougar.jpg
|
||||
- word: Cow
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Cow.jpg
|
||||
- word: Coyote
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Coyote.jpg
|
||||
- word: Crab
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Crab.jpg
|
||||
- word: Crane
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Crane.jpg
|
||||
- word: Cricket
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Cricket.jpg
|
||||
- word: Crocodile
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Crocodile.jpg
|
||||
- word: Crow
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Crow.jpg
|
||||
- word: Cuckoo
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Cuckoo.jpg
|
||||
- word: Deer
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Deer.jpg
|
||||
- word: Dinosaur
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Dinosaur.jpg
|
||||
- word: Dog
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Dog.jpg
|
||||
- word: Dolphin
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Dolphin.jpg
|
||||
- word: Donkey
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Donkey.jpg
|
||||
- word: Dove
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Dove.jpg
|
||||
- word: Dragonfly
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Dragonfly.jpg
|
||||
- word: Duck
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Duck.jpg
|
||||
- word: Eel
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Eel.jpg
|
||||
- word: Elephant
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Elephant.jpg
|
||||
- word: Emu
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Emu.jpg
|
||||
- word: Falcon
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Falcon.jpg
|
||||
- word: Ferret
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Ferret.jpg
|
||||
- word: Finch
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Finch.jpg
|
||||
- word: Fish
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Fish.jpg
|
||||
- word: Flamingo
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Flamingo.jpg
|
||||
- word: Flea
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Flea.jpg
|
||||
- word: Fly
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Fly.jpg
|
||||
- word: Fox
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Fox.jpg
|
||||
- word: Frog
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Frog.jpg
|
||||
- word: Goat
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Goat.jpg
|
||||
- word: Golden Eagle
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Golden Eagle.jpg
|
||||
- word: Goose
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Goose.jpg
|
||||
- word: Gopher
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Gopher.jpg
|
||||
- word: Gorilla
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Gorilla.jpg
|
||||
- word: Grasshopper
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Grasshopper.jpg
|
||||
- word: Hamster
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Hamster.jpg
|
||||
- word: Hare
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Hare.jpg
|
||||
- word: Hawk
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Hawk.jpg
|
||||
- word: Hippopotamus
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Hippopotamus.jpg
|
||||
- word: Horse
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Horse.jpg
|
||||
- word: Hummingbird
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Hummingbird.jpg
|
||||
- word: Husky
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Husky.jpg
|
||||
- word: Iguana
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Iguana.jpg
|
||||
- word: Impala
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Impala.jpg
|
||||
- word: Kangaroo
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Kangaroo.jpg
|
||||
- word: Ladybug
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Ladybug.jpg
|
||||
- word: Leopard
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Leopard.jpg
|
||||
- word: Lion
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Lion.jpg
|
||||
- word: Lizard
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Lizard.jpg
|
||||
- word: Llama
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Llama.jpg
|
||||
- word: Lobster
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Lobster.jpg
|
||||
- word: Mongoose
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Mongoose.jpg
|
||||
- word: Monitor lizard
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Monitor lizard.jpg
|
||||
- word: Monkey
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Monkey.jpg
|
||||
- word: Moose
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Moose.jpg
|
||||
- word: Mosquito
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Mosquito.jpg
|
||||
- word: Moth
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Moth.jpg
|
||||
- word: Mountain goat
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Mountain goat.jpg
|
||||
- word: Mouse
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Mouse.jpg
|
||||
- word: Mule
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Mule.jpg
|
||||
- word: Octopus
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Octopus.jpg
|
||||
- word: Orca
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Orca.jpg
|
||||
- word: Ostrich
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Ostrich.jpg
|
||||
- word: Otter
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Otter.jpg
|
||||
- word: Owl
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Owl.jpg
|
||||
- word: Ox
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Ox.jpg
|
||||
- word: Oyster
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Oyster.jpg
|
||||
- word: Panda
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Panda.jpg
|
||||
- word: Parrot
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Parrot.jpg
|
||||
- word: Peacock
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Peacock.jpg
|
||||
- word: Pelican
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Pelican.jpg
|
||||
- word: Penguin
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Penguin.jpg
|
||||
- word: Perch
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Perch.jpg
|
||||
- word: Pheasant
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Pheasant.jpg
|
||||
- word: Pig
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Pig.jpg
|
||||
- word: Pigeon
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Pigeon.jpg
|
||||
- word: Polar bear
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Polar bear.jpg
|
||||
- word: Porcupine
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Porcupine.jpg
|
||||
- word: Quail
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Quail.jpg
|
||||
- word: Rabbit
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Rabbit.jpg
|
||||
- word: Raccoon
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Raccoon.jpg
|
||||
- word: Rat
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Rat.jpg
|
||||
- word: Rattlesnake
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Rattlesnake.jpg
|
||||
- word: Raven
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Raven.jpg
|
||||
- word: Reindeer
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Reindeer.jpg
|
||||
- word: Rooster
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Rooster.jpg
|
||||
- word: Sea lion
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Sea lion.jpg
|
||||
- word: Seal
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Seal.jpg
|
||||
- word: Sheep
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Sheep.jpg
|
||||
- word: Shrew
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Shrew.jpg
|
||||
- word: Skunk
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Skunk.jpg
|
||||
- word: Snail
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Snail.jpg
|
||||
- word: Snake
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Snake.jpg
|
||||
- word: Spider
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Spider.jpg
|
||||
- word: Tiger
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Tiger.jpg
|
||||
- word: Walrus
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Walrus.jpg
|
||||
- word: Whale
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Whale.jpg
|
||||
- word: Wolf
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Wolf.jpg
|
||||
- word: Zebra
|
||||
imageUrl: https://cdn.nadeko.bot/animals/Zebra
|
766
src/EllieBot/data/hangman/anime.yml
Normal file
|
@ -0,0 +1,766 @@
|
|||
- word: 'Fullmetal Alchemist: Brotherhood'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Fullmetal_Alchemist_Brotherhood.jpg
|
||||
- word: Steins;Gate
|
||||
imageUrl: https://cdn.nadeko.bot/animu/SteinsGate.jpg
|
||||
- word: Hunter x Hunter (2011)
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hunter_x_Hunter_2011.jpg
|
||||
- word: Ginga Eiyuu Densetsu
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ginga_Eiyuu_Densetsu.jpg
|
||||
- word: 'Fruits Basket: The Final'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Fruits_Basket_The_Final.jpg
|
||||
- word: Koe no Katachi
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Koe_no_Katachi.jpg
|
||||
- word: 'Clannad: After Story'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Clannad_After_Story.jpg
|
||||
- word: Gintama
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Gintama.jpg
|
||||
- word: Kimi no Na wa.
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kimi_no_Na_wa..jpg
|
||||
- word: 'Code Geass: Hangyaku no Lelouch R2'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Code_Geass_Hangyaku_no_Lelouch_R2.jpg
|
||||
- word: 'Haikyuu!!: Karasuno Koukou vs. Shiratorizawa Gakuen Koukou'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Haikyuu_Karasuno_Koukou_vs._Shiratorizawa_Gakuen_Koukou.jpg
|
||||
- word: Mob Psycho 100 II
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mob_Psycho_100_II.jpg
|
||||
- word: 'Kizumonogatari III: Reiketsu-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kizumonogatari_III_Reiketsu-hen.jpg
|
||||
- word: Sen to Chihiro no Kamikakushi
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Sen_to_Chihiro_no_Kamikakushi.jpg
|
||||
- word: Violet Evergarden Movie
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Violet_Evergarden_Movie.jpg
|
||||
- word: 'Monogatari Series: Second Season'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Monogatari_Series_Second_Season.jpg
|
||||
- word: Monster
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Monster.jpg
|
||||
- word: 'Shouwa Genroku Rakugo Shinjuu: Sukeroku Futatabi-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shouwa_Genroku_Rakugo_Shinjuu_Sukeroku_Futatabi-hen.jpg
|
||||
- word: Cowboy Bebop
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Cowboy_Bebop.jpg
|
||||
- word: Jujutsu Kaisen (TV)
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Jujutsu_Kaisen_TV.jpg
|
||||
- word: 'Kimetsu no Yaiba Movie: Mugen Ressha-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kimetsu_no_Yaiba_Movie_Mugen_Ressha-hen.jpg
|
||||
- word: Mushishi Zoku Shou 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mushishi_Zoku_Shou_2nd_Season.jpg
|
||||
- word: Hajime no Ippo
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hajime_no_Ippo.jpg
|
||||
- word: Made in Abyss
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Made_in_Abyss.jpg
|
||||
- word: 'Made in Abyss Movie 3: Fukaki Tamashii no Reimei'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Made_in_Abyss_Movie_3_Fukaki_Tamashii_no_Reimei.jpg
|
||||
- word: Mushishi Zoku Shou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mushishi_Zoku_Shou.jpg
|
||||
- word: 'Rurouni Kenshin: Meiji Kenkaku Romantan - Tsuioku-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Rurouni_Kenshin_Meiji_Kenkaku_Romantan_-_Tsuioku-hen.jpg
|
||||
- word: Shigatsu wa Kimi no Uso
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shigatsu_wa_Kimi_no_Uso.jpg
|
||||
- word: Vinland Saga
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Vinland_Saga.jpg
|
||||
- word: 'Code Geass: Hangyaku no Lelouch'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Code_Geass_Hangyaku_no_Lelouch.jpg
|
||||
- word: Great Teacher Onizuka
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Great_Teacher_Onizuka.jpg
|
||||
- word: Mononoke Hime
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mononoke_Hime.jpg
|
||||
- word: Mushishi
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mushishi.jpg
|
||||
- word: Haikyuu!! Second Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Haikyuu_Second_Season.jpg
|
||||
- word: 'Kaguya-sama wa Kokurasetai?: Tensai-tachi no Renai Zunousen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kaguya-sama_wa_Kokurasetai_Tensai-tachi_no_Renai_Zunousen.jpg
|
||||
- word: 'Hajime no Ippo: New Challenger'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hajime_no_Ippo_New_Challenger.jpg
|
||||
- word: Howl no Ugoku Shiro
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Howl_no_Ugoku_Shiro.jpg
|
||||
- word: Natsume Yuujinchou Shi
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Natsume_Yuujinchou_Shi.jpg
|
||||
- word: Seishun Buta Yarou wa Yumemiru Shoujo no Yume wo Minai
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Seishun_Buta_Yarou_wa_Yumemiru_Shoujo_no_Yume_wo_Minai.jpg
|
||||
- word: Tengen Toppa Gurren Lagann
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tengen_Toppa_Gurren_Lagann.jpg
|
||||
- word: Violet Evergarden
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Violet_Evergarden.jpg
|
||||
- word: Natsume Yuujinchou Roku
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Natsume_Yuujinchou_Roku.jpg
|
||||
- word: Suzumiya Haruhi no Shoushitsu
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Suzumiya_Haruhi_no_Shoushitsu.jpg
|
||||
- word: Death Note
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Death_Note.jpg
|
||||
- word: Fumetsu no Anata e
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Fumetsu_no_Anata_e.jpg
|
||||
- word: 'Mushishi Zoku Shou: Suzu no Shizuku'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mushishi_Zoku_Shou_Suzu_no_Shizuku.jpg
|
||||
- word: Ookami Kodomo no Ame to Yuki
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ookami_Kodomo_no_Ame_to_Yuki.jpg
|
||||
- word: Ping Pong the Animation
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ping_Pong_the_Animation.jpg
|
||||
- word: Yakusoku no Neverland
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yakusoku_no_Neverland.jpg
|
||||
- word: 'Kizumonogatari II: Nekketsu-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kizumonogatari_II_Nekketsu-hen.jpg
|
||||
- word: Yojouhan Shinwa Taikei
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yojouhan_Shinwa_Taikei.jpg
|
||||
- word: Natsume Yuujinchou San
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Natsume_Yuujinchou_San.jpg
|
||||
- word: Shouwa Genroku Rakugo Shinjuu
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shouwa_Genroku_Rakugo_Shinjuu.jpg
|
||||
- word: 'Hajime no Ippo: Rising'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hajime_no_Ippo_Rising.jpg
|
||||
- word: Kimetsu no Yaiba
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kimetsu_no_Yaiba.jpg
|
||||
- word: Kimi no Suizou wo Tabetai
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kimi_no_Suizou_wo_Tabetai.jpg
|
||||
- word: Natsume Yuujinchou Go
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Natsume_Yuujinchou_Go.jpg
|
||||
- word: Re:Zero kara Hajimeru Isekai Seikatsu 2nd Season Part 2
|
||||
imageUrl: https://cdn.nadeko.bot/animu/ReZero_kara_Hajimeru_Isekai_Seikatsu_2nd_Season_Part_2.jpg
|
||||
- word: 'Mushishi: Hihamukage'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mushishi_Hihamukage.jpg
|
||||
- word: Bakuman. 3rd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Bakuman._3rd_Season.jpg
|
||||
- word: 'Kara no Kyoukai 5: Mujun Rasen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kara_no_Kyoukai_5_Mujun_Rasen.jpg
|
||||
- word: Sora yori mo Tooi Basho
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Sora_yori_mo_Tooi_Basho.jpg
|
||||
- word: Zoku Natsume Yuujinchou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Zoku_Natsume_Yuujinchou.jpg
|
||||
- word: One Piece
|
||||
imageUrl: https://cdn.nadeko.bot/animu/One_Piece.jpg
|
||||
- word: Yuru Camp△ Season 2
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yuru_Camp_Season_2.jpg
|
||||
- word: Fruits Basket 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Fruits_Basket_2nd_Season.jpg
|
||||
- word: 'Haikyuu!!: To the Top 2nd Season'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Haikyuu_To_the_Top_2nd_Season.jpg
|
||||
- word: 'Koukaku Kidoutai: Stand Alone Complex 2nd GIG'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Koukaku_Kidoutai_Stand_Alone_Complex_2nd_GIG.jpg
|
||||
- word: One Punch Man
|
||||
imageUrl: https://cdn.nadeko.bot/animu/One_Punch_Man.jpg
|
||||
- word: 'Neon Genesis Evangelion: The End of Evangelion'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Neon_Genesis_Evangelion_The_End_of_Evangelion.jpg
|
||||
- word: Ansatsu Kyoushitsu 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ansatsu_Kyoushitsu_2nd_Season.jpg
|
||||
- word: Slam Dunk
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Slam_Dunk.jpg
|
||||
- word: "Vivy: Fluorite Eye's Song"
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Vivy_Fluorite_Eyes_Song.jpg
|
||||
- word: 'Rainbow: Nisha Rokubou no Shichinin'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Rainbow_Nisha_Rokubou_no_Shichinin.jpg
|
||||
- word: Shingeki no Kyojin
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shingeki_no_Kyojin.jpg
|
||||
- word: Uchuu Kyoudai
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Uchuu_Kyoudai.jpg
|
||||
- word: Aria the Origination
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Aria_the_Origination.jpg
|
||||
- word: Holo no Graffiti
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Holo_no_Graffiti.jpg
|
||||
- word: Hotaru no Haka
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hotaru_no_Haka.jpg
|
||||
- word: Banana Fish
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Banana_Fish.jpg
|
||||
- word: Chihayafuru 3
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Chihayafuru_3.jpg
|
||||
- word: Kenpuu Denki Berserk
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kenpuu_Denki_Berserk.jpg
|
||||
- word: Perfect Blue
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Perfect_Blue.jpg
|
||||
- word: Samurai Champloo
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Samurai_Champloo.jpg
|
||||
- word: Haikyuu!!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Haikyuu.jpg
|
||||
- word: Mo Dao Zu Shi
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mo_Dao_Zu_Shi.jpg
|
||||
- word: Mob Psycho 100
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mob_Psycho_100.jpg
|
||||
- word: Zoku Owarimonogatari
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Zoku_Owarimonogatari.jpg
|
||||
- word: Nana
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Nana.jpg
|
||||
- word: Nichijou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Nichijou.jpg
|
||||
- word: Saenai Heroine no Sodatekata Fine
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Saenai_Heroine_no_Sodatekata_Fine.jpg
|
||||
- word: 'Mushishi Zoku Shou: Odoro no Michi'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mushishi_Zoku_Shou_Odoro_no_Michi.jpg
|
||||
- word: Owarimonogatari
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Owarimonogatari.jpg
|
||||
- word: Saiki Kusuo no Ψ-nan 2
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Saiki_Kusuo_no_-nan_2.jpg
|
||||
- word: Yuu☆Yuu☆Hakusho
|
||||
imageUrl: https://cdn.nadeko.bot/animu/YuuYuuHakusho.jpg
|
||||
- word: Golden Kamuy 3rd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Golden_Kamuy_3rd_Season.jpg
|
||||
- word: 'Koukaku Kidoutai: Stand Alone Complex'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Koukaku_Kidoutai_Stand_Alone_Complex.jpg
|
||||
- word: Mo Dao Zu Shi 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mo_Dao_Zu_Shi_2nd_Season.jpg
|
||||
- word: Re:Zero kara Hajimeru Isekai Seikatsu 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/ReZero_kara_Hajimeru_Isekai_Seikatsu_2nd_Season.jpg
|
||||
- word: Sayonara no Asa ni Yakusoku no Hana wo Kazarou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Sayonara_no_Asa_ni_Yakusoku_no_Hana_wo_Kazarou.jpg
|
||||
- word: Mononoke
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mononoke.jpg
|
||||
- word: Saiki Kusuo no Ψ-nan
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Saiki_Kusuo_no_-nan.jpg
|
||||
- word: Gotcha!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Gotcha.jpg
|
||||
- word: 'Kara no Kyoukai 7: Satsujin Kousatsu (Go)'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kara_no_Kyoukai_7_Satsujin_Kousatsu_Go.jpg
|
||||
- word: Kaze ga Tsuyoku Fuiteiru
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kaze_ga_Tsuyoku_Fuiteiru.jpg
|
||||
- word: 3-gatsu no Lion
|
||||
imageUrl: https://cdn.nadeko.bot/animu/3-gatsu_no_Lion.jpg
|
||||
- word: Cross Game
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Cross_Game.jpg
|
||||
- word: Josee to Tora to Sakana-tachi
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Josee_to_Tora_to_Sakana-tachi.jpg
|
||||
- word: Kono Oto Tomare! 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kono_Oto_Tomare_2nd_Season.jpg
|
||||
- word: 'Natsume Yuujinchou Movie: Utsusemi ni Musubu'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Natsume_Yuujinchou_Movie_Utsusemi_ni_Musubu.jpg
|
||||
- word: Yahari Ore no Seishun Love Comedy wa Machigatteiru. Kan
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yahari_Ore_no_Seishun_Love_Comedy_wa_Machigatteiru._Kan.jpg
|
||||
- word: Non Non Biyori Nonstop
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Non_Non_Biyori_Nonstop.jpg
|
||||
- word: Usagi Drop
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Usagi_Drop.jpg
|
||||
- word: Baccano!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Baccano.jpg
|
||||
- word: Chihayafuru 2
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Chihayafuru_2.jpg
|
||||
- word: 'Douluo Dalu: Xiaowu Juebie'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Douluo_Dalu_Xiaowu_Juebie.jpg
|
||||
- word: Grand Blue
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Grand_Blue.jpg
|
||||
- word: Houseki no Kuni (TV)
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Houseki_no_Kuni_TV.jpg
|
||||
- word: Hunter x Hunter
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hunter_x_Hunter.jpg
|
||||
- word: 'Kaguya-sama wa Kokurasetai: Tensai-tachi no Renai Zunousen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kaguya-sama_wa_Kokurasetai_Tensai-tachi_no_Renai_Zunousen.jpg
|
||||
- word: Barakamon
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Barakamon.jpg
|
||||
- word: 'Kizumonogatari I: Tekketsu-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kizumonogatari_I_Tekketsu-hen.jpg
|
||||
- word: 'Mushoku Tensei: Isekai Ittara Honki Dasu'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mushoku_Tensei_Isekai_Ittara_Honki_Dasu.jpg
|
||||
- word: Natsume Yuujinchou Roku Specials
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Natsume_Yuujinchou_Roku_Specials.jpg
|
||||
- word: 'Violet Evergarden Gaiden: Eien to Jidou Shuki Ningyou'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Violet_Evergarden_Gaiden_Eien_to_Jidou_Shuki_Ningyou.jpg
|
||||
- word: Shiguang Daili Ren
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shiguang_Daili_Ren.jpg
|
||||
- word: Tensei shitara Slime Datta Ken 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tensei_shitara_Slime_Datta_Ken_2nd_Season.jpg
|
||||
- word: Ano Hi Mita Hana no Namae wo Bokutachi wa Mada Shiranai.
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ano_Hi_Mita_Hana_no_Namae_wo_Bokutachi_wa_Mada_Shiranai..jpg
|
||||
- word: 'Cowboy Bebop: Tengoku no Tobira'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Cowboy_Bebop_Tengoku_no_Tobira.jpg
|
||||
- word: Hellsing Ultimate
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hellsing_Ultimate.jpg
|
||||
- word: Kaze no Tani no Nausica
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kaze_no_Tani_no_Nausica.jpg
|
||||
- word: Luo Xiao Hei Zhan Ji (Movie)
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Luo_Xiao_Hei_Zhan_Ji_Movie.jpg
|
||||
- word: Bakuman. 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Bakuman._2nd_Season.jpg
|
||||
- word: 'Kiseijuu: Sei no Kakuritsu'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kiseijuu_Sei_no_Kakuritsu.jpg
|
||||
- word: 'Kamisama Hajimemashita: Kako-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kamisama_Hajimemashita_Kako-hen.jpg
|
||||
- word: Kingdom 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kingdom_2nd_Season.jpg
|
||||
- word: Kingdom 3rd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kingdom_3rd_Season.jpg
|
||||
- word: Mahou Shoujo Madoka Magica
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mahou_Shoujo_MadokaMagica.jpg
|
||||
- word: Psycho-Pass
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Psycho-Pass.jpg
|
||||
- word: Tenki no Ko
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tenki_no_Ko.jpg
|
||||
- word: Heaven Official's Blessing
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tian_Guan_Ci_Fu.jpg
|
||||
- word: Uchuu Senkan Yamato 2199
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Uchuu_Senkan_Yamato_2199.jpg
|
||||
- word: 'Haikyuu!!: To the Top'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Haikyuu_To_the_Top.jpg
|
||||
- word: Bakemonogatari
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Bakemonogatari.jpg
|
||||
- word: Given
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Given.jpg
|
||||
- word: Hotarubi no Mori e
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hotarubi_no_Mori_e.jpg
|
||||
- word: Katanagatari
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Katanagatari.jpg
|
||||
- word: 'Natsume Yuujinchou: Itsuka Yuki no Hi ni'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Natsume_Yuujinchou_Itsuka_Yuki_no_Hi_ni.jpg
|
||||
- word: One Outs
|
||||
imageUrl: https://cdn.nadeko.bot/animu/One_Outs.jpg
|
||||
- word: Ookami to Koushinryou II
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ookami_to_Koushinryou_II.jpg
|
||||
- word: Romeo no Aoi Sora
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Romeo_no_Aoi_Sora.jpg
|
||||
- word: Sakamichi no Apollon
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Sakamichi_no_Apollon.jpg
|
||||
- word: Seishun Buta Yarou wa Bunny Girl Senpai no Yume wo Minai
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Seishun_Buta_Yarou_wa_Bunny_Girl_Senpai_no_Yume_wo_Minai.jpg
|
||||
- word: Boku dake ga Inai Machi
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Boku_dake_ga_Inai_Machi.jpg
|
||||
- word: 'Evangelion: 2.0 You Can (Not) Advance'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Evangelion_2.0_You_Can_Not_Advance.jpg
|
||||
- word: Kemono no Souja Erin
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kemono_no_Souja_Erin.jpg
|
||||
- word: 'Made in Abyss Movie 2: Hourou Suru Tasogare'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Made_in_Abyss_Movie_2_Hourou_Suru_Tasogare.jpg
|
||||
- word: 'Major: World Series'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Major_World_Series.jpg
|
||||
- word: Doukyuusei (Movie)
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Doukyuusei_Movie.jpg
|
||||
- word: K-On! Movie
|
||||
imageUrl: https://cdn.nadeko.bot/animu/K-On_Movie.jpg
|
||||
- word: Natsume Yuujinchou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Natsume_Yuujinchou.jpg
|
||||
- word: Natsume Yuujinchou Go Specials
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Natsume_Yuujinchou_Go_Specials.jpg
|
||||
- word: NHK ni Youkoso!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/NHK_ni_Youkoso.jpg
|
||||
- word: Shelter
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shelter.jpg
|
||||
- word: Shinsekai yori
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shinsekai_yori.jpg
|
||||
- word: Shirobako
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shirobako.jpg
|
||||
- word: Versailles no Bara
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Versailles_no_Bara.jpg
|
||||
- word: Neon Genesis Evangelion
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Neon_Genesis_Evangelion.jpg
|
||||
- word: Dr. Stone
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Dr._Stone.jpg
|
||||
- word: Fate/Zero
|
||||
imageUrl: https://cdn.nadeko.bot/animu/FateZero.jpg
|
||||
- word: Great Pretender
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Great_Pretender.jpg
|
||||
- word: 'Hunter x Hunter: Original Video Animation'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hunter_x_Hunter_Original_Video_Animation.jpg
|
||||
- word: 'Kino no Tabi: The Beautiful World'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kino_no_Tabi_The_Beautiful_World.jpg
|
||||
- word: Kuroko no Basket 3rd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kuroko_no_Basket_3rd_Season.jpg
|
||||
- word: Bakemono no Ko
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Bakemono_no_Ko.jpg
|
||||
- word: Beck
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Beck.jpg
|
||||
- word: 'Diamond no Ace: Second Season'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Diamond_no_Ace_Second_Season.jpg
|
||||
- word: Nodame Cantabile
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Nodame_Cantabile.jpg
|
||||
- word: 'Rurouni Kenshin: Meiji Kenkaku Romantan'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Rurouni_Kenshin_Meiji_Kenkaku_Romantan.jpg
|
||||
- word: 'Tsubasa: Tokyo Revelations'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tsubasa_Tokyo_Revelations.jpg
|
||||
- word: 'Violet Evergarden: Kitto "Ai" wo Shiru Hi ga Kuru no Darou'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Violet_Evergarden_Kitto_Ai_wo_Shiru_Hi_ga_Kuru_no_Darou.jpg
|
||||
- word: Planetes
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Planetes.jpg
|
||||
- word: 'Stranger: Mukou Hadan'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Stranger_Mukou_Hadan.jpg
|
||||
- word: Yuukoku no Moriarty 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yuukoku_no_Moriarty_2nd_Season.jpg
|
||||
- word: Gin no Saji 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Gin_no_Saji_2nd_Season.jpg
|
||||
- word: Hibike! Euphonium 2
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hibike_Euphonium_2.jpg
|
||||
- word: Initial D First Stage
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Initial_D_First_Stage.jpg
|
||||
- word: Kawaki wo Ameku
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kawaki_wo_Ameku.jpg
|
||||
- word: Koukaku Kidoutai
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Koukaku_Kidoutai.jpg
|
||||
- word: Redline
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Redline.jpg
|
||||
- word: Tenkuu no Shiro Laputa
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tenkuu_no_Shiro_Laputa.jpg
|
||||
- word: Tokyo Godfathers
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tokyo_Godfathers.jpg
|
||||
- word: Tonari no Totoro
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tonari_no_Totoro.jpg
|
||||
- word: 'No Game No Life: Zero'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/No_Game_No_Life_Zero.jpg
|
||||
- word: 'Nomad: Megalo Box 2'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Nomad_Megalo_Box_2.jpg
|
||||
- word: Quanzhi Gaoshou Specials
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Quanzhi_Gaoshou_Specials.jpg
|
||||
- word: Ashita no Joe
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ashita_no_Joe.jpg
|
||||
- word: 'Douluo Dalu: Xingdou Xian Ji Pian'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Douluo_Dalu_Xingdou_Xian_Ji_Pian.jpg
|
||||
- word: 'Gyakkyou Burai Kaiji: Ultimate Survivor'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Gyakkyou_Burai_Kaiji_Ultimate_Survivor.jpg
|
||||
- word: 'Hajime no Ippo: Champion Road'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hajime_no_Ippo_Champion_Road.jpg
|
||||
- word: 'Hunter x Hunter: Greed Island Final'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hunter_x_Hunter_Greed_Island_Final.jpg
|
||||
- word: Re:Zero kara Hajimeru Isekai Seikatsu
|
||||
imageUrl: https://cdn.nadeko.bot/animu/ReZero_kara_Hajimeru_Isekai_Seikatsu.jpg
|
||||
- word: Sennen Joyuu
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Sennen_Joyuu.jpg
|
||||
- word: Stand By Me Doraemon 2
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Stand_By_Me_Doraemon_2.jpg
|
||||
- word: Yuru Camp
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yuru_Camp.jpg
|
||||
- word: 'Nodame Cantabile: Finale'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Nodame_Cantabile_Finale.jpg
|
||||
- word: Ookami to Koushinryou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ookami_to_Koushinryou.jpg
|
||||
- word: Space Dandy 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/SpaceDandy_2nd_Season.jpg
|
||||
- word: Youjo Senki Movie
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Youjo_Senki_Movie.jpg
|
||||
- word: Boku no Hero Academia 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Boku_no_Hero_Academia_2nd_Season.jpg
|
||||
- word: Danshi Koukousei no Nichijou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Danshi_Koukousei_no_Nichijou.jpg
|
||||
- word: Kuroko no Basket 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kuroko_no_Basket_2nd_Season.jpg
|
||||
- word: 'Magi: The Kingdom of Magic'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Magi_The_Kingdom_of_Magic.jpg
|
||||
- word: 'Douluo Dalu: Hanhai Qian Kun'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Douluo_Dalu_Hanhai_Qian_Kun.jpg
|
||||
- word: 'Gyakkyou Burai Kaiji: Hakairoku-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Gyakkyou_Burai_Kaiji_Hakairoku-hen.jpg
|
||||
- word: Hachimitsu to Clover II
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hachimitsu_to_Clover_II.jpg
|
||||
- word: Horimiya
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Horimiya.jpg
|
||||
- word: 'Kuroshitsuji Movie: Book of the Atlantic'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kuroshitsuji_Movie_Book_of_the_Atlantic.jpg
|
||||
- word: 'Non Non Biyori Movie: Vacation'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Non_Non_Biyori_Movie_Vacation.jpg
|
||||
- word: Wu Liuqi Zhi Zui Qiang Fa Xing Shi
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Wu_Liuqi_Zhi_Zui_Qiang_Fa_Xing_Shi.jpg
|
||||
- word: Yahari Ore no Seishun Love Comedy wa Machigatteiru. Zoku
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yahari_Ore_no_Seishun_Love_Comedy_wa_Machigatteiru._Zoku.jpg
|
||||
- word: Shokugeki no Souma
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shokugeki_no_Souma.jpg
|
||||
- word: SKET Dance
|
||||
imageUrl: https://cdn.nadeko.bot/animu/SKET_Dance.jpg
|
||||
- word: Wu Liuqi Zhi Xuanwu Guo Pian
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Wu_Liuqi_Zhi_Xuanwu_Guo_Pian.jpg
|
||||
- word: xxxHOLiC Kei
|
||||
imageUrl: https://cdn.nadeko.bot/animu/xxxHOLiC_Kei.jpg
|
||||
- word: Initial D Final Stage
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Initial_D_Final_Stage.jpg
|
||||
- word: 'Diamond no Ace: Act II'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Diamond_no_Ace_Act_II.jpg
|
||||
- word: 'Hajime no Ippo: Mashiba vs. Kimura'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hajime_no_Ippo_Mashiba_vs._Kimura.jpg
|
||||
- word: Kono Sekai no Katasumi ni
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kono_Sekai_no_Katasumi_ni.jpg
|
||||
- word: Majo no Takkyuubin
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Majo_no_Takkyuubin.jpg
|
||||
- word: Mimi wo Sumaseba
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mimi_wo_Sumaseba.jpg
|
||||
- word: Trigun
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Trigun.jpg
|
||||
- word: 'ReLIFE: Kanketsu-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/ReLIFE_Kanketsu-hen.jpg
|
||||
- word: Toaru Kagaku no Railgun T
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Toaru_Kagaku_no_Railgun_T.jpg
|
||||
- word: xxxHOLiC Rou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/xxxHOLiC_Rou.jpg
|
||||
- word: Yoru wa Mijikashi Arukeyo Otome
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yoru_wa_Mijikashi_Arukeyo_Otome.jpg
|
||||
- word: Bakuman.
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Bakuman..jpg
|
||||
- word: 'Cardcaptor Sakura Movie 2: Fuuin Sareta Card'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Cardcaptor_Sakura_Movie_2_Fuuin_Sareta_Card.jpg
|
||||
- word: Chihayafuru
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Chihayafuru.jpg
|
||||
- word: 'Douluo Dalu: Qian Hua Xi Jin'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Douluo_Dalu_Qian_Hua_Xi_Jin.jpg
|
||||
- word: 'Ginga Eiyuu Densetsu: Die Neue These - Seiran 3'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ginga_Eiyuu_Densetsu_Die_Neue_These_-_Seiran_3.jpg
|
||||
- word: Kaguya-hime no Monogatari
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kaguya-hime_no_Monogatari.jpg
|
||||
- word: 'Little Busters!: Refrain'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Little_Busters_Refrain.jpg
|
||||
- word: Dororo
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Dororo.jpg
|
||||
- word: 'Dr. Stone: Stone Wars'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Dr._Stone_Stone_Wars.jpg
|
||||
- word: 'Fate/stay night: Unlimited Blade Works'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Fatestay_night_Unlimited_Blade_Works.jpg
|
||||
- word: Girls & Panzer Movie
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Girls__Panzer_Movie.jpg
|
||||
- word: Golden Kamuy 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Golden_Kamuy_2nd_Season.jpg
|
||||
- word: Higurashi no Naku Koro ni Kai
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Higurashi_no_Naku_Koro_ni_Kai.jpg
|
||||
- word: 'InuYasha: Kanketsu-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/InuYasha_Kanketsu-hen.jpg
|
||||
- word: 'Saiki Kusuo no Ψ-nan: Kanketsu-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Saiki_Kusuo_no_-nan_Kanketsu-hen.jpg
|
||||
- word: 'One Piece Movie 14: Stampede'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/One_Piece_Movie_14_Stampede.jpg
|
||||
- word: 'One Piece: Episode of Merry - Mou Hitori no Nakama no Monogatari'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/One_Piece_Episode_of_Merry_-_Mou_Hitori_no_Nakama_no_Monogatari.jpg
|
||||
- word: Shoujo Kakumei Utena
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shoujo_Kakumei_Utena.jpg
|
||||
- word: Ballroom e Youkoso
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ballroom_e_Youkoso.jpg
|
||||
- word: 'Berserk: Ougon Jidai-hen III - Kourin'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Berserk_Ougon_Jidai-hen_III_-_Kourin.jpg
|
||||
- word: Bungou Stray Dogs 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Bungou_Stray_Dogs_2nd_Season.jpg
|
||||
- word: 'Douluo Dalu: Haishen Zhi Guang'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Douluo_Dalu_Haishen_Zhi_Guang.jpg
|
||||
- word: Fruits Basket 1st Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Fruits_Basket_1st_Season.jpg
|
||||
- word: 'Hunter x Hunter: Greed Island'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hunter_x_Hunter_Greed_Island.jpg
|
||||
- word: Liz to Aoi Tori
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Liz_to_Aoi_Tori.jpg
|
||||
- word: Aria the Natural
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Aria_the_Natural.jpg
|
||||
- word: Asobi Asobase
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Asobi_Asobase.jpg
|
||||
- word: 'Black Lagoon: The Second Barrage'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Black_Lagoon_The_Second_Barrage.jpg
|
||||
- word: Bungou Stray Dogs 3rd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Bungou_Stray_Dogs_3rd_Season.jpg
|
||||
- word: Death Parade
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Death_Parade.jpg
|
||||
- word: 'Digimon Adventure: Last Evolution Kizuna'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Digimon_Adventure_Last_Evolution_Kizuna.jpg
|
||||
- word: Hinamatsuri (TV)
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hinamatsuri_TV.jpg
|
||||
- word: "Kyoukai no Kanata Movie 2: I'll Be Here - Mirai-hen"
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kyoukai_no_Kanata_Movie_2_Ill_Be_Here_-_Mirai-hen.jpg
|
||||
- word: Maison Ikkoku
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Maison_Ikkoku.jpg
|
||||
- word: 'Naruto: Shippuuden'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Naruto_Shippuuden.jpg
|
||||
- word: Non Non Biyori Repeat
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Non_Non_Biyori_Repeat.jpg
|
||||
- word: Noragami Aragoto
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Noragami_Aragoto.jpg
|
||||
- word: Ouran Koukou Host Club
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ouran_Koukou_Host_Club.jpg
|
||||
- word: Senki Zesshou Symphogear XV
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Senki_Zesshou_Symphogear_XV.jpg
|
||||
- word: Shoujo Shuumatsu Ryokou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shoujo_Shuumatsu_Ryokou.jpg
|
||||
- word: Toradora!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Toradora.jpg
|
||||
- word: 'Working!!!: Lord of the Takanashi'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Working_Lord_of_the_Takanashi.jpg
|
||||
- word: Boku no Hero Academia 3rd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Boku_no_Hero_Academia_3rd_Season.jpg
|
||||
- word: 'Douluo Dalu: Jingying Sai'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Douluo_Dalu_Jingying_Sai.jpg
|
||||
- word: 5-toubun no Hanayome ∬
|
||||
imageUrl: https://cdn.nadeko.bot/animu/5-toubun_no_Hanayome_.jpg
|
||||
- word: Akira
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Akira.jpg
|
||||
- word: Gankutsuou
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Gankutsuou.jpg
|
||||
- word: Kamisama Hajimemashita◎
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kamisama_Hajimemashita.jpg
|
||||
- word: 'Lupin III: Part 5'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Lupin_III_Part_5.jpg
|
||||
- word: Mo Dao Zu Shi Q
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mo_Dao_Zu_Shi_Q.jpg
|
||||
- word: Nisemonogatari
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Nisemonogatari.jpg
|
||||
- word: 'One Piece Film: Z'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/One_Piece_Film_Z.jpg
|
||||
- word: Quanzhi Gaoshou Zhi Dianfeng Rongyao
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Quanzhi_Gaoshou_Zhi_Dianfeng_Rongyao.jpg
|
||||
- word: Toki wo Kakeru Shoujo
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Toki_wo_Kakeru_Shoujo.jpg
|
||||
- word: No Game No Life
|
||||
imageUrl: https://cdn.nadeko.bot/animu/No_Game_No_Life.jpg
|
||||
- word: 'Nodame Cantabile: Paris-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Nodame_Cantabile_Paris-hen.jpg
|
||||
- word: Sakura-sou no Pet na Kanojo
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Sakura-sou_no_Pet_na_Kanojo.jpg
|
||||
- word: Seirei no Moribito
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Seirei_no_Moribito.jpg
|
||||
- word: 'Shokugeki no Souma: Ni no Sara'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shokugeki_no_Souma_Ni_no_Sara.jpg
|
||||
- word: Cardcaptor Sakura
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Cardcaptor_Sakura.jpg
|
||||
- word: Detective Conan
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Detective_Conan.jpg
|
||||
- word: Durarara!!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Durarara.jpg
|
||||
- word: Eizouken ni wa Te wo Dasu na!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Eizouken_ni_wa_Te_wo_Dasu_na.jpg
|
||||
- word: Fate/Grand Carnival
|
||||
imageUrl: https://cdn.nadeko.bot/animu/FateGrand_Carnival.jpg
|
||||
- word: Kaiba
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kaiba.jpg
|
||||
- word: Katekyo Hitman Reborn!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Katekyo_Hitman_Reborn.jpg
|
||||
- word: "Mahou Shoujo Lyrical Nanoha: The Movie 2nd A's"
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mahou_Shoujo_Lyrical_Nanoha_The_Movie_2nd_As.jpg
|
||||
- word: Dragon Ball Z
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Dragon_Ball_Z.jpg
|
||||
- word: Fullmetal Alchemist
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Fullmetal_Alchemist.jpg
|
||||
- word: Ginga Eiyuu Densetsu Gaiden
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ginga_Eiyuu_Densetsu_Gaiden.jpg
|
||||
- word: Given Movie
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Given_Movie.jpg
|
||||
- word: K-On!!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/K-On.jpg
|
||||
- word: 'Lupin III: Cagliostro no Shiro'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Lupin_III_Cagliostro_no_Shiro.jpg
|
||||
- word: 'One Piece Film: Strong World'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/One_Piece_Film_Strong_World.jpg
|
||||
- word: Tanoshii Muumin Ikka
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tanoshii_Muumin_Ikka.jpg
|
||||
- word: 'One Piece: Episode of Nami - Koukaishi no Namida to Nakama no Kizuna'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/One_Piece_Episode_of_Nami_-_Koukaishi_no_Namida_to_Nakama_no_Kizuna.jpg
|
||||
- word: Princess Tutu
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Princess_Tutu.jpg
|
||||
- word: Tokyo Revengers
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tokyo_Revengers.jpg
|
||||
- word: Tsuki ga Kirei
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tsuki_ga_Kirei.jpg
|
||||
- word: 'Chuunibyou demo Koi ga Shitai! Movie: Take On Me'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Chuunibyou_demo_Koi_ga_Shitai_Movie_Take_On_Me.jpg
|
||||
- word: 'Douluo Dalu: Hao Tian Yang Wei'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Douluo_Dalu_Hao_Tian_Yang_Wei.jpg
|
||||
- word: 'Honzuki no Gekokujou: Shisho ni Naru Tame ni wa Shudan wo Erandeiraremasen 2nd Season'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Honzuki_no_Gekokujou_Shisho_ni_Naru_Tame_ni_wa_Shudan_wo_Erandeiraremasen_2nd_Season.jpg
|
||||
- word: Initial D Fourth Stage
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Initial_D_Fourth_Stage.jpg
|
||||
- word: 'Interstella5555: The 5tory of The 5ecret 5tar 5ystem'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Interstella5555_The_5tory_of_The_5ecret_5tar_5ystem.jpg
|
||||
- word: Kono Subarashii Sekai ni Shukufuku wo!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kono_Subarashii_Sekai_ni_Shukufuku_wo.jpg
|
||||
- word: 'Made in Abyss Movie 1: Tabidachi no Yoake'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Made_in_Abyss_Movie_1_Tabidachi_no_Yoake.jpg
|
||||
- word: Baccano! Specials
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Baccano_Specials.jpg
|
||||
- word: Detroit Metal City
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Detroit_Metal_City.jpg
|
||||
- word: Hyouka
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hyouka.jpg
|
||||
- word: Kanata no Astra
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kanata_no_Astra.jpg
|
||||
- word: 'Koukaku Kidoutai: Stand Alone Complex - Solid State Society'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Koukaku_Kidoutai_Stand_Alone_Complex_-_Solid_State_Society.jpg
|
||||
- word: Kuragehime
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kuragehime.jpg
|
||||
- word: 'Mahoutsukai no Yome: Hoshi Matsu Hito'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mahoutsukai_no_Yome_Hoshi_Matsu_Hito.jpg
|
||||
- word: Mobile Suit Gundam 00
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mobile_Suit_Gundam_00.jpg
|
||||
- word: Tsukimonogatari
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tsukimonogatari.jpg
|
||||
- word: Uchouten Kazoku 2
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Uchouten_Kazoku_2.jpg
|
||||
- word: Pui Pui Molcar
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Pui_Pui_Molcar.jpg
|
||||
- word: 'Saiki Kusuo no Ψ-nan: Ψ-shidou-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Saiki_Kusuo_no_-nan_-shidou-hen.jpg
|
||||
- word: 'Tsubasa: Shunraiki'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tsubasa_Shunraiki.jpg
|
||||
- word: Zankyou no Terror
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Zankyou_no_Terror.jpg
|
||||
- word: Angel Beats!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Angel_Beats.jpg
|
||||
- word: 'Ginga Eiyuu Densetsu: Arata Naru Tatakai no Overture'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ginga_Eiyuu_Densetsu_Arata_Naru_Tatakai_no_Overture.jpg
|
||||
- word: 'IDOLiSH7: Second Beat!'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/IDOLiSH7_Second_Beat.jpg
|
||||
- word: Initial D Second Stage
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Initial_D_Second_Stage.jpg
|
||||
- word: Kuroko no Basket
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kuroko_no_Basket.jpg
|
||||
- word: Ansatsu Kyoushitsu
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ansatsu_Kyoushitsu.jpg
|
||||
- word: Diamond no Ace
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Diamond_no_Ace.jpg
|
||||
- word: 'Dragon Ball Super: Broly'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Dragon_Ball_Super_Broly.jpg
|
||||
- word: 'Haikyuu!! Movie 4: Concept no Tatakai'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Haikyuu_Movie_4_Concept_no_Tatakai.jpg
|
||||
- word: Karakai Jouzu no Takagi-san 2
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Karakai_Jouzu_no_Takagi-san_2.jpg
|
||||
- word: Kaze Tachinu
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kaze_Tachinu.jpg
|
||||
- word: Skip Beat!
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Skip_Beat.jpg
|
||||
- word: 'Saint Seiya: The Lost Canvas - Meiou Shinwa 2'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Saint_Seiya_The_Lost_Canvas_-_Meiou_Shinwa_2.jpg
|
||||
- word: 'Tamayura: Sotsugyou Shashin Part 4 - Ashita'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tamayura_Sotsugyou_Shashin_Part_4_-_Ashita.jpg
|
||||
- word: Wonder Egg Priority
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Wonder_Egg_Priority.jpg
|
||||
- word: World Trigger 2nd Season
|
||||
imageUrl: https://cdn.nadeko.bot/animu/World_Trigger_2nd_Season.jpg
|
||||
- word: 'Yowamushi Pedal: Grande Road'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yowamushi_Pedal_Grande_Road.jpg
|
||||
- word: 'Darker than Black: Kuro no Keiyakusha'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Darker_than_Black_Kuro_no_Keiyakusha.jpg
|
||||
- word: 'Evangelion: 3.0+1.0 Thrice Upon a Time'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Evangelion_3.01.0_Thrice_Upon_a_Time.jpg
|
||||
- word: Gin no Saji
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Gin_no_Saji.jpg
|
||||
- word: 'Hajime no Ippo: Boxer no Kobushi'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hajime_no_Ippo_Boxer_no_Kobushi.jpg
|
||||
- word: Hikaru no Go
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hikaru_no_Go.jpg
|
||||
- word: 'JoJo no Kimyou na Bouken Part 3: Stardust Crusaders'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/JoJo_no_Kimyou_na_Bouken_Part_3_Stardust_Crusaders.jpg
|
||||
- word: 'Kamisama Hajimemashita: Kamisama, Shiawase ni Naru'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kamisama_Hajimemashita_Kamisama_Shiawase_ni_Naru.jpg
|
||||
- word: 'Kuroko no Basket: Saikou no Present Desu'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kuroko_no_Basket_Saikou_no_Present_Desu.jpg
|
||||
- word: 'Kuroshitsuji: Book of Circus'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kuroshitsuji_Book_of_Circus.jpg
|
||||
- word: Akatsuki no Yona OVA
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Akatsuki_no_Yona_OVA.jpg
|
||||
- word: Dorohedoro
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Dorohedoro.jpg
|
||||
- word: Durarara!!x2 Ketsu
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Durararax2_Ketsu.jpg
|
||||
- word: 'Ginga Eiyuu Densetsu: Die Neue These - Seiran 2'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ginga_Eiyuu_Densetsu_Die_Neue_These_-_Seiran_2.jpg
|
||||
- word: Gosick
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Gosick.jpg
|
||||
- word: 'Hidamari Sketch: Sae Hiro Sotsugyou-hen'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Hidamari_Sketch_Sae_Hiro_Sotsugyou-hen.jpg
|
||||
- word: 'Koukaku Kidoutai: Stand Alone Complex - The Laughing Man'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Koukaku_Kidoutai_Stand_Alone_Complex_-_The_Laughing_Man.jpg
|
||||
- word: 'Kuroshitsuji: Book of Murder'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kuroshitsuji_Book_of_Murder.jpg
|
||||
- word: Mirai Shounen Conan
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Mirai_Shounen_Conan.jpg
|
||||
- word: Omoide no Marnie
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Omoide_no_Marnie.jpg
|
||||
- word: Shijou Saikyou no Deshi Kenichi
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shijou_Saikyou_no_Deshi_Kenichi.jpg
|
||||
- word: 'Shokugeki no Souma: San no Sara'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Shokugeki_no_Souma_San_no_Sara.jpg
|
||||
- word: Tensei shitara Slime Datta Ken
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Tensei_shitara_Slime_Datta_Ken.jpg
|
||||
- word: 'Ramayana: The Legend of Prince Rama'
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ramayana_The_Legend_of_Prince_Rama.jpg
|
||||
- word: Summer Wars
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Summer_Wars.jpg
|
||||
- word: Yuusha-Ou GaoGaiGar Final
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Yuusha-Ou_GaoGaiGar_Final.jpg
|
||||
- word: Dennou Coil
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Dennou_Coil.jpg
|
||||
- word: Ginga Eiyuu Densetsu Gaiden (1999)
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Ginga_Eiyuu_Densetsu_Gaiden_1999.jpg
|
||||
- word: Glass no Kamen (2005)
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Glass_no_Kamen_2005.jpg
|
||||
- word: Kill la Kill
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Kill_la_Kill.jpg
|
||||
- word: Koukyoushihen Eureka Seven
|
||||
imageUrl: https://cdn.nadeko.bot/animu/Koukyoushihen_Eureka_Seven.jpg
|
390
src/EllieBot/data/hangman/countries.yml
Normal file
|
@ -0,0 +1,390 @@
|
|||
- word: Afghanistan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/af-flag.gif
|
||||
- word: Albania
|
||||
imageUrl: https://cdn.nadeko.bot/flags/al-flag.gif
|
||||
- word: Algeria
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ag-flag.gif
|
||||
- word: Andorra
|
||||
imageUrl: https://cdn.nadeko.bot/flags/an-flag.gif
|
||||
- word: Angola
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ao-flag.gif
|
||||
- word: Antigua and Barbuda
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ac-flag.gif
|
||||
- word: Argentina
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ar-flag.gif
|
||||
- word: Armenia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/am-flag.gif
|
||||
- word: Australia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/as-flag.gif
|
||||
- word: Austria
|
||||
imageUrl: https://cdn.nadeko.bot/flags/au-flag.gif
|
||||
- word: Azerbaijan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/aj-flag.gif
|
||||
- word: Bahamas
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bf-flag.gif
|
||||
- word: Bahrain
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ba-flag.gif
|
||||
- word: Bangladesh
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bg-flag.gif
|
||||
- word: Barbados
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bb-flag.gif
|
||||
- word: Belarus
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bo-flag.gif
|
||||
- word: Belgium
|
||||
imageUrl: https://cdn.nadeko.bot/flags/be-flag.gif
|
||||
- word: Belize
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bh-flag.gif
|
||||
- word: Benin
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bn-flag.gif
|
||||
- word: Bhutan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bt-flag.gif
|
||||
- word: Bolivia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bl-flag.gif
|
||||
- word: Bosnia and Herzegovina
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bk-flag.gif
|
||||
- word: Botswana
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bc-flag.gif
|
||||
- word: Brazil
|
||||
imageUrl: https://cdn.nadeko.bot/flags/br-flag.gif
|
||||
- word: Brunei
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bx-flag.gif
|
||||
- word: Bulgaria
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bu-flag.gif
|
||||
- word: Burkina Faso
|
||||
imageUrl: https://cdn.nadeko.bot/flags/uv-flag.gif
|
||||
- word: Burundi
|
||||
imageUrl: https://cdn.nadeko.bot/flags/by-flag.gif
|
||||
- word: Ivory Coast
|
||||
imageUrl: https://cdn.nadeko.bot/flags/iv-flag.gif
|
||||
- word: Cabo Verde
|
||||
imageUrl: https://cdn.nadeko.bot/flags/cv-flag.gif
|
||||
- word: Cambodia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/cb-flag.gif
|
||||
- word: Cameroon
|
||||
imageUrl: https://cdn.nadeko.bot/flags/cm-flag.gif
|
||||
- word: Canada
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ca-flag.gif
|
||||
- word: Central African Republic
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ct-flag.gif
|
||||
- word: Chad
|
||||
imageUrl: https://cdn.nadeko.bot/flags/cd-flag.gif
|
||||
- word: Chile
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ci-flag.gif
|
||||
- word: China
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ch-flag.gif
|
||||
- word: Colombia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/co-flag.gif
|
||||
- word: Comoros
|
||||
imageUrl: https://cdn.nadeko.bot/flags/cn-flag.gif
|
||||
- word: Congo
|
||||
imageUrl: https://cdn.nadeko.bot/flags/cg-flag.gif
|
||||
- word: Costa Rica
|
||||
imageUrl: https://cdn.nadeko.bot/flags/cs-flag.gif
|
||||
- word: Croatia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/hr-flag.gif
|
||||
- word: Cuba
|
||||
imageUrl: https://cdn.nadeko.bot/flags/cu-flag.gif
|
||||
- word: Cyprus
|
||||
imageUrl: https://cdn.nadeko.bot/flags/cy-flag.gif
|
||||
- word: Czechia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ez-flag.gif
|
||||
- word: Denmark
|
||||
imageUrl: https://cdn.nadeko.bot/flags/da-flag.gif
|
||||
- word: Djibouti
|
||||
imageUrl: https://cdn.nadeko.bot/flags/dj-flag.gif
|
||||
- word: Dominica
|
||||
imageUrl: https://cdn.nadeko.bot/flags/do-flag.gif
|
||||
- word: Dominican Republic
|
||||
imageUrl: https://cdn.nadeko.bot/flags/dr-flag.gif
|
||||
- word: Democratic People's Republic of Korea
|
||||
imageUrl: https://cdn.nadeko.bot/flags/kn-flag.gif
|
||||
- word: Democratic Republic of the Congo
|
||||
imageUrl: https://cdn.nadeko.bot/flags/congo-flag.gif
|
||||
- word: Ecuador
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ec-flag.gif
|
||||
- word: Egypt
|
||||
imageUrl: https://cdn.nadeko.bot/flags/eg-flag.gif
|
||||
- word: El Salvador
|
||||
imageUrl: https://cdn.nadeko.bot/flags/es-flag.gif
|
||||
- word: Equatorial Guinea
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ek-flag.gif
|
||||
- word: Eritrea
|
||||
imageUrl: https://cdn.nadeko.bot/flags/er-flag.gif
|
||||
- word: Estonia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/en-flag.gif
|
||||
- word: Eswatini
|
||||
imageUrl: https://cdn.nadeko.bot/flags/wz-flag.gif
|
||||
- word: Ethiopia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/et-flag.gif
|
||||
- word: Fiji
|
||||
imageUrl: https://cdn.nadeko.bot/flags/fj-flag.gif
|
||||
- word: Finland
|
||||
imageUrl: https://cdn.nadeko.bot/flags/fi-flag.gif
|
||||
- word: France
|
||||
imageUrl: https://cdn.nadeko.bot/flags/fr-flag.gif
|
||||
- word: Gabon
|
||||
imageUrl: https://cdn.nadeko.bot/flags/gb-flag.gif
|
||||
- word: Gambia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ga-flag.gif
|
||||
- word: Georgia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/gg-flag.gif
|
||||
- word: Germany
|
||||
imageUrl: https://cdn.nadeko.bot/flags/gm-flag.gif
|
||||
- word: Ghana
|
||||
imageUrl: https://cdn.nadeko.bot/flags/gh-flag.gif
|
||||
- word: Greece
|
||||
imageUrl: https://cdn.nadeko.bot/flags/gr-flag.gif
|
||||
- word: Grenada
|
||||
imageUrl: https://cdn.nadeko.bot/flags/gj-flag.gif
|
||||
- word: Guatemala
|
||||
imageUrl: https://cdn.nadeko.bot/flags/gt-flag.gif
|
||||
- word: Guinea
|
||||
imageUrl: https://cdn.nadeko.bot/flags/gv-flag.gif
|
||||
- word: Guinea-Bissau
|
||||
imageUrl: https://cdn.nadeko.bot/flags/pu-flag.gif
|
||||
- word: Guyana
|
||||
imageUrl: https://cdn.nadeko.bot/flags/gy-flag.gif
|
||||
- word: Haiti
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ha-flag.gif
|
||||
- word: Holy See
|
||||
imageUrl: https://cdn.nadeko.bot/flags/vt-flag.gif
|
||||
- word: Honduras
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ho-flag.gif
|
||||
- word: Hungary
|
||||
imageUrl: https://cdn.nadeko.bot/flags/hu-flag.gif
|
||||
- word: Iceland
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ic-flag.gif
|
||||
- word: India
|
||||
imageUrl: https://cdn.nadeko.bot/flags/in-flag.gif
|
||||
- word: Indonesia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/id-flag.gif
|
||||
- word: Iran
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ir-flag.gif
|
||||
- word: Iraq
|
||||
imageUrl: https://cdn.nadeko.bot/flags/iz-flag.gif
|
||||
- word: Ireland
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ei-flag.gif
|
||||
- word: Israel
|
||||
imageUrl: https://cdn.nadeko.bot/flags/is-flag.gif
|
||||
- word: Italy
|
||||
imageUrl: https://cdn.nadeko.bot/flags/it-flag.gif
|
||||
- word: Jamaica
|
||||
imageUrl: https://cdn.nadeko.bot/flags/jm-flag.gif
|
||||
- word: Japan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ja-flag.gif
|
||||
- word: Jordan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/jo-flag.gif
|
||||
- word: Kazakhstan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/kz-flag.gif
|
||||
- word: Kenya
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ke-flag.gif
|
||||
- word: Kiribati
|
||||
imageUrl: https://cdn.nadeko.bot/flags/kr-flag.gif
|
||||
- word: Kuwait
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ku-flag.gif
|
||||
- word: Kyrgyzstan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/kg-flag.gif
|
||||
- word: Laos
|
||||
imageUrl: https://cdn.nadeko.bot/flags/la-flag.gif
|
||||
- word: Latvia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/lg-flag.gif
|
||||
- word: Lebanon
|
||||
imageUrl: https://cdn.nadeko.bot/flags/le-flag.gif
|
||||
- word: Lesotho
|
||||
imageUrl: https://cdn.nadeko.bot/flags/lt-flag.gif
|
||||
- word: Liberia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/li-flag.gif
|
||||
- word: Libya
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ly-flag.gif
|
||||
- word: Liechtenstein
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ls-flag.gif
|
||||
- word: Lithuania
|
||||
imageUrl: https://cdn.nadeko.bot/flags/lh-flag.gif
|
||||
- word: Luxembourg
|
||||
imageUrl: https://cdn.nadeko.bot/flags/lu-flag.gif
|
||||
- word: Madagascar
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ma-flag.gif
|
||||
- word: Malawi
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mi-flag.gif
|
||||
- word: Malaysia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/my-flag.gif
|
||||
- word: Maldives
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mv-flag.gif
|
||||
- word: Mali
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ml-flag.gif
|
||||
- word: Malta
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mt-flag.gif
|
||||
- word: Marshall Islands
|
||||
imageUrl: https://cdn.nadeko.bot/flags/rm-flag.gif
|
||||
- word: Mauritania
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mr-flag.gif
|
||||
- word: Mauritius
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mp-flag.gif
|
||||
- word: Mexico
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mx-flag.gif
|
||||
- word: Micronesia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/fm-flag.gif
|
||||
- word: Moldova
|
||||
imageUrl: https://cdn.nadeko.bot/flags/md-flag.gif
|
||||
- word: Monaco
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mn-flag.gif
|
||||
- word: Mongolia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mg-flag.gif
|
||||
- word: Montenegro
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mj-flag.gif
|
||||
- word: Morocco
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mo-flag.gif
|
||||
- word: Mozambique
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mz-flag.gif
|
||||
- word: Myanmar
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bm-flag.gif
|
||||
- word: Namibia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/wa-flag.gif
|
||||
- word: Nauru
|
||||
imageUrl: https://cdn.nadeko.bot/flags/nr-flag.gif
|
||||
- word: Nepal
|
||||
imageUrl: https://cdn.nadeko.bot/flags/np-flag.gif
|
||||
- word: Netherlands
|
||||
imageUrl: https://cdn.nadeko.bot/flags/nl-flag.gif
|
||||
- word: New Zealand
|
||||
imageUrl: https://cdn.nadeko.bot/flags/nz-flag.gif
|
||||
- word: Nicaragua
|
||||
imageUrl: https://cdn.nadeko.bot/flags/nu-flag.gif
|
||||
- word: Niger
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ng-flag.gif
|
||||
- word: Nigeria
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ni-flag.gif
|
||||
- word: North Macedonia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mk-flag.gif
|
||||
- word: Norway
|
||||
imageUrl: https://cdn.nadeko.bot/flags/no-flag.gif
|
||||
- word: Oman
|
||||
imageUrl: https://cdn.nadeko.bot/flags/mu-flag.gif
|
||||
- word: Pakistan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/pk-flag.gif
|
||||
- word: Palau
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ps-flag.gif
|
||||
- word: Panama
|
||||
imageUrl: https://cdn.nadeko.bot/flags/pm-flag.gif
|
||||
- word: Papua New Guinea
|
||||
imageUrl: https://cdn.nadeko.bot/flags/pp-flag.gif
|
||||
- word: Paraguay
|
||||
imageUrl: https://cdn.nadeko.bot/flags/pa-flag.gif
|
||||
- word: Peru
|
||||
imageUrl: https://cdn.nadeko.bot/flags/pe-flag.gif
|
||||
- word: Philippines
|
||||
imageUrl: https://cdn.nadeko.bot/flags/rp-flag.gif
|
||||
- word: Poland
|
||||
imageUrl: https://cdn.nadeko.bot/flags/pl-flag.gif
|
||||
- word: Portugal
|
||||
imageUrl: https://cdn.nadeko.bot/flags/po-flag.gif
|
||||
- word: Qatar
|
||||
imageUrl: https://cdn.nadeko.bot/flags/qa-flag.gif
|
||||
- word: Romania
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ro-flag.gif
|
||||
- word: Russia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/rs-flag.gif
|
||||
- word: Rwanda
|
||||
imageUrl: https://cdn.nadeko.bot/flags/rw-flag.gif
|
||||
- word: Saint Kitts and Nevis
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sc-flag.gif
|
||||
- word: Saint Lucia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/st-flag.gif
|
||||
- word: Samoa
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ws-flag.gif
|
||||
- word: San Marino
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sm-flag.gif
|
||||
- word: Sao Tome and Principe
|
||||
imageUrl: https://cdn.nadeko.bot/flags/tp-flag.gif
|
||||
- word: Saudi Arabia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sa-flag.gif
|
||||
- word: Senegal
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sg-flag.gif
|
||||
- word: Serbia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ri-flag.gif
|
||||
- word: Seychelles
|
||||
imageUrl: https://cdn.nadeko.bot/flags/se-flag.gif
|
||||
- word: Sierra Leone
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sl-flag.gif
|
||||
- word: Singapore
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sn-flag.gif
|
||||
- word: Slovakia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/lo-flag.gif
|
||||
- word: Slovenia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/si-flag.gif
|
||||
- word: Solomon Islands
|
||||
imageUrl: https://cdn.nadeko.bot/flags/bp-flag.gif
|
||||
- word: Somalia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/so-flag.gif
|
||||
- word: South Africa
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sf-flag.gif
|
||||
- word: South Korea
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ks-flag.gif
|
||||
- word: South Sudan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/od-flag.gif
|
||||
- word: Spain
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sp-flag.gif
|
||||
- word: Sri Lanka
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ce-flag.gif
|
||||
- word: St. Vincent Grenadines
|
||||
imageUrl: https://cdn.nadeko.bot/flags/vc-flag.gif
|
||||
- word: State of Palestine
|
||||
imageUrl: https://cdn.nadeko.bot/flags/palestine-flag.gif
|
||||
- word: Sudan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/su-flag.gif
|
||||
- word: Suriname
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ns-flag.gif
|
||||
- word: Sweden
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sw-flag.gif
|
||||
- word: Switzerland
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sz-flag.gif
|
||||
- word: Syria
|
||||
imageUrl: https://cdn.nadeko.bot/flags/sy-flag.gif
|
||||
- word: Tajikistan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ti-flag.gif
|
||||
- word: Tanzania
|
||||
imageUrl: https://cdn.nadeko.bot/flags/tz-flag.gif
|
||||
- word: Thailand
|
||||
imageUrl: https://cdn.nadeko.bot/flags/th-flag.gif
|
||||
- word: Timor-Leste
|
||||
imageUrl: https://cdn.nadeko.bot/flags/tt-flag.gif
|
||||
- word: Togo
|
||||
imageUrl: https://cdn.nadeko.bot/flags/to-flag.gif
|
||||
- word: Tonga
|
||||
imageUrl: https://cdn.nadeko.bot/flags/tn-flag.gif
|
||||
- word: Trinidad and Tobago
|
||||
imageUrl: https://cdn.nadeko.bot/flags/td-flag.gif
|
||||
- word: Tunisia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ts-flag.gif
|
||||
- word: Turkey
|
||||
imageUrl: https://cdn.nadeko.bot/flags/tu-flag.gif
|
||||
- word: Turkmenistan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/tx-flag.gif
|
||||
- word: Tuvalu
|
||||
imageUrl: https://cdn.nadeko.bot/flags/tv-flag.gif
|
||||
- word: United Arab Emirates
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ae-flag.gif
|
||||
- word: United Kingdom
|
||||
imageUrl: https://cdn.nadeko.bot/flags/uk-flag.gif
|
||||
- word: United States Of America
|
||||
imageUrl: https://cdn.nadeko.bot/flags/us-flag.gif
|
||||
- word: Uganda
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ug-flag.gif
|
||||
- word: Ukraine
|
||||
imageUrl: https://cdn.nadeko.bot/flags/up-flag.gif
|
||||
- word: Uruguay
|
||||
imageUrl: https://cdn.nadeko.bot/flags/uy-flag.gif
|
||||
- word: Uzbekistan
|
||||
imageUrl: https://cdn.nadeko.bot/flags/uz-flag.gif
|
||||
- word: Vanuatu
|
||||
imageUrl: https://cdn.nadeko.bot/flags/nh-flag.gif
|
||||
- word: Venezuela
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ve-flag.gif
|
||||
- word: Vietnam
|
||||
imageUrl: https://cdn.nadeko.bot/flags/vm-flag.gif
|
||||
- word: Yemen
|
||||
imageUrl: https://cdn.nadeko.bot/flags/ym-flag.gif
|
||||
- word: Zambia
|
||||
imageUrl: https://cdn.nadeko.bot/flags/za-flag.gif
|
||||
- word: Zimbabwe
|
||||
imageUrl: https://cdn.nadeko.bot/flags/zi-flag.gif
|
400
src/EllieBot/data/hangman/movies.yml
Normal file
|
@ -0,0 +1,400 @@
|
|||
- word: 'Underworld: Blood Wars'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/PIXSMakrO3s2dqA7mCvAAoVR0E.jpg
|
||||
- word: Fantastic Beasts and Where to Find Them
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6I2tPx6KIiBB4TWFiWwNUzrbxUn.jpg
|
||||
- word: Suicide Squad
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/34dxtTxMHGKw1njHpTjDqR8UBHd.jpg
|
||||
- word: Miss Peregrine's Home for Peculiar Children
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/qXQinDhDZkTiqEGLnav0h1YSUu8.jpg
|
||||
- word: Sully
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/vC9H1ZVdXi1KjH4aPfGB54mvDNh.jpg
|
||||
- word: Arrival
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/yIZ1xendyqKvY3FGeeUYUd5X9Mm.jpg
|
||||
- word: Doctor Strange
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/tFI8VLMgSTTU38i8TIsklfqS9Nl.jpg
|
||||
- word: 'Mad Max: Fury Road'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/tbhdm8UJAb4ViCTsulYFL3lxMCd.jpg
|
||||
- word: Interstellar
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/xu9zaAevzQ5nnrsXN6JcahLnG4i.jpg
|
||||
- word: Jason Bourne
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/AoT2YrJUJlg5vKE3iMOLvHlTd3m.jpg
|
||||
- word: 'Captain America: Civil War'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/m5O3SZvQ6EgD5XXXLPIP1wLppeW.jpg
|
||||
- word: Moana
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/1qGzqGUd1pa05aqYXGSbLkiBlLB.jpg
|
||||
- word: 'The Hunger Games: Mockingjay - Part 1'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/83nHcz2KcnEpPXY50Ky2VldewJJ.jpg
|
||||
- word: Underworld
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/cPhRPAJWK8BuuJqqf6PztzvOlnZ.jpg
|
||||
- word: The Secret Life of Pets
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/lubzBMQLLmG88CLQ4F3TxZr2Q7N.jpg
|
||||
- word: Insurgent
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/L5QRL1O3fGs2hH1LbtYyVl8Tce.jpg
|
||||
- word: Jurassic World
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/dkMD5qlogeRMiEixC4YNPUvax2T.jpg
|
||||
- word: Ben-Hur
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/A4xbEpe9LevQCdvaNC0z6r8AfYk.jpg
|
||||
- word: Finding Dory
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/iWRKYHTFlsrxQtfQqFOQyceL83P.jpg
|
||||
- word: Guardians of the Galaxy
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/bHarw8xrmQeqf3t8HpuMY7zoK4x.jpg
|
||||
- word: Ghostbusters
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/58bvfg9b040GDmKffLUJsEjg779.jpg
|
||||
- word: Inferno
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/anmLLbDx9d98NMZRyVUtxwJR6ab.jpg
|
||||
- word: Star Trek Beyond
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6uBlEXZCUHM15UNZqNig17VdN4m.jpg
|
||||
- word: The BFG
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/eYT9XQBo1eC4DwPYqhCol0dFFc2.jpg
|
||||
- word: Pete's Dragon
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/AaRhHX0Jfpju0O6hNzScPRgX9Mm.jpg
|
||||
- word: 'Mechanic: Resurrection'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6kMu4vECAyTpj2Z7n8viJ4RAaYh.jpg
|
||||
- word: The Imitation Game
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/qcb6z1HpokTOKdjqDTsnjJk0Xvg.jpg
|
||||
- word: 'X-Men: Apocalypse'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/oQWWth5AOtbWG9o8SCAviGcADed.jpg
|
||||
- word: John Wick
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/mFb0ygcue4ITixDkdr7wm1Tdarx.jpg
|
||||
- word: Deadpool
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/n1y094tVDFATSzkTnFxoGZ1qNsG.jpg
|
||||
- word: 'Batman v Superman: Dawn of Justice'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/vsjBeMPZtyB7yNsYY56XYxifaQZ.jpg
|
||||
- word: The Revenant
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/kiWvoV78Cc3fUwkOHKzyBgVdrDD.jpg
|
||||
- word: Now You See Me 2
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/zrAO2OOa6s6dQMQ7zsUbDyIBrAP.jpg
|
||||
- word: 'Star Wars: The Force Awakens'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/c2Ax8Rox5g6CneChwy1gmu4UbSb.jpg
|
||||
- word: The Martian
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/sy3e2e4JwdAtd2oZGA2uUilZe8j.jpg
|
||||
- word: Bridget Jones's Baby
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/w9VNDcQet0TUoLRWHg8JbT4QjpW.jpg
|
||||
- word: Minions
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/uX7LXnsC7bZJZjn048UCOwkPXWJ.jpg
|
||||
- word: Star Wars
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/4iJfYYoQzZcONB9hNzg0J0wWyPH.jpg
|
||||
- word: Spectre
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/wVTYlkKPKrljJfugXN7UlLNjtuJ.jpg
|
||||
- word: 'Rogue One: A Star Wars Story'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/tZjVVIYXACV4IIIhXeIM59ytqwS.jpg
|
||||
- word: Big Hero 6
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/2BXd0t9JdVqCp9sKf6kzMkr7QjB.jpg
|
||||
- word: 'Independence Day: Resurgence'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/8SqBiesvo1rh9P1hbJTmnVum6jv.jpg
|
||||
- word: The Dark Knight
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/nnMC0BM6XbjIIrT4miYmMtPGcQV.jpg
|
||||
- word: Sausage Party
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/nBvyktlVHjLx5nZ9Oxaoqo5jwbf.jpg
|
||||
- word: Gone Girl
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/bt6DhdALyhf90gReozoQ0y3R3vZ.jpg
|
||||
- word: Zootopia
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/mhdeE1yShHTaDbJVdWyTlzFvNkr.jpg
|
||||
- word: Allegiant
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/sFthBeT0Y3WVfg6b3MkcJs9qfzq.jpg
|
||||
- word: 'The Hobbit: The Battle of the Five Armies'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/qhH3GyIfAnGv1pjdV3mw03qAilg.jpg
|
||||
- word: Fury
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/pKawqrtCBMmxarft7o1LbEynys7.jpg
|
||||
- word: The Jungle Book
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/eIOTsGg9FCVrBc4r2nXaV61JF4F.jpg
|
||||
- word: 'Jack Reacher: Never Go Back'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/4ynQYtSEuU5hyipcGkfD6ncwtwz.jpg
|
||||
- word: 'Pirates of the Caribbean: The Curse of the Black Pearl'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/8AUQ7YlJJA9C8kWk8P4YNHIcFDE.jpg
|
||||
- word: 'The Hunger Games: Mockingjay - Part 2'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/qjn3fzCAHGfl0CzeUlFbjrsmu4c.jpg
|
||||
- word: Hell or High Water
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/5GbRKOQSY08U3SQXXcQAKEnL2rE.jpg
|
||||
- word: Terminator Genisys
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/bIlYH4l2AyYvEysmS2AOfjO7Dn8.jpg
|
||||
- word: Ant-Man
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/kvXLZqY0Ngl1XSw7EaMQO0C1CCj.jpg
|
||||
- word: The Shallows
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/lEkHdk4g0nAKtMcHBtSmC1ON3O1.jpg
|
||||
- word: 'Underworld: Awakening'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/8iNsXY3LPsuT0gnUiTBMoNuRZI7.jpg
|
||||
- word: Inception
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/s2bT29y0ngXxxu2IA8AOzzXTRhd.jpg
|
||||
- word: 'Underworld: Evolution'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/lLZTsh8qDZsHCG9GMnqZKIlluZT.jpg
|
||||
- word: The Hateful Eight
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/sSvgNBeBNzAuKl8U8sP50ETJPgx.jpg
|
||||
- word: Fight Club
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/wSJPjqp2AZWQ6REaqkMuXsCIs64.jpg
|
||||
- word: 'Avengers: Age of Ultron'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/570qhjGZmGPrBGnfx70jcwIuBr4.jpg
|
||||
- word: 'Underworld: Rise of the Lycans'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/gVfnoiCJBdv2SN7poIbU7eyNVq1.jpg
|
||||
- word: Tomorrowland
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/fQbc5XuB4vWA9gnY1CmyxFaOufF.jpg
|
||||
- word: The Matrix
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/7u3pxc0K1wx32IleAkLv78MKgrw.jpg
|
||||
- word: Furious 7
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/ypyeMfKydpyuuTMdp36rMlkGDUL.jpg
|
||||
- word: Pixels
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/nvZVu6inpwLHKqRXZhye3S4uqei.jpg
|
||||
- word: Emerald Green
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/ioKU3dEgx0HeUWp3KI2X7YF8FdC.jpg
|
||||
- word: Whiplash
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6bbZ6XyvgfjhQwbplnUh1LSj1ky.jpg
|
||||
- word: The Legend of Tarzan
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/pWNBPN8ghaKtGLcQBMwNyM32Wbm.jpg
|
||||
- word: Lucy
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/eCgIoGvfNXrbSiQGqQHccuHjQHm.jpg
|
||||
- word: Allied
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6o4KCKjP1WcefXLBNRyhEenB2nW.jpg
|
||||
- word: Eliminators
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/lNhwy85HjhieIVfmWoDAL2wCchB.jpg
|
||||
- word: 'Ice Age: Collision Course'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/o29BFNqgXOUT1yHNYusnITsH7P9.jpg
|
||||
- word: Batman Begins
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/65JWXDCAfwHhJKnDwRnEgVB411X.jpg
|
||||
- word: Teenage Mutant Ninja Turtles
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/OqCXGt5nl1cHPeotxCDvXLLe6p.jpg
|
||||
- word: Birdman
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUDEHvhNJLNcb83Pp7xnFn0Wj09.jpg
|
||||
- word: Avatar
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/5XPPB44RQGfkBrbJxmtdndKz05n.jpg
|
||||
- word: 'Kingsman: The Secret Service'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/pfyWJUxrBTT2UIPoEQF3iFTHcQT.jpg
|
||||
- word: "Pirates of the Caribbean: At World's End"
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/8ZgpAftUiYTU76IhUADITa3Ur9n.jpg
|
||||
- word: Bad Moms
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/elPpPBuYB2Zn5wFwz2FSlJlKUjp.jpg
|
||||
- word: Inside Out
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/szytSpLAyBh3ULei3x663mAv5ZT.jpg
|
||||
- word: Harry Potter and the Philosopher's Stone
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/uD93T339xX1k3fnDUaeopZBiajY.jpg
|
||||
- word: Dawn of the Planet of the Apes
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/rjUl3pd1LHVOVfG4IGcyA1cId5l.jpg
|
||||
- word: Iron Man
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/ZQixhAZx6fH1VNafFXsqa1B8QI.jpg
|
||||
- word: The Dark Knight Rises
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/3bgtUfKQKNi3nJsAB5URpP2wdRt.jpg
|
||||
- word: Don't Breathe
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/bCThHXQ3aLLDU3KFST0rC8mTan5.jpg
|
||||
- word: The Avengers
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/hbn46fQaRmlpBuUrEiFqv0GDL6Y.jpg
|
||||
- word: Alice Through the Looking Glass
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/rWlXrfmX1FgcPyj7oQmLfwKRaam.jpg
|
||||
- word: Now You See Me
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/9wbXqcx6rHhoZ9Esp03C7amQzom.jpg
|
||||
- word: The Accountant
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/i9flZtw3BwukADQpu5PlrkwPYSY.jpg
|
||||
- word: The Maze Runner
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/yTbPPmLAn7DiiM0sPYfZduoAjB.jpg
|
||||
- word: Forrest Gump
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/ctOEhQiFIHWkiaYp7b0ibSTe5IL.jpg
|
||||
- word: 'The Hunger Games: Catching Fire'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/wRCPG1lsgfTFkWJ7G3eWgxCgv0C.jpg
|
||||
- word: Warcraft
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/5SX2rgKXZ7NVmAJR5z5LprqSXKa.jpg
|
||||
- word: 'Eddie Izzard: Glorious'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/2XHkh7xLqH168KRqMwDuiTmuyQi.jpg
|
||||
- word: Office Christmas Party
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/bzguuhqUI9G8jJ3EBtJ9p12g1Lr.jpg
|
||||
- word: 'Transformers: The Last Knight'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/lQmtMCQgBwcODAyNnKGQrW0Kza8.jpg
|
||||
- word: Skyfall
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/AunH2MIKIbnU9khgFp45eJlydPu.jpg
|
||||
- word: 'The Hobbit: An Unexpected Journey'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/jjAq3tCezdlQduusgtMhpY2XzW0.jpg
|
||||
- word: 'The Lord of the Rings: The Fellowship of the Ring'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/pIUvQ9Ed35wlWhY2oU6OmwEsmzG.jpg
|
||||
- word: Kubo and the Two Strings
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/n4FeRnlH0ERa1kCUh0NXOyQvxnd.jpg
|
||||
- word: 10 Cloverfield Lane
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/qEu2EJBQUNFx5mSKOCItwZm74ZE.jpg
|
||||
- word: 'Captain America: The First Avenger'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/pmZtj1FKvQqISS6iQbkiLg5TAsr.jpg
|
||||
- word: The Shawshank Redemption
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/xBKGJQsAIeweesB79KC89FpBrVr.jpg
|
||||
- word: Me Before You
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/o4lxNwKJz8oq3R0kLOIsDlHbDhZ.jpg
|
||||
- word: The Magnificent Seven
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/T3LrH6bnV74llVbFpQsCBrGaU9.jpg
|
||||
- word: The Lion King
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/klI0K4oQMsLhHdjA9Uw8WLugk9v.jpg
|
||||
- word: Quantum of Solace
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/hfZVY8lMiE7HH1cDc2qzSFF6Kbt.jpg
|
||||
- word: Sing
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/srpt7oa3AmmJXd2K5x9ZVzmV0I3.jpg
|
||||
- word: Nightcrawler
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/ts4j3zYaPzdUVF3ijBeBdGVDWjX.jpg
|
||||
- word: Snowden
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/qzGFm7uF1HExPUAcAPwC3Hzk5WR.jpg
|
||||
- word: The Equalizer
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/b1uY9m6sZLLfa8jxtBvZg9esSvd.jpg
|
||||
- word: Divergent
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/g6WT9zxATzTy9NVu2xwbxDAxvjd.jpg
|
||||
- word: "Pirates of the Caribbean: Dead Man's Chest"
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/hdHgIcljPHli4xaJGt0INz8Gn3J.jpg
|
||||
- word: War Dogs
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/2cLndRZy8e3das3vVaK3BdJfRIi.jpg
|
||||
- word: Pulp Fiction
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/mte63qJaVnoxkkXbHkdFujBnBgd.jpg
|
||||
- word: 'Transformers: Age of Extinction'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/cHy7nSitAVgvZ7qfCK4JO47t3oZ.jpg
|
||||
- word: Gladiator
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/5vZw7ltCKI0JiOYTtRxaIC3DX0e.jpg
|
||||
- word: Nerve
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/a0wohltYr7Tzkgg2X6QKBe3txj1.jpg
|
||||
- word: I Am Legend
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/u6Qg7TH7Oh1IFWCQSRr4htFFt0A.jpg
|
||||
- word: 'Teenage Mutant Ninja Turtles: Out of the Shadows'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/999RuhZvog8ocyvcccVV9yGmMjL.jpg
|
||||
- word: Cinderella
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/aUYcExsGuRaw7PLGmAmXubt1dfG.jpg
|
||||
- word: The Big Short
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/jmlMLYEsYY1kRc5qHIyTdxCeVmZ.jpg
|
||||
- word: 'X-Men: Days of Future Past'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/5LBcSLHAtEIIgvNkA2dPmYH5wR7.jpg
|
||||
- word: Shutter Island
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/fmLWuAfDPaUa3Vi5nO1YUUyZaX6.jpg
|
||||
- word: 'Captain America: The Winter Soldier'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/4qfXT9BtxeFuamR4F49m2mpKQI1.jpg
|
||||
- word: San Andreas
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/cUfGqafAVQkatQ7N4y08RNV3bgu.jpg
|
||||
- word: The Mummy
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/3qthpSSyBY6Efeu1sqkO8L1Eyyb.jpg
|
||||
- word: Chappie
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/y5lG7TBpeOMG0jxAaTK0ghZSzBJ.jpg
|
||||
- word: Bridge of Spies
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/3amJMLyjx0rDbwhnKNG8d6gzDSV.jpg
|
||||
- word: 'The Lord of the Rings: The Return of the King'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/8BPZO0Bf8TeAy8znF43z8soK3ys.jpg
|
||||
- word: 'Kill Bill: Vol. 1'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/kkS8PKa8c134vXsj2fQkNqOaCXU.jpg
|
||||
- word: 'Pirates of the Caribbean: On Stranger Tides'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/l7zANdjgTvYqwZUx76Vk0EKpCH5.jpg
|
||||
- word: The Nice Guys
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/a7eSkK4bkLwKCXYDdYIqLxrqT2n.jpg
|
||||
- word: Harry Potter and the Chamber of Secrets
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/avqzwKn89VetTEvAlBePt3Us6Al.jpg
|
||||
- word: 'The Lord of the Rings: The Two Towers'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/dG4BmM32XJmKiwopLDQmvXEhuHB.jpg
|
||||
- word: Titanic
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/2Te2YJoLtT2cHME7i5kDuEwJWZc.jpg
|
||||
- word: "The Huntsman: Winter's War"
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/nQ0UvXdxoMZguLuPj0sdV0U36KR.jpg
|
||||
- word: Thor
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6UxFfo8K3vcihtUpX1ek2ucGeEZ.jpg
|
||||
- word: Taken 3
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/razvUuLkF7CX4XsLyj02ksC0ayy.jpg
|
||||
- word: Lights Out
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/nQsdGCVTEq78XwIgR6QUVxiNERI.jpg
|
||||
- word: 'Night at the Museum: Secret of the Tomb'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6tKleiKS54focE4z0sdtLOIEgK8.jpg
|
||||
- word: Up
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/qMjbMPkSCc1K19zuXNM2BgIsIRz.jpg
|
||||
- word: The Bourne Identity
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/2Fr1vqBiDn8xRJM9elcplzHctTN.jpg
|
||||
- word: Frozen
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/cAhCDpAq80QCeQvHytY9JkBalpH.jpg
|
||||
- word: 'Thor: The Dark World'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/3FweBee0xZoY77uO1bhUOlQorNH.jpg
|
||||
- word: The Wolf of Wall Street
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/rP36Rx5RQh0rmH2ynEIaG8DxbV2.jpg
|
||||
- word: Resident Evil
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/s41Er80jGJf3tNkgYHxUCttjmwv.jpg
|
||||
- word: Iron Man 3
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/n9X2DKItL3V0yq1q1jrk8z5UAki.jpg
|
||||
- word: Ice Age
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/oDqbewoFuIEWA7UWurole6MzDGn.jpg
|
||||
- word: Central Intelligence
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/zaGmoSackrRF7w6NieQ0FWY0M7k.jpg
|
||||
- word: 'The Mummy: Tomb of the Dragon Emperor'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/caB8JFUigSHdGsdxOxaK4vZtOiN.jpg
|
||||
- word: The Man from U.N.C.L.E.
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/bKxcCNv2xq8M3GD5iSrv9bMGDVa.jpg
|
||||
- word: Gravity
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/9aoWzwOwy9NLuSk9LkBwwrBdPYM.jpg
|
||||
- word: Ex Machina
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/9X3cDZb4GYGQeOnZHLwMcCFz2Ro.jpg
|
||||
- word: Harry Potter and the Goblet of Fire
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/gzKW3emulMxIHzuXxZoyDB1lei9.jpg
|
||||
- word: Man of Steel
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/jYLh4mdOqkt30i7LTFs3o02UcGF.jpg
|
||||
- word: 'Harry Potter and the Deathly Hallows: Part 1'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/8YA36faYlkpfp6aozcGsqq68pZ9.jpg
|
||||
- word: Iron Man 2
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/jxdSxqAFrdioKgXwgTs5Qfbazjq.jpg
|
||||
- word: The Angry Birds Movie
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/3mJcfL2lPfRky16EPi95d2YrKqu.jpg
|
||||
- word: Room
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/tBhp8MGaiL3BXpPCSl5xY397sGH.jpg
|
||||
- word: Ted 2
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/nkwoiSVJLeK0NI8kTqioBna61bm.jpg
|
||||
- word: 'Exodus: Gods and Kings'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/hOOgtrByGgWfqGTTn5VL7jkLYXJ.jpg
|
||||
- word: 'Terminator 2: Judgment Day'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/d9AqtruwS8nljKjL5aYzM42hQJr.jpg
|
||||
- word: Harry Potter and the Prisoner of Azkaban
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/wWdlIBxn9xCmySxnSWtI2BjZZkF.jpg
|
||||
- word: Genius
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/5eovlgVfijObBm4TtW1QSaj32q3.jpg
|
||||
- word: Inglourious Basterds
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/bk0GylJLneaSbpQZXpgTwleYigq.jpg
|
||||
- word: Hacksaw Ridge
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/zBK4QZONMQXhcgaJv1YYTdCW7q9.jpg
|
||||
- word: Trolls
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/wc1JxADaBLuWhySkaawCBTpixCo.jpg
|
||||
- word: The Shining
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/h4DcDCOkQBENWBJZjNlPv3adQfM.jpg
|
||||
- word: A Walk Among the Tombstones
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/e56QsaJy1weAUukiK2ZmIGVUALF.jpg
|
||||
- word: Dr. No
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/bplDiT5JhaXf9S5arO8g5QsFtDi.jpg
|
||||
- word: 'Mission: Impossible'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/7CiZuIPCLvhhMICT2PONuwr2BMG.jpg
|
||||
- word: 'The Purge: Anarchy'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/1sOkQtqmBji7iquGfQlHOrFqplN.jpg
|
||||
- word: 'Maze Runner: The Scorch Trials'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/iapRFMGKvN9tsjqPlN7MIDTCezG.jpg
|
||||
- word: Jupiter Ascending
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/4liSXBZZdURI0c1Id1zLJo6Z3Gu.jpg
|
||||
- word: Morgan
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/g3XhTkjUxLbzVVa63vuopSNNZE8.jpg
|
||||
- word: Se7en
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/ba4CpvnaxvAgff2jHiaqJrVpZJ5.jpg
|
||||
- word: Charlie and the Chocolate Factory
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/mrRAx1OsNVEKJv0ktQprspieqnS.jpg
|
||||
- word: Self/less
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/fpKyGCOZJsYe2TAKLtziLe6EPj9.jpg
|
||||
- word: The Prestige
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/c5o7FN2vzI7xlU6IF1y64mgcH9E.jpg
|
||||
- word: The Departed
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/8Od5zV7Q7zNOX0y9tyNgpTmoiGA.jpg
|
||||
- word: Monsters, Inc.
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/eKBUYeSgGVvztO2MZxD5YMcz6kv.jpg
|
||||
- word: Raiders of the Lost Ark
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/dU1CArBM4YsKLfG8YvhtuTJJaGR.jpg
|
||||
- word: The Terminator
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6yFoLNQgFdVbA8TZMdfgVpszOla.jpg
|
||||
- word: The Mummy Returns
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/22SUPrwoHNbMjZci1kRvBmCqrek.jpg
|
||||
- word: Pan
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6Ym6bgfhvpgQS5Sg8kKnfW1hX7P.jpg
|
||||
- word: Fifty Shades of Grey
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/zw3fM9KYYhYGsIQUJOyQNbeZSnn.jpg
|
||||
- word: Casino Royale
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/xq6hXdBpDPIXWjtmvbFmtLvBFJt.jpg
|
||||
- word: Free State of Jones
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/zgo0s1fmBCWUweNrCVIK1dZEOJ6.jpg
|
||||
- word: Despicable Me
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/yo1ef57MEPkEE4BDZKTZGH9uDcX.jpg
|
||||
- word: Creed
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/nF4kmc4gDRQU4OJiJgk6sZtbJbl.jpg
|
||||
- word: Seventh Son
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/lNnrr7OR7dkqfgIyju8nUJHcf8x.jpg
|
||||
- word: The Hunger Games
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/1LTLrl06uII4w2BTpnQnmWwrKi.jpg
|
||||
- word: Saving Private Ryan
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/gRtLcCQOpYUI9ThdVzi4VUP8QO3.jpg
|
||||
- word: 'Harry Potter and the Deathly Hallows: Part 2'
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6n0DAcyjTHS6888mt8U9ZsLy9nR.jpg
|
||||
- word: 12 Years a Slave
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/xnRPoFI7wzOYviw3PmoG94X2Lnc.jpg
|
||||
- word: Dracula Untold
|
||||
imageUrl: https://image.tmdb.org/t/p/w300_and_h450_bestv2/6UPlIYKxZqUR6Xbpgu1JKG0J7UC.jpg
|
380
src/EllieBot/data/hangman/things.yml
Normal file
|
@ -0,0 +1,380 @@
|
|||
- word: apple
|
||||
imageUrl: https://www.randomlists.com/img/things/apple.jpg.jpg
|
||||
- word: bag
|
||||
imageUrl: https://www.randomlists.com/img/things/bag.jpg.jpg
|
||||
- word: balloon
|
||||
imageUrl: https://www.randomlists.com/img/things/balloon.jpg.jpg
|
||||
- word: bananas
|
||||
imageUrl: https://www.randomlists.com/img/things/bananas.jpg.jpg
|
||||
- word: bed
|
||||
imageUrl: https://www.randomlists.com/img/things/bed.jpg.jpg
|
||||
- word: beef
|
||||
imageUrl: https://www.randomlists.com/img/things/beef.jpg.jpg
|
||||
- word: blouse
|
||||
imageUrl: https://www.randomlists.com/img/things/blouse.jpg.jpg
|
||||
- word: book
|
||||
imageUrl: https://www.randomlists.com/img/things/book.jpg.jpg
|
||||
- word: bookmark
|
||||
imageUrl: https://www.randomlists.com/img/things/bookmark.jpg.jpg
|
||||
- word: boom box
|
||||
imageUrl: https://www.randomlists.com/img/things/boom_box.jpg.jpg
|
||||
- word: bottle
|
||||
imageUrl: https://www.randomlists.com/img/things/bottle.jpg.jpg
|
||||
- word: bottle cap
|
||||
imageUrl: https://www.randomlists.com/img/things/bottle_cap.jpg
|
||||
- word: bow
|
||||
imageUrl: https://www.randomlists.com/img/things/bow.jpg
|
||||
- word: bowl
|
||||
imageUrl: https://www.randomlists.com/img/things/bowl.jpg
|
||||
- word: box
|
||||
imageUrl: https://www.randomlists.com/img/things/box.jpg
|
||||
- word: bracelet
|
||||
imageUrl: https://www.randomlists.com/img/things/bracelet.jpg
|
||||
- word: bread
|
||||
imageUrl: https://www.randomlists.com/img/things/bread.jpg
|
||||
- word: broccoli
|
||||
imageUrl: https://www.randomlists.com/img/things/brocolli.jpg
|
||||
- word: hair brush
|
||||
imageUrl: https://www.randomlists.com/img/things/hair_brush.jpg
|
||||
- word: buckle
|
||||
imageUrl: https://www.randomlists.com/img/things/buckel.jpg
|
||||
- word: button
|
||||
imageUrl: https://www.randomlists.com/img/things/button.jpg
|
||||
- word: camera
|
||||
imageUrl: https://www.randomlists.com/img/things/camera.jpg
|
||||
- word: candle
|
||||
imageUrl: https://www.randomlists.com/img/things/candle.jpg
|
||||
- word: candy wrapper
|
||||
imageUrl: https://www.randomlists.com/img/things/candy_wrapper.jpg
|
||||
- word: canvas
|
||||
imageUrl: https://www.randomlists.com/img/things/canvas.jpg
|
||||
- word: car
|
||||
imageUrl: https://www.randomlists.com/img/things/car.jpg
|
||||
- word: greeting card
|
||||
imageUrl: https://www.randomlists.com/img/things/greeting_card.jpg
|
||||
- word: playing card
|
||||
imageUrl: https://www.randomlists.com/img/things/playing_card.jpg
|
||||
- word: carrots
|
||||
imageUrl: https://www.randomlists.com/img/things/carrots.jpg
|
||||
- word: cat
|
||||
imageUrl: https://www.randomlists.com/img/things/cat.jpg
|
||||
- word: CD
|
||||
imageUrl: https://www.randomlists.com/img/things/CD.jpg
|
||||
- word: cell phone
|
||||
imageUrl: https://www.randomlists.com/img/things/cell_phone.jpg
|
||||
- word: packing peanuts
|
||||
imageUrl: https://www.randomlists.com/img/things/packing_peanuts.jpg
|
||||
- word: cinder block
|
||||
imageUrl: https://www.randomlists.com/img/things/cinder_block.jpg
|
||||
- word: chair
|
||||
imageUrl: https://www.randomlists.com/img/things/chair.jpg
|
||||
- word: chalk
|
||||
imageUrl: https://www.randomlists.com/img/things/chalk.jpg
|
||||
- word: newspaper
|
||||
imageUrl: https://www.randomlists.com/img/things/newspaper.jpg
|
||||
- word: soy sauce packet
|
||||
imageUrl: https://www.randomlists.com/img/things/soy_sauce_packet.jpg
|
||||
- word: chapter book
|
||||
imageUrl: https://www.randomlists.com/img/things/chapter_book.jpg
|
||||
- word: checkbook
|
||||
imageUrl: https://www.randomlists.com/img/things/checkbook.jpg
|
||||
- word: chocolate
|
||||
imageUrl: https://www.randomlists.com/img/things/chocolate.jpg
|
||||
- word: clay pot
|
||||
imageUrl: https://www.randomlists.com/img/things/clay_pot.jpg
|
||||
- word: clock
|
||||
imageUrl: https://www.randomlists.com/img/things/clock.jpg
|
||||
- word: clothes
|
||||
imageUrl: https://www.randomlists.com/img/things/clothes.jpg
|
||||
- word: computer
|
||||
imageUrl: https://www.randomlists.com/img/things/computer.jpg
|
||||
- word: conditioner
|
||||
imageUrl: https://www.randomlists.com/img/things/conditioner.jpg
|
||||
- word: cookie jar
|
||||
imageUrl: https://www.randomlists.com/img/things/cookie_jar.jpg
|
||||
- word: cork
|
||||
imageUrl: https://www.randomlists.com/img/things/cork.jpg
|
||||
- word: couch
|
||||
imageUrl: https://www.randomlists.com/img/things/couch.jpg
|
||||
- word: credit card
|
||||
imageUrl: https://www.randomlists.com/img/things/credit_card.jpg
|
||||
- word: cup
|
||||
imageUrl: https://www.randomlists.com/img/things/cup.jpg
|
||||
- word: deodorant
|
||||
imageUrl: https://www.randomlists.com/img/things/deodorant_.jpg
|
||||
- word: desk
|
||||
imageUrl: https://www.randomlists.com/img/things/desk.jpg
|
||||
- word: door
|
||||
imageUrl: https://www.randomlists.com/img/things/door.jpg
|
||||
- word: drawer
|
||||
imageUrl: https://www.randomlists.com/img/things/drawer.jpg
|
||||
- word: drill press
|
||||
imageUrl: https://www.randomlists.com/img/things/drill_press.jpg
|
||||
- word: eraser
|
||||
imageUrl: https://www.randomlists.com/img/things/earser.jpg
|
||||
- word: eye liner
|
||||
imageUrl: https://www.randomlists.com/img/things/eye_liner.jpg
|
||||
- word: face wash
|
||||
imageUrl: https://www.randomlists.com/img/things/face_wash.jpg
|
||||
- word: fake flowers
|
||||
imageUrl: https://www.randomlists.com/img/things/fake_flowers.jpg
|
||||
- word: flag
|
||||
imageUrl: https://www.randomlists.com/img/things/flag.jpg
|
||||
- word: floor
|
||||
imageUrl: https://www.randomlists.com/img/things/floor.jpg
|
||||
- word: flowers
|
||||
imageUrl: https://www.randomlists.com/img/things/flowers.jpg
|
||||
- word: food
|
||||
imageUrl: https://www.randomlists.com/img/things/food.jpg
|
||||
- word: fork
|
||||
imageUrl: https://www.randomlists.com/img/things/fork.jpg
|
||||
- word: fridge
|
||||
imageUrl: https://www.randomlists.com/img/things/fridge.jpg
|
||||
- word: glass
|
||||
imageUrl: https://www.randomlists.com/img/things/glass.jpg
|
||||
- word: glasses
|
||||
imageUrl: https://www.randomlists.com/img/things/glasses.jpg
|
||||
- word: glow stick
|
||||
imageUrl: https://www.randomlists.com/img/things/glow_stick.jpg
|
||||
- word: grid paper
|
||||
imageUrl: https://www.randomlists.com/img/things/grid_paper.jpg
|
||||
- word: hair tie
|
||||
imageUrl: https://www.randomlists.com/img/things/hair_tie.jpg
|
||||
- word: hanger
|
||||
imageUrl: https://www.randomlists.com/img/things/hanger.jpg
|
||||
- word: helmet
|
||||
imageUrl: https://www.randomlists.com/img/things/helmet.jpg
|
||||
- word: house
|
||||
imageUrl: https://www.randomlists.com/img/things/house.jpg
|
||||
- word: ipod
|
||||
imageUrl: https://www.randomlists.com/img/things/ipod.jpg
|
||||
- word: charger
|
||||
imageUrl: https://www.randomlists.com/img/things/charger.jpg
|
||||
- word: key chain
|
||||
imageUrl: https://www.randomlists.com/img/things/key_chain.jpg
|
||||
- word: keyboard
|
||||
imageUrl: https://www.randomlists.com/img/things/keyboard.jpg
|
||||
- word: keys
|
||||
imageUrl: https://www.randomlists.com/img/things/keys.jpg
|
||||
- word: knife
|
||||
imageUrl: https://www.randomlists.com/img/things/knife.jpg
|
||||
- word: lace
|
||||
imageUrl: https://www.randomlists.com/img/things/lace.jpg
|
||||
- word: lamp
|
||||
imageUrl: https://www.randomlists.com/img/things/lamp.jpg
|
||||
- word: lamp shade
|
||||
imageUrl: https://www.randomlists.com/img/things/lamp_shade.jpg
|
||||
- word: leg warmers
|
||||
imageUrl: https://www.randomlists.com/img/things/leg_warmers.jpg
|
||||
- word: lip gloss
|
||||
imageUrl: https://www.randomlists.com/img/things/lip_gloss.jpg
|
||||
- word: lotion
|
||||
imageUrl: https://www.randomlists.com/img/things/lotion.jpg
|
||||
- word: milk
|
||||
imageUrl: https://www.randomlists.com/img/things/milk.jpg
|
||||
- word: mirror
|
||||
imageUrl: https://www.randomlists.com/img/things/mirror.jpg
|
||||
- word: model car
|
||||
imageUrl: https://www.randomlists.com/img/things/model_car.jpg
|
||||
- word: money
|
||||
imageUrl: https://www.randomlists.com/img/things/money.jpg
|
||||
- word: monitor
|
||||
imageUrl: https://www.randomlists.com/img/things/monitor.jpg
|
||||
- word: mop
|
||||
imageUrl: https://www.randomlists.com/img/things/mop.jpg
|
||||
- word: mouse pad
|
||||
imageUrl: https://www.randomlists.com/img/things/mouse_pad.jpg
|
||||
- word: mp3 player
|
||||
imageUrl: https://www.randomlists.com/img/things/mp3_player.jpg
|
||||
- word: nail clippers
|
||||
imageUrl: https://www.randomlists.com/img/things/nail_clippers.jpg
|
||||
- word: nail file
|
||||
imageUrl: https://www.randomlists.com/img/things/nail_file.jpg
|
||||
- word: needle
|
||||
imageUrl: https://www.randomlists.com/img/things/needle.jpg
|
||||
- word: outlet
|
||||
imageUrl: https://www.randomlists.com/img/things/outlet.jpg
|
||||
- word: paint brush
|
||||
imageUrl: https://www.randomlists.com/img/things/paint_brush.jpg
|
||||
- word: pants
|
||||
imageUrl: https://www.randomlists.com/img/things/pants.jpg
|
||||
- word: paper
|
||||
imageUrl: https://www.randomlists.com/img/things/paper.jpg
|
||||
- word: pen
|
||||
imageUrl: https://www.randomlists.com/img/things/pen.jpg
|
||||
- word: pencil
|
||||
imageUrl: https://www.randomlists.com/img/things/pencil.jpg
|
||||
- word: perfume
|
||||
imageUrl: https://www.randomlists.com/img/things/perfume.jpg
|
||||
- word: phone
|
||||
imageUrl: https://www.randomlists.com/img/things/phone.jpg
|
||||
- word: photo album
|
||||
imageUrl: https://www.randomlists.com/img/things/photo_album.jpg
|
||||
- word: picture frame
|
||||
imageUrl: https://www.randomlists.com/img/things/picture_frame.jpg
|
||||
- word: pillow
|
||||
imageUrl: https://www.randomlists.com/img/things/pillow.jpg
|
||||
- word: plastic fork
|
||||
imageUrl: https://www.randomlists.com/img/things/plastic_fork.jpg
|
||||
- word: plate
|
||||
imageUrl: https://www.randomlists.com/img/things/plate.jpg
|
||||
- word: pool stick
|
||||
imageUrl: https://www.randomlists.com/img/things/pool_stick.jpg
|
||||
- word: soda can
|
||||
imageUrl: https://www.randomlists.com/img/things/soda_can.jpg
|
||||
- word: puddle
|
||||
imageUrl: https://www.randomlists.com/img/things/puddle.jpg
|
||||
- word: purse
|
||||
imageUrl: https://www.randomlists.com/img/things/purse.jpg
|
||||
- word: blanket
|
||||
imageUrl: https://www.randomlists.com/img/things/blanket.jpg
|
||||
- word: radio
|
||||
imageUrl: https://www.randomlists.com/img/things/radio.jpg
|
||||
- word: remote
|
||||
imageUrl: https://www.randomlists.com/img/things/remote.jpg
|
||||
- word: ring
|
||||
imageUrl: https://www.randomlists.com/img/things/ring.jpg
|
||||
- word: rubber band
|
||||
imageUrl: https://www.randomlists.com/img/things/rubber_band.jpg
|
||||
- word: rubber duck
|
||||
imageUrl: https://www.randomlists.com/img/things/rubber_duck.jpg
|
||||
- word: rug
|
||||
imageUrl: https://www.randomlists.com/img/things/rug.jpg
|
||||
- word: rusty nail
|
||||
imageUrl: https://www.randomlists.com/img/things/rusty_nail.jpg
|
||||
- word: sailboat
|
||||
imageUrl: https://www.randomlists.com/img/things/sailboat.jpg
|
||||
- word: sand paper
|
||||
imageUrl: https://www.randomlists.com/img/things/sand_paper.jpg
|
||||
- word: sandal
|
||||
imageUrl: https://www.randomlists.com/img/things/sandal.jpg
|
||||
- word: scotch tape
|
||||
imageUrl: https://www.randomlists.com/img/things/scotch_tape.jpg
|
||||
- word: screw
|
||||
imageUrl: https://www.randomlists.com/img/things/screw.jpg
|
||||
- word: seat belt
|
||||
imageUrl: https://www.randomlists.com/img/things/seat_belt.jpg
|
||||
- word: shampoo
|
||||
imageUrl: https://www.randomlists.com/img/things/shampoo.jpg
|
||||
- word: sharpie
|
||||
imageUrl: https://www.randomlists.com/img/things/sharpie.jpg
|
||||
- word: shawl
|
||||
imageUrl: https://www.randomlists.com/img/things/shawl.jpg
|
||||
- word: shirt
|
||||
imageUrl: https://www.randomlists.com/img/things/shirt.jpg
|
||||
- word: shoe lace
|
||||
imageUrl: https://www.randomlists.com/img/things/shoe_lace.jpg
|
||||
- word: shoes
|
||||
imageUrl: https://www.randomlists.com/img/things/shoes.jpg
|
||||
- word: shovel
|
||||
imageUrl: https://www.randomlists.com/img/things/shovel.jpg
|
||||
- word: sidewalk
|
||||
imageUrl: https://www.randomlists.com/img/things/sidewalk.jpg
|
||||
- word: sketch pad
|
||||
imageUrl: https://www.randomlists.com/img/things/sketch_pad.jpg
|
||||
- word: slipper
|
||||
imageUrl: https://www.randomlists.com/img/things/slipper.jpg
|
||||
- word: soap
|
||||
imageUrl: https://www.randomlists.com/img/things/soap.jpg
|
||||
- word: socks
|
||||
imageUrl: https://www.randomlists.com/img/things/socks.jpg
|
||||
- word: sofa
|
||||
imageUrl: https://www.randomlists.com/img/things/sofa.jpg
|
||||
- word: speakers
|
||||
imageUrl: https://www.randomlists.com/img/things/speakers.jpg
|
||||
- word: sponge
|
||||
imageUrl: https://www.randomlists.com/img/things/sponge.jpg
|
||||
- word: spoon
|
||||
imageUrl: https://www.randomlists.com/img/things/spoon.jpg
|
||||
- word: spring
|
||||
imageUrl: https://www.randomlists.com/img/things/spring.jpg
|
||||
- word: sticky note
|
||||
imageUrl: https://www.randomlists.com/img/things/sticky_note.jpg
|
||||
- word: stockings
|
||||
imageUrl: https://www.randomlists.com/img/things/stockings.jpg
|
||||
- word: stop sign
|
||||
imageUrl: https://www.randomlists.com/img/things/stop_sign.jpg
|
||||
- word: street lights
|
||||
imageUrl: https://www.randomlists.com/img/things/street_lights.jpg
|
||||
- word: sun glasses
|
||||
imageUrl: https://www.randomlists.com/img/things/sun_glasses.jpg
|
||||
- word: table
|
||||
imageUrl: https://www.randomlists.com/img/things/table.jpg
|
||||
- word: teddies
|
||||
imageUrl: https://www.randomlists.com/img/things/teddies.jpg
|
||||
- word: television
|
||||
imageUrl: https://www.randomlists.com/img/things/television.jpg
|
||||
- word: thermometer
|
||||
imageUrl: https://www.randomlists.com/img/things/thermometer.jpg
|
||||
- word: thread
|
||||
imageUrl: https://www.randomlists.com/img/things/thread.jpg
|
||||
- word: tire swing
|
||||
imageUrl: https://www.randomlists.com/img/things/tire_swing.jpg
|
||||
- word: tissue box
|
||||
imageUrl: https://www.randomlists.com/img/things/tissue_box.jpg
|
||||
- word: toe ring
|
||||
imageUrl: https://www.randomlists.com/img/things/toe_ring.jpg
|
||||
- word: toilet
|
||||
imageUrl: https://www.randomlists.com/img/things/toilet.jpg
|
||||
- word: tomato
|
||||
imageUrl: https://www.randomlists.com/img/things/tomato.jpg
|
||||
- word: tooth picks
|
||||
imageUrl: https://www.randomlists.com/img/things/tooth_picks.jpg
|
||||
- word: toothbrush
|
||||
imageUrl: https://www.randomlists.com/img/things/toothbrush.jpg
|
||||
- word: toothpaste
|
||||
imageUrl: https://www.randomlists.com/img/things/toothpaste.jpg
|
||||
- word: towel
|
||||
imageUrl: https://www.randomlists.com/img/things/towel.jpg
|
||||
- word: tree
|
||||
imageUrl: https://www.randomlists.com/img/things/tree.jpg
|
||||
- word: truck
|
||||
imageUrl: https://www.randomlists.com/img/things/truck.jpg
|
||||
- word: tv
|
||||
imageUrl: https://www.randomlists.com/img/things/tv.jpg
|
||||
- word: tweezers
|
||||
imageUrl: https://www.randomlists.com/img/things/twezzers.jpg
|
||||
- word: twister
|
||||
imageUrl: https://www.randomlists.com/img/things/twister.jpg
|
||||
- word: vase
|
||||
imageUrl: https://www.randomlists.com/img/things/vase.jpg
|
||||
- word: video games
|
||||
imageUrl: https://www.randomlists.com/img/things/video_games.jpg
|
||||
- word: wallet
|
||||
imageUrl: https://www.randomlists.com/img/things/wallet.jpg
|
||||
- word: washing machine
|
||||
imageUrl: https://www.randomlists.com/img/things/washing_machine.jpg
|
||||
- word: watch
|
||||
imageUrl: https://www.randomlists.com/img/things/watch.jpg
|
||||
- word: water bottle
|
||||
imageUrl: https://www.randomlists.com/img/things/water_bottle.jpg
|
||||
- word: doll
|
||||
imageUrl: https://www.randomlists.com/img/things/doll.jpg
|
||||
- word: magnet
|
||||
imageUrl: https://www.randomlists.com/img/things/magnet.jpg
|
||||
- word: wagon
|
||||
imageUrl: https://www.randomlists.com/img/things/wagon.jpg
|
||||
- word: headphones
|
||||
imageUrl: https://www.randomlists.com/img/things/headphones.jpg
|
||||
- word: clamp
|
||||
imageUrl: https://www.randomlists.com/img/things/clamp.jpg
|
||||
- word: USB drive
|
||||
imageUrl: https://www.randomlists.com/img/things/USB_drive.jpg
|
||||
- word: air freshener
|
||||
imageUrl: https://www.randomlists.com/img/things/air_freshener.jpg
|
||||
- word: piano
|
||||
imageUrl: https://www.randomlists.com/img/things/piano.jpg
|
||||
- word: ice cube tray
|
||||
imageUrl: https://www.randomlists.com/img/things/ice_cube_tray.jpg
|
||||
- word: white out
|
||||
imageUrl: https://www.randomlists.com/img/things/white_out.jpg
|
||||
- word: window
|
||||
imageUrl: https://www.randomlists.com/img/things/window.jpg
|
||||
- word: controller
|
||||
imageUrl: https://www.randomlists.com/img/things/controller.jpg
|
||||
- word: coasters
|
||||
imageUrl: https://www.randomlists.com/img/things/coasters.jpg
|
||||
- word: thermostat
|
||||
imageUrl: https://www.randomlists.com/img/things/thermostat.jpg
|
||||
- word: zipper
|
||||
imageUrl: https://www.randomlists.com/img/things/zipper.jpg
|
39
src/EllieBot/data/images.yml
Normal file
|
@ -0,0 +1,39 @@
|
|||
# DO NOT CHANGE
|
||||
version: 4
|
||||
coins:
|
||||
heads:
|
||||
- https://cdn.nadeko.bot/coins/heads3.png
|
||||
tails:
|
||||
- https://cdn.nadeko.bot/coins/tails3.png
|
||||
currency:
|
||||
- https://cdn.nadeko.bot/other/currency/0.jpg
|
||||
- https://cdn.nadeko.bot/other/currency/1.jpg
|
||||
- https://cdn.nadeko.bot/other/currency/2.jpg
|
||||
dice:
|
||||
- https://cdn.nadeko.bot/other/dice/0.png
|
||||
- https://cdn.nadeko.bot/other/dice/1.png
|
||||
- https://cdn.nadeko.bot/other/dice/2.png
|
||||
- https://cdn.nadeko.bot/other/dice/3.png
|
||||
- https://cdn.nadeko.bot/other/dice/4.png
|
||||
- https://cdn.nadeko.bot/other/dice/5.png
|
||||
- https://cdn.nadeko.bot/other/dice/6.png
|
||||
- https://cdn.nadeko.bot/other/dice/7.png
|
||||
- https://cdn.nadeko.bot/other/dice/8.png
|
||||
- https://cdn.nadeko.bot/other/dice/9.png
|
||||
rategirl:
|
||||
matrix: https://cdn.nadeko.bot/other/rategirl/matrix.png
|
||||
dot: https://cdn.nadeko.bot/other/rategirl/dot.png
|
||||
xp:
|
||||
bg: https://cdn.nadeko.bot/other/xp/bg_k.png
|
||||
rip:
|
||||
bg: https://cdn.nadeko.bot/other/rip/rip.png
|
||||
overlay: https://cdn.nadeko.bot/other/rip/overlay.png
|
||||
slots:
|
||||
emojis:
|
||||
- https://cdn.nadeko.bot/slots/0.png
|
||||
- https://cdn.nadeko.bot/slots/1.png
|
||||
- https://cdn.nadeko.bot/slots/2.png
|
||||
- https://cdn.nadeko.bot/slots/3.png
|
||||
- https://cdn.nadeko.bot/slots/4.png
|
||||
- https://cdn.nadeko.bot/slots/5.png
|
||||
bg: https://cdn.nadeko.bot/slots/slots_bg.png
|
BIN
src/EllieBot/data/images/cards/ace_of_clubs.jpg
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
src/EllieBot/data/images/cards/ace_of_diamonds.jpg
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/EllieBot/data/images/cards/ace_of_hearts.jpg
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/EllieBot/data/images/cards/ace_of_spades.jpg
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/EllieBot/data/images/cards/black_joker.jpg
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
src/EllieBot/data/images/cards/eight_of_clubs.jpg
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
src/EllieBot/data/images/cards/eight_of_diamonds.jpg
Normal file
After Width: | Height: | Size: 6.5 KiB |