Gambling moved to a separate project. Project builds

This commit is contained in:
Kwoth
2023-03-18 18:36:04 +01:00
parent db2328cdaf
commit 09171fb10a
361 changed files with 532 additions and 476 deletions

View File

@@ -1,4 +1,6 @@
#nullable disable
using NadekoBot.Common;
namespace NadekoBot;
public interface IBotCredentials
@@ -9,14 +11,14 @@ public interface IBotCredentials
bool UsePrivilegedIntents { get; }
string RapidApiKey { get; }
IDbOptions Db { get; }
Creds.DbOptions Db { get; }
string OsuApiKey { get; }
int TotalShards { get; }
IPatreonSettings Patreon { get; }
Creds.PatreonSettings Patreon { get; }
string CleverbotApiKey { get; }
string Gpt3ApiKey { get; }
IRestartConfig RestartCommand { get; }
IVotesSettings Votes { get; }
RestartConfig RestartCommand { get; }
Creds.VotesSettings Votes { get; }
string BotListToken { get; }
string RedisOptions { get; }
string LocationIqApiKey { get; }
@@ -26,7 +28,7 @@ public interface IBotCredentials
string CoordinatorUrl { get; set; }
string TwitchClientId { get; set; }
string TwitchClientSecret { get; set; }
IGoogleApiConfig Google { get; set; }
GoogleApiConfig Google { get; set; }
BotCacheImplemenation BotCache { get; set; }
}

View File

@@ -0,0 +1,12 @@
using System.Runtime.CompilerServices;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class AliasesAttribute : AliasAttribute
{
public AliasesAttribute([CallerMemberName] string memberName = "")
: base(CommandNameLoadHelper.GetAliasesFor(memberName))
{
}
}

View File

@@ -0,0 +1,18 @@
using System.Runtime.CompilerServices;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class CmdAttribute : CommandAttribute
{
public string MethodName { get; }
public CmdAttribute([CallerMemberName] string memberName = "")
: base(CommandNameLoadHelper.GetCommandNameFor(memberName))
{
MethodName = memberName.ToLowerInvariant();
Aliases = CommandNameLoadHelper.GetAliasesFor(memberName);
Remarks = memberName.ToLowerInvariant();
Summary = memberName.ToLowerInvariant();
}
}

View File

@@ -0,0 +1,11 @@
#nullable disable
namespace NadekoBot.Common;
/// <summary>
/// Classed marked with this attribute will not be added to the service provider
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class DIIgnoreAttribute : Attribute
{
}

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class NadekoOptionsAttribute<TOption> : Attribute
where TOption: INadekoCommandOptions
{
}

View File

@@ -0,0 +1,21 @@
#nullable disable
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Common;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
public sealed class NoPublicBotAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
#if GLOBAL_NADEKO
return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://nadekobot.readthedocs.io/en/latest/)."));
#else
return Task.FromResult(PreconditionResult.FromSuccess());
#endif
}
}

View File

@@ -0,0 +1,21 @@
#nullable disable
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Common;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
#if GLOBAL_NADEKO || DEBUG
return Task.FromResult(PreconditionResult.FromSuccess());
#else
return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
#endif
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class OwnerOnlyAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
return Task.FromResult(creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id
? PreconditionResult.FromSuccess()
: PreconditionResult.FromError("Not owner"));
}
}

View File

@@ -0,0 +1,38 @@
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class RatelimitAttribute : PreconditionAttribute
{
public int Seconds { get; }
public RatelimitAttribute(int seconds)
{
if (seconds <= 0)
throw new ArgumentOutOfRangeException(nameof(seconds));
Seconds = seconds;
}
public override async Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
if (Seconds == 0)
return PreconditionResult.FromSuccess();
var cache = services.GetRequiredService<IBotCache>();
var rem = await cache.GetRatelimitAsync(
new($"precondition:{context.User.Id}:{command.Name}"),
Seconds.Seconds());
if (rem is null)
return PreconditionResult.FromSuccess();
var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s.";
return PreconditionResult.FromError(msgContent);
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.Extensions.DependencyInjection;
namespace Discord;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class UserPermAttribute : RequireUserPermissionAttribute
{
public UserPermAttribute(GuildPerm permission)
: base(permission)
{
}
public UserPermAttribute(ChannelPerm permission)
: base(permission)
{
}
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo command,
IServiceProvider services)
{
var permService = services.GetRequiredService<IDiscordPermOverrideService>();
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out _))
return Task.FromResult(PreconditionResult.FromSuccess());
return base.CheckPermissionsAsync(context, command, services);
}
}

View File

@@ -0,0 +1,32 @@
#nullable disable
using NadekoBot.Services;
namespace NadekoBot.Common.TypeReaders;
public sealed class CommandTypeReader : NadekoTypeReader<CommandInfo>
{
private readonly CommandService _cmds;
private readonly ICommandHandler _handler;
public CommandTypeReader(ICommandHandler handler, CommandService cmds)
{
_handler = handler;
_cmds = cmds;
}
public override ValueTask<TypeReaderResult<CommandInfo>> ReadAsync(ICommandContext ctx, string input)
{
input = input.ToUpperInvariant();
var prefix = _handler.GetPrefix(ctx.Guild);
if (!input.StartsWith(prefix.ToUpperInvariant(), StringComparison.InvariantCulture))
return new(TypeReaderResult.FromError<CommandInfo>(CommandError.ParseFailed, "No such command found."));
input = input[prefix.Length..];
var cmd = _cmds.Commands.FirstOrDefault(c => c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input));
if (cmd is null)
return new(TypeReaderResult.FromError<CommandInfo>(CommandError.ParseFailed, "No such command found."));
return new(TypeReaderResult.FromSuccess(cmd));
}
}

View File

@@ -0,0 +1,10 @@
#nullable disable
using System.Runtime.InteropServices;
namespace NadekoBot.Modules.Permissions;
[StructLayout(LayoutKind.Sequential, Size = 1)]
public readonly struct CleverBotResponseStr
{
public const string CLEVERBOT_RESPONSE = "cleverbot:response";
}

View File

@@ -0,0 +1,31 @@
using YamlDotNet.Serialization;
namespace NadekoBot.Common.Attributes;
public static class CommandNameLoadHelper
{
private static readonly IDeserializer _deserializer = new Deserializer();
private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases
= new(() => LoadAliases());
public static Dictionary<string, string[]> LoadAliases(string aliasesFilePath = "data/aliases.yml")
{
var text = File.ReadAllText(aliasesFilePath);
return _deserializer.Deserialize<Dictionary<string, string[]>>(text);
}
public static string[] GetAliasesFor(string methodName)
=> _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
? aliases.Skip(1).ToArray()
: Array.Empty<string>();
public static string GetCommandNameFor(string methodName)
{
methodName = methodName.ToLowerInvariant();
var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
? aliases[0]
: methodName;
return toReturn;
}
}

View File

@@ -0,0 +1,10 @@
#nullable disable
namespace NadekoBot.Common;
public enum AddRemove
{
Add = int.MinValue,
Remove = int.MinValue + 1,
Rem = int.MinValue + 1,
Rm = int.MinValue + 1
}

View File

@@ -0,0 +1,17 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Common;
public class CmdStrings
{
public string[] Usages { get; }
public string Description { get; }
[JsonConstructor]
public CmdStrings([JsonProperty("args")] string[] usages, [JsonProperty("desc")] string description)
{
Usages = usages;
Description = description;
}
}

View File

@@ -0,0 +1,9 @@
#nullable disable
namespace NadekoBot.Common;
public class CommandData
{
public string Cmd { get; set; }
public string Desc { get; set; }
public string[] Usage { get; set; }
}

View File

@@ -0,0 +1,38 @@
#nullable disable
namespace NadekoBot.Common;
public class DownloadTracker : INService
{
private ConcurrentDictionary<ulong, DateTime> LastDownloads { get; } = new();
private readonly SemaphoreSlim _downloadUsersSemaphore = new(1, 1);
/// <summary>
/// Ensures all users on the specified guild were downloaded within the last hour.
/// </summary>
/// <param name="guild">Guild to check and potentially download users from</param>
/// <returns>Task representing download state</returns>
public async Task EnsureUsersDownloadedAsync(IGuild guild)
{
#if GLOBAL_NADEKO
return;
#endif
await _downloadUsersSemaphore.WaitAsync();
try
{
var now = DateTime.UtcNow;
// download once per hour at most
var added = LastDownloads.AddOrUpdate(guild.Id,
now,
(_, old) => now - old > TimeSpan.FromHours(1) ? now : old);
// means that this entry was just added - download the users
if (added == now)
await guild.DownloadUsersAsync();
}
finally
{
_downloadUsersSemaphore.Release();
}
}
}

View File

@@ -0,0 +1,13 @@
#nullable disable
namespace NadekoBot.Common;
public static class Helpers
{
public static void ReadErrorAndExit(int exitCode)
{
if (!Console.IsInputRedirected)
Console.ReadKey();
Environment.Exit(exitCode);
}
}

View File

@@ -0,0 +1,51 @@
#nullable disable
using NadekoBot.Common.Yml;
using Cloneable;
namespace NadekoBot.Common;
[Cloneable]
public partial class ImageUrls : ICloneable<ImageUrls>
{
[Comment("DO NOT CHANGE")]
public int Version { get; set; } = 3;
public CoinData Coins { get; set; }
public Uri[] Currency { get; set; }
public Uri[] Dice { get; set; }
public RategirlData Rategirl { get; set; }
public XpData Xp { get; set; }
//new
public RipData Rip { get; set; }
public SlotData Slots { get; set; }
public class RipData
{
public Uri Bg { get; set; }
public Uri Overlay { get; set; }
}
public class SlotData
{
public Uri[] Emojis { get; set; }
public Uri Bg { get; set; }
}
public class CoinData
{
public Uri[] Heads { get; set; }
public Uri[] Tails { get; set; }
}
public class RategirlData
{
public Uri Matrix { get; set; }
public Uri Dot { get; set; }
}
public class XpData
{
public Uri Bg { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Common.JsonConverters;
public class CultureInfoConverter : JsonConverter<CultureInfo>
{
public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> new(reader.GetString() ?? "en-US");
public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.Name);
}

View File

@@ -0,0 +1,14 @@
using SixLabors.ImageSharp.PixelFormats;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Common.JsonConverters;
public class Rgba32Converter : JsonConverter<Rgba32>
{
public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> Rgba32.ParseHex(reader.GetString());
public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToHex());
}

View File

@@ -0,0 +1,14 @@
#nullable disable
using CommandLine;
namespace NadekoBot.Common;
public class LbOpts : INadekoCommandOptions
{
[Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")]
public bool Clean { get; set; }
public void NormalizeOptions()
{
}
}

View File

@@ -0,0 +1,16 @@
#nullable disable
using LinqToDB;
using System.Linq.Expressions;
namespace NadekoBot.Common;
public static class Linq2DbExpressions
{
[ExpressionMethod(nameof(GuildOnShardExpression))]
public static bool GuildOnShard(ulong guildId, int totalShards, int shardId)
=> throw new NotSupportedException();
private static Expression<Func<ulong, int, int, bool>> GuildOnShardExpression()
=> (guildId, totalShards, shardId)
=> guildId / 4194304 % (ulong)totalShards == (ulong)shardId;
}

View File

@@ -0,0 +1,52 @@
#nullable disable
using System.Net;
using System.Runtime.CompilerServices;
namespace NadekoBot.Common;
public class LoginErrorHandler
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Handle(Exception ex)
=> Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord");
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Handle(HttpException ex)
{
switch (ex.HttpCode)
{
case HttpStatusCode.Unauthorized:
Log.Error("Your bot token is wrong.\n"
+ "You can find the bot token under the Bot tab in the developer page.\n"
+ "Fix your token in the credentials file and restart the bot");
break;
case HttpStatusCode.BadRequest:
Log.Error("Something has been incorrectly formatted in your credentials file.\n"
+ "Use the JSON Guide as reference to fix it and restart the bot");
Log.Error("If you are on Linux, make sure Redis is installed and running");
break;
case HttpStatusCode.RequestTimeout:
Log.Error("The request timed out. Make sure you have no external program blocking the bot "
+ "from connecting to the internet");
break;
case HttpStatusCode.ServiceUnavailable:
case HttpStatusCode.InternalServerError:
Log.Error("Discord is having internal issues. Please, try again later");
break;
case HttpStatusCode.TooManyRequests:
Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n"
+ "Global ratelimits usually last for an hour");
break;
default:
Log.Warning("An error occurred while attempting to connect to Discord");
break;
}
Log.Fatal(ex, "Fatal error occurred while loading credentials");
}
}

View File

@@ -0,0 +1,46 @@
#nullable disable
namespace NadekoBot.Common;
public class OldCreds
{
public string Token { get; set; } = string.Empty;
public ulong[] OwnerIds { get; set; } = new ulong[1];
public string LoLApiKey { get; set; } = string.Empty;
public string GoogleApiKey { get; set; } = string.Empty;
public string MashapeKey { get; set; } = string.Empty;
public string OsuApiKey { get; set; } = string.Empty;
public string SoundCloudClientId { get; set; } = string.Empty;
public string CleverbotApiKey { get; set; } = string.Empty;
public string CarbonKey { get; set; } = string.Empty;
public int TotalShards { get; set; } = 1;
public string PatreonAccessToken { get; set; } = string.Empty;
public string PatreonCampaignId { get; set; } = "334038";
public RestartConfig RestartCommand { get; set; }
public string ShardRunCommand { get; set; } = string.Empty;
public string ShardRunArguments { get; set; } = string.Empty;
public int? ShardRunPort { get; set; }
public string MiningProxyUrl { get; set; } = string.Empty;
public string MiningProxyCreds { get; set; } = string.Empty;
public string BotListToken { get; set; } = string.Empty;
public string TwitchClientId { get; set; } = string.Empty;
public string VotesToken { get; set; } = string.Empty;
public string VotesUrl { get; set; } = string.Empty;
public string RedisOptions { get; set; } = string.Empty;
public string LocationIqApiKey { get; set; } = string.Empty;
public string TimezoneDbApiKey { get; set; } = string.Empty;
public string CoinmarketcapApiKey { get; set; } = string.Empty;
public class RestartConfig
{
public string Cmd { get; set; }
public string Args { get; set; }
public RestartConfig(string cmd, string args)
{
Cmd = cmd;
Args = args;
}
}
}

View File

@@ -0,0 +1,23 @@
using CommandLine;
namespace NadekoBot.Common;
public static class OptionsParser
{
public static T ParseFrom<T>(string[]? args)
where T : INadekoCommandOptions, new()
=> ParseFrom(new T(), args).Item1;
public static (T, bool) ParseFrom<T>(T options, string[]? args)
where T : INadekoCommandOptions
{
using var p = new Parser(x =>
{
x.HelpWriter = null;
});
var res = p.ParseArguments<T>(args);
var output = res.MapResult(x => x, _ => options);
output.NormalizeOptions();
return (output, res.Tag == ParserResultType.Parsed);
}
}

View File

@@ -0,0 +1,9 @@
#nullable disable
namespace NadekoBot.Common;
public class OsuMapData
{
public string Title { get; set; }
public string Artist { get; set; }
public string Version { get; set; }
}

View File

@@ -0,0 +1,58 @@
#nullable disable
using Newtonsoft.Json;
namespace NadekoBot.Common;
public class OsuUserBests
{
[JsonProperty("beatmap_id")]
public string BeatmapId { get; set; }
[JsonProperty("score_id")]
public string ScoreId { get; set; }
[JsonProperty("score")]
public string Score { get; set; }
[JsonProperty("maxcombo")]
public string Maxcombo { get; set; }
[JsonProperty("count50")]
public double Count50 { get; set; }
[JsonProperty("count100")]
public double Count100 { get; set; }
[JsonProperty("count300")]
public double Count300 { get; set; }
[JsonProperty("countmiss")]
public int Countmiss { get; set; }
[JsonProperty("countkatu")]
public double Countkatu { get; set; }
[JsonProperty("countgeki")]
public double Countgeki { get; set; }
[JsonProperty("perfect")]
public string Perfect { get; set; }
[JsonProperty("enabled_mods")]
public int EnabledMods { get; set; }
[JsonProperty("user_id")]
public string UserId { get; set; }
[JsonProperty("date")]
public string Date { get; set; }
[JsonProperty("rank")]
public string Rank { get; set; }
[JsonProperty("pp")]
public double Pp { get; set; }
[JsonProperty("replay_available")]
public string ReplayAvailable { get; set; }
}

View File

@@ -0,0 +1,8 @@
#nullable disable
namespace NadekoBot.Common.Pokemon;
public class PokemonNameId
{
public int Id { get; set; }
public string Name { get; set; }
}

View File

@@ -0,0 +1,42 @@
#nullable disable
using System.Text.Json.Serialization;
namespace NadekoBot.Common.Pokemon;
public class SearchPokemon
{
[JsonPropertyName("num")]
public int Id { get; set; }
public string Species { get; set; }
public string[] Types { get; set; }
public GenderRatioClass GenderRatio { get; set; }
public BaseStatsClass BaseStats { get; set; }
public Dictionary<string, string> Abilities { get; set; }
public float HeightM { get; set; }
public float WeightKg { get; set; }
public string Color { get; set; }
public string[] Evos { get; set; }
public string[] EggGroups { get; set; }
public class GenderRatioClass
{
public float M { get; set; }
public float F { get; set; }
}
public class BaseStatsClass
{
public int Hp { get; set; }
public int Atk { get; set; }
public int Def { get; set; }
public int Spa { get; set; }
public int Spd { get; set; }
public int Spe { get; set; }
public override string ToString()
=> $@"💚**HP:** {Hp,-4} ⚔**ATK:** {Atk,-4} 🛡**DEF:** {Def,-4}
✨**SPA:** {Spa,-4} 🎇**SPD:** {Spd,-4} 💨**SPE:** {Spe,-4}";
}
}

View File

@@ -0,0 +1,10 @@
#nullable disable
namespace NadekoBot.Common.Pokemon;
public class SearchPokemonAbility
{
public string Desc { get; set; }
public string ShortDesc { get; set; }
public string Name { get; set; }
public float Rating { get; set; }
}

View File

@@ -0,0 +1,15 @@
#nullable disable
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace NadekoBot.Common;
public class RequireObjectPropertiesContractResolver : DefaultContractResolver
{
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
contract.ItemRequired = Required.DisallowNull;
return contract;
}
}

View File

@@ -0,0 +1,11 @@
#nullable disable
namespace NadekoBot.Modules.Games.Common.Trivia;
public sealed class TriviaQuestionModel
{
public string Category { get; init; }
public string Question { get; init; }
public string ImageUrl { get; init; }
public string AnswerImageUrl { get; init; }
public string Answer { get; init; }
}

View File

@@ -0,0 +1,13 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders;
public sealed class EmoteTypeReader : NadekoTypeReader<Emote>
{
public override ValueTask<TypeReaderResult<Emote>> ReadAsync(ICommandContext ctx, string input)
{
if (!Emote.TryParse(input, out var emote))
return new(TypeReaderResult.FromError<Emote>(CommandError.ParseFailed, "Input is not a valid emote"));
return new(TypeReaderResult.FromSuccess(emote));
}
}

View File

@@ -0,0 +1,24 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders;
public sealed class GuildTypeReader : NadekoTypeReader<IGuild>
{
private readonly DiscordSocketClient _client;
public GuildTypeReader(DiscordSocketClient client)
=> _client = client;
public override ValueTask<TypeReaderResult<IGuild>> ReadAsync(ICommandContext context, string input)
{
input = input.Trim().ToUpperInvariant();
var guilds = _client.Guilds;
IGuild guild = guilds.FirstOrDefault(g => g.Id.ToString().Trim().ToUpperInvariant() == input) //by id
?? guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == input); //by name
if (guild is not null)
return new(TypeReaderResult.FromSuccess(guild));
return new(
TypeReaderResult.FromError<IGuild>(CommandError.ParseFailed, "No guild by that name or Id found"));
}
}

View File

@@ -0,0 +1,33 @@
namespace NadekoBot.Common.TypeReaders;
public sealed class GuildUserTypeReader : NadekoTypeReader<IGuildUser>
{
public override async ValueTask<TypeReaderResult<IGuildUser>> ReadAsync(ICommandContext ctx, string input)
{
if (ctx.Guild is null)
return TypeReaderResult.FromError<IGuildUser>(CommandError.Unsuccessful, "Must be in a guild.");
input = input.Trim();
IGuildUser? user = null;
if (MentionUtils.TryParseUser(input, out var id))
user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload);
if (ulong.TryParse(input, out id))
user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload);
if (user is null)
{
var users = await ctx.Guild.GetUsersAsync(CacheMode.CacheOnly);
user = users.FirstOrDefault(x => x.Username == input)
?? users.FirstOrDefault(x =>
string.Equals(x.ToString(), input, StringComparison.InvariantCultureIgnoreCase))
?? users.FirstOrDefault(x =>
string.Equals(x.Username, input, StringComparison.InvariantCultureIgnoreCase));
}
if (user is null)
return TypeReaderResult.FromError<IGuildUser>(CommandError.ObjectNotFound, "User not found.");
return TypeReaderResult.FromSuccess(user);
}
}

View File

@@ -0,0 +1,21 @@
#nullable disable
using Nadeko.Common;
namespace NadekoBot.Common.TypeReaders;
public sealed class KwumTypeReader : NadekoTypeReader<kwum>
{
public override ValueTask<TypeReaderResult<kwum>> ReadAsync(ICommandContext context, string input)
{
if (kwum.TryParse(input, out var val))
return new(TypeReaderResult.FromSuccess(val));
return new(TypeReaderResult.FromError<kwum>(CommandError.ParseFailed, "Input is not a valid kwum"));
}
}
public sealed class SmartTextTypeReader : NadekoTypeReader<SmartText>
{
public override ValueTask<TypeReaderResult<SmartText>> ReadAsync(ICommandContext ctx, string input)
=> new(TypeReaderResult.FromSuccess(SmartText.CreateFrom(input)));
}

View File

@@ -0,0 +1,27 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders.Models;
public class PermissionAction
{
public static PermissionAction Enable
=> new(true);
public static PermissionAction Disable
=> new(false);
public bool Value { get; }
public PermissionAction(bool value)
=> Value = value;
public override bool Equals(object obj)
{
if (obj is null || GetType() != obj.GetType())
return false;
return Value == ((PermissionAction)obj).Value;
}
public override int GetHashCode()
=> Value.GetHashCode();
}

View File

@@ -0,0 +1,55 @@
#nullable disable
using System.Text.RegularExpressions;
namespace NadekoBot.Common.TypeReaders.Models;
public class StoopidTime
{
private static readonly Regex _regex = new(
@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$",
RegexOptions.Compiled | RegexOptions.Multiline);
public string Input { get; set; }
public TimeSpan Time { get; set; }
private StoopidTime() { }
public static StoopidTime FromInput(string input)
{
var m = _regex.Match(input);
if (m.Length == 0)
throw new ArgumentException("Invalid string input format.");
var namesAndValues = new Dictionary<string, int>();
foreach (var groupName in _regex.GetGroupNames())
{
if (groupName == "0")
continue;
if (!int.TryParse(m.Groups[groupName].Value, out var value))
{
namesAndValues[groupName] = 0;
continue;
}
if (value < 1)
throw new ArgumentException($"Invalid {groupName} value.");
namesAndValues[groupName] = value;
}
var ts = new TimeSpan((30 * namesAndValues["months"]) + (7 * namesAndValues["weeks"]) + namesAndValues["days"],
namesAndValues["hours"],
namesAndValues["minutes"],
namesAndValues["seconds"]);
if (ts > TimeSpan.FromDays(90))
throw new ArgumentException("Time is too long.");
return new()
{
Input = input,
Time = ts
};
}
}

View File

@@ -0,0 +1,50 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders;
public sealed class ModuleTypeReader : NadekoTypeReader<ModuleInfo>
{
private readonly CommandService _cmds;
public ModuleTypeReader(CommandService cmds)
=> _cmds = cmds;
public override ValueTask<TypeReaderResult<ModuleInfo>> ReadAsync(ICommandContext context, string input)
{
input = input.ToUpperInvariant();
var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
.FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)
?.Key;
if (module is null)
return new(TypeReaderResult.FromError<ModuleInfo>(CommandError.ParseFailed, "No such module found."));
return new(TypeReaderResult.FromSuccess(module));
}
}
public sealed class ModuleOrCrTypeReader : NadekoTypeReader<ModuleOrCrInfo>
{
private readonly CommandService _cmds;
public ModuleOrCrTypeReader(CommandService cmds)
=> _cmds = cmds;
public override ValueTask<TypeReaderResult<ModuleOrCrInfo>> ReadAsync(ICommandContext context, string input)
{
input = input.ToUpperInvariant();
var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
.FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)
?.Key;
if (module is null && input != "ACTUALEXPRESSIONS")
return new(TypeReaderResult.FromError<ModuleOrCrInfo>(CommandError.ParseFailed, "No such module found."));
return new(TypeReaderResult.FromSuccess(new ModuleOrCrInfo
{
Name = input
}));
}
}
public sealed class ModuleOrCrInfo
{
public string Name { get; set; }
}

View File

@@ -0,0 +1,39 @@
#nullable disable
using NadekoBot.Common.TypeReaders.Models;
namespace NadekoBot.Common.TypeReaders;
/// <summary>
/// Used instead of bool for more flexible keywords for true/false only in the permission module
/// </summary>
public sealed class PermissionActionTypeReader : NadekoTypeReader<PermissionAction>
{
public override ValueTask<TypeReaderResult<PermissionAction>> ReadAsync(ICommandContext context, string input)
{
input = input.ToUpperInvariant();
switch (input)
{
case "1":
case "T":
case "TRUE":
case "ENABLE":
case "ENABLED":
case "ALLOW":
case "PERMIT":
case "UNBAN":
return new(TypeReaderResult.FromSuccess(PermissionAction.Enable));
case "0":
case "F":
case "FALSE":
case "DENY":
case "DISABLE":
case "DISABLED":
case "DISALLOW":
case "BAN":
return new(TypeReaderResult.FromSuccess(PermissionAction.Disable));
default:
return new(TypeReaderResult.FromError<PermissionAction>(CommandError.ParseFailed,
"Did not receive a valid boolean value"));
}
}
}

View File

@@ -0,0 +1,20 @@
using Color = SixLabors.ImageSharp.Color;
#nullable disable
namespace NadekoBot.Common.TypeReaders;
public sealed class Rgba32TypeReader : NadekoTypeReader<Color>
{
public override ValueTask<TypeReaderResult<Color>> ReadAsync(ICommandContext context, string input)
{
input = input.Replace("#", "", StringComparison.InvariantCulture);
try
{
return ValueTask.FromResult(TypeReaderResult.FromSuccess(Color.ParseHex(input)));
}
catch
{
return ValueTask.FromResult(TypeReaderResult.FromError<Color>(CommandError.ParseFailed, "Parameter is not a valid color hex."));
}
}
}

View File

@@ -0,0 +1,22 @@
#nullable disable
using NadekoBot.Common.TypeReaders.Models;
namespace NadekoBot.Common.TypeReaders;
public sealed class StoopidTimeTypeReader : NadekoTypeReader<StoopidTime>
{
public override ValueTask<TypeReaderResult<StoopidTime>> ReadAsync(ICommandContext context, string input)
{
if (string.IsNullOrWhiteSpace(input))
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Unsuccessful, "Input is empty."));
try
{
var time = StoopidTime.FromInput(input);
return new(TypeReaderResult.FromSuccess(time));
}
catch (Exception ex)
{
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Exception, ex.Message));
}
}
}

View File

@@ -0,0 +1,275 @@
#nullable disable
using NadekoBot.Common.Yml;
namespace NadekoBot.Common;
public sealed class Creds : IBotCredentials
{
[Comment("""DO NOT CHANGE""")]
public int Version { get; set; }
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
public string Token { get; set; }
[Comment("""
List of Ids of the users who have bot owner permissions
**DO NOT ADD PEOPLE YOU DON'T TRUST**
""")]
public ICollection<ulong> OwnerIds { get; set; }
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
public bool UsePrivilegedIntents { get; set; }
[Comment("""
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 NadekoBot.Coordinator to start the bot, and it will correctly override this value.
""")]
public int TotalShards { get; set; }
[Comment(
"""
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).
""")]
public string GoogleApiKey { get; set; }
[Comment(
"""
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
""")]
public GoogleApiConfig Google { get; set; }
[Comment("""Settings for voting system for discordbots. Meant for use on global Nadeko.""")]
public VotesSettings Votes { get; set; }
[Comment("""
Patreon auto reward system settings.
go to https://www.patreon.com/portal -> my clients -> create client
""")]
public PatreonSettings Patreon { get; set; }
[Comment("""Api key for sending stats to DiscordBotList.""")]
public string BotListToken { get; set; }
[Comment("""Official cleverbot api key.""")]
public string CleverbotApiKey { get; set; }
[Comment(@"Official GPT-3 api key.")]
public string Gpt3ApiKey { get; set; }
[Comment("""
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
""")]
public BotCacheImplemenation BotCache { get; set; }
[Comment("""
Redis connection string. Don't change if you don't know what you're doing.
Only used if botCache is set to 'redis'
""")]
public string RedisOptions { get; set; }
[Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")]
public DbOptions Db { get; set; }
[Comment("""
Address and port of the coordinator endpoint. Leave empty for default.
Change only if you've changed the coordinator address or port.
""")]
public string CoordinatorUrl { get; set; }
[Comment(
"""Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)""")]
public string RapidApiKey { get; set; }
[Comment("""
https://locationiq.com api key (register and you will receive the token in the email).
Used only for .time command.
""")]
public string LocationIqApiKey { get; set; }
[Comment("""
https://timezonedb.com api key (register and you will receive the token in the email).
Used only for .time command
""")]
public string TimezoneDbApiKey { get; set; }
[Comment("""
https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
Used for cryptocurrency related commands.
""")]
public string CoinmarketcapApiKey { get; set; }
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
// Used for stocks related commands.")]
// public string PolygonIoApiKey { get; set; }
[Comment("""Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api""")]
public string OsuApiKey { get; set; }
[Comment("""
Optional Trovo client id.
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
""")]
public string TrovoClientId { get; set; }
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
public string TwitchClientId { get; set; }
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
public string TwitchClientSecret { get; set; }
[Comment("""
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: "NadekoBot.dll -- {0}"
Windows default
cmd: NadekoBot.exe
args: "{0}"
""")]
public RestartConfig RestartCommand { get; set; }
public Creds()
{
Version = 7;
Token = string.Empty;
UsePrivilegedIntents = true;
OwnerIds = new List<ulong>();
TotalShards = 1;
GoogleApiKey = string.Empty;
Votes = new VotesSettings(string.Empty, string.Empty, string.Empty, string.Empty);
Patreon = new PatreonSettings(string.Empty, string.Empty, string.Empty, string.Empty);
BotListToken = string.Empty;
CleverbotApiKey = string.Empty;
Gpt3ApiKey = string.Empty;
BotCache = BotCacheImplemenation.Memory;
RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
Db = new DbOptions()
{
Type = "sqlite",
ConnectionString = "Data Source=data/NadekoBot.db"
};
CoordinatorUrl = "http://localhost:3442";
RestartCommand = new RestartConfig();
Google = new GoogleApiConfig();
}
public class DbOptions
: IDbOptions
{
[Comment("""
Database type. "sqlite", "mysql" and "postgresql" are supported.
Default is "sqlite"
""")]
public string Type { get; set; }
[Comment("""
Database connection string.
You MUST change this if you're not using "sqlite" type.
Default is "Data Source=data/NadekoBot.db"
Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=nadeko"
Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=nadeko;"
""")]
public string ConnectionString { get; set; }
}
public sealed record PatreonSettings : IPatreonSettings
{
public string ClientId { get; set; }
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public string ClientSecret { get; set; }
[Comment(
"""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)""")]
public string CampaignId { get; set; }
public PatreonSettings(
string accessToken,
string refreshToken,
string clientSecret,
string campaignId)
{
AccessToken = accessToken;
RefreshToken = refreshToken;
ClientSecret = clientSecret;
CampaignId = campaignId;
}
public PatreonSettings()
{
}
}
public sealed record VotesSettings : IVotesSettings
{
[Comment("""
top.gg votes service url
This is the url of your instance of the NadekoBot.Votes api
Example: https://votes.my.cool.bot.com
""")]
public string TopggServiceUrl { get; set; }
[Comment("""
Authorization header value sent to the TopGG service url with each request
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file
""")]
public string TopggKey { get; set; }
[Comment("""
discords.com votes service url
This is the url of your instance of the NadekoBot.Votes api
Example: https://votes.my.cool.bot.com
""")]
public string DiscordsServiceUrl { get; set; }
[Comment("""
Authorization header value sent to the Discords service url with each request
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file
""")]
public string DiscordsKey { get; set; }
public VotesSettings()
{
}
public VotesSettings(
string topggServiceUrl,
string topggKey,
string discordsServiceUrl,
string discordsKey)
{
TopggServiceUrl = topggServiceUrl;
TopggKey = topggKey;
DiscordsServiceUrl = discordsServiceUrl;
DiscordsKey = discordsKey;
}
}
}
public class GoogleApiConfig : IGoogleApiConfig
{
public string SearchId { get; init; }
public string ImageSearchId { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace NadekoBot.Services.Currency;
public enum CurrencyType
{
Default
}

View File

@@ -0,0 +1,40 @@
using NadekoBot.Services.Currency;
namespace NadekoBot.Services;
public interface ICurrencyService
{
Task<IWallet> GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default);
Task AddBulkAsync(
IReadOnlyCollection<ulong> userIds,
long amount,
TxData? txData,
CurrencyType type = CurrencyType.Default);
Task RemoveBulkAsync(
IReadOnlyCollection<ulong> userIds,
long amount,
TxData? txData,
CurrencyType type = CurrencyType.Default);
Task AddAsync(
ulong userId,
long amount,
TxData? txData);
Task AddAsync(
IUser user,
long amount,
TxData? txData);
Task<bool> RemoveAsync(
ulong userId,
long amount,
TxData? txData);
Task<bool> RemoveAsync(
IUser user,
long amount,
TxData? txData);
}

View File

@@ -0,0 +1,9 @@
using NadekoBot.Services.Currency;
namespace NadekoBot.Services;
public interface ITxTracker
{
Task TrackAdd(long amount, TxData? txData);
Task TrackRemove(long amount, TxData? txData);
}

View File

@@ -0,0 +1,40 @@
namespace NadekoBot.Services.Currency;
public interface IWallet
{
public ulong UserId { get; }
public Task<long> GetBalance();
public Task<bool> Take(long amount, TxData? txData);
public Task Add(long amount, TxData? txData);
public async Task<bool> Transfer(
long amount,
IWallet to,
TxData? txData)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0.");
if (txData is not null)
txData = txData with
{
OtherId = to.UserId
};
var succ = await Take(amount, txData);
if (!succ)
return false;
if (txData is not null)
txData = txData with
{
OtherId = UserId
};
await to.Add(amount, txData);
return true;
}
}

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Services.Currency;
public record class TxData(
string Type,
string Extra,
string? Note = "",
ulong? OtherId = null);

View File

@@ -1,16 +1,16 @@
// // global using System.Collections.Concurrent;
// global using NonBlocking;
global using NonBlocking;
//
// // packages
// global using Serilog;
global using Humanizer;
//
// // nadekobot
// global using NadekoBot;
// global using NadekoBot.Services;
global using NadekoBot;
global using NadekoBot.Services;
global using Nadeko.Common; // new project
// global using NadekoBot.Common; // old + nadekobot specific things
// global using NadekoBot.Common.Attributes;
global using NadekoBot.Common; // old + nadekobot specific things
global using NadekoBot.Common.Attributes;
global using NadekoBot.Extensions;
// global using Nadeko.Snake;
@@ -28,4 +28,7 @@ global using LeftoverAttribute = Discord.Commands.RemainderAttribute;
// global using TypeReaderResult = NadekoBot.Common.TypeReaders.TypeReaderResult;
// non-essential
global using JetBrains.Annotations;
global using JetBrains.Annotations;
global using Serilog;

View File

@@ -0,0 +1,12 @@
#nullable disable
using NadekoBot.Services.Database.Models;
namespace NadekoBot;
public interface IBot
{
IReadOnlyList<ulong> GetCurrentGuildIds();
event Func<GuildConfig, Task> JoinedGuild;
IReadOnlyCollection<GuildConfig> AllGuildConfigs { get; }
bool IsReady { get; }
}

View File

@@ -0,0 +1,6 @@
namespace Nadeko.Bot.Common;
public interface ICurrencyProvider
{
string GetCurrencySign();
}

View File

@@ -0,0 +1,7 @@
#nullable disable
namespace Nadeko.Common;
public interface IDiscordPermOverrideService
{
bool TryGetOverrides(ulong guildId, string commandName, out Nadeko.Bot.Db.GuildPerm? perm);
}

View File

@@ -0,0 +1,7 @@
#nullable disable
namespace NadekoBot.Common;
public interface INadekoCommandOptions
{
void NormalizeOptions();
}

View File

@@ -0,0 +1,15 @@
using NadekoBot;
using NadekoBot.Services;
using OneOf;
using OneOf.Types;
namespace Nadeko.Bot.Common;
public interface IPermissionChecker
{
Task<OneOf<Success, Error<LocStr>>> CheckAsync(IGuild guild,
IMessageChannel channel,
IUser author,
string module,
string cmd);
}

View File

@@ -15,10 +15,12 @@
<ProjectReference Include="..\Nadeko.Bot.Db\Nadeko.Bot.Db.csproj" />
<ProjectReference Include="..\Nadeko.Common\Nadeko.Common.csproj" />
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Humanizer" Version="2.14.1">
<PrivateAssets>all</PrivateAssets>
<Publish>True</Publish>
@@ -29,9 +31,6 @@
</ItemGroup>
<ItemGroup>
<None Update="data\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<AdditionalFiles Include="data\strings\responses\responses.en-US.json" />
<AdditionalFiles Include="..\NadekoBot\data\strings\responses\responses.en-US.json" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
#nullable disable
namespace NadekoBot.Common.TypeReaders;
[MeansImplicitUse(ImplicitUseTargetFlags.Default | ImplicitUseTargetFlags.WithInheritors)]
public abstract class NadekoTypeReader<T> : TypeReader
{
public abstract ValueTask<TypeReaderResult<T>> ReadAsync(ICommandContext ctx, string input);
public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
ICommandContext ctx,
string input,
IServiceProvider services)
=> await ReadAsync(ctx, input);
}

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Modules.Utility.Patronage;
public readonly struct FeatureLimitKey
{
public string PrettyName { get; init; }
public string Key { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Utility.Patronage;
public readonly struct FeatureQuotaStats
{
public (uint Cur, uint Max) Hourly { get; init; }
public (uint Cur, uint Max) Daily { get; init; }
public (uint Cur, uint Max) Monthly { get; init; }
}

View File

@@ -0,0 +1,11 @@
namespace NadekoBot.Modules.Utility;
public interface ISubscriberData
{
public string UniquePlatformUserId { get; }
public ulong UserId { get; }
public int Cents { get; }
public DateTime? LastCharge { get; }
public SubscriptionChargeStatus ChargeStatus { get; }
}

View File

@@ -0,0 +1,56 @@
using NadekoBot.Db.Models;
using OneOf;
namespace NadekoBot.Modules.Utility.Patronage;
/// <summary>
/// Manages patrons and provides access to their data
/// </summary>
public interface IPatronageService
{
/// <summary>
/// Called when the payment is made.
/// Either as a single payment for that patron,
/// or as a recurring monthly donation.
/// </summary>
public event Func<Patron, Task> OnNewPatronPayment;
/// <summary>
/// Called when the patron changes the pledge amount
/// (Patron old, Patron new) => Task
/// </summary>
public event Func<Patron, Patron, Task> OnPatronUpdated;
/// <summary>
/// Called when the patron refunds the purchase or it's marked as fraud
/// </summary>
public event Func<Patron, Task> OnPatronRefunded;
/// <summary>
/// Gets a Patron with the specified userId
/// </summary>
/// <param name="userId">UserId for which to get the patron data for.</param>
/// <returns>A patron with the specifeid userId</returns>
public Task<Patron> GetPatronAsync(ulong userId);
/// <summary>
/// Gets the quota statistic for the user/patron specified by the userId
/// </summary>
/// <param name="userId">UserId of the user for which to get the quota statistic for</param>
/// <returns>Quota stats for the specified user</returns>
Task<UserQuotaStats> GetUserQuotaStatistic(ulong userId);
Task<FeatureLimit> TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue);
ValueTask<OneOf<(uint Hourly, uint Daily, uint Monthly), QuotaLimit>> TryIncrementQuotaCounterAsync(
ulong userId,
bool isSelf,
FeatureType featureType,
string featureName,
uint? maybeHourly,
uint? maybeDaily,
uint? maybeMonthly);
PatronConfigData GetConfig();
}

View File

@@ -0,0 +1,16 @@
#nullable disable
namespace NadekoBot.Modules.Utility;
/// <summary>
/// Services implementing this interface are handling pledges/subscriptions/payments coming
/// from a payment platform.
/// </summary>
public interface ISubscriptionHandler
{
/// <summary>
/// Get Current patrons in batches.
/// This will only return patrons who have their discord account connected
/// </summary>
/// <returns>Batched patrons</returns>
public IAsyncEnumerable<IReadOnlyCollection<ISubscriberData>> GetPatronsAsync();
}

View File

@@ -0,0 +1,38 @@
namespace NadekoBot.Modules.Utility.Patronage;
public readonly struct Patron
{
/// <summary>
/// Unique id assigned to this patron by the payment platform
/// </summary>
public string UniquePlatformUserId { get; init; }
/// <summary>
/// Discord UserId to which this <see cref="UniquePlatformUserId"/> is connected to
/// </summary>
public ulong UserId { get; init; }
/// <summary>
/// Amount the Patron is currently pledging or paid
/// </summary>
public int Amount { get; init; }
/// <summary>
/// Current Tier of the patron
/// (do not question it in consumer classes, as the calculation should be always internal and may change)
/// </summary>
public PatronTier Tier { get; init; }
/// <summary>
/// When was the last time this <see cref="Amount"/> was paid
/// </summary>
public DateTime PaidAt { get; init; }
/// <summary>
/// After which date does the user's Patronage benefit end
/// </summary>
public DateTime ValidThru { get; init; }
public bool IsActive
=> !ValidThru.IsBeforeToday();
}

View File

@@ -0,0 +1,37 @@
using NadekoBot.Common.Yml;
using Cloneable;
namespace NadekoBot.Modules.Utility.Patronage;
[Cloneable]
public partial class PatronConfigData : ICloneable<PatronConfigData>
{
[Comment("DO NOT CHANGE")]
public int Version { get; set; } = 2;
[Comment("Whether the patronage feature is enabled")]
public bool IsEnabled { get; set; }
[Comment("List of patron only features and relevant quota data")]
public FeatureQuotas Quotas { get; set; }
public PatronConfigData()
{
Quotas = new();
}
public class FeatureQuotas
{
[Comment("Dictionary of feature names with their respective limits. Set to null for unlimited")]
public Dictionary<string, Dictionary<PatronTier, int?>> Features { get; set; } = new();
[Comment("Dictionary of commands with their respective quota data")]
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Commands { get; set; } = new();
[Comment("Dictionary of groups with their respective quota data")]
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Groups { get; set; } = new();
[Comment("Dictionary of modules with their respective quota data")]
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Modules { get; set; } = new();
}
}

View File

@@ -0,0 +1,33 @@
namespace NadekoBot.Modules.Utility.Patronage;
public static class PatronExtensions
{
public static string ToFullName(this PatronTier tier)
=> tier switch
{
_ => $"Patron Tier {tier}",
};
public static string ToFullName(this QuotaPer per)
=> per.Humanize(LetterCasing.LowerCase);
public static DateTime DayOfNextMonth(this DateTime date, int day)
{
var nextMonth = date.AddMonths(1);
var dt = DateTime.SpecifyKind(new(nextMonth.Year, nextMonth.Month, day), DateTimeKind.Utc);
return dt;
}
public static DateTime FirstOfNextMonth(this DateTime date)
=> date.DayOfNextMonth(1);
public static DateTime SecondOfNextMonth(this DateTime date)
=> date.DayOfNextMonth(2);
public static string ToShortAndRelativeTimestampTag(this DateTime date)
{
var fullResetStr = TimestampTag.FromDateTime(date, TimestampTagStyles.ShortDateTime);
var relativeResetStr = TimestampTag.FromDateTime(date, TimestampTagStyles.Relative);
return $"{fullResetStr}\n{relativeResetStr}";
}
}

View File

@@ -0,0 +1,14 @@
// ReSharper disable InconsistentNaming
namespace NadekoBot.Modules.Utility.Patronage;
public enum PatronTier
{
None,
I,
V,
X,
XX,
L,
C,
ComingSoon
}

View File

@@ -0,0 +1,66 @@
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Utility.Patronage;
/// <summary>
/// Represents information about why the user has triggered a quota limit
/// </summary>
public readonly struct QuotaLimit
{
/// <summary>
/// Amount of usages reached, which is the limit
/// </summary>
public uint Quota { get; init; }
/// <summary>
/// Which period is this quota limit for (hourly, daily, monthly, etc...)
/// </summary>
public QuotaPer QuotaPeriod { get; init; }
/// <summary>
/// When does this quota limit reset
/// </summary>
public DateTime ResetsAt { get; init; }
/// <summary>
/// Type of the feature this quota limit is for
/// </summary>
public FeatureType FeatureType { get; init; }
/// <summary>
/// Name of the feature this quota limit is for
/// </summary>
public string Feature { get; init; }
/// <summary>
/// Whether it is the user's own quota (true), or server owners (false)
/// </summary>
public bool IsOwnQuota { get; init; }
}
/// <summary>
/// Respresent information about the feature limit
/// </summary>
public readonly struct FeatureLimit
{
/// <summary>
/// Whether this limit comes from the patronage system
/// </summary>
public bool IsPatronLimit { get; init; } = false;
/// <summary>
/// Maximum limit allowed
/// </summary>
public int? Quota { get; init; } = null;
/// <summary>
/// Name of the limit
/// </summary>
public string Name { get; init; } = string.Empty;
public FeatureLimit()
{
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Utility.Patronage;
public enum QuotaPer
{
PerHour,
PerDay,
PerMonth,
}

View File

@@ -0,0 +1,10 @@
#nullable disable
namespace NadekoBot.Modules.Utility;
public enum SubscriptionChargeStatus
{
Paid,
Refunded,
Unpaid,
Other,
}

View File

@@ -0,0 +1,25 @@
namespace NadekoBot.Modules.Utility.Patronage;
public readonly struct UserQuotaStats
{
private static readonly IReadOnlyDictionary<string, FeatureQuotaStats> _emptyDictionary
= new Dictionary<string, FeatureQuotaStats>();
public PatronTier Tier { get; init; }
= PatronTier.None;
public IReadOnlyDictionary<string, FeatureQuotaStats> Features { get; init; }
= _emptyDictionary;
public IReadOnlyDictionary<string, FeatureQuotaStats> Commands { get; init; }
= _emptyDictionary;
public IReadOnlyDictionary<string, FeatureQuotaStats> Groups { get; init; }
= _emptyDictionary;
public IReadOnlyDictionary<string, FeatureQuotaStats> Modules { get; init; }
= _emptyDictionary;
public UserQuotaStats()
{
}
}

View File

@@ -0,0 +1,427 @@
#nullable disable
using NadekoBot.Common.Configs;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using ExecuteResult = Discord.Commands.ExecuteResult;
using PreconditionResult = Discord.Commands.PreconditionResult;
namespace NadekoBot.Services;
public class CommandHandler : INService, IReadyExecutor, ICommandHandler
{
private const int GLOBAL_COMMANDS_COOLDOWN = 750;
private const float ONE_THOUSANDTH = 1.0f / 1000;
public event Func<IUserMessage, CommandInfo, Task> CommandExecuted = delegate { return Task.CompletedTask; };
public event Func<CommandInfo, ITextChannel, string, Task> CommandErrored = delegate { return Task.CompletedTask; };
//userid/msg count
public ConcurrentDictionary<ulong, uint> UserMessagesSent { get; } = new();
public ConcurrentHashSet<ulong> UsersOnShortCooldown { get; } = new();
private readonly DiscordSocketClient _client;
private readonly CommandService _commandService;
private readonly BotConfigService _bss;
private readonly IBot _bot;
private readonly IBehaviorHandler _behaviorHandler;
private readonly IServiceProvider _services;
private readonly ConcurrentDictionary<ulong, string> _prefixes;
private readonly DbService _db;
// private readonly InteractionService _interactions;
public CommandHandler(
DiscordSocketClient client,
DbService db,
CommandService commandService,
BotConfigService bss,
IBot bot,
IBehaviorHandler behaviorHandler,
// InteractionService interactions,
IServiceProvider services)
{
_client = client;
_commandService = commandService;
_bss = bss;
_bot = bot;
_behaviorHandler = behaviorHandler;
_db = db;
_services = services;
// _interactions = interactions;
_prefixes = bot.AllGuildConfigs.Where(x => x.Prefix is not null)
.ToDictionary(x => x.GuildId, x => x.Prefix)
.ToConcurrent();
}
public async Task OnReadyAsync()
{
Log.Information("Command handler runnning on ready");
// clear users on short cooldown every GLOBAL_COMMANDS_COOLDOWN miliseconds
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(GLOBAL_COMMANDS_COOLDOWN));
while (await timer.WaitForNextTickAsync())
UsersOnShortCooldown.Clear();
}
public string GetPrefix(IGuild guild)
=> GetPrefix(guild?.Id);
public string GetPrefix(ulong? id = null)
{
if (id is null || !_prefixes.TryGetValue(id.Value, out var prefix))
return _bss.Data.Prefix;
return prefix;
}
public string SetDefaultPrefix(string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
throw new ArgumentNullException(nameof(prefix));
_bss.ModifyConfig(bs =>
{
bs.Prefix = prefix;
});
return prefix;
}
public string SetPrefix(IGuild guild, string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
throw new ArgumentNullException(nameof(prefix));
if (guild is null)
throw new ArgumentNullException(nameof(guild));
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guild.Id, set => set);
gc.Prefix = prefix;
uow.SaveChanges();
}
_prefixes[guild.Id] = prefix;
return prefix;
}
public async Task ExecuteExternal(ulong? guildId, ulong channelId, string commandText)
{
if (guildId is not null)
{
var guild = _client.GetGuild(guildId.Value);
if (guild?.GetChannel(channelId) is not SocketTextChannel channel)
{
Log.Warning("Channel for external execution not found");
return;
}
try
{
IUserMessage msg = await channel.SendMessageAsync(commandText);
msg = (IUserMessage)await channel.GetMessageAsync(msg.Id);
await TryRunCommand(guild, channel, msg);
//msg.DeleteAfter(5);
}
catch { }
}
}
public Task StartHandling()
{
_client.MessageReceived += MessageReceivedHandler;
// _client.SlashCommandExecuted += SlashCommandExecuted;
return Task.CompletedTask;
}
// private async Task SlashCommandExecuted(SocketSlashCommand arg)
// {
// var ctx = new SocketInteractionContext<SocketSlashCommand>(_client, arg);
// await _interactions.ExecuteCommandAsync(ctx, _services);
// }
private Task LogSuccessfulExecution(IUserMessage usrMsg, ITextChannel channel, params int[] execPoints)
{
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
{
Log.Information("""
Command Executed after {ExecTime}s
User: {User}
Server: {Server}
Channel: {Channel}
Message: {Message}
""",
string.Join("/", execPoints.Select(x => (x * ONE_THOUSANDTH).ToString("F3"))),
usrMsg.Author + " [" + usrMsg.Author.Id + "]",
channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]",
channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]",
usrMsg.Content);
}
else
{
Log.Information("Succ | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}",
channel?.Guild.Id.ToString() ?? "-",
channel?.Id.ToString() ?? "-",
usrMsg.Author.Id,
usrMsg.Content.TrimTo(10));
}
return Task.CompletedTask;
}
private void LogErroredExecution(
string errorMessage,
IUserMessage usrMsg,
ITextChannel channel,
params int[] execPoints)
{
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
{
Log.Warning("""
Command Errored after {ExecTime}s
User: {User}
Server: {Guild}
Channel: {Channel}
Message: {Message}
Error: {ErrorMessage}
""",
string.Join("/", execPoints.Select(x => (x * ONE_THOUSANDTH).ToString("F3"))),
usrMsg.Author + " [" + usrMsg.Author.Id + "]",
channel is null ? "DM" : channel.Guild.Name + " [" + channel.Guild.Id + "]",
channel is null ? "DM" : channel.Name + " [" + channel.Id + "]",
usrMsg.Content,
errorMessage);
}
else
{
Log.Warning("""
Err | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}
Err: {ErrorMessage}
""",
channel?.Guild.Id.ToString() ?? "-",
channel?.Id.ToString() ?? "-",
usrMsg.Author.Id,
usrMsg.Content.TrimTo(10),
errorMessage);
}
}
private Task MessageReceivedHandler(SocketMessage msg)
{
//no bots, wait until bot connected and initialized
if (msg.Author.IsBot || !_bot.IsReady)
return Task.CompletedTask;
if (msg is not SocketUserMessage usrMsg)
return Task.CompletedTask;
Task.Run(async () =>
{
try
{
#if !GLOBAL_NADEKO
// track how many messages each user is sending
UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (_, old) => ++old);
#endif
var channel = msg.Channel;
var guild = (msg.Channel as SocketTextChannel)?.Guild;
await TryRunCommand(guild, channel, usrMsg);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in CommandHandler");
if (ex.InnerException is not null)
Log.Warning(ex.InnerException, "Inner Exception of the error in CommandHandler");
}
});
return Task.CompletedTask;
}
public async Task TryRunCommand(SocketGuild guild, ISocketMessageChannel channel, IUserMessage usrMsg)
{
var startTime = Environment.TickCount;
var blocked = await _behaviorHandler.RunExecOnMessageAsync(guild, usrMsg);
if (blocked)
return;
var blockTime = Environment.TickCount - startTime;
var messageContent = await _behaviorHandler.RunInputTransformersAsync(guild, usrMsg);
var prefix = GetPrefix(guild?.Id);
var isPrefixCommand = messageContent.StartsWith(".prefix", StringComparison.InvariantCultureIgnoreCase);
// execute the command and measure the time it took
if (isPrefixCommand || messageContent.StartsWith(prefix, StringComparison.InvariantCulture))
{
var context = new CommandContext(_client, usrMsg);
var (success, error, info) = await ExecuteCommandAsync(context,
messageContent,
isPrefixCommand ? 1 : prefix.Length,
_services,
MultiMatchHandling.Best);
startTime = Environment.TickCount - startTime;
// if a command is found
if (info is not null)
{
// if it successfully executed
if (success)
{
await LogSuccessfulExecution(usrMsg, channel as ITextChannel, blockTime, startTime);
await CommandExecuted(usrMsg, info);
await _behaviorHandler.RunPostCommandAsync(context, info.Module.GetTopLevelModule().Name, info);
return;
}
// if it errored
if (error is not null)
{
error = HumanizeError(error);
LogErroredExecution(error, usrMsg, channel as ITextChannel, blockTime, startTime);
if (guild is not null)
await CommandErrored(info, channel as ITextChannel, error);
return;
}
}
}
await _behaviorHandler.RunOnNoCommandAsync(guild, usrMsg);
}
private string HumanizeError(string error)
{
if (error.Contains("parse int", StringComparison.OrdinalIgnoreCase)
|| error.Contains("parse float"))
return "Invalid number specified. Make sure you're specifying parameters in the correct order.";
return error;
}
public Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommandAsync(
ICommandContext context,
string input,
int argPos,
IServiceProvider serviceProvider,
MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
=> ExecuteCommand(context, input[argPos..], serviceProvider, multiMatchHandling);
public async Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommand(
ICommandContext context,
string input,
IServiceProvider services,
MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
{
var searchResult = _commandService.Search(context, input);
if (!searchResult.IsSuccess)
return (false, null, null);
var commands = searchResult.Commands;
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();
foreach (var match in commands)
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services);
var successfulPreconditions = preconditionResults.Where(x => x.Value.IsSuccess).ToArray();
if (successfulPreconditions.Length == 0)
{
//All preconditions failed, return the one from the highest priority command
var bestCandidate = preconditionResults.OrderByDescending(x => x.Key.Command.Priority)
.FirstOrDefault(x => !x.Value.IsSuccess);
return (false, bestCandidate.Value.ErrorReason, commands[0].Command);
}
var parseResultsDict = new Dictionary<CommandMatch, ParseResult>();
foreach (var pair in successfulPreconditions)
{
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services);
if (parseResult.Error == CommandError.MultipleMatches)
{
IReadOnlyList<TypeReaderValue> argList, paramList;
switch (multiMatchHandling)
{
case MultiMatchHandling.Best:
argList = parseResult.ArgValues
.Map(x => x.Values.MaxBy(y => y.Score));
paramList = parseResult.ParamValues
.Map(x => x.Values.MaxBy(y => y.Score));
parseResult = ParseResult.FromSuccess(argList, paramList);
break;
}
}
parseResultsDict[pair.Key] = parseResult;
}
// Calculates the 'score' of a command given a parse result
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
float argValuesScore = 0, paramValuesScore = 0;
if (match.Command.Parameters.Count > 0)
{
var argValuesSum =
parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score)
?? 0;
var paramValuesSum =
parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score)
?? 0;
argValuesScore = argValuesSum / match.Command.Parameters.Count;
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
}
var totalArgsScore = (argValuesScore + paramValuesScore) / 2;
return match.Command.Priority + (totalArgsScore * 0.99f);
}
//Order the parse results by their score so that we choose the most likely result to execute
var parseResults = parseResultsDict.OrderByDescending(x => CalculateScore(x.Key, x.Value)).ToList();
var successfulParses = parseResults.Where(x => x.Value.IsSuccess).ToArray();
if (successfulParses.Length == 0)
{
//All parses failed, return the one from the highest priority command, using score as a tie breaker
var bestMatch = parseResults.FirstOrDefault(x => !x.Value.IsSuccess);
return (false, bestMatch.Value.ErrorReason, commands[0].Command);
}
var cmd = successfulParses[0].Key.Command;
// Bot will ignore commands which are ran more often than what specified by
// GlobalCommandsCooldown constant (miliseconds)
if (!UsersOnShortCooldown.Add(context.Message.Author.Id))
return (false, null, cmd);
//return SearchResult.FromError(CommandError.Exception, "You are on a global cooldown.");
var blocked = await _behaviorHandler.RunPreCommandAsync(context, cmd);
if (blocked)
return (false, null, cmd);
//If we get this far, at least one parse was successful. Execute the most likely overload.
var chosenOverload = successfulParses[0];
var execResult = (ExecuteResult)await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services);
if (execResult.Exception is not null
&& (execResult.Exception is not HttpException he
|| he.DiscordCode != DiscordErrorCode.InsufficientPermissions))
Log.Warning(execResult.Exception, "Command Error");
return (true, null, cmd);
}
}

View File

@@ -0,0 +1,106 @@
#nullable disable
using LinqToDB;
using NadekoBot.Services.Currency;
namespace NadekoBot.Services;
public sealed class CurrencyService : ICurrencyService, INService
{
private readonly DbService _db;
private readonly ITxTracker _txTracker;
public CurrencyService(DbService db, ITxTracker txTracker)
{
_db = db;
_txTracker = txTracker;
}
public Task<IWallet> GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default)
{
if (type == CurrencyType.Default)
return Task.FromResult<IWallet>(new DefaultWallet(userId, _db));
throw new ArgumentOutOfRangeException(nameof(type));
}
public async Task AddBulkAsync(
IReadOnlyCollection<ulong> userIds,
long amount,
TxData txData,
CurrencyType type = CurrencyType.Default)
{
if (type == CurrencyType.Default)
{
foreach (var userId in userIds)
{
var wallet = await GetWalletAsync(userId);
await wallet.Add(amount, txData);
}
return;
}
throw new ArgumentOutOfRangeException(nameof(type));
}
public async Task RemoveBulkAsync(
IReadOnlyCollection<ulong> userIds,
long amount,
TxData txData,
CurrencyType type = CurrencyType.Default)
{
if (type == CurrencyType.Default)
{
await using var ctx = _db.GetDbContext();
await ctx.DiscordUser
.Where(x => userIds.Contains(x.UserId))
.UpdateAsync(du => new()
{
CurrencyAmount = du.CurrencyAmount >= amount
? du.CurrencyAmount - amount
: 0
});
await ctx.SaveChangesAsync();
return;
}
throw new ArgumentOutOfRangeException(nameof(type));
}
public async Task AddAsync(
ulong userId,
long amount,
TxData txData)
{
var wallet = await GetWalletAsync(userId);
await wallet.Add(amount, txData);
await _txTracker.TrackAdd(amount, txData);
}
public async Task AddAsync(
IUser user,
long amount,
TxData txData)
=> await AddAsync(user.Id, amount, txData);
public async Task<bool> RemoveAsync(
ulong userId,
long amount,
TxData txData)
{
if (amount == 0)
return true;
var wallet = await GetWalletAsync(userId);
var result = await wallet.Take(amount, txData);
if(result)
await _txTracker.TrackRemove(amount, txData);
return result;
}
public async Task<bool> RemoveAsync(
IUser user,
long amount,
TxData txData)
=> await RemoveAsync(user.Id, amount, txData);
}

View File

@@ -0,0 +1,39 @@
using NadekoBot.Services.Currency;
namespace NadekoBot.Services;
public static class CurrencyServiceExtensions
{
public static async Task<long> GetBalanceAsync(this ICurrencyService cs, ulong userId)
{
var wallet = await cs.GetWalletAsync(userId);
return await wallet.GetBalance();
}
// FUTURE should be a transaction
public static async Task<bool> TransferAsync(
this ICurrencyService cs,
IEmbedBuilderService ebs,
IUser from,
IUser to,
long amount,
string? note,
string formattedAmount)
{
var fromWallet = await cs.GetWalletAsync(from.Id);
var toWallet = await cs.GetWalletAsync(to.Id);
var extra = new TxData("gift", from.ToString()!, note, from.Id);
if (await fromWallet.Transfer(amount, toWallet, extra))
{
await to.SendConfirmAsync(ebs,
string.IsNullOrWhiteSpace(note)
? $"Received {formattedAmount} from {from} "
: $"Received {formattedAmount} from {from}: {note}");
return true;
}
return false;
}
}

View File

@@ -0,0 +1,113 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Services.Database;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Services.Currency;
public class DefaultWallet : IWallet
{
private readonly DbService _db;
public ulong UserId { get; }
public DefaultWallet(ulong userId, DbService db)
{
UserId = userId;
_db = db;
}
public async Task<long> GetBalance()
{
await using var ctx = _db.GetDbContext();
var userId = UserId;
return await ctx.DiscordUser
.ToLinqToDBTable()
.Where(x => x.UserId == userId)
.Select(x => x.CurrencyAmount)
.FirstOrDefaultAsync();
}
public async Task<bool> Take(long amount, TxData? txData)
{
if (amount < 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount to take must be non negative.");
await using var ctx = _db.GetDbContext();
var userId = UserId;
var changed = await ctx.DiscordUser
.Where(x => x.UserId == userId && x.CurrencyAmount >= amount)
.UpdateAsync(x => new()
{
CurrencyAmount = x.CurrencyAmount - amount
});
if (changed == 0)
return false;
if (txData is not null)
{
await ctx
.GetTable<CurrencyTransaction>()
.InsertAsync(() => new()
{
Amount = -amount,
Note = txData.Note,
UserId = userId,
Type = txData.Type,
Extra = txData.Extra,
OtherId = txData.OtherId,
DateAdded = DateTime.UtcNow
});
}
return true;
}
public async Task Add(long amount, TxData? txData)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0.");
await using var ctx = _db.GetDbContext();
var userId = UserId;
await using (var tran = await ctx.Database.BeginTransactionAsync())
{
var changed = await ctx.DiscordUser
.Where(x => x.UserId == userId)
.UpdateAsync(x => new()
{
CurrencyAmount = x.CurrencyAmount + amount
});
if (changed == 0)
{
await ctx.DiscordUser
.ToLinqToDBTable()
.Value(x => x.UserId, userId)
.Value(x => x.Username, "Unknown")
.Value(x => x.Discriminator, "????")
.Value(x => x.CurrencyAmount, amount)
.InsertAsync();
}
await tran.CommitAsync();
}
if (txData is not null)
{
await ctx.GetTable<CurrencyTransaction>()
.InsertAsync(() => new()
{
Amount = amount,
UserId = userId,
Note = txData.Note,
Type = txData.Type,
Extra = txData.Extra,
OtherId = txData.OtherId,
DateAdded = DateTime.UtcNow
});
}
}
}

View File

@@ -0,0 +1,110 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Services.Currency;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Services;
public sealed class GamblingTxTracker : ITxTracker, INService, IReadyExecutor
{
private static readonly IReadOnlySet<string> _gamblingTypes = new HashSet<string>(new[]
{
"lula",
"betroll",
"betflip",
"blackjack",
"betdraw",
"slot",
});
private ConcurrentDictionary<string, (decimal Bet, decimal PaidOut)> _stats = new();
private readonly DbService _db;
public GamblingTxTracker(DbService db)
{
_db = db;
}
public async Task OnReadyAsync()
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync())
{
await using var ctx = _db.GetDbContext();
await using var trans = await ctx.Database.BeginTransactionAsync();
try
{
var keys = _stats.Keys;
foreach (var key in keys)
{
if (_stats.TryRemove(key, out var stat))
{
await ctx.GetTable<GamblingStats>()
.InsertOrUpdateAsync(() => new()
{
Feature = key,
Bet = stat.Bet,
PaidOut = stat.PaidOut,
DateAdded = DateTime.UtcNow
}, old => new()
{
Bet = old.Bet + stat.Bet,
PaidOut = old.PaidOut + stat.PaidOut,
}, () => new()
{
Feature = key
});
}
}
}
catch (Exception ex)
{
Log.Error(ex, "An error occurred in gambling tx tracker");
}
finally
{
await trans.CommitAsync();
}
}
}
public Task TrackAdd(long amount, TxData? txData)
{
if (txData is null)
return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type))
{
_stats.AddOrUpdate(txData.Type,
_ => (0, amount),
(_, old) => (old.Bet, old.PaidOut + amount));
}
return Task.CompletedTask;
}
public Task TrackRemove(long amount, TxData? txData)
{
if (txData is null)
return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type))
{
_stats.AddOrUpdate(txData.Type,
_ => (amount, 0),
(_, old) => (old.Bet + amount, old.PaidOut));
}
return Task.CompletedTask;
}
public async Task<IReadOnlyCollection<GamblingStats>> GetAllAsync()
{
await using var ctx = _db.GetDbContext();
return await ctx.Set<GamblingStats>()
.ToListAsyncEF();
}
}

View File

@@ -0,0 +1,13 @@
#nullable disable
using NadekoBot.Common.Pokemon;
using NadekoBot.Modules.Games.Common.Trivia;
namespace NadekoBot.Services;
public interface ILocalDataCache
{
Task<IReadOnlyDictionary<string, SearchPokemon>> GetPokemonsAsync();
Task<IReadOnlyDictionary<string, SearchPokemonAbility>> GetPokemonAbilitiesAsync();
Task<TriviaQuestionModel[]> GetTriviaQuestionsAsync();
Task<IReadOnlyDictionary<int, string>> GetPokemonMapAsync();
}

View File

@@ -0,0 +1,12 @@
#nullable disable
namespace NadekoBot.Modules.Utility.Services;
public interface IRemindService
{
Task AddReminderAsync(ulong userId,
ulong targetId,
ulong? guildId,
bool isPrivate,
DateTime time,
string message);
}

View File

@@ -13,11 +13,6 @@ public interface IStatsService
/// </summary>
long CommandsRan { get; }
/// <summary>
/// The Discord framework used by the bot.
/// </summary>
string Library { get; }
/// <summary>
/// The amount of messages seen by the bot since startup.
/// </summary>

View File

@@ -0,0 +1,60 @@
#nullable disable
using SixLabors.Fonts;
namespace NadekoBot.Services;
public class FontProvider : INService
{
public FontFamily DottyFont { get; }
public FontFamily UniSans { get; }
public FontFamily NotoSans { get; }
//public FontFamily Emojis { get; }
/// <summary>
/// Font used for .rip command
/// </summary>
public Font RipFont { get; }
public List<FontFamily> FallBackFonts { get; }
private readonly FontCollection _fonts;
public FontProvider()
{
_fonts = new();
NotoSans = _fonts.Add("data/fonts/NotoSans-Bold.ttf");
UniSans = _fonts.Add("data/fonts/Uni Sans.ttf");
FallBackFonts = new();
//FallBackFonts.Add(_fonts.Install("data/fonts/OpenSansEmoji.ttf"));
// try loading some emoji and jap fonts on windows as fallback fonts
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
try
{
var fontsfolder = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
FallBackFonts.Add(_fonts.Add(Path.Combine(fontsfolder, "seguiemj.ttf")));
FallBackFonts.AddRange(_fonts.AddCollection(Path.Combine(fontsfolder, "msgothic.ttc")));
FallBackFonts.AddRange(_fonts.AddCollection(Path.Combine(fontsfolder, "segoe.ttc")));
}
catch { }
}
// any fonts present in data/fonts should be added as fallback fonts
// this will allow support for special characters when drawing text
foreach (var font in Directory.GetFiles(@"data/fonts"))
{
if (font.EndsWith(".ttf"))
FallBackFonts.Add(_fonts.Add(font));
else if (font.EndsWith(".ttc"))
FallBackFonts.AddRange(_fonts.AddCollection(font));
}
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
DottyFont = FallBackFonts.First(x => x.Name == "dotty");
}
}

View File

@@ -0,0 +1,17 @@
namespace NadekoBot.Services;
public interface IImageCache
{
Task<byte[]?> GetHeadsImageAsync();
Task<byte[]?> GetTailsImageAsync();
Task<byte[]?> GetCurrencyImageAsync();
Task<byte[]?> GetXpBackgroundImageAsync();
Task<byte[]?> GetRategirlBgAsync();
Task<byte[]?> GetRategirlDotAsync();
Task<byte[]?> GetDiceAsync(int num);
Task<byte[]?> GetSlotEmojiAsync(int number);
Task<byte[]?> GetSlotBgAsync();
Task<byte[]?> GetRipBgAsync();
Task<byte[]?> GetRipOverlayAsync();
Task<byte[]?> GetImageDataAsync(Uri url);
}

View File

@@ -0,0 +1,19 @@
using NadekoBot.Common.Configs;
namespace NadekoBot.Services;
public sealed class ImagesConfig : ConfigServiceBase<ImageUrls>
{
private const string PATH = "data/images.yml";
private static readonly TypedKey<ImageUrls> _changeKey =
new("config.images.updated");
public override string Name
=> "images";
public ImagesConfig(IConfigSeria serializer, IPubSub pubSub)
: base(PATH, serializer, pubSub, _changeKey)
{
}
}

View File

@@ -0,0 +1,11 @@
#nullable disable
namespace NadekoBot.Services;
public static class RedisImageExtensions
{
private const string OLD_CDN_URL = "nadeko-pictures.nyc3.digitaloceanspaces.com";
private const string NEW_CDN_URL = "cdn.nadeko.bot";
public static Uri ToNewCdn(this Uri uri)
=> new(uri.ToString().Replace(OLD_CDN_URL, NEW_CDN_URL));
}

View File

@@ -0,0 +1,58 @@
#nullable disable
using System.Diagnostics;
namespace NadekoBot.Services;
public class SingleProcessCoordinator : ICoordinator
{
private readonly IBotCredentials _creds;
private readonly DiscordSocketClient _client;
public SingleProcessCoordinator(IBotCredentials creds, DiscordSocketClient client)
{
_creds = creds;
_client = client;
}
public bool RestartBot()
{
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
|| string.IsNullOrWhiteSpace(_creds.RestartCommand?.Args))
{
Log.Error("You must set RestartCommand.Cmd and RestartCommand.Args in creds.yml");
return false;
}
Process.Start(_creds.RestartCommand.Cmd, _creds.RestartCommand.Args);
_ = Task.Run(async () =>
{
await Task.Delay(2000);
Die();
});
return true;
}
public void Die(bool graceful = false)
=> Environment.Exit(5);
public bool RestartShard(int shardId)
=> RestartBot();
public IList<ShardStatus> GetAllShardStatuses()
=> new[]
{
new ShardStatus
{
ConnectionState = _client.ConnectionState,
GuildCount = _client.Guilds.Count,
LastUpdate = DateTime.UtcNow,
ShardId = _client.ShardId
}
};
public int GetGuildCount()
=> _client.Guilds.Count;
public Task Reload()
=> Task.CompletedTask;
}

View File

@@ -0,0 +1,18 @@
#nullable disable
using System.Collections;
namespace NadekoBot.Services;
public class StartingGuildsService : IEnumerable<ulong>, INService
{
private readonly IReadOnlyList<ulong> _guilds;
public StartingGuildsService(DiscordSocketClient client)
=> _guilds = client.Guilds.Select(x => x.Id).ToList();
public IEnumerator<ulong> GetEnumerator()
=> _guilds.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> _guilds.GetEnumerator();
}

View File

@@ -0,0 +1,78 @@
#nullable disable
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
using Nadeko.Common;
namespace NadekoBot.Services;
public class YtdlOperation
{
private readonly string _baseArgString;
private readonly bool _isYtDlp;
public YtdlOperation(string baseArgString, bool isYtDlp = false)
{
_baseArgString = baseArgString;
_isYtDlp = isYtDlp;
}
private Process CreateProcess(string[] args)
{
var newArgs = args.Map(arg => (object)arg.Replace("\"", ""));
return new()
{
StartInfo = new()
{
FileName = _isYtDlp ? "yt-dlp" : "youtube-dl",
Arguments = string.Format(_baseArgString, newArgs),
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
CreateNoWindow = true
}
};
}
public async Task<string> GetDataAsync(params string[] args)
{
try
{
using var process = CreateProcess(args);
Log.Debug("Executing {FileName} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
process.Start();
var str = await process.StandardOutput.ReadToEndAsync();
var err = await process.StandardError.ReadToEndAsync();
if (!string.IsNullOrEmpty(err))
Log.Warning("YTDL warning: {YtdlWarning}", err);
return str;
}
catch (Win32Exception)
{
Log.Error("youtube-dl is likely not installed. " + "Please install it before running the command again");
return default;
}
catch (Exception ex)
{
Log.Error(ex, "Exception running youtube-dl: {ErrorMessage}", ex.Message);
return default;
}
}
public async IAsyncEnumerable<string> EnumerateDataAsync(params string[] args)
{
using var process = CreateProcess(args);
Log.Debug("Executing {FileName} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
process.Start();
string line;
while ((line = await process.StandardOutput.ReadLineAsync()) is not null)
yield return line;
}
}

View File

@@ -0,0 +1,101 @@
#nullable disable
using System.Globalization;
namespace NadekoBot.Services;
public class BotStrings : IBotStrings
{
/// <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 ILocalization _localization;
private readonly IBotStringsProvider _stringsProvider;
public BotStrings(ILocalization loc, IBotStringsProvider stringsProvider)
{
_localization = loc;
_stringsProvider = stringsProvider;
}
private string GetString(string key, CultureInfo cultureInfo)
=> _stringsProvider.GetText(cultureInfo.Name, key);
public string GetText(string key, ulong? guildId = null, params object[] data)
=> GetText(key, _localization.GetCultureInfo(guildId), data);
public string GetText(string key, CultureInfo cultureInfo)
{
var text = GetString(key, cultureInfo);
if (string.IsNullOrWhiteSpace(text))
{
Log.Warning("'{Key}' key is missing from '{LanguageName}' response strings. You may ignore this message",
key,
cultureInfo.Name);
text = GetString(key, _usCultureInfo) ?? $"Error: dkey {key} not found!";
if (string.IsNullOrWhiteSpace(text))
{
return
"I can't tell you if the command is executed, because there was an error printing out the response."
+ $" Key '{key}' is missing from resources. You may ignore this message.";
}
}
return text;
}
public string GetText(string key, CultureInfo cultureInfo, params object[] data)
{
try
{
return string.Format(GetText(key, cultureInfo), data);
}
catch (FormatException)
{
Log.Warning(
" Key '{Key}' is not properly formatted in '{LanguageName}' response strings. Please report this",
key,
cultureInfo.Name);
if (cultureInfo.Name != _usCultureInfo.Name)
return GetText(key, _usCultureInfo, data);
return
"I can't tell you if the command is executed, because there was an error printing out the response.\n"
+ $"Key '{key}' is not properly formatted. Please report this.";
}
}
public CommandStrings GetCommandStrings(string commandName, ulong? guildId = null)
=> GetCommandStrings(commandName, _localization.GetCultureInfo(guildId));
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. Please report this",
commandName);
return new CommandStrings()
{
Args = new[] { "" },
Desc = "?"
};
}
// Log.Warning(@"'{CommandName}' command strings don't exist in '{LanguageName}' culture.
// This message is safe to ignore, however you can ask in Nadeko support server how you can contribute command translations",
// commandName, cultureInfo.Name);
return GetCommandStrings(commandName, _usCultureInfo);
}
return cmdStrings;
}
public void Reload()
=> _stringsProvider.Reload();
}

View File

@@ -0,0 +1,73 @@
#nullable disable
using Newtonsoft.Json;
using YamlDotNet.Serialization;
namespace NadekoBot.Services;
/// <summary>
/// Loads strings from the local default filepath <see cref="_responsesPath" />
/// </summary>
public class LocalFileStringsSource : IStringsSource
{
private readonly string _responsesPath = "data/strings/responses";
private readonly string _commandsPath = "data/strings/commands";
public LocalFileStringsSource(
string responsesPath = "data/strings/responses",
string commandsPath = "data/strings/commands")
{
_responsesPath = responsesPath;
_commandsPath = commandsPath;
}
public Dictionary<string, Dictionary<string, string>> GetResponseStrings()
{
var outputDict = new Dictionary<string, Dictionary<string, string>>();
foreach (var file in Directory.GetFiles(_responsesPath))
{
try
{
var langDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(file));
var localeName = GetLocaleName(file);
outputDict[localeName] = langDict;
}
catch (Exception ex)
{
Log.Error(ex, "Error loading {FileName} response strings: {ErrorMessage}", file, ex.Message);
}
}
return outputDict;
}
public Dictionary<string, Dictionary<string, CommandStrings>> GetCommandStrings()
{
var deserializer = new DeserializerBuilder().Build();
var outputDict = new Dictionary<string, Dictionary<string, CommandStrings>>();
foreach (var file in Directory.GetFiles(_commandsPath))
{
try
{
var text = File.ReadAllText(file);
var langDict = deserializer.Deserialize<Dictionary<string, CommandStrings>>(text);
var localeName = GetLocaleName(file);
outputDict[localeName] = langDict;
}
catch (Exception ex)
{
Log.Error(ex, "Error loading {FileName} command strings: {ErrorMessage}", file, ex.Message);
}
}
return outputDict;
}
private static string GetLocaleName(string fileName)
{
fileName = Path.GetFileName(fileName);
var dotIndex = fileName.IndexOf('.') + 1;
var secondDotIndex = fileName.LastIndexOf('.');
return fileName.Substring(dotIndex, secondDotIndex - dotIndex);
}
}

View File

@@ -0,0 +1,38 @@
#nullable disable
namespace NadekoBot.Services;
public class MemoryBotStringsProvider : IBotStringsProvider
{
private readonly IStringsSource _source;
private IReadOnlyDictionary<string, Dictionary<string, string>> responseStrings;
private IReadOnlyDictionary<string, Dictionary<string, CommandStrings>> commandStrings;
public MemoryBotStringsProvider(IStringsSource source)
{
_source = source;
Reload();
}
public string GetText(string localeName, string key)
{
if (responseStrings.TryGetValue(localeName, out var langStrings) && langStrings.TryGetValue(key, out var text))
return text;
return null;
}
public void Reload()
{
responseStrings = _source.GetResponseStrings();
commandStrings = _source.GetCommandStrings();
}
public CommandStrings GetCommandStrings(string localeName, string commandName)
{
if (commandStrings.TryGetValue(localeName, out var langStrings)
&& langStrings.TryGetValue(commandName, out var strings))
return strings;
return null;
}
}

View File

@@ -0,0 +1,30 @@
namespace NadekoBot.Common.TypeReaders;
public readonly struct TypeReaderResult<T>
{
public bool IsSuccess
=> _result.IsSuccess;
public IReadOnlyCollection<TypeReaderValue> Values
=> _result.Values;
private readonly Discord.Commands.TypeReaderResult _result;
private TypeReaderResult(in Discord.Commands.TypeReaderResult result)
=> _result = result;
public static implicit operator TypeReaderResult<T>(in Discord.Commands.TypeReaderResult result)
=> new(result);
public static implicit operator Discord.Commands.TypeReaderResult(in TypeReaderResult<T> wrapper)
=> wrapper._result;
}
public static class TypeReaderResult
{
public static TypeReaderResult<T> FromError<T>(CommandError error, string reason)
=> Discord.Commands.TypeReaderResult.FromError(error, reason);
public static TypeReaderResult<T> FromSuccess<T>(in T value)
=> Discord.Commands.TypeReaderResult.FromSuccess(value);
}

View File

@@ -11,6 +11,12 @@ namespace NadekoBot.Extensions;
public static class Extensions
{
public static DateOnly ToDateOnly(this DateTime dateTime)
=> DateOnly.FromDateTime(dateTime);
public static bool IsBeforeToday(this DateTime date)
=> date < DateTime.UtcNow.Date;
private static readonly Regex _urlRegex =
new(@"^(https?|ftp)://(?<path>[^\s/$.?#].[^\s]*)$", RegexOptions.Compiled);
@@ -40,7 +46,7 @@ public static class Extensions
public static ulong[] GetGuildIds(this DiscordSocketClient client)
=> client.Guilds
.Map(x => x.Id);
.Map(x => x.Id);
/// <summary>
/// Generates a string in the format HHH:mm if timespan is &gt;= 2m.
@@ -98,7 +104,7 @@ public static class Extensions
{
description = strings.GetCommandStrings(cmd.Summary, culture).Desc;
}
return string.Format(description, prefix);
}
@@ -123,7 +129,7 @@ public static class Extensions
{
args = strings.GetCommandStrings(cmd.Summary, culture).Args;
}
return args.Map(arg => GetFullUsage(cmd.Aliases.First(), arg, prefix));
}
@@ -154,8 +160,13 @@ public static class Extensions
if (logService is not null)
logService.AddDeleteIgnore(msg.Id);
try { await msg.DeleteAsync(); }
catch { }
try
{
await msg.DeleteAsync();
}
catch
{
}
});
return msg;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +0,0 @@
# 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://i.imgur.com/nKYyqMK.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://nadeko.bot/commands",
"inline": false
},
{
"name": "Nadeko Support Server",
"value": "https://discord.nadeko.bot/ ",
"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

View File

@@ -1,261 +0,0 @@
# 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: Nadeko Flower
# 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 nadeko.
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

@@ -1,70 +0,0 @@
# DO NOT CHANGE
version: 2
# Hangman related settings (.hangman command)
hangman:
# The amount of currency awarded to the winner of a hangman game
currencyReward: 0
# Trivia related settings (.t command)
trivia:
# The amount of currency awarded to the winner of the trivia game.
currencyReward: 0
# Users won't be able to start trivia games which have
# a smaller win requirement than the one specified by this setting.
minimumWinReq: 1
# List of responses for the .8ball command. A random one will be selected every time
eightBallResponses:
- Most definitely yes.
- For sure.
- Totally!
- Of course!
- As I see it, yes.
- My sources say yes.
- Yes.
- Most likely.
- Perhaps...
- Maybe...
- Hm, not sure.
- It is uncertain.
- Ask me again later.
- Don't count on it.
- Probably not.
- Very doubtful.
- Most likely no.
- Nope.
- No.
- My sources say no.
- Don't even think about it.
- Definitely no.
- NO - It may cause disease contraction!
# List of animals which will be used for the animal race game (.race)
raceAnimals:
- icon: "🐼"
name: Panda
- icon: "🐻"
name: Bear
- icon: "🐧"
name: Pengu
- icon: "🐨"
name: Koala
- icon: "🐬"
name: Dolphin
- icon: "🐞"
name: Ladybird
- icon: "🦀"
name: Crab
- icon: "🦄"
name: Unicorn
# Which chatbot API should bot use.
# 'cleverbot' - bot will use Cleverbot API.
# 'gpt3' - bot will use GPT-3 API
chatBot: gpt3
chatGpt:
# Which GPT-3 Model should bot use.
# 'ada001' - cheapest and fastest
# 'babbage001' - 2nd option
# 'curie001' - 3rd option
# 'davinci003' - Most expensive, slowest
model: davinci003
# The maximum number of tokens to use per GPT-3 API call
maxTokens: 100

View File

@@ -1,276 +0,0 @@
- 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

@@ -1,766 +0,0 @@
- 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

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