using NadekoBot.Extensions; using Newtonsoft.Json; using StackExchange.Redis; using System; using System.Linq; using System.Net; using System.Threading.Tasks; using Discord; using Discord.WebSocket; 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; public RedisCache(ConnectionMultiplexer redis, IBotCredentials creds, DiscordSocketClient client) { Redis = redis; _redisEndpoint = Redis.GetEndPoints().First(); LocalImages = new RedisImagesCache(Redis, creds); LocalData = new RedisLocalDataCache(Redis, creds, client.ShardId); _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).ConfigureAwait(false); return (x != 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).ConfigureAwait(false); return (x != null, x); } public Task SetAnimeDataAsync(string key, string data) { var _db = Redis.GetDatabase(); return _db.StringSetAsync("anime_" + key, data, expiry: TimeSpan.FromHours(3)); } public async Task<(bool Success, string Data)> TryGetNovelDataAsync(string key) { var _db = Redis.GetDatabase(); string x = await _db.StringGetAsync("novel_" + key).ConfigureAwait(false); return (x != null, x); } public Task SetNovelDataAsync(string key, string data) { var _db = Redis.GetDatabase(); return _db.StringSetAsync("novel_" + key, data, expiry: TimeSpan.FromHours(3)); } private readonly object timelyLock = new object(); 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, expiry: 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.NotExists)) { return null; } return _db.KeyTimeToLive($"{_redisKey}_ratelimit_{id}_{name}"); } public bool TryGetEconomy(out string data) { var _db = Redis.GetDatabase(); if ((data = _db.StringGet($"{_redisKey}_economy")) != null) { return true; } return false; } public void SetEconomy(string data) { var _db = Redis.GetDatabase(); _db.StringSet($"{_redisKey}_economy", data, expiry: TimeSpan.FromMinutes(3)); } public async Task GetOrAddCachedDataAsync(string key, Func> factory, TParam param, TimeSpan expiry) where TOut : class { var _db = Redis.GetDatabase(); RedisValue data = await _db.StringGetAsync(key).ConfigureAwait(false); if (!data.HasValue) { var obj = await factory(param).ConfigureAwait(false); if (obj is null) return default(TOut); await _db.StringSetAsync(key, JsonConvert.SerializeObject(obj), expiry: expiry).ConfigureAwait(false); 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(str); } public void SetLastCurrencyDecay() { var db = Redis.GetDatabase(); db.StringSet($"{_redisKey}_last_currency_decay", JsonConvert.SerializeObject(DateTime.UtcNow)); } } }