Merge pull request 'Massive system rewrite.' (#1) from dev into main

Reviewed-on: Emotions-stuff/Ellie#1
This commit is contained in:
Toastie (DCS Team) 2024-05-12 00:32:16 -07:00
commit e9e7fd23af
188 changed files with 98269 additions and 1347 deletions

View file

@ -11,13 +11,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Dockerfile = Dockerfile Dockerfile = Dockerfile
LICENSE = LICENSE LICENSE = LICENSE
README.md = README.md README.md = README.md
TODO.md = TODO.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot", "src\EllieBot\EllieBot.csproj", "{BCB21472-84D2-4B63-B5DD-31E6A3EC9791}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot", "src\EllieBot\EllieBot.csproj", "{BCB21472-84D2-4B63-B5DD-31E6A3EC9791}"
EndProject 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 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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.ActiveCfg = Release|Any CPU
{BCB21472-84D2-4B63-B5DD-31E6A3EC9791}.Release|Any CPU.Build.0 = 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 {179DF3B3-AD32-4335-8231-9818338DF3A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AD2EFFB-7774-49B2-A791-3BAC4DAEE067}.Debug|Any CPU.Build.0 = Debug|Any CPU {179DF3B3-AD32-4335-8231-9818338DF3A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AD2EFFB-7774-49B2-A791-3BAC4DAEE067}.Release|Any CPU.ActiveCfg = Release|Any CPU {179DF3B3-AD32-4335-8231-9818338DF3A2}.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}.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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{BCB21472-84D2-4B63-B5DD-31E6A3EC9791} = {B28FB883-9688-41EB-BF5A-945F4A4EB628} {BCB21472-84D2-4B63-B5DD-31E6A3EC9791} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
{872A4C63-833C-4AE0-91AB-3CE348D3E6F8} = {B28FB883-9688-41EB-BF5A-945F4A4EB628} {179DF3B3-AD32-4335-8231-9818338DF3A2} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
{5AD2EFFB-7774-49B2-A791-3BAC4DAEE067} = {872A4C63-833C-4AE0-91AB-3CE348D3E6F8} {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4} SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4}

View file

@ -1,3 +1,9 @@
# List of things to do # 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

View 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);
}

View 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
{
}

View file

@ -0,0 +1,7 @@
namespace Ellie.Marmalade;
[AttributeUsage(AttributeTargets.Method)]
public sealed class bot_owner_onlyAttribute : MarmaladePermAttribute
{
}

View 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;
}
}

View 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;
}
}

View 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
{
}

View file

@ -0,0 +1,10 @@
namespace Ellie.Marmalade;
/// <summary>
/// Marks the parameter to take
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class leftoverAttribute : Attribute
{
}

View 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;
}
}

View 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
}

View 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;
}
}

View 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
{
}

View 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();
}

View 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; }
}

View 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; }
}

View 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>

View file

@ -0,0 +1,8 @@
namespace EllieBot;
public enum EmbedColor
{
Ok,
Pending,
Error
}

View 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);
}

View 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)}");
}

View 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);
}

View 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);
}

View 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,
};
}

View file

@ -0,0 +1 @@
This is the library which is the base of any marmalade.

View 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;
}
}

View 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);
}

View 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);
}

View 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;
}
}

View 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();
}

View 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);
}

View 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");
});
});
}
}
}

View 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>

View 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
}
}
}

View 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();

View 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"
}
}
}
}

View 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;
}

View 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

View 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();
}
}
}

View 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,
});
}
}
}

View 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;
}
}
}

View file

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace EllieBot.Coordinator
{
public class CoordState
{
public List<JsonStatusObject> StatusObjects { get; init; }
}
}

View 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; }
}
}

View 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
);
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View 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"
}
}
}
}

View 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

View 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));
}
}
}

View 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));
}
}

View 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);
}
}
}
}

View 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;
// }
// }

View 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>

View 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;
}
}
}

View 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"

View 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
View file

@ -0,0 +1 @@
store/

View 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)));
}
}
}

View file

@ -0,0 +1,8 @@
namespace EllieBot.VotesApi
{
public static class ConfKeys
{
public const string DISCORDS_KEY = "DiscordsKey";
public const string TOPGG_KEY = "TopGGKey";
}
}

View 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; }
}
}

View file

@ -0,0 +1,8 @@
namespace EllieBot.VotesApi
{
public static class Policies
{
public const string DiscordsAuth = "DiscordsAuth";
public const string TopggAuth = "TopggAuth";
}
}

View 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&amp;b=2.
/// </summary>
public string Query { get; set; }
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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();
}
}
}

View 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"]

View 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>

View 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>(); });

View 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"
}
}
}
}

View 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",
...
```

View 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;
}
}
}

View 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);
}
}

View 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(); });
}
}
}

View file

@ -0,0 +1,7 @@
namespace EllieBot.VotesApi
{
public class Vote
{
public ulong UserId { get; set; }
}
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View 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
View 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
View 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);
}
}

View file

@ -0,0 +1,7 @@
<Project>
<ItemDefinitionGroup>
<ProjectReference>
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemDefinitionGroup>
</Project>

View file

@ -1,10 +1,142 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <LangVersion>preview</LangVersion>
<Nullable>enable</Nullable> <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> </PropertyGroup>
</Project> </Project>

View 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;

View 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:

File diff suppressed because it is too large Load diff

91
src/EllieBot/data/bot.yml Normal file
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View 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

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Some files were not shown because too many files have changed in this diff Show more