mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Gambling moved to a separate project. Project builds
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
|
12
src/Nadeko.Bot.Common/Attributes/AliasesAttribute.cs
Normal file
12
src/Nadeko.Bot.Common/Attributes/AliasesAttribute.cs
Normal 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))
|
||||
{
|
||||
}
|
||||
}
|
18
src/Nadeko.Bot.Common/Attributes/CmdAttribute.cs
Normal file
18
src/Nadeko.Bot.Common/Attributes/CmdAttribute.cs
Normal 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();
|
||||
}
|
||||
}
|
11
src/Nadeko.Bot.Common/Attributes/DIIgnoreAttribute.cs
Normal file
11
src/Nadeko.Bot.Common/Attributes/DIIgnoreAttribute.cs
Normal 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
|
||||
{
|
||||
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class NadekoOptionsAttribute<TOption> : Attribute
|
||||
where TOption: INadekoCommandOptions
|
||||
{
|
||||
}
|
21
src/Nadeko.Bot.Common/Attributes/NoPublicBotAttribute.cs
Normal file
21
src/Nadeko.Bot.Common/Attributes/NoPublicBotAttribute.cs
Normal 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
|
||||
}
|
||||
}
|
21
src/Nadeko.Bot.Common/Attributes/OnlyPublicBotAttribute.cs
Normal file
21
src/Nadeko.Bot.Common/Attributes/OnlyPublicBotAttribute.cs
Normal 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
|
||||
}
|
||||
}
|
19
src/Nadeko.Bot.Common/Attributes/OwnerOnlyAttribute.cs
Normal file
19
src/Nadeko.Bot.Common/Attributes/OwnerOnlyAttribute.cs
Normal 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"));
|
||||
}
|
||||
}
|
38
src/Nadeko.Bot.Common/Attributes/RatelimitAttribute.cs
Normal file
38
src/Nadeko.Bot.Common/Attributes/RatelimitAttribute.cs
Normal 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);
|
||||
}
|
||||
}
|
29
src/Nadeko.Bot.Common/Attributes/UserPermAttribute.cs
Normal file
29
src/Nadeko.Bot.Common/Attributes/UserPermAttribute.cs
Normal 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);
|
||||
}
|
||||
}
|
32
src/Nadeko.Bot.Common/BotCommandTypeReader.cs
Normal file
32
src/Nadeko.Bot.Common/BotCommandTypeReader.cs
Normal 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));
|
||||
}
|
||||
}
|
10
src/Nadeko.Bot.Common/CleverBotResponseStr.cs
Normal file
10
src/Nadeko.Bot.Common/CleverBotResponseStr.cs
Normal 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";
|
||||
}
|
31
src/Nadeko.Bot.Common/CommandNameLoadHelper.cs
Normal file
31
src/Nadeko.Bot.Common/CommandNameLoadHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
10
src/Nadeko.Bot.Common/Common/AddRemove.cs
Normal file
10
src/Nadeko.Bot.Common/Common/AddRemove.cs
Normal 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
|
||||
}
|
17
src/Nadeko.Bot.Common/Common/CmdStrings.cs
Normal file
17
src/Nadeko.Bot.Common/Common/CmdStrings.cs
Normal 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;
|
||||
}
|
||||
}
|
9
src/Nadeko.Bot.Common/Common/CommandData.cs
Normal file
9
src/Nadeko.Bot.Common/Common/CommandData.cs
Normal 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; }
|
||||
}
|
38
src/Nadeko.Bot.Common/Common/DownloadTracker.cs
Normal file
38
src/Nadeko.Bot.Common/Common/DownloadTracker.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
13
src/Nadeko.Bot.Common/Common/Helpers.cs
Normal file
13
src/Nadeko.Bot.Common/Common/Helpers.cs
Normal 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);
|
||||
}
|
||||
}
|
51
src/Nadeko.Bot.Common/Common/ImageUrls.cs
Normal file
51
src/Nadeko.Bot.Common/Common/ImageUrls.cs
Normal 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; }
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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());
|
||||
}
|
14
src/Nadeko.Bot.Common/Common/LbOpts.cs
Normal file
14
src/Nadeko.Bot.Common/Common/LbOpts.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
16
src/Nadeko.Bot.Common/Common/Linq2DbExpressions.cs
Normal file
16
src/Nadeko.Bot.Common/Common/Linq2DbExpressions.cs
Normal 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;
|
||||
}
|
52
src/Nadeko.Bot.Common/Common/LoginErrorHandler.cs
Normal file
52
src/Nadeko.Bot.Common/Common/LoginErrorHandler.cs
Normal 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");
|
||||
}
|
||||
}
|
46
src/Nadeko.Bot.Common/Common/OldCreds.cs
Normal file
46
src/Nadeko.Bot.Common/Common/OldCreds.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
23
src/Nadeko.Bot.Common/Common/OptionsParser.cs
Normal file
23
src/Nadeko.Bot.Common/Common/OptionsParser.cs
Normal 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);
|
||||
}
|
||||
}
|
9
src/Nadeko.Bot.Common/Common/OsuMapData.cs
Normal file
9
src/Nadeko.Bot.Common/Common/OsuMapData.cs
Normal 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; }
|
||||
}
|
58
src/Nadeko.Bot.Common/Common/OsuUserBets.cs
Normal file
58
src/Nadeko.Bot.Common/Common/OsuUserBets.cs
Normal 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; }
|
||||
}
|
8
src/Nadeko.Bot.Common/Common/Pokemon/PokemonNameId.cs
Normal file
8
src/Nadeko.Bot.Common/Common/Pokemon/PokemonNameId.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Common.Pokemon;
|
||||
|
||||
public class PokemonNameId
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
42
src/Nadeko.Bot.Common/Common/Pokemon/SearchPokemon.cs
Normal file
42
src/Nadeko.Bot.Common/Common/Pokemon/SearchPokemon.cs
Normal 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}";
|
||||
}
|
||||
}
|
10
src/Nadeko.Bot.Common/Common/Pokemon/SearchPokemonAbility.cs
Normal file
10
src/Nadeko.Bot.Common/Common/Pokemon/SearchPokemonAbility.cs
Normal 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; }
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
11
src/Nadeko.Bot.Common/Common/TriviaQuestionModel.cs
Normal file
11
src/Nadeko.Bot.Common/Common/TriviaQuestionModel.cs
Normal 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; }
|
||||
}
|
13
src/Nadeko.Bot.Common/Common/TypeReaders/EmoteTypeReader.cs
Normal file
13
src/Nadeko.Bot.Common/Common/TypeReaders/EmoteTypeReader.cs
Normal 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));
|
||||
}
|
||||
}
|
24
src/Nadeko.Bot.Common/Common/TypeReaders/GuildTypeReader.cs
Normal file
24
src/Nadeko.Bot.Common/Common/TypeReaders/GuildTypeReader.cs
Normal 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"));
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
21
src/Nadeko.Bot.Common/Common/TypeReaders/KwumTypeReader.cs
Normal file
21
src/Nadeko.Bot.Common/Common/TypeReaders/KwumTypeReader.cs
Normal 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)));
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
50
src/Nadeko.Bot.Common/Common/TypeReaders/ModuleTypeReader.cs
Normal file
50
src/Nadeko.Bot.Common/Common/TypeReaders/ModuleTypeReader.cs
Normal 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; }
|
||||
}
|
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
20
src/Nadeko.Bot.Common/Common/TypeReaders/Rgba32TypeReader.cs
Normal file
20
src/Nadeko.Bot.Common/Common/TypeReaders/Rgba32TypeReader.cs
Normal 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."));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
275
src/Nadeko.Bot.Common/Creds.cs
Normal file
275
src/Nadeko.Bot.Common/Creds.cs
Normal 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; }
|
||||
}
|
||||
|
||||
|
||||
|
6
src/Nadeko.Bot.Common/Currency/CurrencyType.cs
Normal file
6
src/Nadeko.Bot.Common/Currency/CurrencyType.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace NadekoBot.Services.Currency;
|
||||
|
||||
public enum CurrencyType
|
||||
{
|
||||
Default
|
||||
}
|
40
src/Nadeko.Bot.Common/Currency/ICurrencyService.cs
Normal file
40
src/Nadeko.Bot.Common/Currency/ICurrencyService.cs
Normal 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);
|
||||
}
|
9
src/Nadeko.Bot.Common/Currency/ITxTracker.cs
Normal file
9
src/Nadeko.Bot.Common/Currency/ITxTracker.cs
Normal 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);
|
||||
}
|
40
src/Nadeko.Bot.Common/Currency/IWallet.cs
Normal file
40
src/Nadeko.Bot.Common/Currency/IWallet.cs
Normal 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;
|
||||
}
|
||||
}
|
7
src/Nadeko.Bot.Common/Currency/TxData.cs
Normal file
7
src/Nadeko.Bot.Common/Currency/TxData.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Services.Currency;
|
||||
|
||||
public record class TxData(
|
||||
string Type,
|
||||
string Extra,
|
||||
string? Note = "",
|
||||
ulong? OtherId = null);
|
@@ -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;
|
12
src/Nadeko.Bot.Common/IBot.cs
Normal file
12
src/Nadeko.Bot.Common/IBot.cs
Normal 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; }
|
||||
}
|
6
src/Nadeko.Bot.Common/ICurrencyProvider.cs
Normal file
6
src/Nadeko.Bot.Common/ICurrencyProvider.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Nadeko.Bot.Common;
|
||||
|
||||
public interface ICurrencyProvider
|
||||
{
|
||||
string GetCurrencySign();
|
||||
}
|
7
src/Nadeko.Bot.Common/IDiscordPermOverrideService.cs
Normal file
7
src/Nadeko.Bot.Common/IDiscordPermOverrideService.cs
Normal 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);
|
||||
}
|
7
src/Nadeko.Bot.Common/INadekoCommandOptions.cs
Normal file
7
src/Nadeko.Bot.Common/INadekoCommandOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public interface INadekoCommandOptions
|
||||
{
|
||||
void NormalizeOptions();
|
||||
}
|
15
src/Nadeko.Bot.Common/IPermissionChecker.cs
Normal file
15
src/Nadeko.Bot.Common/IPermissionChecker.cs
Normal 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);
|
||||
}
|
@@ -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>
|
||||
|
15
src/Nadeko.Bot.Common/NadekoTypeReader.cs
Normal file
15
src/Nadeko.Bot.Common/NadekoTypeReader.cs
Normal 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);
|
||||
}
|
7
src/Nadeko.Bot.Common/Patronage/FeatureLimitKey.cs
Normal file
7
src/Nadeko.Bot.Common/Patronage/FeatureLimitKey.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Modules.Utility.Patronage;
|
||||
|
||||
public readonly struct FeatureLimitKey
|
||||
{
|
||||
public string PrettyName { get; init; }
|
||||
public string Key { get; init; }
|
||||
}
|
8
src/Nadeko.Bot.Common/Patronage/FeatureQuotaStats.cs
Normal file
8
src/Nadeko.Bot.Common/Patronage/FeatureQuotaStats.cs
Normal 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; }
|
||||
}
|
11
src/Nadeko.Bot.Common/Patronage/IPatronData.cs
Normal file
11
src/Nadeko.Bot.Common/Patronage/IPatronData.cs
Normal 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; }
|
||||
}
|
56
src/Nadeko.Bot.Common/Patronage/IPatronageService.cs
Normal file
56
src/Nadeko.Bot.Common/Patronage/IPatronageService.cs
Normal 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();
|
||||
}
|
16
src/Nadeko.Bot.Common/Patronage/ISubscriptionHandler.cs
Normal file
16
src/Nadeko.Bot.Common/Patronage/ISubscriptionHandler.cs
Normal 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();
|
||||
}
|
38
src/Nadeko.Bot.Common/Patronage/Patron.cs
Normal file
38
src/Nadeko.Bot.Common/Patronage/Patron.cs
Normal 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();
|
||||
}
|
37
src/Nadeko.Bot.Common/Patronage/PatronConfigData.cs
Normal file
37
src/Nadeko.Bot.Common/Patronage/PatronConfigData.cs
Normal 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();
|
||||
}
|
||||
}
|
33
src/Nadeko.Bot.Common/Patronage/PatronExtensions.cs
Normal file
33
src/Nadeko.Bot.Common/Patronage/PatronExtensions.cs
Normal 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}";
|
||||
}
|
||||
}
|
14
src/Nadeko.Bot.Common/Patronage/PatronTier.cs
Normal file
14
src/Nadeko.Bot.Common/Patronage/PatronTier.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
namespace NadekoBot.Modules.Utility.Patronage;
|
||||
|
||||
public enum PatronTier
|
||||
{
|
||||
None,
|
||||
I,
|
||||
V,
|
||||
X,
|
||||
XX,
|
||||
L,
|
||||
C,
|
||||
ComingSoon
|
||||
}
|
66
src/Nadeko.Bot.Common/Patronage/QuotaLimit.cs
Normal file
66
src/Nadeko.Bot.Common/Patronage/QuotaLimit.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
8
src/Nadeko.Bot.Common/Patronage/QuotaPer.cs
Normal file
8
src/Nadeko.Bot.Common/Patronage/QuotaPer.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Utility.Patronage;
|
||||
|
||||
public enum QuotaPer
|
||||
{
|
||||
PerHour,
|
||||
PerDay,
|
||||
PerMonth,
|
||||
}
|
10
src/Nadeko.Bot.Common/Patronage/SubscriptionChargeStatus.cs
Normal file
10
src/Nadeko.Bot.Common/Patronage/SubscriptionChargeStatus.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Modules.Utility;
|
||||
|
||||
public enum SubscriptionChargeStatus
|
||||
{
|
||||
Paid,
|
||||
Refunded,
|
||||
Unpaid,
|
||||
Other,
|
||||
}
|
25
src/Nadeko.Bot.Common/Patronage/UserQuotaStats.cs
Normal file
25
src/Nadeko.Bot.Common/Patronage/UserQuotaStats.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
427
src/Nadeko.Bot.Common/Services/CommandHandler.cs
Normal file
427
src/Nadeko.Bot.Common/Services/CommandHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
106
src/Nadeko.Bot.Common/Services/Currency/CurrencyService.cs
Normal file
106
src/Nadeko.Bot.Common/Services/Currency/CurrencyService.cs
Normal 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);
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
113
src/Nadeko.Bot.Common/Services/Currency/DefaultWallet.cs
Normal file
113
src/Nadeko.Bot.Common/Services/Currency/DefaultWallet.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
110
src/Nadeko.Bot.Common/Services/Currency/GamblingTxTracker.cs
Normal file
110
src/Nadeko.Bot.Common/Services/Currency/GamblingTxTracker.cs
Normal 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();
|
||||
}
|
||||
}
|
13
src/Nadeko.Bot.Common/Services/ILocalDataCache.cs
Normal file
13
src/Nadeko.Bot.Common/Services/ILocalDataCache.cs
Normal 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();
|
||||
}
|
12
src/Nadeko.Bot.Common/Services/IRemindService.cs
Normal file
12
src/Nadeko.Bot.Common/Services/IRemindService.cs
Normal 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);
|
||||
}
|
@@ -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>
|
||||
|
60
src/Nadeko.Bot.Common/Services/Impl/FontProvider.cs
Normal file
60
src/Nadeko.Bot.Common/Services/Impl/FontProvider.cs
Normal 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");
|
||||
}
|
||||
}
|
17
src/Nadeko.Bot.Common/Services/Impl/IImageCache.cs
Normal file
17
src/Nadeko.Bot.Common/Services/Impl/IImageCache.cs
Normal 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);
|
||||
}
|
19
src/Nadeko.Bot.Common/Services/Impl/ImagesConfig.cs
Normal file
19
src/Nadeko.Bot.Common/Services/Impl/ImagesConfig.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
11
src/Nadeko.Bot.Common/Services/Impl/RedisImageExtensions.cs
Normal file
11
src/Nadeko.Bot.Common/Services/Impl/RedisImageExtensions.cs
Normal 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));
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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();
|
||||
}
|
78
src/Nadeko.Bot.Common/Services/Impl/YtdlOperation.cs
Normal file
78
src/Nadeko.Bot.Common/Services/Impl/YtdlOperation.cs
Normal 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;
|
||||
}
|
||||
}
|
101
src/Nadeko.Bot.Common/Services/strings/impl/BotStrings.cs
Normal file
101
src/Nadeko.Bot.Common/Services/strings/impl/BotStrings.cs
Normal 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();
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
30
src/Nadeko.Bot.Common/TypeReaderResult.cs
Normal file
30
src/Nadeko.Bot.Common/TypeReaderResult.cs
Normal 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);
|
||||
}
|
@@ -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 >= 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
@@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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
Reference in New Issue
Block a user