Abstract away cache. 2 implementations: redis and memory

This commit is contained in:
Kwoth
2022-06-23 13:07:45 +00:00
parent 1716c69132
commit 210da263ad
75 changed files with 11525 additions and 1547 deletions

View File

@@ -1,35 +0,0 @@
#nullable disable
using StackExchange.Redis;
namespace NadekoBot.Services;
public interface IDataCache
{
ConnectionMultiplexer Redis { get; }
IImageCache LocalImages { get; }
ILocalDataCache LocalData { get; }
Task<(bool Success, byte[] Data)> TryGetImageDataAsync(Uri key);
Task<(bool Success, string Data)> TryGetAnimeDataAsync(string key);
Task<(bool Success, string Data)> TryGetNovelDataAsync(string key);
Task SetImageDataAsync(Uri key, byte[] data);
Task SetAnimeDataAsync(string link, string data);
Task SetNovelDataAsync(string link, string data);
TimeSpan? AddTimelyClaim(ulong id, int period);
TimeSpan? TryAddRatelimit(ulong id, string name, int expireIn);
void RemoveAllTimelyClaims();
bool TryAddAffinityCooldown(ulong userId, out TimeSpan? time);
bool TryAddDivorceCooldown(ulong userId, out TimeSpan? time);
bool TryGetEconomy(out string data);
void SetEconomy(string data);
Task<TOut> GetOrAddCachedDataAsync<TParam, TOut>(
string key,
Func<TParam, Task<TOut>> factory,
TParam param,
TimeSpan expiry)
where TOut : class;
DateTime GetLastCurrencyDecay();
void SetLastCurrencyDecay();
}

View File

@@ -1,29 +0,0 @@
#nullable disable
namespace NadekoBot.Services;
public interface IImageCache
{
ImageUrls ImageUrls { get; }
IReadOnlyList<byte[]> Heads { get; }
IReadOnlyList<byte[]> Tails { get; }
IReadOnlyList<byte[]> Dice { get; }
IReadOnlyList<byte[]> SlotEmojis { get; }
IReadOnlyList<byte[]> Currency { get; }
byte[] SlotBackground { get; }
byte[] RategirlMatrix { get; }
byte[] RategirlDot { get; }
byte[] XpBackground { get; }
byte[] Rip { get; }
byte[] RipOverlay { get; }
byte[] GetCard(string key);
Task Reload();
}

View File

@@ -6,8 +6,8 @@ namespace NadekoBot.Services;
public interface ILocalDataCache
{
IReadOnlyDictionary<string, SearchPokemon> Pokemons { get; }
IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities { get; }
IReadOnlyDictionary<int, string> PokemonMap { get; }
TriviaQuestion[] TriviaQuestions { get; }
Task<IReadOnlyDictionary<string, SearchPokemon>> GetPokemonsAsync();
Task<IReadOnlyDictionary<string, SearchPokemonAbility>> GetPokemonAbilitiesAsync();
Task<TriviaQuestionModel[]> GetTriviaQuestionsAsync();
Task<PokemonNameId[]> GetPokemonMapAsync();
}

View File

@@ -172,9 +172,10 @@ public sealed class BotCredsProvider : IBotCredsProvider
if (File.Exists(CREDS_FILE_NAME))
{
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
if (creds.Version <= 4)
if (creds.Version <= 5)
{
creds.Version = 5;
creds.Version = 6;
creds.BotCache = BotCacheImplemenation.Redis;
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
}
}

View File

@@ -0,0 +1,82 @@
using NadekoBot.Common.Pokemon;
using NadekoBot.Modules.Games.Common.Trivia;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Services;
public sealed class LocalDataCache : ILocalDataCache, INService
{
private const string POKEMON_ABILITIES_FILE = "data/pokemon/pokemon_abilities.json";
private const string POKEMON_LIST_FILE = "data/pokemon/pokemon_list.json";
private const string POKEMON_MAP_PATH = "data/pokemon/name-id_map.json";
private const string QUESTIONS_FILE = "data/trivia_questions.json";
private readonly IBotCache _cache;
private readonly JsonSerializerOptions _opts = new JsonSerializerOptions()
{
AllowTrailingCommas = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
PropertyNameCaseInsensitive = true
};
public LocalDataCache(IBotCache cache)
=> _cache = cache;
private async Task<T?> GetOrCreateCachedDataAsync<T>(
TypedKey<T> key,
string fileName)
=> await _cache.GetOrAddAsync(key,
async () =>
{
if (!File.Exists(fileName))
{
Log.Warning($"{fileName} is missing. Relevant data can't be loaded");
return default;
}
try
{
await using var stream = File.OpenRead(fileName);
return await JsonSerializer.DeserializeAsync<T>(stream, _opts);
}
catch (Exception ex)
{
Log.Error(ex,
"Error reading {FileName} file: {ErrorMessage}",
fileName,
ex.Message);
return default;
}
});
private static TypedKey<IReadOnlyDictionary<string, SearchPokemon>> _pokemonListKey
= new("pokemon:list");
public async Task<IReadOnlyDictionary<string, SearchPokemon>?> GetPokemonsAsync()
=> await GetOrCreateCachedDataAsync(_pokemonListKey, POKEMON_LIST_FILE);
private static TypedKey<IReadOnlyDictionary<string, SearchPokemonAbility>> _pokemonAbilitiesKey
= new("pokemon:abilities");
public async Task<IReadOnlyDictionary<string, SearchPokemonAbility>?> GetPokemonAbilitiesAsync()
=> await GetOrCreateCachedDataAsync(_pokemonAbilitiesKey, POKEMON_ABILITIES_FILE);
private static TypedKey<PokemonNameId[]> _pokeMapKey
= new("pokemon:ab_map");
public async Task<PokemonNameId[]?> GetPokemonMapAsync()
=> await GetOrCreateCachedDataAsync(_pokeMapKey, POKEMON_MAP_PATH);
private static TypedKey<TriviaQuestionModel[]> _triviaKey
= new("trivia:questions");
public async Task<TriviaQuestionModel[]?> GetTriviaQuestionsAsync()
=> await GetOrCreateCachedDataAsync(_triviaKey, QUESTIONS_FILE);
}

View File

@@ -1,216 +0,0 @@
#nullable disable
using Newtonsoft.Json;
using StackExchange.Redis;
using System.Net;
namespace NadekoBot.Services;
public class RedisCache : IDataCache
{
public ConnectionMultiplexer Redis { get; }
public IImageCache LocalImages { get; }
public ILocalDataCache LocalData { get; }
private readonly string _redisKey;
private readonly EndPoint _redisEndpoint;
private readonly object _timelyLock = new();
public RedisCache(
ConnectionMultiplexer redis,
IBotCredentials creds,
IImageCache imageCache,
ILocalDataCache dataCache)
{
Redis = redis;
_redisEndpoint = Redis.GetEndPoints().First();
LocalImages = imageCache;
LocalData = dataCache;
_redisKey = creds.RedisKey();
}
// things here so far don't need the bot id
// because it's a good thing if different bots
// which are hosted on the same PC
// can re-use the same image/anime data
public async Task<(bool Success, byte[] Data)> TryGetImageDataAsync(Uri key)
{
var db = Redis.GetDatabase();
byte[] x = await db.StringGetAsync("image_" + key);
return (x is not null, x);
}
public Task SetImageDataAsync(Uri key, byte[] data)
{
var db = Redis.GetDatabase();
return db.StringSetAsync("image_" + key, data);
}
public async Task<(bool Success, string Data)> TryGetAnimeDataAsync(string key)
{
var db = Redis.GetDatabase();
string x = await db.StringGetAsync("anime_" + key);
return (x is not null, x);
}
public Task SetAnimeDataAsync(string key, string data)
{
var db = Redis.GetDatabase();
return db.StringSetAsync("anime_" + key, data, TimeSpan.FromHours(3));
}
public async Task<(bool Success, string Data)> TryGetNovelDataAsync(string key)
{
var db = Redis.GetDatabase();
string x = await db.StringGetAsync("novel_" + key);
return (x is not null, x);
}
public Task SetNovelDataAsync(string key, string data)
{
var db = Redis.GetDatabase();
return db.StringSetAsync("novel_" + key, data, TimeSpan.FromHours(3));
}
public TimeSpan? AddTimelyClaim(ulong id, int period)
{
if (period == 0)
return null;
lock (_timelyLock)
{
var time = TimeSpan.FromHours(period);
var db = Redis.GetDatabase();
if ((bool?)db.StringGet($"{_redisKey}_timelyclaim_{id}") is null)
{
db.StringSet($"{_redisKey}_timelyclaim_{id}", true, time);
return null;
}
return db.KeyTimeToLive($"{_redisKey}_timelyclaim_{id}");
}
}
public void RemoveAllTimelyClaims()
{
var server = Redis.GetServer(_redisEndpoint);
var db = Redis.GetDatabase();
foreach (var k in server.Keys(pattern: $"{_redisKey}_timelyclaim_*"))
db.KeyDelete(k, CommandFlags.FireAndForget);
}
public bool TryAddAffinityCooldown(ulong userId, out TimeSpan? time)
{
var db = Redis.GetDatabase();
time = db.KeyTimeToLive($"{_redisKey}_affinity_{userId}");
if (time is null)
{
time = TimeSpan.FromMinutes(30);
db.StringSet($"{_redisKey}_affinity_{userId}", true, time);
return true;
}
return false;
}
public bool TryAddDivorceCooldown(ulong userId, out TimeSpan? time)
{
var db = Redis.GetDatabase();
time = db.KeyTimeToLive($"{_redisKey}_divorce_{userId}");
if (time is null)
{
time = TimeSpan.FromHours(6);
db.StringSet($"{_redisKey}_divorce_{userId}", true, time);
return true;
}
return false;
}
public Task SetStreamDataAsync(string url, string data)
{
var db = Redis.GetDatabase();
return db.StringSetAsync($"{_redisKey}_stream_{url}", data, TimeSpan.FromHours(6));
}
public bool TryGetStreamData(string url, out string dataStr)
{
var db = Redis.GetDatabase();
dataStr = db.StringGet($"{_redisKey}_stream_{url}");
return !string.IsNullOrWhiteSpace(dataStr);
}
public TimeSpan? TryAddRatelimit(ulong id, string name, int expireIn)
{
var db = Redis.GetDatabase();
if (db.StringSet($"{_redisKey}_ratelimit_{id}_{name}",
0, // i don't use the value
TimeSpan.FromSeconds(expireIn),
when: When.NotExists))
{
return null;
}
return db.KeyTimeToLive($"{_redisKey}_ratelimit_{id}_{name}");
}
public bool TryGetEconomy(out string data)
{
var db = Redis.GetDatabase();
data = db.StringGet($"{_redisKey}_economy");
if (data is not null)
return true;
return false;
}
public void SetEconomy(string data)
{
var db = Redis.GetDatabase();
db.StringSet($"{_redisKey}_economy", data, TimeSpan.FromMinutes(3));
}
public async Task<TOut> GetOrAddCachedDataAsync<TParam, TOut>(
string key,
Func<TParam, Task<TOut>> factory,
TParam param,
TimeSpan expiry)
where TOut : class
{
var db = Redis.GetDatabase();
var data = await db.StringGetAsync(key);
if (!data.HasValue)
{
var obj = await factory(param);
if (obj is null)
return default;
await db.StringSetAsync(key, JsonConvert.SerializeObject(obj), expiry);
return obj;
}
return (TOut)JsonConvert.DeserializeObject(data, typeof(TOut));
}
public DateTime GetLastCurrencyDecay()
{
var db = Redis.GetDatabase();
var str = (string)db.StringGet($"{_redisKey}_last_currency_decay");
if (string.IsNullOrEmpty(str))
return DateTime.MinValue;
return JsonConvert.DeserializeObject<DateTime>(str);
}
public void SetLastCurrencyDecay()
{
var db = Redis.GetDatabase();
db.StringSet($"{_redisKey}_last_currency_decay", JsonConvert.SerializeObject(DateTime.UtcNow));
}
}

View File

@@ -1,297 +1,111 @@
#nullable disable
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.Yml;
using Newtonsoft.Json;
using StackExchange.Redis;
using NadekoBot.Common.Configs;
namespace NadekoBot.Services;
public sealed class RedisImagesCache : IImageCache, IReadyExecutor
public sealed class ImagesConfig : ConfigServiceBase<ImageUrls>
{
public enum ImageKeys
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)
{
CoinHeads,
CoinTails,
Dice,
SlotBg,
SlotEmojis,
Currency,
RategirlMatrix,
RategirlDot,
RipOverlay,
RipBg,
XpBg
}
}
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();
}
public sealed class ImageCache : IImageCache, INService
{
private readonly IBotCache _cache;
private readonly ImagesConfig _ic;
private readonly Random _rng;
private readonly IHttpClientFactory _httpFactory;
public ImageCache(
IBotCache cache,
ImagesConfig ic,
IHttpClientFactory httpFactory)
{
_cache = cache;
_ic = ic;
_httpFactory = httpFactory;
_rng = new NadekoRandom();
}
private const string BASE_PATH = "data/";
private const string CARDS_PATH = $"{BASE_PATH}images/cards";
private static TypedKey<byte[]> GetImageKey(Uri url)
=> new($"image:{url}");
private IDatabase Db
=> _con.GetDatabase();
public ImageUrls ImageUrls { get; private set; }
public IReadOnlyList<byte[]> Heads
=> GetByteArrayData(ImageKeys.CoinHeads);
public IReadOnlyList<byte[]> Tails
=> GetByteArrayData(ImageKeys.CoinTails);
public IReadOnlyList<byte[]> Dice
=> GetByteArrayData(ImageKeys.Dice);
public IReadOnlyList<byte[]> SlotEmojis
=> GetByteArrayData(ImageKeys.SlotEmojis);
public IReadOnlyList<byte[]> Currency
=> GetByteArrayData(ImageKeys.Currency);
public byte[] SlotBackground
=> GetByteData(ImageKeys.SlotBg);
public byte[] RategirlMatrix
=> GetByteData(ImageKeys.RategirlMatrix);
public byte[] RategirlDot
=> GetByteData(ImageKeys.RategirlDot);
public byte[] XpBackground
=> GetByteData(ImageKeys.XpBg);
public byte[] Rip
=> GetByteData(ImageKeys.RipBg);
public byte[] RipOverlay
=> GetByteData(ImageKeys.RipOverlay);
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private readonly HttpClient _http;
private readonly string _imagesPath;
public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds)
{
_con = con;
_creds = creds;
_http = new();
_imagesPath = Path.Combine(BASE_PATH, "images.yml");
Migrate();
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
}
public byte[] GetCard(string key)
// since cards are always local for now, don't cache them
=> File.ReadAllBytes(Path.Join(CARDS_PATH, key + ".jpg"));
public async Task OnReadyAsync()
{
if (await AllKeysExist())
return;
await Reload();
}
private void Migrate()
{
// migrate to yml
if (File.Exists(Path.Combine(BASE_PATH, "images.json")))
{
var oldFilePath = Path.Combine(BASE_PATH, "images.json");
var backupFilePath = Path.Combine(BASE_PATH, "images.json.backup");
var oldData = JsonConvert.DeserializeObject<OldImageUrls>(File.ReadAllText(oldFilePath));
if (oldData is not null)
private async Task<byte[]?> GetImageDataAsync(Uri url)
=> await _cache.GetOrAddAsync(
GetImageKey(url),
async () =>
{
var newData = new ImageUrls
{
Coins =
new()
{
Heads =
oldData.Coins.Heads.Length == 1
&& oldData.Coins.Heads[0].ToString()
== "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png"
? new[] { new Uri("https://cdn.nadeko.bot/coins/heads3.png") }
: oldData.Coins.Heads,
Tails = oldData.Coins.Tails.Length == 1
&& oldData.Coins.Tails[0].ToString()
== "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png"
? new[] { new Uri("https://cdn.nadeko.bot/coins/tails3.png") }
: oldData.Coins.Tails
},
Dice = oldData.Dice.Map(x => x.ToNewCdn()),
Currency = oldData.Currency.Map(x => x.ToNewCdn()),
Rategirl =
new()
{
Dot = oldData.Rategirl.Dot.ToNewCdn(),
Matrix = oldData.Rategirl.Matrix.ToNewCdn()
},
Rip = new()
{
Bg = oldData.Rip.Bg.ToNewCdn(),
Overlay = oldData.Rip.Overlay.ToNewCdn()
},
Slots = new()
{
Bg = new("https://cdn.nadeko.bot/slots/slots_bg.png"),
Emojis = new[]
{
"https://cdn.nadeko.bot/slots/0.png", "https://cdn.nadeko.bot/slots/1.png",
"https://cdn.nadeko.bot/slots/2.png", "https://cdn.nadeko.bot/slots/3.png",
"https://cdn.nadeko.bot/slots/4.png", "https://cdn.nadeko.bot/slots/5.png"
}.Map(x => new Uri(x))
},
Xp = new()
{
Bg = oldData.Xp.Bg.ToNewCdn()
},
Version = 2
};
File.Move(oldFilePath, backupFilePath, true);
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(newData));
}
}
// removed numbers from slots
var localImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
if (localImageUrls.Version == 2)
{
localImageUrls.Version = 3;
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(localImageUrls));
}
if (localImageUrls.Version == 3)
{
localImageUrls.Version = 4;
if (localImageUrls.Xp?.Bg.ToString() == "https://cdn.nadeko.bot/other/xp/bg.png")
localImageUrls.Xp.Bg = new("https://cdn.nadeko.bot/other/xp/bg_k.png");
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(localImageUrls));
}
}
public async Task Reload()
{
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(await File.ReadAllTextAsync(_imagesPath));
foreach (var key in GetAllKeys())
{
switch (key)
{
case ImageKeys.CoinHeads:
await Load(key, ImageUrls.Coins.Heads);
break;
case ImageKeys.CoinTails:
await Load(key, ImageUrls.Coins.Tails);
break;
case ImageKeys.Dice:
await Load(key, ImageUrls.Dice);
break;
case ImageKeys.SlotBg:
await Load(key, ImageUrls.Slots.Bg);
break;
case ImageKeys.SlotEmojis:
await Load(key, ImageUrls.Slots.Emojis);
break;
case ImageKeys.Currency:
await Load(key, ImageUrls.Currency);
break;
case ImageKeys.RategirlMatrix:
await Load(key, ImageUrls.Rategirl.Matrix);
break;
case ImageKeys.RategirlDot:
await Load(key, ImageUrls.Rategirl.Dot);
break;
case ImageKeys.RipOverlay:
await Load(key, ImageUrls.Rip.Overlay);
break;
case ImageKeys.RipBg:
await Load(key, ImageUrls.Rip.Bg);
break;
case ImageKeys.XpBg:
await Load(key, ImageUrls.Xp.Bg);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
private async Task Load(ImageKeys key, Uri uri)
{
var data = await GetImageData(uri);
if (data is null)
return;
await Db.StringSetAsync(GetRedisKey(key), data);
}
private async Task Load(ImageKeys key, Uri[] uris)
{
await Db.KeyDeleteAsync(GetRedisKey(key));
var imageData = await uris.Select(GetImageData).WhenAll();
var vals = imageData.Where(x => x is not null).Select(x => (RedisValue)x).ToArray();
await Db.ListRightPushAsync(GetRedisKey(key), vals);
if (uris.Length != vals.Length)
{
Log.Information(
"{Loaded}/{Max} URIs for the key '{ImageKey}' have been loaded.\n"
+ "Some of the supplied URIs are either unavailable or invalid",
vals.Length,
uris.Length,
key);
}
}
private async Task<byte[]> GetImageData(Uri uri)
{
if (uri.IsFile)
{
try
{
var bytes = await File.ReadAllBytesAsync(uri.LocalPath);
using var http = _httpFactory.CreateClient();
var bytes = await http.GetByteArrayAsync(url);
return bytes;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed reading image bytes from uri: {Uri}", uri.ToString());
return null;
}
}
},
expiry: 48.Hours());
try
{
return await _http.GetByteArrayAsync(uri);
}
catch (Exception ex)
{
Log.Warning(ex, "Image url you provided is not a valid image: {Uri}", uri.ToString());
return null;
}
}
private async Task<bool> AllKeysExist()
private async Task<byte[]?> GetRandomImageDataAsync(Uri[] urls)
{
var tasks = await GetAllKeys().Select(x => Db.KeyExistsAsync(GetRedisKey(x))).WhenAll();
if (urls.Length == 0)
return null;
return tasks.All(exist => exist);
var url = urls[_rng.Next(0, urls.Length)];
var data = await GetImageDataAsync(url);
return data;
}
private IEnumerable<ImageKeys> GetAllKeys()
=> Enum.GetValues<ImageKeys>();
public Task<byte[]?> GetHeadsImageAsync()
=> GetRandomImageDataAsync(_ic.Data.Coins.Heads);
private byte[][] GetByteArrayData(ImageKeys key)
=> Db.ListRange(GetRedisKey(key)).Map(x => (byte[])x);
public Task<byte[]?> GetTailsImageAsync()
=> GetRandomImageDataAsync(_ic.Data.Coins.Tails);
private byte[] GetByteData(ImageKeys key)
=> Db.StringGet(GetRedisKey(key));
public Task<byte[]?> GetCurrencyImageAsync()
=> GetRandomImageDataAsync(_ic.Data.Currency);
private RedisKey GetRedisKey(ImageKeys key)
=> _creds.RedisKey() + "_image_" + key;
}
public Task<byte[]?> GetXpBackgroundImageAsync()
=> GetImageDataAsync(_ic.Data.Xp.Bg);
public Task<byte[]?> GetRategirlBgAsync()
=> GetImageDataAsync(_ic.Data.Rategirl.Matrix);
public Task<byte[]?> GetRategirlDotAsync()
=> GetImageDataAsync(_ic.Data.Rategirl.Dot);
public Task<byte[]?> GetDiceAsync(int num)
=> GetImageDataAsync(_ic.Data.Dice[num]);
public Task<byte[]?> GetSlotEmojiAsync(int number)
=> GetImageDataAsync(_ic.Data.Slots.Emojis[number]);
public Task<byte[]?> GetSlotBgAsync()
=> GetImageDataAsync(_ic.Data.Slots.Bg);
public Task<byte[]?> GetRipBgAsync()
=> GetImageDataAsync(_ic.Data.Rip.Bg);
public Task<byte[]?> GetRipOverlayAsync()
=> GetImageDataAsync(_ic.Data.Rip.Overlay);
}

View File

@@ -1,90 +0,0 @@
#nullable disable
using NadekoBot.Common.Pokemon;
using NadekoBot.Modules.Games.Common.Trivia;
using Newtonsoft.Json;
using StackExchange.Redis;
namespace NadekoBot.Services;
public class RedisLocalDataCache : ILocalDataCache
{
private const string POKEMON_ABILITIES_FILE = "data/pokemon/pokemon_abilities.json";
private const string POKEMON_LIST_FILE = "data/pokemon/pokemon_list.json";
private const string POKEMON_MAP_PATH = "data/pokemon/name-id_map.json";
private const string QUESTIONS_FILE = "data/trivia_questions.json";
public IReadOnlyDictionary<string, SearchPokemon> Pokemons
{
get => Get<Dictionary<string, SearchPokemon>>("pokemon_list");
private init => Set("pokemon_list", value);
}
public IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities
{
get => Get<Dictionary<string, SearchPokemonAbility>>("pokemon_abilities");
private init => Set("pokemon_abilities", value);
}
public TriviaQuestion[] TriviaQuestions
{
get => Get<TriviaQuestion[]>("trivia_questions");
private init => Set("trivia_questions", value);
}
public IReadOnlyDictionary<int, string> PokemonMap
{
get => Get<Dictionary<int, string>>("pokemon_map");
private init => Set("pokemon_map", value);
}
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
public RedisLocalDataCache(ConnectionMultiplexer con, IBotCredentials creds, DiscordSocketClient client)
{
_con = con;
_creds = creds;
var shardId = client.ShardId;
if (shardId == 0)
{
if (!File.Exists(POKEMON_LIST_FILE))
Log.Warning($"{POKEMON_LIST_FILE} is missing. Pokemon abilities not loaded");
else
{
Pokemons =
JsonConvert.DeserializeObject<Dictionary<string, SearchPokemon>>(
File.ReadAllText(POKEMON_LIST_FILE));
}
if (!File.Exists(POKEMON_ABILITIES_FILE))
Log.Warning($"{POKEMON_ABILITIES_FILE} is missing. Pokemon abilities not loaded.");
else
{
PokemonAbilities =
JsonConvert.DeserializeObject<Dictionary<string, SearchPokemonAbility>>(
File.ReadAllText(POKEMON_ABILITIES_FILE));
}
try
{
TriviaQuestions = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(QUESTIONS_FILE));
PokemonMap = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(POKEMON_MAP_PATH))
?.ToDictionary(x => x.Id, x => x.Name)
?? new();
}
catch (Exception ex)
{
Log.Error(ex, "Error loading local data");
throw;
}
}
}
private T Get<T>(string key)
where T : class
=> JsonConvert.DeserializeObject<T>(_con.GetDatabase().StringGet($"{_creds.RedisKey()}_localdata_{key}"));
private void Set(string key, object obj)
=> _con.GetDatabase().StringSet($"{_creds.RedisKey()}_localdata_{key}", JsonConvert.SerializeObject(obj));
}

View File

@@ -1,13 +1,13 @@
#nullable disable
namespace NadekoBot.Services;
public class LocalBotStringsProvider : IBotStringsProvider
public class MemoryBotStringsProvider : IBotStringsProvider
{
private readonly IStringsSource _source;
private IReadOnlyDictionary<string, Dictionary<string, string>> responseStrings;
private IReadOnlyDictionary<string, Dictionary<string, CommandStrings>> commandStrings;
public LocalBotStringsProvider(IStringsSource source)
public MemoryBotStringsProvider(IStringsSource source)
{
_source = source;
Reload();