mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
Merge branch 'v4-noredis' into 'v4'
Abstract away cache. 2 implementations: redis and memory See merge request Kwoth/nadekobot!255
This commit is contained in:
20
CHANGELOG.md
20
CHANGELOG.md
@@ -3,6 +3,26 @@
|
|||||||
|
|
||||||
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- New cache abstraction added
|
||||||
|
- 2 implemenations: redis and memory
|
||||||
|
- All current bots will stay on redis cache, all new bots will use in-process memory cache
|
||||||
|
- This change removes bot's hard dependency on redis
|
||||||
|
- Configurable in `creds.yml` (please read the comments)
|
||||||
|
- You **MUST** use 'redis' if your bot runs on more than 1 shard (2000+ servers)
|
||||||
|
- [dev] Using new non-locking ConcurrentDictionary
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `.xp` will now show default user avatars too
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed `.imagesreload` as images are now lazily loaded on request and then cached
|
||||||
|
|
||||||
## [4.2.6] - 22.06.2022
|
## [4.2.6] - 22.06.2022
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@@ -21,7 +21,7 @@ namespace NadekoBot.Tests
|
|||||||
var stringsSource = new LocalFileStringsSource(
|
var stringsSource = new LocalFileStringsSource(
|
||||||
responsesPath,
|
responsesPath,
|
||||||
commandsPath);
|
commandsPath);
|
||||||
var strings = new LocalBotStringsProvider(stringsSource);
|
var strings = new MemoryBotStringsProvider(stringsSource);
|
||||||
|
|
||||||
var culture = new CultureInfo("en-US");
|
var culture = new CultureInfo("en-US");
|
||||||
|
|
||||||
|
@@ -102,20 +102,20 @@ public sealed class Bot
|
|||||||
var svcs = new ServiceCollection().AddTransient(_ => _credsProvider.GetCreds()) // bot creds
|
var svcs = new ServiceCollection().AddTransient(_ => _credsProvider.GetCreds()) // bot creds
|
||||||
.AddSingleton(_credsProvider)
|
.AddSingleton(_credsProvider)
|
||||||
.AddSingleton(_db) // database
|
.AddSingleton(_db) // database
|
||||||
.AddRedis(_creds.RedisOptions) // redis
|
|
||||||
.AddSingleton(Client) // discord socket client
|
.AddSingleton(Client) // discord socket client
|
||||||
.AddSingleton(_commandService)
|
.AddSingleton(_commandService)
|
||||||
// .AddSingleton(_interactionService)
|
// .AddSingleton(_interactionService)
|
||||||
.AddSingleton(this)
|
.AddSingleton(this)
|
||||||
.AddSingleton<ISeria, JsonSeria>()
|
.AddSingleton<ISeria, JsonSeria>()
|
||||||
.AddSingleton<IPubSub, RedisPubSub>()
|
|
||||||
.AddSingleton<IConfigSeria, YamlSeria>()
|
.AddSingleton<IConfigSeria, YamlSeria>()
|
||||||
.AddBotStringsServices(_creds.TotalShards)
|
|
||||||
.AddConfigServices()
|
.AddConfigServices()
|
||||||
.AddConfigMigrators()
|
.AddConfigMigrators()
|
||||||
.AddMemoryCache()
|
.AddMemoryCache()
|
||||||
// music
|
// music
|
||||||
.AddMusic();
|
.AddMusic()
|
||||||
|
// cache
|
||||||
|
.AddCache(_creds);
|
||||||
|
|
||||||
// admin
|
// admin
|
||||||
#if GLOBAL_NADEKO
|
#if GLOBAL_NADEKO
|
||||||
svcs.AddSingleton<ILogCommandService, DummyLogCommandService>();
|
svcs.AddSingleton<ILogCommandService, DummyLogCommandService>();
|
||||||
@@ -143,13 +143,6 @@ public sealed class Bot
|
|||||||
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RemoteGrpcCoordinator>());
|
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RemoteGrpcCoordinator>());
|
||||||
}
|
}
|
||||||
|
|
||||||
svcs.AddSingleton<RedisLocalDataCache>()
|
|
||||||
.AddSingleton<ILocalDataCache>(x => x.GetRequiredService<RedisLocalDataCache>())
|
|
||||||
.AddSingleton<RedisImagesCache>()
|
|
||||||
.AddSingleton<IImageCache>(x => x.GetRequiredService<RedisImagesCache>())
|
|
||||||
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RedisImagesCache>())
|
|
||||||
.AddSingleton<IDataCache, RedisCache>();
|
|
||||||
|
|
||||||
svcs.Scan(scan => scan.FromAssemblyOf<IReadyExecutor>()
|
svcs.Scan(scan => scan.FromAssemblyOf<IReadyExecutor>()
|
||||||
.AddClasses(classes => classes.AssignableToAny(
|
.AddClasses(classes => classes.AssignableToAny(
|
||||||
// services
|
// services
|
||||||
|
@@ -15,22 +15,24 @@ public sealed class RatelimitAttribute : PreconditionAttribute
|
|||||||
Seconds = seconds;
|
Seconds = seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
public override async Task<PreconditionResult> CheckPermissionsAsync(
|
||||||
ICommandContext context,
|
ICommandContext context,
|
||||||
CommandInfo command,
|
CommandInfo command,
|
||||||
IServiceProvider services)
|
IServiceProvider services)
|
||||||
{
|
{
|
||||||
if (Seconds == 0)
|
if (Seconds == 0)
|
||||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
return PreconditionResult.FromSuccess();
|
||||||
|
|
||||||
var cache = services.GetRequiredService<IDataCache>();
|
var cache = services.GetRequiredService<IBotCache>();
|
||||||
var rem = cache.TryAddRatelimit(context.User.Id, command.Name, Seconds);
|
var rem = await cache.GetRatelimitAsync(
|
||||||
|
new($"precondition:{context.User.Id}:{command.Name}"),
|
||||||
|
Seconds.Seconds());
|
||||||
|
|
||||||
if (rem is null)
|
if (rem is null)
|
||||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
return PreconditionResult.FromSuccess();
|
||||||
|
|
||||||
var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s.";
|
var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s.";
|
||||||
|
|
||||||
return Task.FromResult(PreconditionResult.FromError(msgContent));
|
return PreconditionResult.FromError(msgContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
46
src/NadekoBot/Common/Cache/BotCacheExtensions.cs
Normal file
46
src/NadekoBot/Common/Cache/BotCacheExtensions.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace NadekoBot.Common;
|
||||||
|
|
||||||
|
public static class BotCacheExtensions
|
||||||
|
{
|
||||||
|
public static async ValueTask<T?> GetOrDefaultAsync<T>(this IBotCache cache, TypedKey<T> key)
|
||||||
|
{
|
||||||
|
var result = await cache.GetAsync(key);
|
||||||
|
if (result.TryGetValue(out var val))
|
||||||
|
return val;
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TypedKey<byte[]> GetImgKey(Uri uri)
|
||||||
|
=> new($"image:{uri}");
|
||||||
|
|
||||||
|
public static ValueTask SetImageDataAsync(this IBotCache c, string key, byte[] data)
|
||||||
|
=> c.SetImageDataAsync(new Uri(key), data);
|
||||||
|
public static async ValueTask SetImageDataAsync(this IBotCache c, Uri key, byte[] data)
|
||||||
|
=> await c.AddAsync(GetImgKey(key), data, expiry: TimeSpan.FromHours(48));
|
||||||
|
|
||||||
|
public static async ValueTask<OneOf<byte[], None>> GetImageDataAsync(this IBotCache c, Uri key)
|
||||||
|
=> await c.GetAsync(GetImgKey(key));
|
||||||
|
|
||||||
|
public static async Task<TimeSpan?> GetRatelimitAsync(
|
||||||
|
this IBotCache c,
|
||||||
|
TypedKey<long> key,
|
||||||
|
TimeSpan length)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var nowB = now.ToBinary();
|
||||||
|
|
||||||
|
var cachedValue = await c.GetOrAddAsync(key,
|
||||||
|
() => Task.FromResult(now.ToBinary()),
|
||||||
|
expiry: length);
|
||||||
|
|
||||||
|
if (cachedValue == nowB)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var diff = now - DateTime.FromBinary(cachedValue);
|
||||||
|
return length - diff;
|
||||||
|
}
|
||||||
|
}
|
47
src/NadekoBot/Common/Cache/IBotCache.cs
Normal file
47
src/NadekoBot/Common/Cache/IBotCache.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace NadekoBot.Common;
|
||||||
|
|
||||||
|
public interface IBotCache
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item to the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Key to add</param>
|
||||||
|
/// <param name="value">Value to add to the cache</param>
|
||||||
|
/// <param name="expiry">Optional expiry</param>
|
||||||
|
/// <param name="overwrite">Whether old value should be overwritten</param>
|
||||||
|
/// <typeparam name="T">Type of the value</typeparam>
|
||||||
|
/// <returns>Returns whether add was sucessful. Always true unless ovewrite = false</returns>
|
||||||
|
ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get an element from the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Key</param>
|
||||||
|
/// <typeparam name="T">Type of the value</typeparam>
|
||||||
|
/// <returns>Either a value or <see cref="None"/></returns>
|
||||||
|
ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a key from the cache
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Key to remove</param>
|
||||||
|
/// <typeparam name="T">Type of the value</typeparam>
|
||||||
|
/// <returns>Whether there was item</returns>
|
||||||
|
ValueTask<bool> RemoveAsync<T>(TypedKey<T> key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the key if it exists or add a new one
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Key to get and potentially add</param>
|
||||||
|
/// <param name="createFactory">Value creation factory</param>
|
||||||
|
/// <param name="expiry">Optional expiry</param>
|
||||||
|
/// <typeparam name="T">Type of the value</typeparam>
|
||||||
|
/// <returns>The retrieved or newly added value</returns>
|
||||||
|
ValueTask<T?> GetOrAddAsync<T>(
|
||||||
|
TypedKey<T> key,
|
||||||
|
Func<Task<T?>> createFactory,
|
||||||
|
TimeSpan? expiry = null);
|
||||||
|
}
|
71
src/NadekoBot/Common/Cache/MemoryBotCache.cs
Normal file
71
src/NadekoBot/Common/Cache/MemoryBotCache.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
// ReSharper disable InconsistentlySynchronizedField
|
||||||
|
|
||||||
|
namespace NadekoBot.Common;
|
||||||
|
|
||||||
|
public sealed class MemoryBotCache : IBotCache
|
||||||
|
{
|
||||||
|
// needed for overwrites and Delete return value
|
||||||
|
private readonly object _cacheLock = new object();
|
||||||
|
private readonly MemoryCache _cache;
|
||||||
|
|
||||||
|
public MemoryBotCache()
|
||||||
|
{
|
||||||
|
_cache = new MemoryCache(new MemoryCacheOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
|
||||||
|
{
|
||||||
|
if (overwrite)
|
||||||
|
{
|
||||||
|
using var item = _cache.CreateEntry(key.Key);
|
||||||
|
item.Value = value;
|
||||||
|
item.AbsoluteExpirationRelativeToNow = expiry;
|
||||||
|
return new(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_cacheLock)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(key.Key, out var old) && old is not null)
|
||||||
|
return new(false);
|
||||||
|
|
||||||
|
using var item = _cache.CreateEntry(key.Key);
|
||||||
|
item.Value = value;
|
||||||
|
item.AbsoluteExpirationRelativeToNow = expiry;
|
||||||
|
return new(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<T?> GetOrAddAsync<T>(
|
||||||
|
TypedKey<T> key,
|
||||||
|
Func<Task<T?>> createFactory,
|
||||||
|
TimeSpan? expiry = null)
|
||||||
|
=> await _cache.GetOrCreateAsync(key.Key,
|
||||||
|
async ce =>
|
||||||
|
{
|
||||||
|
ce.AbsoluteExpirationRelativeToNow = expiry;
|
||||||
|
var val = await createFactory();
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
public ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
|
||||||
|
{
|
||||||
|
if (!_cache.TryGetValue(key.Key, out var val) || val is null)
|
||||||
|
return new(new None());
|
||||||
|
|
||||||
|
return new((T)val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
|
||||||
|
{
|
||||||
|
lock (_cacheLock)
|
||||||
|
{
|
||||||
|
var toReturn = _cache.TryGetValue(key.Key, out var old ) && old is not null;
|
||||||
|
_cache.Remove(key.Key);
|
||||||
|
return new(toReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
119
src/NadekoBot/Common/Cache/RedisBotCache.cs
Normal file
119
src/NadekoBot/Common/Cache/RedisBotCache.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Common;
|
||||||
|
|
||||||
|
public sealed class RedisBotCache : IBotCache
|
||||||
|
{
|
||||||
|
private static readonly Type[] _supportedTypes = new []
|
||||||
|
{
|
||||||
|
typeof(bool), typeof(int), typeof(uint), typeof(long),
|
||||||
|
typeof(ulong), typeof(float), typeof(double),
|
||||||
|
typeof(string), typeof(byte[]), typeof(ReadOnlyMemory<byte>), typeof(Memory<byte>),
|
||||||
|
typeof(RedisValue),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions _opts = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
IgnoreReadOnlyProperties = false,
|
||||||
|
};
|
||||||
|
private readonly ConnectionMultiplexer _conn;
|
||||||
|
|
||||||
|
public RedisBotCache(ConnectionMultiplexer conn)
|
||||||
|
{
|
||||||
|
_conn = conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
|
||||||
|
{
|
||||||
|
// if a null value is passed, remove the key
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
await RemoveAsync(key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var db = _conn.GetDatabase();
|
||||||
|
RedisValue val = IsSupportedType(typeof(T))
|
||||||
|
? RedisValue.Unbox(value)
|
||||||
|
: JsonSerializer.Serialize(value, _opts);
|
||||||
|
|
||||||
|
var success = await db.StringSetAsync(key.Key,
|
||||||
|
val,
|
||||||
|
expiry: expiry,
|
||||||
|
when: overwrite ? When.Always : When.NotExists);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSupportedType(Type type)
|
||||||
|
{
|
||||||
|
if (type.IsGenericType)
|
||||||
|
{
|
||||||
|
var typeDef = type.GetGenericTypeDefinition();
|
||||||
|
if (typeDef == typeof(Nullable<>))
|
||||||
|
return IsSupportedType(type.GenericTypeArguments[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var t in _supportedTypes)
|
||||||
|
{
|
||||||
|
if (type == t)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
|
||||||
|
{
|
||||||
|
var db = _conn.GetDatabase();
|
||||||
|
var val = await db.StringGetAsync(key.Key);
|
||||||
|
if (val == default)
|
||||||
|
return new None();
|
||||||
|
|
||||||
|
if (IsSupportedType(typeof(T)))
|
||||||
|
return (T)((IConvertible)val).ToType(typeof(T), null);
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<T>(val.ToString(), _opts)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
|
||||||
|
{
|
||||||
|
var db = _conn.GetDatabase();
|
||||||
|
|
||||||
|
return await db.KeyDeleteAsync(key.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<T?> GetOrAddAsync<T>(TypedKey<T> key, Func<Task<T?>> createFactory, TimeSpan? expiry = null)
|
||||||
|
{
|
||||||
|
var result = await GetAsync(key);
|
||||||
|
|
||||||
|
return await result.Match<Task<T?>>(
|
||||||
|
v => Task.FromResult<T?>(v),
|
||||||
|
async _ =>
|
||||||
|
{
|
||||||
|
var factoryValue = await createFactory();
|
||||||
|
|
||||||
|
if (factoryValue is null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
await AddAsync(key, factoryValue, expiry);
|
||||||
|
|
||||||
|
// get again to make sure it's the cached value
|
||||||
|
// and not the late factory value, in case there's a race condition
|
||||||
|
|
||||||
|
var newResult = await GetAsync(key);
|
||||||
|
|
||||||
|
// it's fine to do this, it should blow up if something went wrong.
|
||||||
|
return newResult.Match<T?>(
|
||||||
|
v => v,
|
||||||
|
_ => default);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -12,7 +12,7 @@ namespace NadekoBot.Common.Configs;
|
|||||||
public sealed partial class BotConfig : ICloneable<BotConfig>
|
public sealed partial class BotConfig : ICloneable<BotConfig>
|
||||||
{
|
{
|
||||||
[Comment(@"DO NOT CHANGE")]
|
[Comment(@"DO NOT CHANGE")]
|
||||||
public int Version { get; set; } = 2;
|
public int Version { get; set; } = 3;
|
||||||
|
|
||||||
[Comment(@"Most commands, when executed, have a small colored line
|
[Comment(@"Most commands, when executed, have a small colored line
|
||||||
next to the response. The color depends whether the command
|
next to the response. The color depends whether the command
|
||||||
@@ -182,4 +182,4 @@ public enum ConsoleOutputType
|
|||||||
Normal = 0,
|
Normal = 0,
|
||||||
Simple = 1,
|
Simple = 1,
|
||||||
None = 2
|
None = 2
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,10 @@ public sealed class Creds : IBotCredentials
|
|||||||
public bool UsePrivilegedIntents { get; set; }
|
public bool UsePrivilegedIntents { get; set; }
|
||||||
|
|
||||||
[Comment(@"The number of shards that the bot will be running on.
|
[Comment(@"The number of shards that the bot will be running on.
|
||||||
Leave at 1 if you don't know what you're doing.")]
|
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; }
|
public int TotalShards { get; set; }
|
||||||
|
|
||||||
[Comment(
|
[Comment(
|
||||||
@@ -50,8 +53,14 @@ go to https://www.patreon.com/portal -> my clients -> create client")]
|
|||||||
|
|
||||||
[Comment(@"Official cleverbot api key.")]
|
[Comment(@"Official cleverbot api key.")]
|
||||||
public string CleverbotApiKey { get; set; }
|
public string CleverbotApiKey { get; set; }
|
||||||
|
|
||||||
[Comment(@"Redis connection string. Don't change if you don't know what you're doing.")]
|
[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; }
|
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")]
|
[Comment(@"Database options. Don't change if you don't know what you're doing. Leave null for default values")]
|
||||||
@@ -104,12 +113,12 @@ Linux default
|
|||||||
args: ""NadekoBot.dll -- {0}""
|
args: ""NadekoBot.dll -- {0}""
|
||||||
Windows default
|
Windows default
|
||||||
cmd: NadekoBot.exe
|
cmd: NadekoBot.exe
|
||||||
args: {0}")]
|
args: ""{0}""")]
|
||||||
public RestartConfig RestartCommand { get; set; }
|
public RestartConfig RestartCommand { get; set; }
|
||||||
|
|
||||||
public Creds()
|
public Creds()
|
||||||
{
|
{
|
||||||
Version = 5;
|
Version = 6;
|
||||||
Token = string.Empty;
|
Token = string.Empty;
|
||||||
UsePrivilegedIntents = true;
|
UsePrivilegedIntents = true;
|
||||||
OwnerIds = new List<ulong>();
|
OwnerIds = new List<ulong>();
|
||||||
@@ -119,6 +128,7 @@ Windows default
|
|||||||
Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
|
Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
|
||||||
BotListToken = string.Empty;
|
BotListToken = string.Empty;
|
||||||
CleverbotApiKey = string.Empty;
|
CleverbotApiKey = string.Empty;
|
||||||
|
BotCache = BotCacheImplemenation.Memory;
|
||||||
RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
|
RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
|
||||||
Db = new()
|
Db = new()
|
||||||
{
|
{
|
||||||
@@ -217,4 +227,10 @@ public class GoogleApiConfig
|
|||||||
{
|
{
|
||||||
public string SearchId { get; init; }
|
public string SearchId { get; init; }
|
||||||
public string ImageSearchId { get; init; }
|
public string ImageSearchId { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BotCacheImplemenation
|
||||||
|
{
|
||||||
|
Memory,
|
||||||
|
Redis
|
||||||
}
|
}
|
@@ -26,6 +26,7 @@ public interface IBotCredentials
|
|||||||
string TwitchClientId { get; set; }
|
string TwitchClientId { get; set; }
|
||||||
string TwitchClientSecret { get; set; }
|
string TwitchClientSecret { get; set; }
|
||||||
GoogleApiConfig Google { get; set; }
|
GoogleApiConfig Google { get; set; }
|
||||||
|
BotCacheImplemenation BotCache { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RestartConfig
|
public class RestartConfig
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using NadekoBot.Common.Yml;
|
using NadekoBot.Common.Yml;
|
||||||
|
using Cloneable;
|
||||||
|
|
||||||
namespace NadekoBot.Common;
|
namespace NadekoBot.Common;
|
||||||
|
|
||||||
public class ImageUrls
|
[Cloneable]
|
||||||
|
public partial class ImageUrls : ICloneable<ImageUrls>
|
||||||
{
|
{
|
||||||
[Comment("DO NOT CHANGE")]
|
[Comment("DO NOT CHANGE")]
|
||||||
public int Version { get; set; } = 3;
|
public int Version { get; set; } = 3;
|
||||||
|
10
src/NadekoBot/Common/OneOfExtensions.cs
Normal file
10
src/NadekoBot/Common/OneOfExtensions.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using OneOf.Types;
|
||||||
|
using OneOf;
|
||||||
|
|
||||||
|
namespace NadekoBot.Common;
|
||||||
|
|
||||||
|
public static class OneOfExtensions
|
||||||
|
{
|
||||||
|
public static bool TryGetValue<T>(this OneOf<T, None> oneOf, out T value)
|
||||||
|
=> oneOf.TryPickT0(out value, out _);
|
||||||
|
}
|
@@ -1,11 +1,12 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace NadekoBot.Common.Pokemon;
|
namespace NadekoBot.Common.Pokemon;
|
||||||
|
|
||||||
public class SearchPokemon
|
public class SearchPokemon
|
||||||
{
|
{
|
||||||
[JsonProperty("num")]
|
[JsonPropertyName("num")]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
public string Species { get; set; }
|
public string Species { get; set; }
|
||||||
|
13
src/NadekoBot/Db/Models/StreamOnlineMessage.cs
Normal file
13
src/NadekoBot/Db/Models/StreamOnlineMessage.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#nullable disable
|
||||||
|
using NadekoBot.Services.Database.Models;
|
||||||
|
|
||||||
|
namespace NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
public class StreamOnlineMessage : DbEntity
|
||||||
|
{
|
||||||
|
public ulong ChannelId { get; set; }
|
||||||
|
public ulong MessageId { get; set; }
|
||||||
|
|
||||||
|
public FollowedStream.FType Type { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
@@ -58,6 +58,9 @@ public abstract class NadekoContext : DbContext
|
|||||||
public DbSet<PatronUser> Patrons { get; set; }
|
public DbSet<PatronUser> Patrons { get; set; }
|
||||||
|
|
||||||
public DbSet<PatronQuota> PatronQuotas { get; set; }
|
public DbSet<PatronQuota> PatronQuotas { get; set; }
|
||||||
|
|
||||||
|
public DbSet<StreamOnlineMessage> StreamOnlineMessages { get; set; }
|
||||||
|
|
||||||
|
|
||||||
#region Mandatory Provider-Specific Values
|
#region Mandatory Provider-Specific Values
|
||||||
|
|
||||||
|
3514
src/NadekoBot/Migrations/MySql/20220623090718_stondel-db-cache.Designer.cs
generated
Normal file
3514
src/NadekoBot/Migrations/MySql/20220623090718_stondel-db-cache.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.Mysql
|
||||||
|
{
|
||||||
|
public partial class stondeldbcache : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "streamonlinemessages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
channelid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
|
||||||
|
messageid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
|
||||||
|
type = table.Column<int>(type: "int", nullable: false),
|
||||||
|
name = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_streamonlinemessages", x => x.id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "streamonlinemessages");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -16,7 +16,7 @@ namespace NadekoBot.Migrations.Mysql
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "6.0.5")
|
.HasAnnotation("ProductVersion", "6.0.6")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
|
||||||
@@ -333,6 +333,39 @@ namespace NadekoBot.Migrations.Mysql
|
|||||||
b.ToTable("patrons", (string)null);
|
b.ToTable("patrons", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Db.Models.StreamOnlineMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<ulong>("ChannelId")
|
||||||
|
.HasColumnType("bigint unsigned")
|
||||||
|
.HasColumnName("channelid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateAdded")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("dateadded");
|
||||||
|
|
||||||
|
b.Property<ulong>("MessageId")
|
||||||
|
.HasColumnType("bigint unsigned")
|
||||||
|
.HasColumnName("messageid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_streamonlinemessages");
|
||||||
|
|
||||||
|
b.ToTable("streamonlinemessages", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
|
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
3656
src/NadekoBot/Migrations/Postgresql/20220623090729_stondel-db-cache.Designer.cs
generated
Normal file
3656
src/NadekoBot/Migrations/Postgresql/20220623090729_stondel-db-cache.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
public partial class stondeldbcache : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "streamonlinemessages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
messageid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
type = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
name = table.Column<string>(type: "text", nullable: true),
|
||||||
|
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_streamonlinemessages", x => x.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "streamonlinemessages");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -17,7 +17,7 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "6.0.5")
|
.HasAnnotation("ProductVersion", "6.0.6")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
@@ -343,6 +343,41 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
b.ToTable("patrons", (string)null);
|
b.ToTable("patrons", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Db.Models.StreamOnlineMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("ChannelId")
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("channelid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateAdded")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("dateadded");
|
||||||
|
|
||||||
|
b.Property<decimal>("MessageId")
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("messageid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("name");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("type");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_streamonlinemessages");
|
||||||
|
|
||||||
|
b.ToTable("streamonlinemessages", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
|
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
2823
src/NadekoBot/Migrations/Sqlite/20220623073903_stondel-db-cache.Designer.cs
generated
Normal file
2823
src/NadekoBot/Migrations/Sqlite/20220623073903_stondel-db-cache.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
public partial class stondeldbcache : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "StreamOnlineMessages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_StreamOnlineMessages", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "StreamOnlineMessages");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -15,7 +15,7 @@ namespace NadekoBot.Migrations
|
|||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "6.0.5");
|
modelBuilder.HasAnnotation("ProductVersion", "6.0.6");
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
|
||||||
{
|
{
|
||||||
@@ -262,6 +262,33 @@ namespace NadekoBot.Migrations
|
|||||||
b.ToTable("Patrons");
|
b.ToTable("Patrons");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Db.Models.StreamOnlineMessage", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<ulong>("ChannelId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateAdded")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<ulong>("MessageId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("StreamOnlineMessages");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
|
modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
@@ -500,14 +500,6 @@ public partial class Administration
|
|||||||
await ReplyConfirmLocalizedAsync(strs.message_sent);
|
await ReplyConfirmLocalizedAsync(strs.message_sent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[OwnerOnly]
|
|
||||||
public async partial Task ImagesReload()
|
|
||||||
{
|
|
||||||
await _service.ReloadImagesAsync();
|
|
||||||
await ReplyConfirmLocalizedAsync(strs.images_loading);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
public async partial Task StringsReload()
|
public async partial Task StringsReload()
|
||||||
|
@@ -20,7 +20,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
|||||||
|
|
||||||
private ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>> autoCommands = new();
|
private ConcurrentDictionary<ulong?, ConcurrentDictionary<int, Timer>> autoCommands = new();
|
||||||
|
|
||||||
private readonly IImageCache _imgs;
|
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly BotConfigService _bss;
|
private readonly BotConfigService _bss;
|
||||||
private readonly IPubSub _pubSub;
|
private readonly IPubSub _pubSub;
|
||||||
@@ -28,7 +27,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
|||||||
|
|
||||||
//keys
|
//keys
|
||||||
private readonly TypedKey<ActivityPubData> _activitySetKey;
|
private readonly TypedKey<ActivityPubData> _activitySetKey;
|
||||||
private readonly TypedKey<bool> _imagesReloadKey;
|
|
||||||
private readonly TypedKey<string> _guildLeaveKey;
|
private readonly TypedKey<string> _guildLeaveKey;
|
||||||
|
|
||||||
public SelfService(
|
public SelfService(
|
||||||
@@ -37,7 +35,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
|||||||
DbService db,
|
DbService db,
|
||||||
IBotStrings strings,
|
IBotStrings strings,
|
||||||
IBotCredentials creds,
|
IBotCredentials creds,
|
||||||
IDataCache cache,
|
|
||||||
IHttpClientFactory factory,
|
IHttpClientFactory factory,
|
||||||
BotConfigService bss,
|
BotConfigService bss,
|
||||||
IPubSub pubSub,
|
IPubSub pubSub,
|
||||||
@@ -48,20 +45,15 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
|||||||
_strings = strings;
|
_strings = strings;
|
||||||
_client = client;
|
_client = client;
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
_imgs = cache.LocalImages;
|
|
||||||
_httpFactory = factory;
|
_httpFactory = factory;
|
||||||
_bss = bss;
|
_bss = bss;
|
||||||
_pubSub = pubSub;
|
_pubSub = pubSub;
|
||||||
_eb = eb;
|
_eb = eb;
|
||||||
_activitySetKey = new("activity.set");
|
_activitySetKey = new("activity.set");
|
||||||
_imagesReloadKey = new("images.reload");
|
|
||||||
_guildLeaveKey = new("guild.leave");
|
_guildLeaveKey = new("guild.leave");
|
||||||
|
|
||||||
HandleStatusChanges();
|
HandleStatusChanges();
|
||||||
|
|
||||||
if (_client.ShardId == 0)
|
|
||||||
_pubSub.Sub(_imagesReloadKey, async _ => await _imgs.Reload());
|
|
||||||
|
|
||||||
_pubSub.Sub(_guildLeaveKey,
|
_pubSub.Sub(_guildLeaveKey,
|
||||||
async input =>
|
async input =>
|
||||||
{
|
{
|
||||||
@@ -325,9 +317,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
|||||||
uow.SaveChanges();
|
uow.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ReloadImagesAsync()
|
|
||||||
=> _pubSub.Pub(_imagesReloadKey, true);
|
|
||||||
|
|
||||||
public bool ForwardMessages()
|
public bool ForwardMessages()
|
||||||
{
|
{
|
||||||
var isForwarding = false;
|
var isForwarding = false;
|
||||||
|
@@ -19,8 +19,8 @@ public partial class Gambling
|
|||||||
private static readonly char[] _fateRolls = { '-', ' ', '+' };
|
private static readonly char[] _fateRolls = { '-', ' ', '+' };
|
||||||
private readonly IImageCache _images;
|
private readonly IImageCache _images;
|
||||||
|
|
||||||
public DiceRollCommands(IDataCache data)
|
public DiceRollCommands(ImageCache images)
|
||||||
=> _images = data.LocalImages;
|
=> _images = images;
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async partial Task Roll()
|
public async partial Task Roll()
|
||||||
@@ -31,10 +31,10 @@ public partial class Gambling
|
|||||||
var num1 = gen / 10;
|
var num1 = gen / 10;
|
||||||
var num2 = gen % 10;
|
var num2 = gen % 10;
|
||||||
|
|
||||||
using var img1 = GetDice(num1);
|
using var img1 = await GetDiceAsync(num1);
|
||||||
using var img2 = GetDice(num2);
|
using var img2 = await GetDiceAsync(num2);
|
||||||
using var img = new[] { img1, img2 }.Merge(out var format);
|
using var img = new[] { img1, img2 }.Merge(out var format);
|
||||||
await using var ms = img.ToStream(format);
|
await using var ms = await img.ToStreamAsync(format);
|
||||||
await ctx.Channel.SendFileAsync(ms,
|
await ctx.Channel.SendFileAsync(ms,
|
||||||
$"dice.{format.FileExtensions.First()}",
|
$"dice.{format.FileExtensions.First()}",
|
||||||
Format.Bold(ctx.User.ToString()) + " " + GetText(strs.dice_rolled(Format.Code(gen.ToString()))));
|
Format.Bold(ctx.User.ToString()) + " " + GetText(strs.dice_rolled(Format.Code(gen.ToString()))));
|
||||||
@@ -96,7 +96,7 @@ public partial class Gambling
|
|||||||
else
|
else
|
||||||
toInsert = dice.Count;
|
toInsert = dice.Count;
|
||||||
|
|
||||||
dice.Insert(toInsert, GetDice(randomNumber));
|
dice.Insert(toInsert, await GetDiceAsync(randomNumber));
|
||||||
values.Insert(toInsert, randomNumber);
|
values.Insert(toInsert, randomNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,20 +195,19 @@ public partial class Gambling
|
|||||||
await ReplyConfirmLocalizedAsync(strs.dice_rolled(Format.Bold(rolled.ToString())));
|
await ReplyConfirmLocalizedAsync(strs.dice_rolled(Format.Bold(rolled.ToString())));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Image<Rgba32> GetDice(int num)
|
private async Task<Image<Rgba32>> GetDiceAsync(int num)
|
||||||
{
|
{
|
||||||
if (num is < 0 or > 10)
|
if (num is < 0 or > 10)
|
||||||
throw new ArgumentOutOfRangeException(nameof(num));
|
throw new ArgumentOutOfRangeException(nameof(num));
|
||||||
|
|
||||||
if (num == 10)
|
if (num == 10)
|
||||||
{
|
{
|
||||||
var images = _images.Dice;
|
using var imgOne = Image.Load(await _images.GetDiceAsync(1));
|
||||||
using var imgOne = Image.Load(images[1]);
|
using var imgZero = Image.Load(await _images.GetDiceAsync(0));
|
||||||
using var imgZero = Image.Load(images[0]);
|
|
||||||
return new[] { imgOne, imgZero }.Merge();
|
return new[] { imgOne, imgZero }.Merge();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Image.Load(_images.Dice[num]);
|
return Image.Load(await _images.GetDiceAsync(num));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -14,8 +14,8 @@ public partial class Gambling
|
|||||||
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
|
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
|
||||||
private readonly IImageCache _images;
|
private readonly IImageCache _images;
|
||||||
|
|
||||||
public DrawCommands(IDataCache data)
|
public DrawCommands(IImageCache images)
|
||||||
=> _images = data.LocalImages;
|
=> _images = images;
|
||||||
|
|
||||||
private async Task<(Stream ImageStream, string ToSend)> InternalDraw(int num, ulong? guildId = null)
|
private async Task<(Stream ImageStream, string ToSend)> InternalDraw(int num, ulong? guildId = null)
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,8 @@ public partial class Gambling
|
|||||||
|
|
||||||
var currentCard = cards.Draw();
|
var currentCard = cards.Draw();
|
||||||
cardObjects.Add(currentCard);
|
cardObjects.Add(currentCard);
|
||||||
images.Add(Image.Load(_images.GetCard(currentCard.ToString().ToLowerInvariant().Replace(' ', '_'))));
|
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
|
||||||
|
images.Add(Image.Load(await File.ReadAllBytesAsync($"data/images/cards/{cardName}.png")));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var img = images.Merge();
|
using var img = images.Merge();
|
||||||
|
@@ -25,11 +25,17 @@ public partial class Gambling
|
|||||||
private static readonly NadekoRandom _rng = new();
|
private static readonly NadekoRandom _rng = new();
|
||||||
private readonly IImageCache _images;
|
private readonly IImageCache _images;
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
|
private readonly ImagesConfig _ic;
|
||||||
|
|
||||||
public FlipCoinCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gss)
|
public FlipCoinCommands(
|
||||||
|
IImageCache images,
|
||||||
|
ImagesConfig ic,
|
||||||
|
ICurrencyService cs,
|
||||||
|
GamblingConfigService gss)
|
||||||
: base(gss)
|
: base(gss)
|
||||||
{
|
{
|
||||||
_images = data.LocalImages;
|
_ic = ic;
|
||||||
|
_images = images;
|
||||||
_cs = cs;
|
_cs = cs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +53,8 @@ public partial class Gambling
|
|||||||
var imgs = new Image<Rgba32>[count];
|
var imgs = new Image<Rgba32>[count];
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var headsArr = _images.Heads[_rng.Next(0, _images.Heads.Count)];
|
var headsArr = await _images.GetHeadsImageAsync();
|
||||||
var tailsArr = _images.Tails[_rng.Next(0, _images.Tails.Count)];
|
var tailsArr = await _images.GetTailsImageAsync();
|
||||||
if (_rng.Next(0, 10) < 5)
|
if (_rng.Next(0, 10) < 5)
|
||||||
{
|
{
|
||||||
imgs[i] = Image.Load(headsArr);
|
imgs[i] = Image.Load(headsArr);
|
||||||
@@ -94,7 +100,7 @@ public partial class Gambling
|
|||||||
|
|
||||||
BetFlipGuess result;
|
BetFlipGuess result;
|
||||||
Uri imageToSend;
|
Uri imageToSend;
|
||||||
var coins = _images.ImageUrls.Coins;
|
var coins = _ic.Data.Coins;
|
||||||
if (_rng.Next(0, 1000) <= 499)
|
if (_rng.Next(0, 1000) <= 499)
|
||||||
{
|
{
|
||||||
imageToSend = coins.Heads[_rng.Next(0, coins.Heads.Length)];
|
imageToSend = coins.Heads[_rng.Next(0, coins.Heads.Length)];
|
||||||
|
@@ -38,7 +38,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
|
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly IDataCache _cache;
|
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly NumberFormatInfo _enUsCulture;
|
private readonly NumberFormatInfo _enUsCulture;
|
||||||
private readonly DownloadTracker _tracker;
|
private readonly DownloadTracker _tracker;
|
||||||
@@ -51,7 +50,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
public Gambling(
|
public Gambling(
|
||||||
DbService db,
|
DbService db,
|
||||||
ICurrencyService currency,
|
ICurrencyService currency,
|
||||||
IDataCache cache,
|
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
DownloadTracker tracker,
|
DownloadTracker tracker,
|
||||||
GamblingConfigService configService,
|
GamblingConfigService configService,
|
||||||
@@ -61,7 +59,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_cs = currency;
|
_cs = currency;
|
||||||
_cache = cache;
|
|
||||||
_client = client;
|
_client = client;
|
||||||
_bank = bank;
|
_bank = bank;
|
||||||
_ps = ps;
|
_ps = ps;
|
||||||
@@ -124,7 +121,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_cache.AddTimelyClaim(ctx.User.Id, period) is { } rem)
|
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } rem)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative);
|
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative);
|
||||||
@@ -145,7 +142,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
public async partial Task TimelyReset()
|
public async partial Task TimelyReset()
|
||||||
{
|
{
|
||||||
_cache.RemoveAllTimelyClaims();
|
await _service.RemoveAllTimelyClaimsAsync();
|
||||||
await ReplyConfirmLocalizedAsync(strs.timely_reset);
|
await ReplyConfirmLocalizedAsync(strs.timely_reset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,16 +1,13 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
using NadekoBot.Db;
|
using NadekoBot.Db;
|
||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
using NadekoBot.Migrations;
|
|
||||||
using NadekoBot.Modules.Gambling.Common;
|
using NadekoBot.Modules.Gambling.Common;
|
||||||
using NadekoBot.Modules.Gambling.Common.Connect4;
|
using NadekoBot.Modules.Gambling.Common.Connect4;
|
||||||
using NadekoBot.Modules.Gambling.Common.Slot;
|
using NadekoBot.Modules.Gambling.Common.Slot;
|
||||||
using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
|
using NadekoBot.Modules.Gambling.Common.WheelOfFortune;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Gambling.Services;
|
namespace NadekoBot.Modules.Gambling.Services;
|
||||||
|
|
||||||
@@ -22,7 +19,7 @@ public class GamblingService : INService, IReadyExecutor
|
|||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly Bot _bot;
|
private readonly Bot _bot;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IDataCache _cache;
|
private readonly IBotCache _cache;
|
||||||
private readonly GamblingConfigService _gss;
|
private readonly GamblingConfigService _gss;
|
||||||
|
|
||||||
public GamblingService(
|
public GamblingService(
|
||||||
@@ -30,7 +27,7 @@ public class GamblingService : INService, IReadyExecutor
|
|||||||
Bot bot,
|
Bot bot,
|
||||||
ICurrencyService cs,
|
ICurrencyService cs,
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
IDataCache cache,
|
IBotCache cache,
|
||||||
GamblingConfigService gss)
|
GamblingConfigService gss)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
@@ -73,6 +70,7 @@ public class GamblingService : INService, IReadyExecutor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay");
|
||||||
private async Task CurrencyDecayLoopAsync()
|
private async Task CurrencyDecayLoopAsync()
|
||||||
{
|
{
|
||||||
if (_bot.Client.ShardId != 0)
|
if (_bot.Client.ShardId != 0)
|
||||||
@@ -88,11 +86,16 @@ public class GamblingService : INService, IReadyExecutor
|
|||||||
if (config.Decay.Percent is <= 0 or > 1 || maxDecay < 0)
|
if (config.Decay.Percent is <= 0 or > 1 || maxDecay < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
var lastCurrencyDecay = _cache.GetLastCurrencyDecay();
|
var result = await _cache.GetAsync(_curDecayKey);
|
||||||
|
|
||||||
if (DateTime.UtcNow - lastCurrencyDecay < TimeSpan.FromHours(config.Decay.HourInterval))
|
if (result.TryPickT0(out var bin, out _)
|
||||||
|
&& (now - DateTime.FromBinary(bin) < TimeSpan.FromHours(config.Decay.HourInterval)))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Log.Information(@"Decaying users' currency - decay: {ConfigDecayPercent}%
|
Log.Information(@"Decaying users' currency - decay: {ConfigDecayPercent}%
|
||||||
| max: {MaxDecay}
|
| max: {MaxDecay}
|
||||||
@@ -115,8 +118,9 @@ public class GamblingService : INService, IReadyExecutor
|
|||||||
: old.CurrencyAmount - maxDecay
|
: old.CurrencyAmount - maxDecay
|
||||||
});
|
});
|
||||||
|
|
||||||
_cache.SetLastCurrencyDecay();
|
|
||||||
await uow.SaveChangesAsync();
|
await uow.SaveChangesAsync();
|
||||||
|
|
||||||
|
await _cache.AddAsync(_curDecayKey, now.ToBinary());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -161,60 +165,100 @@ public class GamblingService : INService, IReadyExecutor
|
|||||||
return toReturn;
|
return toReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly TypedKey<EconomyResult> _ecoKey = new("nadeko:economy");
|
||||||
|
|
||||||
public async Task<EconomyResult> GetEconomyAsync()
|
public async Task<EconomyResult> GetEconomyAsync()
|
||||||
{
|
{
|
||||||
if (_cache.TryGetEconomy(out var data))
|
var data = await _cache.GetOrAddAsync(_ecoKey,
|
||||||
{
|
async () =>
|
||||||
try
|
|
||||||
{
|
{
|
||||||
return JsonConvert.DeserializeObject<EconomyResult>(data);
|
await using var uow = _db.GetDbContext();
|
||||||
}
|
var cash = uow.DiscordUser.GetTotalCurrency();
|
||||||
catch { }
|
var onePercent = uow.DiscordUser.GetTopOnePercentCurrency(_client.CurrentUser.Id);
|
||||||
}
|
decimal planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount);
|
||||||
|
var waifus = uow.WaifuInfo.GetTotalValue();
|
||||||
|
var bot = uow.DiscordUser.GetUserCurrency(_client.CurrentUser.Id);
|
||||||
|
decimal bank = await uow.GetTable<BankUser>()
|
||||||
|
.SumAsyncLinqToDB(x => x.Balance);
|
||||||
|
|
||||||
decimal cash;
|
var result = new EconomyResult
|
||||||
decimal onePercent;
|
{
|
||||||
decimal planted;
|
Cash = cash,
|
||||||
decimal waifus;
|
Planted = planted,
|
||||||
decimal bank;
|
Bot = bot,
|
||||||
long bot;
|
Waifus = waifus,
|
||||||
|
OnePercent = onePercent,
|
||||||
|
Bank = bank
|
||||||
|
};
|
||||||
|
|
||||||
using (var uow = _db.GetDbContext())
|
return result;
|
||||||
{
|
},
|
||||||
cash = uow.DiscordUser.GetTotalCurrency();
|
TimeSpan.FromMinutes(3));
|
||||||
onePercent = uow.DiscordUser.GetTopOnePercentCurrency(_client.CurrentUser.Id);
|
|
||||||
planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount);
|
|
||||||
waifus = uow.WaifuInfo.GetTotalValue();
|
|
||||||
bot = uow.DiscordUser.GetUserCurrency(_client.CurrentUser.Id);
|
|
||||||
bank = await uow.GetTable<BankUser>()
|
|
||||||
.SumAsyncLinqToDB(x => x.Balance);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = new EconomyResult
|
return data;
|
||||||
{
|
|
||||||
Cash = cash,
|
|
||||||
Planted = planted,
|
|
||||||
Bot = bot,
|
|
||||||
Waifus = waifus,
|
|
||||||
OnePercent = onePercent,
|
|
||||||
Bank = bank
|
|
||||||
};
|
|
||||||
|
|
||||||
_cache.SetEconomy(JsonConvert.SerializeObject(result));
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<WheelOfFortuneGame.Result> WheelOfFortuneSpinAsync(ulong userId, long bet)
|
public Task<WheelOfFortuneGame.Result> WheelOfFortuneSpinAsync(ulong userId, long bet)
|
||||||
=> new WheelOfFortuneGame(userId, bet, _gss.Data, _cs).SpinAsync();
|
=> new WheelOfFortuneGame(userId, bet, _gss.Data, _cs).SpinAsync();
|
||||||
|
|
||||||
|
|
||||||
public struct EconomyResult
|
private static readonly SemaphoreSlim _timelyLock = new (1, 1);
|
||||||
|
|
||||||
|
private static TypedKey<Dictionary<ulong, long>> _timelyKey
|
||||||
|
= new("timely:claims");
|
||||||
|
public async Task<TimeSpan?> ClaimTimelyAsync(ulong userId, int period)
|
||||||
{
|
{
|
||||||
public decimal Cash { get; set; }
|
if (period == 0)
|
||||||
public decimal Planted { get; set; }
|
return null;
|
||||||
public decimal Waifus { get; set; }
|
|
||||||
public decimal OnePercent { get; set; }
|
await _timelyLock.WaitAsync();
|
||||||
public decimal Bank { get; set; }
|
try
|
||||||
public long Bot { get; set; }
|
{
|
||||||
|
// get the dictionary from the cache or get a new one
|
||||||
|
var dict = (await _cache.GetOrAddAsync(_timelyKey,
|
||||||
|
() => Task.FromResult(new Dictionary<ulong, long>())))!;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var nowB = now.ToBinary();
|
||||||
|
|
||||||
|
// try to get users last claim
|
||||||
|
if (!dict.TryGetValue(userId, out var lastB))
|
||||||
|
lastB = dict[userId] = now.ToBinary();
|
||||||
|
|
||||||
|
var diff = now - DateTime.FromBinary(lastB);
|
||||||
|
|
||||||
|
// if its now, or too long ago => success
|
||||||
|
if (lastB == nowB || diff > period.Hours())
|
||||||
|
{
|
||||||
|
// update the cache
|
||||||
|
dict[userId] = nowB;
|
||||||
|
await _cache.AddAsync(_timelyKey, dict);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// otherwise return the remaining time
|
||||||
|
return period.Hours() - diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await _timelyLock.WaitAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAllTimelyClaimsAsync()
|
||||||
|
=> await _cache.RemoveAsync(_timelyKey);
|
||||||
|
|
||||||
|
|
||||||
|
public readonly struct EconomyResult
|
||||||
|
{
|
||||||
|
public decimal Cash { get; init; }
|
||||||
|
public decimal Planted { get; init; }
|
||||||
|
public decimal Waifus { get; init; }
|
||||||
|
public decimal OnePercent { get; init; }
|
||||||
|
public decimal Bank { get; init; }
|
||||||
|
public long Bot { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -34,7 +34,7 @@ public class PlantPickService : INService, IExecNoCommand
|
|||||||
DbService db,
|
DbService db,
|
||||||
CommandHandler cmd,
|
CommandHandler cmd,
|
||||||
IBotStrings strings,
|
IBotStrings strings,
|
||||||
IDataCache cache,
|
IImageCache images,
|
||||||
FontProvider fonts,
|
FontProvider fonts,
|
||||||
ICurrencyService cs,
|
ICurrencyService cs,
|
||||||
CommandHandler cmdHandler,
|
CommandHandler cmdHandler,
|
||||||
@@ -43,7 +43,7 @@ public class PlantPickService : INService, IExecNoCommand
|
|||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_strings = strings;
|
_strings = strings;
|
||||||
_images = cache.LocalImages;
|
_images = images;
|
||||||
_fonts = fonts;
|
_fonts = fonts;
|
||||||
_cs = cs;
|
_cs = cs;
|
||||||
_cmdHandler = cmdHandler;
|
_cmdHandler = cmdHandler;
|
||||||
@@ -110,30 +110,21 @@ public class PlantPickService : INService, IExecNoCommand
|
|||||||
/// <param name="pass">Optional password to add to top left corner.</param>
|
/// <param name="pass">Optional password to add to top left corner.</param>
|
||||||
/// <param name="extension">Extension of the file, defaults to png</param>
|
/// <param name="extension">Extension of the file, defaults to png</param>
|
||||||
/// <returns>Stream of the currency image</returns>
|
/// <returns>Stream of the currency image</returns>
|
||||||
public Stream GetRandomCurrencyImage(string pass, out string extension)
|
public async Task<(Stream, string)> GetRandomCurrencyImageAsync(string pass)
|
||||||
{
|
{
|
||||||
// get a random currency image bytes
|
var curImg = await _images.GetCurrencyImageAsync();
|
||||||
var rng = new NadekoRandom();
|
|
||||||
var curImg = _images.Currency[rng.Next(0, _images.Currency.Count)];
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(pass))
|
if (string.IsNullOrWhiteSpace(pass))
|
||||||
{
|
{
|
||||||
// determine the extension
|
// determine the extension
|
||||||
using (_ = Image.Load(curImg, out var format))
|
using var load = _ = Image.Load(curImg, out var format);
|
||||||
{
|
|
||||||
extension = format.FileExtensions.FirstOrDefault() ?? "png";
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the image
|
// return the image
|
||||||
return curImg.ToStream();
|
return (curImg.ToStream(), format.FileExtensions.FirstOrDefault() ?? "png");
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the image stream and extension
|
// get the image stream and extension
|
||||||
var (s, ext) = AddPassword(curImg, pass);
|
return AddPassword(curImg, pass);
|
||||||
// set the out extension parameter to the extension we've got
|
|
||||||
extension = ext;
|
|
||||||
// return the image
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -214,10 +205,10 @@ public class PlantPickService : INService, IExecNoCommand
|
|||||||
var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null;
|
var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null;
|
||||||
|
|
||||||
IUserMessage sent;
|
IUserMessage sent;
|
||||||
await using (var stream = GetRandomCurrencyImage(pw, out var ext))
|
var (stream, ext) = await GetRandomCurrencyImageAsync(pw);
|
||||||
{
|
|
||||||
|
await using (stream)
|
||||||
sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend);
|
sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend);
|
||||||
}
|
|
||||||
|
|
||||||
await AddPlantToDatabase(channel.GuildId,
|
await AddPlantToDatabase(channel.GuildId,
|
||||||
channel.Id,
|
channel.Id,
|
||||||
@@ -278,7 +269,7 @@ public class PlantPickService : INService, IExecNoCommand
|
|||||||
if (amount > 0)
|
if (amount > 0)
|
||||||
// give the picked currency to the user
|
// give the picked currency to the user
|
||||||
await _cs.AddAsync(uid, amount, new("currency", "collect"));
|
await _cs.AddAsync(uid, amount, new("currency", "collect"));
|
||||||
uow.SaveChanges();
|
await uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -316,11 +307,14 @@ public class PlantPickService : INService, IExecNoCommand
|
|||||||
msgToSend += " " + GetText(gid, strs.pick_sn(prefix));
|
msgToSend += " " + GetText(gid, strs.pick_sn(prefix));
|
||||||
|
|
||||||
//get the image
|
//get the image
|
||||||
await using var stream = GetRandomCurrencyImage(pass, out var ext);
|
var (stream, ext) = await GetRandomCurrencyImageAsync(pass);
|
||||||
// send it
|
// send it
|
||||||
var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend);
|
await using (stream)
|
||||||
// return sent message's id (in order to be able to delete it when it's picked)
|
{
|
||||||
return msg.Id;
|
var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend);
|
||||||
|
// return sent message's id (in order to be able to delete it when it's picked)
|
||||||
|
return msg.Id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@@ -32,13 +32,13 @@ public partial class Gambling
|
|||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
|
|
||||||
public SlotCommands(
|
public SlotCommands(
|
||||||
IDataCache data,
|
ImageCache images,
|
||||||
FontProvider fonts,
|
FontProvider fonts,
|
||||||
DbService db,
|
DbService db,
|
||||||
GamblingConfigService gamb)
|
GamblingConfigService gamb)
|
||||||
: base(gamb)
|
: base(gamb)
|
||||||
{
|
{
|
||||||
_images = data.LocalImages;
|
_images = images;
|
||||||
_fonts = fonts;
|
_fonts = fonts;
|
||||||
_db = db;
|
_db = db;
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,8 @@ public partial class Gambling
|
|||||||
?? 0;
|
?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var bgImage = Image.Load<Rgba32>(_images.SlotBackground, out _))
|
var slotBg = await _images.GetSlotBgAsync();
|
||||||
|
using (var bgImage = Image.Load<Rgba32>(slotBg, out _))
|
||||||
{
|
{
|
||||||
var numbers = new int[3];
|
var numbers = new int[3];
|
||||||
result.Rolls.CopyTo(numbers, 0);
|
result.Rolls.CopyTo(numbers, 0);
|
||||||
@@ -184,7 +185,7 @@ public partial class Gambling
|
|||||||
|
|
||||||
for (var i = 0; i < 3; i++)
|
for (var i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
using var img = Image.Load(_images.SlotEmojis[numbers[i]]);
|
using var img = Image.Load(await _images.GetSlotEmojiAsync(numbers[i]));
|
||||||
bgImage.Mutate(x => x.DrawImage(img, new Point(148 + (105 * i), 217), 1f));
|
bgImage.Mutate(x => x.DrawImage(img, new Point(148 + (105 * i), 217), 1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +202,7 @@ public partial class Gambling
|
|||||||
msg = GetText(strs.slot_jackpot(30));
|
msg = GetText(strs.slot_jackpot(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
await using (var imgStream = bgImage.ToStream())
|
await using (var imgStream = await bgImage.ToStreamAsync())
|
||||||
{
|
{
|
||||||
await ctx.Channel.SendFileAsync(imgStream,
|
await ctx.Channel.SendFileAsync(imgStream,
|
||||||
"result.png",
|
"result.png",
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
using NadekoBot.Db;
|
using NadekoBot.Db;
|
||||||
@@ -14,7 +15,7 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
{
|
{
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly IDataCache _cache;
|
private readonly IBotCache _cache;
|
||||||
private readonly GamblingConfigService _gss;
|
private readonly GamblingConfigService _gss;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCredentials _creds;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
@@ -22,7 +23,7 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
public WaifuService(
|
public WaifuService(
|
||||||
DbService db,
|
DbService db,
|
||||||
ICurrencyService cs,
|
ICurrencyService cs,
|
||||||
IDataCache cache,
|
IBotCache cache,
|
||||||
GamblingConfigService gss,
|
GamblingConfigService gss,
|
||||||
IBotCredentials creds,
|
IBotCredentials creds,
|
||||||
DiscordSocketClient client)
|
DiscordSocketClient client)
|
||||||
@@ -236,8 +237,13 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
var newAff = target is null ? null : uow.GetOrCreateUser(target);
|
var newAff = target is null ? null : uow.GetOrCreateUser(target);
|
||||||
if (w?.Affinity?.UserId == target?.Id)
|
if (w?.Affinity?.UserId == target?.Id)
|
||||||
{
|
{
|
||||||
|
return (null, false, null);
|
||||||
}
|
}
|
||||||
else if (!_cache.TryAddAffinityCooldown(user.Id, out remaining))
|
|
||||||
|
remaining = await _cache.GetRatelimitAsync(GetAffinityKey(user.Id),
|
||||||
|
30.Minutes());
|
||||||
|
|
||||||
|
if (remaining is not null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
else if (w is null)
|
else if (w is null)
|
||||||
@@ -294,6 +300,12 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
return uow.WaifuInfo.GetWaifuUserId(ownerId, name);
|
return uow.WaifuInfo.GetWaifuUserId(ownerId, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TypedKey<long> GetDivorceKey(ulong userId)
|
||||||
|
=> new($"waifu:divorce_cd:{userId}");
|
||||||
|
|
||||||
|
private static TypedKey<long> GetAffinityKey(ulong userId)
|
||||||
|
=> new($"waifu:affinity:{userId}");
|
||||||
|
|
||||||
public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
|
public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
|
||||||
{
|
{
|
||||||
DivorceResult result;
|
DivorceResult result;
|
||||||
@@ -305,10 +317,15 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
w = uow.WaifuInfo.ByWaifuUserId(targetId);
|
w = uow.WaifuInfo.ByWaifuUserId(targetId);
|
||||||
if (w?.Claimer is null || w.Claimer.UserId != user.Id)
|
if (w?.Claimer is null || w.Claimer.UserId != user.Id)
|
||||||
result = DivorceResult.NotYourWife;
|
result = DivorceResult.NotYourWife;
|
||||||
else if (!_cache.TryAddDivorceCooldown(user.Id, out remaining))
|
|
||||||
result = DivorceResult.Cooldown;
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
remaining = await _cache.GetRatelimitAsync(GetDivorceKey(user.Id), 6.Hours());
|
||||||
|
if (remaining is TimeSpan rem)
|
||||||
|
{
|
||||||
|
result = DivorceResult.Cooldown;
|
||||||
|
return (w, result, amount, rem);
|
||||||
|
}
|
||||||
|
|
||||||
amount = w.Price / 2;
|
amount = w.Price / 2;
|
||||||
|
|
||||||
if (w.Affinity?.UserId == user.Id)
|
if (w.Affinity?.UserId == user.Id)
|
||||||
@@ -486,13 +503,13 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly TypedKey<long> _waifuDecayKey = $"waifu:last_decay";
|
||||||
public async Task OnReadyAsync()
|
public async Task OnReadyAsync()
|
||||||
{
|
{
|
||||||
// only decay waifu values from shard 0
|
// only decay waifu values from shard 0
|
||||||
if (_client.ShardId != 0)
|
if (_client.ShardId != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var redisKey = $"{_creds.RedisKey()}_last_waifu_decay";
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -504,28 +521,31 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
if (multi is < 0f or > 1f || decayInterval < 0)
|
if (multi is < 0f or > 1f || decayInterval < 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var val = await _cache.Redis.GetDatabase().StringGetAsync(redisKey);
|
var now = DateTime.UtcNow;
|
||||||
if (val != default)
|
var nowB = now.ToBinary();
|
||||||
|
|
||||||
|
var result = await _cache.GetAsync(_waifuDecayKey);
|
||||||
|
|
||||||
|
if (result.TryGetValue(out var val))
|
||||||
{
|
{
|
||||||
var lastDecay = DateTime.FromBinary((long)val);
|
var lastDecay = DateTime.FromBinary(val);
|
||||||
var toWait = decayInterval.Hours() - (DateTime.UtcNow - lastDecay);
|
var toWait = decayInterval.Hours() - (DateTime.UtcNow - lastDecay);
|
||||||
|
|
||||||
if (toWait > 0.Hours())
|
if (toWait > 0.Hours())
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cache.Redis.GetDatabase().StringSetAsync(redisKey, DateTime.UtcNow.ToBinary());
|
await _cache.AddAsync(_waifuDecayKey, nowB);
|
||||||
|
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
await uow.WaifuInfo
|
await uow.GetTable<WaifuInfo>()
|
||||||
.Where(x => x.Price > minPrice && x.ClaimerId == null)
|
.Where(x => x.Price > minPrice && x.ClaimerId == null)
|
||||||
.UpdateAsync(old => new()
|
.UpdateAsync(old => new()
|
||||||
{
|
{
|
||||||
Price = (long)(old.Price * multi)
|
Price = (long)(old.Price * multi)
|
||||||
});
|
});
|
||||||
|
|
||||||
await uow.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@@ -14,9 +14,9 @@ public partial class Games : NadekoModule<GamesService>
|
|||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly Random _rng = new();
|
private readonly Random _rng = new();
|
||||||
|
|
||||||
public Games(IDataCache data, IHttpClientFactory factory)
|
public Games(IImageCache images, IHttpClientFactory factory)
|
||||||
{
|
{
|
||||||
_images = data.LocalImages;
|
_images = images;
|
||||||
_httpFactory = factory;
|
_httpFactory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,11 +28,12 @@ public class GirlRating
|
|||||||
Roll = roll;
|
Roll = roll;
|
||||||
Advice = advice; // convenient to have it here, even though atm there are only few different ones.
|
Advice = advice; // convenient to have it here, even though atm there are only few different ones.
|
||||||
|
|
||||||
Stream = new(() =>
|
Stream = new(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var img = Image.Load(_images.RategirlMatrix);
|
var bgBytes = await _images.GetRategirlBgAsync();
|
||||||
|
using var img = Image.Load(bgBytes);
|
||||||
const int minx = 35;
|
const int minx = 35;
|
||||||
const int miny = 385;
|
const int miny = 385;
|
||||||
const int length = 345;
|
const int length = 345;
|
||||||
@@ -40,7 +41,8 @@ public class GirlRating
|
|||||||
var pointx = (int)(minx + (length * (Hot / 10)));
|
var pointx = (int)(minx + (length * (Hot / 10)));
|
||||||
var pointy = (int)(miny - (length * ((Crazy - 4) / 6)));
|
var pointy = (int)(miny - (length * ((Crazy - 4) / 6)));
|
||||||
|
|
||||||
using (var pointImg = Image.Load(_images.RategirlDot))
|
var dotBytes = await _images.GetRategirlDotAsync();
|
||||||
|
using (var pointImg = Image.Load(dotBytes))
|
||||||
{
|
{
|
||||||
img.Mutate(x => x.DrawImage(pointImg, new(pointx - 10, pointy - 10), new GraphicsOptions()));
|
img.Mutate(x => x.DrawImage(pointImg, new(pointx - 10, pointy - 10), new GraphicsOptions()));
|
||||||
}
|
}
|
||||||
|
@@ -9,14 +9,14 @@ public partial class Games
|
|||||||
[Group]
|
[Group]
|
||||||
public partial class TriviaCommands : NadekoModule<GamesService>
|
public partial class TriviaCommands : NadekoModule<GamesService>
|
||||||
{
|
{
|
||||||
private readonly IDataCache _cache;
|
private readonly ILocalDataCache _cache;
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly GamesConfigService _gamesConfig;
|
private readonly GamesConfigService _gamesConfig;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
public TriviaCommands(
|
public TriviaCommands(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
IDataCache cache,
|
ILocalDataCache cache,
|
||||||
ICurrencyService cs,
|
ICurrencyService cs,
|
||||||
GamesConfigService gamesConfig)
|
GamesConfigService gamesConfig)
|
||||||
{
|
{
|
||||||
|
@@ -17,7 +17,7 @@ public class TriviaGame
|
|||||||
public bool GameActive { get; private set; }
|
public bool GameActive { get; private set; }
|
||||||
public bool ShouldStopGame { get; private set; }
|
public bool ShouldStopGame { get; private set; }
|
||||||
private readonly SemaphoreSlim _guessLock = new(1, 1);
|
private readonly SemaphoreSlim _guessLock = new(1, 1);
|
||||||
private readonly IDataCache _cache;
|
private readonly ILocalDataCache _cache;
|
||||||
private readonly IBotStrings _strings;
|
private readonly IBotStrings _strings;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly GamesConfig _config;
|
private readonly GamesConfig _config;
|
||||||
@@ -35,7 +35,7 @@ public class TriviaGame
|
|||||||
IBotStrings strings,
|
IBotStrings strings,
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
GamesConfig config,
|
GamesConfig config,
|
||||||
IDataCache cache,
|
ILocalDataCache cache,
|
||||||
ICurrencyService cs,
|
ICurrencyService cs,
|
||||||
IGuild guild,
|
IGuild guild,
|
||||||
ITextChannel channel,
|
ITextChannel channel,
|
||||||
@@ -70,7 +70,7 @@ public class TriviaGame
|
|||||||
showHowToQuit = !showHowToQuit;
|
showHowToQuit = !showHowToQuit;
|
||||||
|
|
||||||
// load question
|
// load question
|
||||||
CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.IsPokemon);
|
CurrentQuestion = await _questionPool.GetRandomQuestionAsync(OldQuestions, _options.IsPokemon);
|
||||||
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer)
|
if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer)
|
||||||
|| string.IsNullOrWhiteSpace(CurrentQuestion.Question))
|
|| string.IsNullOrWhiteSpace(CurrentQuestion.Question))
|
||||||
{
|
{
|
||||||
|
@@ -4,6 +4,15 @@ using System.Text.RegularExpressions;
|
|||||||
// THANKS @ShoMinamimoto for suggestions and coding help
|
// THANKS @ShoMinamimoto for suggestions and coding help
|
||||||
namespace NadekoBot.Modules.Games.Common.Trivia;
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
public class TriviaQuestion
|
public class TriviaQuestion
|
||||||
{
|
{
|
||||||
public const int MAX_STRING_LENGTH = 22;
|
public const int MAX_STRING_LENGTH = 22;
|
||||||
@@ -17,29 +26,30 @@ public class TriviaQuestion
|
|||||||
new(22, 3)
|
new(22, 3)
|
||||||
};
|
};
|
||||||
|
|
||||||
public string Category { get; set; }
|
public string Category
|
||||||
public string Question { get; set; }
|
=> _qModel.Category;
|
||||||
public string ImageUrl { get; set; }
|
|
||||||
public string AnswerImageUrl { get; set; }
|
public string Question
|
||||||
public string Answer { get; set; }
|
=> _qModel.Question;
|
||||||
|
|
||||||
|
public string ImageUrl
|
||||||
|
=> _qModel.ImageUrl;
|
||||||
|
|
||||||
|
public string AnswerImageUrl
|
||||||
|
=> _qModel.AnswerImageUrl ?? ImageUrl;
|
||||||
|
|
||||||
|
public string Answer
|
||||||
|
=> _qModel.Answer;
|
||||||
|
|
||||||
public string CleanAnswer
|
public string CleanAnswer
|
||||||
=> cleanAnswer ?? (cleanAnswer = Clean(Answer));
|
=> cleanAnswer ?? (cleanAnswer = Clean(Answer));
|
||||||
|
|
||||||
private string cleanAnswer;
|
private string cleanAnswer;
|
||||||
|
private readonly TriviaQuestionModel _qModel;
|
||||||
|
|
||||||
public TriviaQuestion(
|
public TriviaQuestion(TriviaQuestionModel qModel)
|
||||||
string q,
|
|
||||||
string a,
|
|
||||||
string c,
|
|
||||||
string img = null,
|
|
||||||
string answerImage = null)
|
|
||||||
{
|
{
|
||||||
Question = q;
|
_qModel = qModel;
|
||||||
Answer = a;
|
|
||||||
Category = c;
|
|
||||||
ImageUrl = img;
|
|
||||||
AnswerImageUrl = answerImage ?? img;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetHint()
|
public string GetHint()
|
||||||
|
@@ -1,45 +1,48 @@
|
|||||||
#nullable disable
|
|
||||||
namespace NadekoBot.Modules.Games.Common.Trivia;
|
namespace NadekoBot.Modules.Games.Common.Trivia;
|
||||||
|
|
||||||
public class TriviaQuestionPool
|
public class TriviaQuestionPool
|
||||||
{
|
{
|
||||||
private TriviaQuestion[] Pool
|
private readonly ILocalDataCache _cache;
|
||||||
=> _cache.LocalData.TriviaQuestions;
|
|
||||||
|
|
||||||
private IReadOnlyDictionary<int, string> Map
|
|
||||||
=> _cache.LocalData.PokemonMap;
|
|
||||||
|
|
||||||
private readonly IDataCache _cache;
|
|
||||||
private readonly int _maxPokemonId;
|
private readonly int _maxPokemonId;
|
||||||
|
|
||||||
private readonly NadekoRandom _rng = new();
|
private readonly NadekoRandom _rng = new();
|
||||||
|
|
||||||
public TriviaQuestionPool(IDataCache cache)
|
public TriviaQuestionPool(ILocalDataCache cache)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_maxPokemonId = 721; //xd
|
_maxPokemonId = 721; //xd
|
||||||
}
|
}
|
||||||
|
|
||||||
public TriviaQuestion GetRandomQuestion(HashSet<TriviaQuestion> exclude, bool isPokemon)
|
public async Task<TriviaQuestion?> GetRandomQuestionAsync(HashSet<TriviaQuestion> exclude, bool isPokemon)
|
||||||
{
|
{
|
||||||
if (Pool.Length == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (isPokemon)
|
if (isPokemon)
|
||||||
{
|
{
|
||||||
|
var pokes = await _cache.GetPokemonMapAsync();
|
||||||
|
|
||||||
|
if (pokes is null or { Length: 0 })
|
||||||
|
return default;
|
||||||
|
|
||||||
var num = _rng.Next(1, _maxPokemonId + 1);
|
var num = _rng.Next(1, _maxPokemonId + 1);
|
||||||
return new("Who's That Pokémon?",
|
return new(new()
|
||||||
Map[num].ToTitleCase(),
|
{
|
||||||
"Pokemon",
|
Question = "Who's That Pokémon?",
|
||||||
$@"https://nadeko.bot/images/pokemon/shadows/{num}.png",
|
Answer = pokes[num].Name.ToTitleCase(),
|
||||||
$@"https://nadeko.bot/images/pokemon/real/{num}.png");
|
Category = "Pokemon",
|
||||||
|
ImageUrl = $@"https://nadeko.bot/images/pokemon/shadows/{num}.png",
|
||||||
|
AnswerImageUrl = $@"https://nadeko.bot/images/pokemon/real/{num}.png"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
TriviaQuestion randomQuestion;
|
TriviaQuestion randomQuestion;
|
||||||
while (exclude.Contains(randomQuestion = Pool[_rng.Next(0, Pool.Length)]))
|
var pool = await _cache.GetTriviaQuestionsAsync();
|
||||||
|
|
||||||
|
if(pool is null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
while (exclude.Contains(randomQuestion = new(pool[_rng.Next(0, pool.Length)])))
|
||||||
{
|
{
|
||||||
// if too many questions are excluded, clear the exclusion list and start over
|
// if too many questions are excluded, clear the exclusion list and start over
|
||||||
if (exclude.Count > Pool.Length / 10 * 9)
|
if (exclude.Count > pool.Length / 10 * 9)
|
||||||
{
|
{
|
||||||
exclude.Clear();
|
exclude.Clear();
|
||||||
break;
|
break;
|
||||||
|
@@ -1,209 +0,0 @@
|
|||||||
using StackExchange.Redis;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Music;
|
|
||||||
|
|
||||||
public sealed class RedisTrackCacher : ITrackCacher
|
|
||||||
{
|
|
||||||
private readonly ConnectionMultiplexer _multiplexer;
|
|
||||||
|
|
||||||
public RedisTrackCacher(ConnectionMultiplexer multiplexer)
|
|
||||||
=> _multiplexer = multiplexer;
|
|
||||||
|
|
||||||
public async Task<string?> GetOrCreateStreamLink(
|
|
||||||
string id,
|
|
||||||
MusicPlatform platform,
|
|
||||||
Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory)
|
|
||||||
{
|
|
||||||
var trackStreamKey = CreateStreamKey(id, platform);
|
|
||||||
|
|
||||||
var value = await GetStreamFromCacheInternalAsync(trackStreamKey);
|
|
||||||
|
|
||||||
// if there is no cached value
|
|
||||||
if (value == default)
|
|
||||||
{
|
|
||||||
// otherwise retrieve and cache a new value, and run this method again
|
|
||||||
var success = await CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory);
|
|
||||||
if (!success)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return await GetOrCreateStreamLink(id, platform, streamUrlFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache new one for future use
|
|
||||||
_ = Task.Run(() => CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory));
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static string CreateStreamKey(string id, MusicPlatform platform)
|
|
||||||
=> $"track:stream:{platform}:{id}";
|
|
||||||
|
|
||||||
private async Task<bool> CreateAndCacheStreamUrlAsync(
|
|
||||||
string trackStreamKey,
|
|
||||||
Func<Task<(string StreamUrl, TimeSpan Expiry)>> factory)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var data = await factory();
|
|
||||||
if (data == default)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
await CacheStreamUrlInternalAsync(trackStreamKey, data.StreamUrl, data.Expiry);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex, "Error resolving stream link for {TrackCacheKey}", trackStreamKey);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task CacheStreamUrlAsync(
|
|
||||||
string id,
|
|
||||||
MusicPlatform platform,
|
|
||||||
string url,
|
|
||||||
TimeSpan expiry)
|
|
||||||
=> CacheStreamUrlInternalAsync(CreateStreamKey(id, platform), url, expiry);
|
|
||||||
|
|
||||||
private async Task CacheStreamUrlInternalAsync(string trackStreamKey, string url, TimeSpan expiry)
|
|
||||||
{
|
|
||||||
// keys need to be expired after an hour
|
|
||||||
// to make sure client doesn't get an expired stream url
|
|
||||||
// to achieve this, track keys will be just pointers to real data
|
|
||||||
// but that data will expire
|
|
||||||
|
|
||||||
var db = _multiplexer.GetDatabase();
|
|
||||||
var dataKey = $"entry:{Guid.NewGuid()}:{trackStreamKey}";
|
|
||||||
await db.StringSetAsync(dataKey, url, expiry);
|
|
||||||
await db.ListRightPushAsync(trackStreamKey, dataKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string?> GetStreamFromCacheInternalAsync(string trackStreamKey)
|
|
||||||
{
|
|
||||||
// Job of the method which retrieves keys is to pop the elements
|
|
||||||
// from the list of cached trackurls until it finds a non-expired key
|
|
||||||
|
|
||||||
var db = _multiplexer.GetDatabase();
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
string? dataKey = await db.ListLeftPopAsync(trackStreamKey);
|
|
||||||
if (dataKey == default)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var streamUrl = await db.StringGetAsync(dataKey);
|
|
||||||
if (streamUrl == default)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
return streamUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static string CreateCachedDataKey(string id, MusicPlatform platform)
|
|
||||||
=> $"track:data:{platform}:{id}";
|
|
||||||
|
|
||||||
public Task CacheTrackDataAsync(ICachableTrackData data)
|
|
||||||
{
|
|
||||||
var db = _multiplexer.GetDatabase();
|
|
||||||
|
|
||||||
var trackDataKey = CreateCachedDataKey(data.Id, data.Platform);
|
|
||||||
var dataString = JsonSerializer.Serialize((object)data);
|
|
||||||
// cache for 1 day
|
|
||||||
return db.StringSetAsync(trackDataKey, dataString, TimeSpan.FromDays(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform)
|
|
||||||
{
|
|
||||||
var db = _multiplexer.GetDatabase();
|
|
||||||
|
|
||||||
var trackDataKey = CreateCachedDataKey(id, platform);
|
|
||||||
var data = await db.StringGetAsync(trackDataKey);
|
|
||||||
if (data == default)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<CachableTrackData>(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static string CreateCachedQueryDataKey(string query, MusicPlatform platform)
|
|
||||||
=> $"track:query_to_id:{platform}:{query}";
|
|
||||||
|
|
||||||
public async Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform)
|
|
||||||
{
|
|
||||||
query = Uri.EscapeDataString(query.Trim());
|
|
||||||
|
|
||||||
var db = _multiplexer.GetDatabase();
|
|
||||||
var queryDataKey = CreateCachedQueryDataKey(query, platform);
|
|
||||||
|
|
||||||
var trackId = await db.StringGetAsync(queryDataKey);
|
|
||||||
if (trackId == default)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return await GetCachedDataByIdAsync(trackId, platform);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data)
|
|
||||||
{
|
|
||||||
query = Uri.EscapeDataString(query.Trim());
|
|
||||||
|
|
||||||
// first cache the data
|
|
||||||
await CacheTrackDataAsync(data);
|
|
||||||
|
|
||||||
// then map the query to cached data's id
|
|
||||||
var db = _multiplexer.GetDatabase();
|
|
||||||
|
|
||||||
var queryDataKey = CreateCachedQueryDataKey(query, data.Platform);
|
|
||||||
await db.StringSetAsync(queryDataKey, data.Id, TimeSpan.FromDays(7));
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static string CreateCachedPlaylistKey(string playlistId, MusicPlatform platform)
|
|
||||||
=> $"playlist:{platform}:{playlistId}";
|
|
||||||
|
|
||||||
public async Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform)
|
|
||||||
{
|
|
||||||
var db = _multiplexer.GetDatabase();
|
|
||||||
var key = CreateCachedPlaylistKey(playlistId, platform);
|
|
||||||
var vals = await db.ListRangeAsync(key);
|
|
||||||
if (vals == default || vals.Length == 0)
|
|
||||||
return Array.Empty<string>();
|
|
||||||
|
|
||||||
return vals.Select(x => x.ToString()).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids)
|
|
||||||
{
|
|
||||||
var db = _multiplexer.GetDatabase();
|
|
||||||
var key = CreateCachedPlaylistKey(playlistId, platform);
|
|
||||||
await db.ListRightPushAsync(key, ids.Select(x => (RedisValue)x).ToArray());
|
|
||||||
await db.KeyExpireAsync(key, TimeSpan.FromDays(7));
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static string CreateCachedPlaylistQueryKey(string query, MusicPlatform platform)
|
|
||||||
=> $"playlist:query:{platform}:{query}";
|
|
||||||
|
|
||||||
public Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId)
|
|
||||||
{
|
|
||||||
query = Uri.EscapeDataString(query.Trim());
|
|
||||||
var key = CreateCachedPlaylistQueryKey(query, platform);
|
|
||||||
var db = _multiplexer.GetDatabase();
|
|
||||||
return db.StringSetAsync(key, playlistId, TimeSpan.FromDays(7));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform)
|
|
||||||
{
|
|
||||||
query = Uri.EscapeDataString(query.Trim());
|
|
||||||
var key = CreateCachedPlaylistQueryKey(query, platform);
|
|
||||||
|
|
||||||
var val = await _multiplexer.GetDatabase().StringGetAsync(key);
|
|
||||||
if (val == default)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
95
src/NadekoBot/Modules/Music/_Common/Impl/TrackCacher.cs
Normal file
95
src/NadekoBot/Modules/Music/_Common/Impl/TrackCacher.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
namespace NadekoBot.Modules.Music;
|
||||||
|
|
||||||
|
public sealed class TrackCacher : ITrackCacher
|
||||||
|
{
|
||||||
|
private readonly IBotCache _cache;
|
||||||
|
|
||||||
|
public TrackCacher(IBotCache cache)
|
||||||
|
=> _cache = cache;
|
||||||
|
|
||||||
|
|
||||||
|
private TypedKey<string> GetStreamLinkKey(MusicPlatform platform, string id)
|
||||||
|
=> new($"music:stream:{platform}:{id}");
|
||||||
|
|
||||||
|
public async Task<string?> GetOrCreateStreamLink(
|
||||||
|
string id,
|
||||||
|
MusicPlatform platform,
|
||||||
|
Func<Task<(string StreamUrl, TimeSpan Expiry)>> streamUrlFactory)
|
||||||
|
{
|
||||||
|
var key = GetStreamLinkKey(platform, id);
|
||||||
|
|
||||||
|
var streamUrl = await _cache.GetOrDefaultAsync(key);
|
||||||
|
await _cache.RemoveAsync(key);
|
||||||
|
|
||||||
|
if (streamUrl == default)
|
||||||
|
{
|
||||||
|
(streamUrl, _) = await streamUrlFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a new one for later use
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
(streamUrl, var expiry) = await streamUrlFactory();
|
||||||
|
await CacheStreamUrlAsync(id, platform, streamUrl, expiry);
|
||||||
|
});
|
||||||
|
|
||||||
|
return streamUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CacheStreamUrlAsync(
|
||||||
|
string id,
|
||||||
|
MusicPlatform platform,
|
||||||
|
string url,
|
||||||
|
TimeSpan expiry)
|
||||||
|
=> await _cache.AddAsync(GetStreamLinkKey(platform, id), url, expiry);
|
||||||
|
|
||||||
|
// track data by id
|
||||||
|
private TypedKey<ICachableTrackData> GetTrackDataKey(MusicPlatform platform, string id)
|
||||||
|
=> new($"music:track:{platform}:{id}");
|
||||||
|
public async Task CacheTrackDataAsync(ICachableTrackData data)
|
||||||
|
=> await _cache.AddAsync(GetTrackDataKey(data.Platform, data.Id), data);
|
||||||
|
|
||||||
|
public async Task<ICachableTrackData?> GetCachedDataByIdAsync(string id, MusicPlatform platform)
|
||||||
|
=> await _cache.GetOrDefaultAsync(GetTrackDataKey(platform, id));
|
||||||
|
|
||||||
|
|
||||||
|
// track data by query
|
||||||
|
private TypedKey<ICachableTrackData> GetTrackDataQueryKey(MusicPlatform platform, string query)
|
||||||
|
=> new($"music:track:{platform}:q:{query}");
|
||||||
|
|
||||||
|
public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data)
|
||||||
|
=> await Task.WhenAll(
|
||||||
|
_cache.AddAsync(GetTrackDataQueryKey(data.Platform, query), data).AsTask(),
|
||||||
|
_cache.AddAsync(GetTrackDataKey(data.Platform, data.Id), data).AsTask());
|
||||||
|
|
||||||
|
public async Task<ICachableTrackData?> GetCachedDataByQueryAsync(string query, MusicPlatform platform)
|
||||||
|
=> await _cache.GetOrDefaultAsync(GetTrackDataQueryKey(platform, query));
|
||||||
|
|
||||||
|
|
||||||
|
// playlist track ids by playlist id
|
||||||
|
private TypedKey<IReadOnlyCollection<string>> GetPlaylistTracksCacheKey(string playlist, MusicPlatform platform)
|
||||||
|
=> new($"music:playlist_tracks:{platform}:{playlist}");
|
||||||
|
|
||||||
|
public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable<string> ids)
|
||||||
|
=> await _cache.AddAsync(GetPlaylistTracksCacheKey(playlistId, platform), ids.ToList());
|
||||||
|
|
||||||
|
public async Task<IReadOnlyCollection<string>> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform)
|
||||||
|
{
|
||||||
|
var result = await _cache.GetAsync(GetPlaylistTracksCacheKey(playlistId, platform));
|
||||||
|
if (result.TryGetValue(out var val))
|
||||||
|
return val;
|
||||||
|
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// playlist id by query
|
||||||
|
private TypedKey<string> GetPlaylistCacheKey(string query, MusicPlatform platform)
|
||||||
|
=> new($"music:playlist_id:{platform}:{query}");
|
||||||
|
|
||||||
|
public async Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId)
|
||||||
|
=> await _cache.AddAsync(GetPlaylistCacheKey(query, platform), playlistId);
|
||||||
|
|
||||||
|
public async Task<string?> GetPlaylistIdByQueryAsync(string query, MusicPlatform platform)
|
||||||
|
=> await _cache.GetOrDefaultAsync(GetPlaylistCacheKey(query, platform));
|
||||||
|
}
|
@@ -1,34 +1,38 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using Newtonsoft.Json;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches.Common;
|
namespace NadekoBot.Modules.Searches.Common;
|
||||||
|
|
||||||
public class AnimeResult
|
public class AnimeResult
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
public string AiringStatus
|
[JsonPropertyName("airing_status")]
|
||||||
=> AiringStatusParsed.ToTitleCase();
|
|
||||||
|
|
||||||
[JsonProperty("airing_status")]
|
|
||||||
public string AiringStatusParsed { get; set; }
|
public string AiringStatusParsed { get; set; }
|
||||||
|
|
||||||
[JsonProperty("title_english")]
|
[JsonPropertyName("title_english")]
|
||||||
public string TitleEnglish { get; set; }
|
public string TitleEnglish { get; set; }
|
||||||
|
|
||||||
[JsonProperty("total_episodes")]
|
[JsonPropertyName("total_episodes")]
|
||||||
public int TotalEpisodes { get; set; }
|
public int TotalEpisodes { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
[JsonProperty("image_url_lge")]
|
[JsonPropertyName("image_url_lge")]
|
||||||
public string ImageUrlLarge { get; set; }
|
public string ImageUrlLarge { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("genres")]
|
||||||
public string[] Genres { get; set; }
|
public string[] Genres { get; set; }
|
||||||
|
|
||||||
[JsonProperty("average_score")]
|
[JsonPropertyName("average_score")]
|
||||||
public string AverageScore { get; set; }
|
public float AverageScore { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public string AiringStatus
|
||||||
|
=> AiringStatusParsed.ToTitleCase();
|
||||||
|
|
||||||
public string Link
|
public string Link
|
||||||
=> "http://anilist.co/anime/" + Id;
|
=> "http://anilist.co/anime/" + Id;
|
||||||
|
|
||||||
|
@@ -1,17 +1,15 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using AngleSharp;
|
|
||||||
using AngleSharp.Html.Dom;
|
|
||||||
using NadekoBot.Modules.Searches.Common;
|
using NadekoBot.Modules.Searches.Common;
|
||||||
using Newtonsoft.Json;
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches.Services;
|
namespace NadekoBot.Modules.Searches.Services;
|
||||||
|
|
||||||
public class AnimeSearchService : INService
|
public class AnimeSearchService : INService
|
||||||
{
|
{
|
||||||
private readonly IDataCache _cache;
|
private readonly IBotCache _cache;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
|
||||||
public AnimeSearchService(IDataCache cache, IHttpClientFactory httpFactory)
|
public AnimeSearchService(IBotCache cache, IHttpClientFactory httpFactory)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_httpFactory = httpFactory;
|
_httpFactory = httpFactory;
|
||||||
@@ -21,24 +19,25 @@ public class AnimeSearchService : INService
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(query))
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
throw new ArgumentNullException(nameof(query));
|
throw new ArgumentNullException(nameof(query));
|
||||||
|
|
||||||
|
TypedKey<AnimeResult> GetKey(string link)
|
||||||
|
=> new TypedKey<AnimeResult>($"anime2:{link}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var link = "https://aniapi.nadeko.bot/anime/"
|
var suffix = Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
|
||||||
+ Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
|
var link = $"https://aniapi.nadeko.bot/anime/{suffix}";
|
||||||
link = link.ToLowerInvariant();
|
link = link.ToLowerInvariant();
|
||||||
var (ok, data) = await _cache.TryGetAnimeDataAsync(link);
|
var result = await _cache.GetAsync(GetKey(link));
|
||||||
if (!ok)
|
if (!result.TryPickT0(out var data, out _))
|
||||||
{
|
{
|
||||||
using (var http = _httpFactory.CreateClient())
|
using var http = _httpFactory.CreateClient();
|
||||||
{
|
data = await http.GetFromJsonAsync<AnimeResult>(link);
|
||||||
data = await http.GetStringAsync(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _cache.SetAnimeDataAsync(link, data);
|
await _cache.AddAsync(GetKey(link), data, expiry: TimeSpan.FromHours(12));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
return JsonConvert.DeserializeObject<AnimeResult>(data);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -46,95 +45,31 @@ public class AnimeSearchService : INService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<NovelResult> GetNovelData(string query)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(query))
|
|
||||||
throw new ArgumentNullException(nameof(query));
|
|
||||||
|
|
||||||
query = query.Replace(" ", "-", StringComparison.InvariantCulture);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var link = "https://www.novelupdates.com/series/"
|
|
||||||
+ Uri.EscapeDataString(query.Replace(" ", "-").Replace("/", " "));
|
|
||||||
link = link.ToLowerInvariant();
|
|
||||||
var (ok, data) = await _cache.TryGetNovelDataAsync(link);
|
|
||||||
if (!ok)
|
|
||||||
{
|
|
||||||
var config = Configuration.Default.WithDefaultLoader();
|
|
||||||
using var document = await BrowsingContext.New(config).OpenAsync(link);
|
|
||||||
var imageElem = document.QuerySelector("div.seriesimg > img");
|
|
||||||
if (imageElem is null)
|
|
||||||
return null;
|
|
||||||
var imageUrl = ((IHtmlImageElement)imageElem).Source;
|
|
||||||
|
|
||||||
var descElem = document.QuerySelector("div#editdescription > p");
|
|
||||||
var desc = descElem.InnerHtml;
|
|
||||||
|
|
||||||
var genres = document.QuerySelector("div#seriesgenre")
|
|
||||||
.Children.Select(x => x as IHtmlAnchorElement)
|
|
||||||
.Where(x => x is not null)
|
|
||||||
.Select(x => $"[{x.InnerHtml}]({x.Href})")
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var authors = document.QuerySelector("div#showauthors")
|
|
||||||
.Children.Select(x => x as IHtmlAnchorElement)
|
|
||||||
.Where(x => x is not null)
|
|
||||||
.Select(x => $"[{x.InnerHtml}]({x.Href})")
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var score = ((IHtmlSpanElement)document.QuerySelector("h5.seriesother > span.uvotes")).InnerHtml;
|
|
||||||
|
|
||||||
var status = document.QuerySelector("div#editstatus").InnerHtml;
|
|
||||||
var title = document.QuerySelector("div.w-blog-content > div.seriestitlenu").InnerHtml;
|
|
||||||
|
|
||||||
var obj = new NovelResult
|
|
||||||
{
|
|
||||||
Description = desc,
|
|
||||||
Authors = authors,
|
|
||||||
Genres = genres,
|
|
||||||
ImageUrl = imageUrl,
|
|
||||||
Link = link,
|
|
||||||
Score = score,
|
|
||||||
Status = status,
|
|
||||||
Title = title
|
|
||||||
};
|
|
||||||
|
|
||||||
await _cache.SetNovelDataAsync(link, JsonConvert.SerializeObject(obj));
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<NovelResult>(data);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex, "Error getting novel data");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MangaResult> GetMangaData(string query)
|
public async Task<MangaResult> GetMangaData(string query)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(query))
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
throw new ArgumentNullException(nameof(query));
|
throw new ArgumentNullException(nameof(query));
|
||||||
|
|
||||||
|
TypedKey<MangaResult> GetKey(string link)
|
||||||
|
=> new TypedKey<MangaResult>($"manga2:{link}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var link = "https://aniapi.nadeko.bot/manga/"
|
var link = "https://aniapi.nadeko.bot/manga/"
|
||||||
+ Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
|
+ Uri.EscapeDataString(query.Replace("/", " ", StringComparison.InvariantCulture));
|
||||||
link = link.ToLowerInvariant();
|
link = link.ToLowerInvariant();
|
||||||
var (ok, data) = await _cache.TryGetAnimeDataAsync(link);
|
|
||||||
if (!ok)
|
var result = await _cache.GetAsync(GetKey(link));
|
||||||
|
if (!result.TryPickT0(out var data, out _))
|
||||||
{
|
{
|
||||||
using (var http = _httpFactory.CreateClient())
|
using var http = _httpFactory.CreateClient();
|
||||||
{
|
data = await http.GetFromJsonAsync<MangaResult>(link);
|
||||||
data = await http.GetStringAsync(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _cache.SetAnimeDataAsync(link, data);
|
await _cache.AddAsync(GetKey(link), data, expiry: TimeSpan.FromHours(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<MangaResult>(data);
|
return data;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@@ -1,32 +1,36 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using Newtonsoft.Json;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches.Common;
|
namespace NadekoBot.Modules.Searches.Common;
|
||||||
|
|
||||||
public class MangaResult
|
public class MangaResult
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
[JsonProperty("publishing_status")]
|
[JsonPropertyName("publishing_status")]
|
||||||
public string PublishingStatus { get; set; }
|
public string PublishingStatus { get; set; }
|
||||||
|
|
||||||
[JsonProperty("image_url_lge")]
|
[JsonPropertyName("image_url_lge")]
|
||||||
public string ImageUrlLge { get; set; }
|
public string ImageUrlLge { get; set; }
|
||||||
|
|
||||||
[JsonProperty("title_english")]
|
[JsonPropertyName("title_english")]
|
||||||
public string TitleEnglish { get; set; }
|
public string TitleEnglish { get; set; }
|
||||||
|
|
||||||
[JsonProperty("total_chapters")]
|
[JsonPropertyName("total_chapters")]
|
||||||
public int TotalChapters { get; set; }
|
public int TotalChapters { get; set; }
|
||||||
|
|
||||||
[JsonProperty("total_volumes")]
|
[JsonPropertyName("total_volumes")]
|
||||||
public int TotalVolumes { get; set; }
|
public int TotalVolumes { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("genres")]
|
||||||
public string[] Genres { get; set; }
|
public string[] Genres { get; set; }
|
||||||
|
|
||||||
[JsonProperty("average_score")]
|
[JsonPropertyName("average_score")]
|
||||||
public string AverageScore { get; set; }
|
public float AverageScore { get; set; }
|
||||||
|
|
||||||
public string Link
|
public string Link
|
||||||
=> "http://anilist.co/manga/" + Id;
|
=> "http://anilist.co/manga/" + Id;
|
||||||
|
@@ -15,13 +15,13 @@ namespace NadekoBot.Modules.Searches.Services;
|
|||||||
|
|
||||||
public class CryptoService : INService
|
public class CryptoService : INService
|
||||||
{
|
{
|
||||||
private readonly IDataCache _cache;
|
private readonly IBotCache _cache;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCredentials _creds;
|
||||||
|
|
||||||
private readonly SemaphoreSlim _getCryptoLock = new(1, 1);
|
private readonly SemaphoreSlim _getCryptoLock = new(1, 1);
|
||||||
|
|
||||||
public CryptoService(IDataCache cache, IHttpClientFactory httpFactory, IBotCredentials creds)
|
public CryptoService(IBotCache cache, IHttpClientFactory httpFactory, IBotCredentials creds)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_httpFactory = httpFactory;
|
_httpFactory = httpFactory;
|
||||||
@@ -40,7 +40,8 @@ public class CryptoService : INService
|
|||||||
Span<PointF> points = new PointF[gElement.ChildNodes.Count];
|
Span<PointF> points = new PointF[gElement.ChildNodes.Count];
|
||||||
var cnt = 0;
|
var cnt = 0;
|
||||||
|
|
||||||
bool GetValuesFromAttributes(XmlAttributeCollection attrs,
|
bool GetValuesFromAttributes(
|
||||||
|
XmlAttributeCollection attrs,
|
||||||
out float x1,
|
out float x1,
|
||||||
out float y1,
|
out float y1,
|
||||||
out float x2,
|
out float x2,
|
||||||
@@ -56,7 +57,7 @@ public class CryptoService : INService
|
|||||||
&& attrs["y2"]?.Value is string y2Str
|
&& attrs["y2"]?.Value is string y2Str
|
||||||
&& float.TryParse(y2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y2);
|
&& float.TryParse(y2Str, NumberStyles.Any, CultureInfo.InvariantCulture, out y2);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (XmlElement x in gElement.ChildNodes)
|
foreach (XmlElement x in gElement.ChildNodes)
|
||||||
{
|
{
|
||||||
if (x.Name != "line")
|
if (x.Name != "line")
|
||||||
@@ -67,22 +68,22 @@ public class CryptoService : INService
|
|||||||
points[cnt++] = new(x1, y1);
|
points[cnt++] = new(x1, y1);
|
||||||
// this point will be set twice to the same value
|
// this point will be set twice to the same value
|
||||||
// on all points except the last one
|
// on all points except the last one
|
||||||
if(cnt + 1 < points.Length)
|
if (cnt + 1 < points.Length)
|
||||||
points[cnt + 1] = new(x2, y2);
|
points[cnt + 1] = new(x2, y2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cnt == 0)
|
if (cnt == 0)
|
||||||
return Array.Empty<PointF>();
|
return Array.Empty<PointF>();
|
||||||
|
|
||||||
return points.Slice(0, cnt).ToArray();
|
return points.Slice(0, cnt).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SixLabors.ImageSharp.Image<Rgba32> GenerateSparklineChart(PointF[] points, bool up)
|
private SixLabors.ImageSharp.Image<Rgba32> GenerateSparklineChart(PointF[] points, bool up)
|
||||||
{
|
{
|
||||||
const int width = 164;
|
const int width = 164;
|
||||||
const int height = 48;
|
const int height = 48;
|
||||||
|
|
||||||
var img = new Image<Rgba32>(width, height, Color.Transparent);
|
var img = new Image<Rgba32>(width, height, Color.Transparent);
|
||||||
var color = up
|
var color = up
|
||||||
? Color.Green
|
? Color.Green
|
||||||
@@ -92,10 +93,10 @@ public class CryptoService : INService
|
|||||||
{
|
{
|
||||||
x.DrawLines(color, 2, points);
|
x.DrawLines(color, 2, points);
|
||||||
});
|
});
|
||||||
|
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(CmcResponseData? Data, CmcResponseData? Nearest)> GetCryptoData(string name)
|
public async Task<(CmcResponseData? Data, CmcResponseData? Nearest)> GetCryptoData(string name)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
@@ -130,20 +131,20 @@ public class CryptoService : INService
|
|||||||
await _getCryptoLock.WaitAsync();
|
await _getCryptoLock.WaitAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fullStrData = await _cache.GetOrAddCachedDataAsync("nadeko:crypto_data",
|
var data = await _cache.GetOrAddAsync(new("nadeko:crypto_data"),
|
||||||
async _ =>
|
async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
var strData = await http.GetFromJsonAsync<CryptoResponse>(
|
var data = await http.GetFromJsonAsync<CryptoResponse>(
|
||||||
"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?"
|
"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?"
|
||||||
+ $"CMC_PRO_API_KEY={_creds.CoinmarketcapApiKey}"
|
+ $"CMC_PRO_API_KEY={_creds.CoinmarketcapApiKey}"
|
||||||
+ "&start=1"
|
+ "&start=1"
|
||||||
+ "&limit=5000"
|
+ "&limit=5000"
|
||||||
+ "&convert=USD");
|
+ "&convert=USD");
|
||||||
|
|
||||||
return JsonSerializer.Serialize(strData);
|
return data;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -151,13 +152,12 @@ public class CryptoService : INService
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"",
|
|
||||||
TimeSpan.FromHours(2));
|
TimeSpan.FromHours(2));
|
||||||
|
|
||||||
if (fullStrData is null)
|
if (data is null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<CryptoResponse>(fullStrData)?.Data ?? new();
|
return data.Data;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -170,44 +170,33 @@ public class CryptoService : INService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TypedKey<byte[]> GetSparklineKey(int id)
|
||||||
|
=> new($"crypto:sparkline:{id}");
|
||||||
|
|
||||||
public async Task<Stream?> GetSparklineAsync(int id, bool up)
|
public async Task<Stream?> GetSparklineAsync(int id, bool up)
|
||||||
{
|
|
||||||
var key = $"crypto:sparkline:{id}";
|
|
||||||
|
|
||||||
// attempt to get from cache
|
|
||||||
var db = _cache.Redis.GetDatabase();
|
|
||||||
byte[] bytes = await db.StringGetAsync(key);
|
|
||||||
// if it succeeds, return it
|
|
||||||
if (bytes is { Length: > 0 })
|
|
||||||
{
|
|
||||||
return bytes.ToStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it fails, generate a new one
|
|
||||||
var points = await DownloadSparklinePointsAsync(id);
|
|
||||||
if (points is null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var sparkline = GenerateSparklineChart(points, up);
|
|
||||||
|
|
||||||
// add to cache for 1h and return it
|
|
||||||
|
|
||||||
var stream = sparkline.ToStream();
|
|
||||||
await db.StringSetAsync(key, stream.ToArray(), expiry: TimeSpan.FromHours(1));
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<PointF[]?> DownloadSparklinePointsAsync(int id)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var http = _httpFactory.CreateClient();
|
var bytes = await _cache.GetOrAddAsync(GetSparklineKey(id),
|
||||||
var str = await http.GetStringAsync(
|
async () =>
|
||||||
$"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{id}.svg");
|
{
|
||||||
var points = GetSparklinePointsFromSvgText(str);
|
// if it fails, generate a new one
|
||||||
return points;
|
var points = await DownloadSparklinePointsAsync(id);
|
||||||
|
var sparkline = GenerateSparklineChart(points, up);
|
||||||
|
|
||||||
|
using var stream = await sparkline.ToStreamAsync();
|
||||||
|
return stream.ToArray();
|
||||||
|
},
|
||||||
|
TimeSpan.FromHours(1));
|
||||||
|
|
||||||
|
if (bytes is { Length: > 0 })
|
||||||
|
{
|
||||||
|
return bytes.ToStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Warning(ex,
|
Log.Warning(ex,
|
||||||
"Exception occurred while downloading sparkline points: {ErrorMessage}",
|
"Exception occurred while downloading sparkline points: {ErrorMessage}",
|
||||||
@@ -215,4 +204,13 @@ public class CryptoService : INService
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<PointF[]> DownloadSparklinePointsAsync(int id)
|
||||||
|
{
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
var str = await http.GetStringAsync(
|
||||||
|
$"https://s3.coinmarketcap.com/generated/sparklines/web/7d/usd/{id}.svg");
|
||||||
|
var points = GetSparklinePointsFromSvgText(str);
|
||||||
|
return points;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -9,15 +9,9 @@ public partial class Searches
|
|||||||
[Group]
|
[Group]
|
||||||
public partial class PokemonSearchCommands : NadekoModule<SearchesService>
|
public partial class PokemonSearchCommands : NadekoModule<SearchesService>
|
||||||
{
|
{
|
||||||
public IReadOnlyDictionary<string, SearchPokemon> Pokemons
|
private readonly ILocalDataCache _cache;
|
||||||
=> _cache.LocalData.Pokemons;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities
|
public PokemonSearchCommands(ILocalDataCache cache)
|
||||||
=> _cache.LocalData.PokemonAbilities;
|
|
||||||
|
|
||||||
private readonly IDataCache _cache;
|
|
||||||
|
|
||||||
public PokemonSearchCommands(IDataCache cache)
|
|
||||||
=> _cache = cache;
|
=> _cache = cache;
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
@@ -27,7 +21,7 @@ public partial class Searches
|
|||||||
if (string.IsNullOrWhiteSpace(pokemon))
|
if (string.IsNullOrWhiteSpace(pokemon))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var kvp in Pokemons)
|
foreach (var kvp in await _cache.GetPokemonsAsync())
|
||||||
{
|
{
|
||||||
if (kvp.Key.ToUpperInvariant() == pokemon.ToUpperInvariant())
|
if (kvp.Key.ToUpperInvariant() == pokemon.ToUpperInvariant())
|
||||||
{
|
{
|
||||||
@@ -58,7 +52,7 @@ public partial class Searches
|
|||||||
ability = ability?.Trim().ToUpperInvariant().Replace(" ", "", StringComparison.InvariantCulture);
|
ability = ability?.Trim().ToUpperInvariant().Replace(" ", "", StringComparison.InvariantCulture);
|
||||||
if (string.IsNullOrWhiteSpace(ability))
|
if (string.IsNullOrWhiteSpace(ability))
|
||||||
return;
|
return;
|
||||||
foreach (var kvp in PokemonAbilities)
|
foreach (var kvp in await _cache.GetPokemonAbilitiesAsync())
|
||||||
{
|
{
|
||||||
if (kvp.Key.ToUpperInvariant() == ability)
|
if (kvp.Key.ToUpperInvariant() == ability)
|
||||||
{
|
{
|
||||||
|
@@ -10,14 +10,14 @@ public partial class Searches
|
|||||||
public partial class SearchCommands : NadekoModule
|
public partial class SearchCommands : NadekoModule
|
||||||
{
|
{
|
||||||
private readonly ISearchServiceFactory _searchFactory;
|
private readonly ISearchServiceFactory _searchFactory;
|
||||||
private readonly ConnectionMultiplexer _redis;
|
private readonly IBotCache _cache;
|
||||||
|
|
||||||
public SearchCommands(
|
public SearchCommands(
|
||||||
ISearchServiceFactory searchFactory,
|
ISearchServiceFactory searchFactory,
|
||||||
ConnectionMultiplexer redis)
|
IBotCache cache)
|
||||||
{
|
{
|
||||||
_searchFactory = searchFactory;
|
_searchFactory = searchFactory;
|
||||||
_redis = redis;
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
@@ -127,18 +127,17 @@ public partial class Searches
|
|||||||
await ctx.Channel.EmbedAsync(null, embeds: embeds);
|
await ctx.Channel.EmbedAsync(null, embeds: embeds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TypedKey<string> GetYtCacheKey(string query)
|
||||||
|
=> new($"search:youtube:{query}");
|
||||||
|
|
||||||
private async Task AddYoutubeUrlToCacheAsync(string query, string url)
|
private async Task AddYoutubeUrlToCacheAsync(string query, string url)
|
||||||
{
|
=> await _cache.AddAsync(GetYtCacheKey(query), url, expiry: 1.Hours());
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
await db.StringSetAsync($"search:youtube:{query}", url, expiry: 1.Hours());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<VideoInfo?> GetYoutubeUrlFromCacheAsync(string query)
|
private async Task<VideoInfo?> GetYoutubeUrlFromCacheAsync(string query)
|
||||||
{
|
{
|
||||||
var db = _redis.GetDatabase();
|
var result = await _cache.GetAsync(GetYtCacheKey(query));
|
||||||
var url = await db.StringGetAsync($"search:youtube:{query}");
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(url))
|
if (!result.TryGetValue(out var url) || string.IsNullOrWhiteSpace(url))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new VideoInfo()
|
return new VideoInfo()
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using AngleSharp.Html.Dom;
|
|
||||||
using AngleSharp.Html.Parser;
|
|
||||||
using Html2Markdown;
|
using Html2Markdown;
|
||||||
using NadekoBot.Modules.Searches.Common;
|
using NadekoBot.Modules.Searches.Common;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -10,7 +8,6 @@ using SixLabors.ImageSharp;
|
|||||||
using SixLabors.ImageSharp.Drawing.Processing;
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
using System.Net;
|
|
||||||
using Color = SixLabors.ImageSharp.Color;
|
using Color = SixLabors.ImageSharp.Color;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
|
|
||||||
@@ -31,9 +28,9 @@ public class SearchesService : INService
|
|||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly IGoogleApiService _google;
|
private readonly IGoogleApiService _google;
|
||||||
private readonly IImageCache _imgs;
|
private readonly IImageCache _imgs;
|
||||||
private readonly IDataCache _cache;
|
private readonly IBotCache _c;
|
||||||
private readonly FontProvider _fonts;
|
private readonly FontProvider _fonts;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCredsProvider _creds;
|
||||||
private readonly NadekoRandom _rng;
|
private readonly NadekoRandom _rng;
|
||||||
private readonly List<string> _yomamaJokes;
|
private readonly List<string> _yomamaJokes;
|
||||||
|
|
||||||
@@ -42,15 +39,16 @@ public class SearchesService : INService
|
|||||||
|
|
||||||
public SearchesService(
|
public SearchesService(
|
||||||
IGoogleApiService google,
|
IGoogleApiService google,
|
||||||
IDataCache cache,
|
IImageCache images,
|
||||||
|
IBotCache c,
|
||||||
IHttpClientFactory factory,
|
IHttpClientFactory factory,
|
||||||
FontProvider fonts,
|
FontProvider fonts,
|
||||||
IBotCredentials creds)
|
IBotCredsProvider creds)
|
||||||
{
|
{
|
||||||
_httpFactory = factory;
|
_httpFactory = factory;
|
||||||
_google = google;
|
_google = google;
|
||||||
_imgs = cache.LocalImages;
|
_imgs = images;
|
||||||
_cache = cache;
|
_c = c;
|
||||||
_fonts = fonts;
|
_fonts = fonts;
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
_rng = new();
|
_rng = new();
|
||||||
@@ -76,36 +74,28 @@ public class SearchesService : INService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Stream> GetRipPictureAsync(string text, Uri imgUrl)
|
public async Task<Stream> GetRipPictureAsync(string text, Uri imgUrl)
|
||||||
{
|
=> (await GetRipPictureFactory(text, imgUrl)).ToStream();
|
||||||
var data = await _cache.GetOrAddCachedDataAsync($"nadeko_rip_{text}_{imgUrl}",
|
|
||||||
GetRipPictureFactory,
|
|
||||||
(text, imgUrl),
|
|
||||||
TimeSpan.FromDays(1));
|
|
||||||
|
|
||||||
return data.ToStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawAvatar(Image bg, Image avatarImage)
|
private void DrawAvatar(Image bg, Image avatarImage)
|
||||||
=> bg.Mutate(x => x.Grayscale().DrawImage(avatarImage, new(83, 139), new GraphicsOptions()));
|
=> bg.Mutate(x => x.Grayscale().DrawImage(avatarImage, new(83, 139), new GraphicsOptions()));
|
||||||
|
|
||||||
public async Task<byte[]> GetRipPictureFactory((string text, Uri avatarUrl) arg)
|
public async Task<byte[]> GetRipPictureFactory(string text, Uri avatarUrl)
|
||||||
{
|
{
|
||||||
var (text, avatarUrl) = arg;
|
using var bg = Image.Load<Rgba32>(await _imgs.GetRipBgAsync());
|
||||||
using var bg = Image.Load<Rgba32>(_imgs.Rip.ToArray());
|
var result = await _c.GetImageDataAsync(avatarUrl);
|
||||||
var (succ, data) = (false, (byte[])null); //await _cache.TryGetImageDataAsync(avatarUrl);
|
if (!result.TryPickT0(out var data, out _))
|
||||||
if (!succ)
|
|
||||||
{
|
{
|
||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
data = await http.GetByteArrayAsync(avatarUrl);
|
data = await http.GetByteArrayAsync(avatarUrl);
|
||||||
using (var avatarImg = Image.Load<Rgba32>(data))
|
using (var avatarImg = Image.Load<Rgba32>(data))
|
||||||
{
|
{
|
||||||
avatarImg.Mutate(x => x.Resize(85, 85).ApplyRoundedCorners(42));
|
avatarImg.Mutate(x => x.Resize(85, 85).ApplyRoundedCorners(42));
|
||||||
await using var avStream = avatarImg.ToStream();
|
await using var avStream = await avatarImg.ToStreamAsync();
|
||||||
data = avStream.ToArray();
|
data = avStream.ToArray();
|
||||||
DrawAvatar(bg, avatarImg);
|
DrawAvatar(bg, avatarImg);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cache.SetImageDataAsync(avatarUrl, data);
|
await _c.SetImageDataAsync(avatarUrl, data);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -128,7 +118,7 @@ public class SearchesService : INService
|
|||||||
new(25, 225)));
|
new(25, 225)));
|
||||||
|
|
||||||
//flowa
|
//flowa
|
||||||
using (var flowers = Image.Load(_imgs.RipOverlay.ToArray()))
|
using (var flowers = Image.Load(await _imgs.GetRipOverlayAsync()))
|
||||||
{
|
{
|
||||||
bg.Mutate(x => x.DrawImage(flowers, new(0, 0), new GraphicsOptions()));
|
bg.Mutate(x => x.DrawImage(flowers, new(0, 0), new GraphicsOptions()));
|
||||||
}
|
}
|
||||||
@@ -137,13 +127,12 @@ public class SearchesService : INService
|
|||||||
return stream.ToArray();
|
return stream.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<WeatherData> GetWeatherDataAsync(string query)
|
public async Task<WeatherData> GetWeatherDataAsync(string query)
|
||||||
{
|
{
|
||||||
query = query.Trim().ToLowerInvariant();
|
query = query.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
return _cache.GetOrAddCachedDataAsync($"nadeko_weather_{query}",
|
return await _c.GetOrAddAsync(new($"nadeko_weather_{query}"),
|
||||||
GetWeatherDataFactory,
|
async () => await GetWeatherDataFactory(query),
|
||||||
query,
|
|
||||||
TimeSpan.FromHours(3));
|
TimeSpan.FromHours(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,26 +173,28 @@ public class SearchesService : INService
|
|||||||
if (string.IsNullOrEmpty(query))
|
if (string.IsNullOrEmpty(query))
|
||||||
return (default, TimeErrors.InvalidInput);
|
return (default, TimeErrors.InvalidInput);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_creds.LocationIqApiKey) || string.IsNullOrWhiteSpace(_creds.TimezoneDbApiKey))
|
|
||||||
|
var locIqKey = _creds.GetCreds().LocationIqApiKey;
|
||||||
|
var tzDbKey = _creds.GetCreds().TimezoneDbApiKey;
|
||||||
|
if (string.IsNullOrWhiteSpace(locIqKey) || string.IsNullOrWhiteSpace(tzDbKey))
|
||||||
return (default, TimeErrors.ApiKeyMissing);
|
return (default, TimeErrors.ApiKeyMissing);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
var res = await _cache.GetOrAddCachedDataAsync($"geo_{query}",
|
var res = await _c.GetOrAddAsync(new($"searches:geo:{query}"),
|
||||||
_ =>
|
async () =>
|
||||||
{
|
{
|
||||||
var url = "https://eu1.locationiq.com/v1/search.php?"
|
var url = "https://eu1.locationiq.com/v1/search.php?"
|
||||||
+ (string.IsNullOrWhiteSpace(_creds.LocationIqApiKey)
|
+ (string.IsNullOrWhiteSpace(locIqKey)
|
||||||
? "key="
|
? "key="
|
||||||
: $"key={_creds.LocationIqApiKey}&")
|
: $"key={locIqKey}&")
|
||||||
+ $"q={Uri.EscapeDataString(query)}&"
|
+ $"q={Uri.EscapeDataString(query)}&"
|
||||||
+ "format=json";
|
+ "format=json";
|
||||||
|
|
||||||
var res = http.GetStringAsync(url);
|
var res = await http.GetStringAsync(url);
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
"",
|
|
||||||
TimeSpan.FromHours(1));
|
TimeSpan.FromHours(1));
|
||||||
|
|
||||||
var responses = JsonConvert.DeserializeObject<LocationIqResponse[]>(res);
|
var responses = JsonConvert.DeserializeObject<LocationIqResponse[]>(res);
|
||||||
@@ -217,7 +208,7 @@ public class SearchesService : INService
|
|||||||
|
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Get,
|
using var req = new HttpRequestMessage(HttpMethod.Get,
|
||||||
"http://api.timezonedb.com/v2.1/get-time-zone?"
|
"http://api.timezonedb.com/v2.1/get-time-zone?"
|
||||||
+ $"key={_creds.TimezoneDbApiKey}"
|
+ $"key={tzDbKey}"
|
||||||
+ $"&format=json"
|
+ $"&format=json"
|
||||||
+ $"&by=position"
|
+ $"&by=position"
|
||||||
+ $"&lat={geoData.Lat}"
|
+ $"&lat={geoData.Lat}"
|
||||||
@@ -315,9 +306,8 @@ public class SearchesService : INService
|
|||||||
public async Task<MtgData> GetMtgCardAsync(string search)
|
public async Task<MtgData> GetMtgCardAsync(string search)
|
||||||
{
|
{
|
||||||
search = search.Trim().ToLowerInvariant();
|
search = search.Trim().ToLowerInvariant();
|
||||||
var data = await _cache.GetOrAddCachedDataAsync($"nadeko_mtg_{search}",
|
var data = await _c.GetOrAddAsync(new($"mtg:{search}"),
|
||||||
GetMtgCardFactory,
|
async () => await GetMtgCardFactory(search),
|
||||||
search,
|
|
||||||
TimeSpan.FromDays(1));
|
TimeSpan.FromDays(1));
|
||||||
|
|
||||||
if (data is null || data.Length == 0)
|
if (data is null || data.Length == 0)
|
||||||
@@ -368,12 +358,11 @@ public class SearchesService : INService
|
|||||||
return await cards.Select(GetMtgDataAsync).WhenAll();
|
return await cards.Select(GetMtgDataAsync).WhenAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<HearthstoneCardData> GetHearthstoneCardDataAsync(string name)
|
public async Task<HearthstoneCardData> GetHearthstoneCardDataAsync(string name)
|
||||||
{
|
{
|
||||||
name = name.ToLowerInvariant();
|
name = name.ToLowerInvariant();
|
||||||
return _cache.GetOrAddCachedDataAsync($"nadeko_hearthstone_{name}",
|
return await _c.GetOrAddAsync($"hearthstone:{name}",
|
||||||
HearthstoneCardDataFactory,
|
() => HearthstoneCardDataFactory(name),
|
||||||
name,
|
|
||||||
TimeSpan.FromDays(1));
|
TimeSpan.FromDays(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +370,7 @@ public class SearchesService : INService
|
|||||||
{
|
{
|
||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
http.DefaultRequestHeaders.Clear();
|
http.DefaultRequestHeaders.Clear();
|
||||||
http.DefaultRequestHeaders.Add("x-rapidapi-key", _creds.RapidApiKey);
|
http.DefaultRequestHeaders.Add("x-rapidapi-key", _creds.GetCreds().RapidApiKey);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await http.GetStringAsync("https://omgvamp-hearthstone-v1.p.rapidapi.com/"
|
var response = await http.GetStringAsync("https://omgvamp-hearthstone-v1.p.rapidapi.com/"
|
||||||
@@ -410,16 +399,22 @@ public class SearchesService : INService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<OmdbMovie> GetMovieDataAsync(string name)
|
public async Task<OmdbMovie> GetMovieDataAsync(string name)
|
||||||
{
|
{
|
||||||
name = name.Trim().ToLowerInvariant();
|
name = name.Trim().ToLowerInvariant();
|
||||||
return _cache.GetOrAddCachedDataAsync($"nadeko_movie_{name}", GetMovieDataFactory, name, TimeSpan.FromDays(1));
|
return await _c.GetOrAddAsync(new($"movie:{name}"),
|
||||||
|
() => GetMovieDataFactory(name),
|
||||||
|
TimeSpan.FromDays(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<OmdbMovie> GetMovieDataFactory(string name)
|
private async Task<OmdbMovie> GetMovieDataFactory(string name)
|
||||||
{
|
{
|
||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
var res = await http.GetStringAsync(string.Format("https://omdbapi.nadeko.bot/?t={0}&y=&plot=full&r=json",
|
var res = await http.GetStringAsync(string.Format("https://omdbapi.nadeko.bot/"
|
||||||
|
+ "?t={0}"
|
||||||
|
+ "&y="
|
||||||
|
+ "&plot=full"
|
||||||
|
+ "&r=json",
|
||||||
name.Trim().Replace(' ', '+')));
|
name.Trim().Replace(' ', '+')));
|
||||||
var movie = JsonConvert.DeserializeObject<OmdbMovie>(res);
|
var movie = JsonConvert.DeserializeObject<OmdbMovie>(res);
|
||||||
if (movie?.Title is null)
|
if (movie?.Title is null)
|
||||||
@@ -432,10 +427,11 @@ public class SearchesService : INService
|
|||||||
{
|
{
|
||||||
const string steamGameIdsKey = "steam_names_to_appid";
|
const string steamGameIdsKey = "steam_names_to_appid";
|
||||||
|
|
||||||
var gamesMap = await _cache.GetOrAddCachedDataAsync(steamGameIdsKey,
|
var gamesMap = await _c.GetOrAddAsync(new(steamGameIdsKey),
|
||||||
async _ =>
|
async () =>
|
||||||
{
|
{
|
||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
|
|
||||||
// https://api.steampowered.com/ISteamApps/GetAppList/v2/
|
// https://api.steampowered.com/ISteamApps/GetAppList/v2/
|
||||||
var gamesStr = await http.GetStringAsync("https://api.steampowered.com/ISteamApps/GetAppList/v2/");
|
var gamesStr = await http.GetStringAsync("https://api.steampowered.com/ISteamApps/GetAppList/v2/");
|
||||||
var apps = JsonConvert
|
var apps = JsonConvert
|
||||||
@@ -446,23 +442,18 @@ public class SearchesService : INService
|
|||||||
{
|
{
|
||||||
apps = new List<SteamGameId>()
|
apps = new List<SteamGameId>()
|
||||||
}
|
}
|
||||||
})
|
})!
|
||||||
.applist.apps;
|
.applist.apps;
|
||||||
|
|
||||||
return apps.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
return apps.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.GroupBy(x => x.Name)
|
.GroupBy(x => x.Name)
|
||||||
.ToDictionary(x => x.Key, x => x.First().AppId);
|
.ToDictionary(x => x.Key, x => x.First().AppId);
|
||||||
//await db.HashSetAsync("steam_game_ids", apps.Select(app => new HashEntry(app.Name.Trim().ToLowerInvariant(), app.AppId)).ToArray());
|
|
||||||
//await db.StringSetAsync("steam_game_ids", gamesStr, TimeSpan.FromHours(24));
|
|
||||||
//await db.KeyExpireAsync("steam_game_ids", TimeSpan.FromHours(24), CommandFlags.FireAndForget);
|
|
||||||
},
|
},
|
||||||
default(string),
|
|
||||||
TimeSpan.FromHours(24));
|
TimeSpan.FromHours(24));
|
||||||
|
|
||||||
if (gamesMap is null)
|
if (gamesMap is null)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
|
|
||||||
query = query.Trim();
|
query = query.Trim();
|
||||||
|
|
||||||
var keyList = gamesMap.Keys.ToList();
|
var keyList = gamesMap.Keys.ToList();
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
using NadekoBot.Db;
|
using NadekoBot.Db;
|
||||||
@@ -6,10 +8,98 @@ using NadekoBot.Db.Models;
|
|||||||
using NadekoBot.Modules.Searches.Common;
|
using NadekoBot.Modules.Searches.Common;
|
||||||
using NadekoBot.Modules.Searches.Common.StreamNotifications;
|
using NadekoBot.Modules.Searches.Common.StreamNotifications;
|
||||||
using NadekoBot.Services.Database.Models;
|
using NadekoBot.Services.Database.Models;
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches.Services;
|
namespace NadekoBot.Modules.Searches.Services;
|
||||||
|
|
||||||
|
public sealed class StreamOnlineMessageDeleterService : INService, IReadyExecutor
|
||||||
|
{
|
||||||
|
private readonly StreamNotificationService _notifService;
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly IPubSub _pubSub;
|
||||||
|
|
||||||
|
public StreamOnlineMessageDeleterService(
|
||||||
|
StreamNotificationService notifService,
|
||||||
|
DbService db,
|
||||||
|
IPubSub pubSub,
|
||||||
|
DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
_notifService = notifService;
|
||||||
|
_db = db;
|
||||||
|
_client = client;
|
||||||
|
_pubSub = pubSub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
_notifService.OnlineMessagesSent += OnOnlineMessagesSent;
|
||||||
|
|
||||||
|
if(_client.ShardId == 0)
|
||||||
|
await _pubSub.Sub(_notifService.StreamsOfflineKey, OnStreamsOffline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnOnlineMessagesSent(FollowedStream.FType type, string name, IReadOnlyCollection<(ulong, ulong)> pairs)
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
foreach (var (channelId, messageId) in pairs)
|
||||||
|
{
|
||||||
|
await ctx.GetTable<StreamOnlineMessage>()
|
||||||
|
.InsertAsync(() => new()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Type = type,
|
||||||
|
MessageId = messageId,
|
||||||
|
ChannelId = channelId,
|
||||||
|
DateAdded = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask OnStreamsOffline(List<StreamData> streamDatas)
|
||||||
|
{
|
||||||
|
if (_client.ShardId != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pairs = await GetMessagesToDelete(streamDatas);
|
||||||
|
|
||||||
|
foreach (var (channelId, messageId) in pairs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var textChannel = await _client.GetChannelAsync(channelId) as ITextChannel;
|
||||||
|
if (textChannel is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await textChannel.DeleteMessageAsync(messageId);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IEnumerable<(ulong, ulong)>> GetMessagesToDelete(List<StreamData> streamDatas)
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
|
||||||
|
var toReturn = new List<(ulong, ulong)>();
|
||||||
|
foreach (var sd in streamDatas)
|
||||||
|
{
|
||||||
|
var key = sd.CreateKey();
|
||||||
|
var toDelete = await ctx.GetTable<StreamOnlineMessage>()
|
||||||
|
.Where(x => (x.Type == key.Type && x.Name == key.Name)
|
||||||
|
|| Sql.DateDiff(Sql.DateParts.Day, x.DateAdded, DateTime.UtcNow) > 1)
|
||||||
|
.DeleteWithOutputAsync();
|
||||||
|
|
||||||
|
toReturn.AddRange(toDelete.Select(x => (x.ChannelId, x.MessageId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public sealed class StreamNotificationService : INService, IReadyExecutor
|
public sealed class StreamNotificationService : INService, IReadyExecutor
|
||||||
{
|
{
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
@@ -29,18 +119,22 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
|
|||||||
private readonly IPubSub _pubSub;
|
private readonly IPubSub _pubSub;
|
||||||
private readonly IEmbedBuilderService _eb;
|
private readonly IEmbedBuilderService _eb;
|
||||||
|
|
||||||
private readonly TypedKey<List<StreamData>> _streamsOnlineKey;
|
public TypedKey<List<StreamData>> StreamsOnlineKey { get; }
|
||||||
private readonly TypedKey<List<StreamData>> _streamsOfflineKey;
|
public TypedKey<List<StreamData>> StreamsOfflineKey { get; }
|
||||||
|
|
||||||
private readonly TypedKey<FollowStreamPubData> _streamFollowKey;
|
private readonly TypedKey<FollowStreamPubData> _streamFollowKey;
|
||||||
private readonly TypedKey<FollowStreamPubData> _streamUnfollowKey;
|
private readonly TypedKey<FollowStreamPubData> _streamUnfollowKey;
|
||||||
private readonly ConnectionMultiplexer _redis;
|
|
||||||
|
public event Func<
|
||||||
|
FollowedStream.FType,
|
||||||
|
string,
|
||||||
|
IReadOnlyCollection<(ulong, ulong)>,
|
||||||
|
Task> OnlineMessagesSent = static delegate { return Task.CompletedTask; };
|
||||||
|
|
||||||
public StreamNotificationService(
|
public StreamNotificationService(
|
||||||
DbService db,
|
DbService db,
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
IBotStrings strings,
|
IBotStrings strings,
|
||||||
ConnectionMultiplexer redis,
|
|
||||||
IBotCredsProvider creds,
|
IBotCredsProvider creds,
|
||||||
IHttpClientFactory httpFactory,
|
IHttpClientFactory httpFactory,
|
||||||
Bot bot,
|
Bot bot,
|
||||||
@@ -52,11 +146,11 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
|
|||||||
_strings = strings;
|
_strings = strings;
|
||||||
_pubSub = pubSub;
|
_pubSub = pubSub;
|
||||||
_eb = eb;
|
_eb = eb;
|
||||||
_redis = redis;
|
|
||||||
_streamTracker = new(httpFactory, creds, redis, creds.GetCreds().RedisKey(), client.ShardId == 0);
|
_streamTracker = new(httpFactory, creds);
|
||||||
|
|
||||||
_streamsOnlineKey = new("streams.online");
|
StreamsOnlineKey = new("streams.online");
|
||||||
_streamsOfflineKey = new("streams.offline");
|
StreamsOfflineKey = new("streams.offline");
|
||||||
|
|
||||||
_streamFollowKey = new("stream.follow");
|
_streamFollowKey = new("stream.follow");
|
||||||
_streamUnfollowKey = new("stream.unfollow");
|
_streamUnfollowKey = new("stream.unfollow");
|
||||||
@@ -100,7 +194,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
|
|||||||
var allFollowedStreams = uow.Set<FollowedStream>().AsQueryable().ToList();
|
var allFollowedStreams = uow.Set<FollowedStream>().AsQueryable().ToList();
|
||||||
|
|
||||||
foreach (var fs in allFollowedStreams)
|
foreach (var fs in allFollowedStreams)
|
||||||
_streamTracker.CacheAddData(fs.CreateKey(), null, false);
|
_streamTracker.AddLastData(fs.CreateKey(), null, false);
|
||||||
|
|
||||||
_trackCounter = allFollowedStreams.GroupBy(x => new
|
_trackCounter = allFollowedStreams.GroupBy(x => new
|
||||||
{
|
{
|
||||||
@@ -112,8 +206,8 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_pubSub.Sub(_streamsOfflineKey, HandleStreamsOffline);
|
_pubSub.Sub(StreamsOfflineKey, HandleStreamsOffline);
|
||||||
_pubSub.Sub(_streamsOnlineKey, HandleStreamsOnline);
|
_pubSub.Sub(StreamsOnlineKey, HandleStreamsOnline);
|
||||||
|
|
||||||
if (client.ShardId == 0)
|
if (client.ShardId == 0)
|
||||||
{
|
{
|
||||||
@@ -186,7 +280,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private ValueTask HandleFollowStream(FollowStreamPubData info)
|
private ValueTask HandleFollowStream(FollowStreamPubData info)
|
||||||
{
|
{
|
||||||
_streamTracker.CacheAddData(info.Key, null, false);
|
_streamTracker.AddLastData(info.Key, null, false);
|
||||||
lock (_shardLock)
|
lock (_shardLock)
|
||||||
{
|
{
|
||||||
var key = info.Key;
|
var key = info.Key;
|
||||||
@@ -251,45 +345,8 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
|
|||||||
.WhenAll();
|
.WhenAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_client.ShardId == 0)
|
|
||||||
{
|
|
||||||
foreach (var stream in offlineStreams)
|
|
||||||
{
|
|
||||||
await DeleteOnlineMessages(stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteOnlineMessages(StreamData stream)
|
|
||||||
{
|
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
var data = await db.ListRangeAsync($"streams_online_del:{stream.CreateKey()}");
|
|
||||||
await db.KeyDeleteAsync($"streams_online_del:{stream.CreateKey()}");
|
|
||||||
|
|
||||||
foreach (string pair in data)
|
|
||||||
{
|
|
||||||
var pairArr = pair.Split(',');
|
|
||||||
if (pairArr.Length != 2)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!ulong.TryParse(pairArr[0], out var chId) || !ulong.TryParse(pairArr[1], out var msgId))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var textChannel = await _client.GetChannelAsync(chId) as ITextChannel;
|
|
||||||
if (textChannel is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
await textChannel.DeleteMessageAsync(msgId);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async ValueTask HandleStreamsOnline(List<StreamData> onlineStreams)
|
private async ValueTask HandleStreamsOnline(List<StreamData> onlineStreams)
|
||||||
{
|
{
|
||||||
@@ -331,14 +388,11 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
|
|||||||
{
|
{
|
||||||
var pairs = messages
|
var pairs = messages
|
||||||
.Where(x => x != default)
|
.Where(x => x != default)
|
||||||
.Select(x => (RedisValue)$"{x.Item1},{x.Item2}")
|
.Select(x => (x.Item1, x.Item2))
|
||||||
.ToArray();
|
.ToList();
|
||||||
|
|
||||||
if (pairs.Length > 0)
|
if (pairs.Count > 0)
|
||||||
{
|
await OnlineMessagesSent(key.Type, key.Name, pairs);
|
||||||
var db = _redis.GetDatabase();
|
|
||||||
await db.ListRightPushAsync($"streams_online_del:{key}", pairs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -349,10 +403,10 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Task OnStreamsOnline(List<StreamData> data)
|
private Task OnStreamsOnline(List<StreamData> data)
|
||||||
=> _pubSub.Pub(_streamsOnlineKey, data);
|
=> _pubSub.Pub(StreamsOnlineKey, data);
|
||||||
|
|
||||||
private Task OnStreamsOffline(List<StreamData> data)
|
private Task OnStreamsOffline(List<StreamData> data)
|
||||||
=> _pubSub.Pub(_streamsOfflineKey, data);
|
=> _pubSub.Pub(StreamsOfflineKey, data);
|
||||||
|
|
||||||
private Task ClientOnJoinedGuild(GuildConfig guildConfig)
|
private Task ClientOnJoinedGuild(GuildConfig guildConfig)
|
||||||
{
|
{
|
||||||
|
@@ -6,10 +6,9 @@ namespace NadekoBot.Modules.Searches.Common;
|
|||||||
|
|
||||||
public readonly struct StreamDataKey
|
public readonly struct StreamDataKey
|
||||||
{
|
{
|
||||||
public FollowedStream.FType Type { get; }
|
public FollowedStream.FType Type { get; init; }
|
||||||
public string Name { get; }
|
public string Name { get; init; }
|
||||||
|
|
||||||
[JsonConstructor]
|
|
||||||
public StreamDataKey(FollowedStream.FType type, string name)
|
public StreamDataKey(FollowedStream.FType type, string name)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
using NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
using NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches.Common.StreamNotifications;
|
namespace NadekoBot.Modules.Searches.Common.StreamNotifications;
|
||||||
|
|
||||||
@@ -9,30 +7,22 @@ public class NotifChecker
|
|||||||
{
|
{
|
||||||
public event Func<List<StreamData>, Task> OnStreamsOffline = _ => Task.CompletedTask;
|
public event Func<List<StreamData>, Task> OnStreamsOffline = _ => Task.CompletedTask;
|
||||||
public event Func<List<StreamData>, Task> OnStreamsOnline = _ => Task.CompletedTask;
|
public event Func<List<StreamData>, Task> OnStreamsOnline = _ => Task.CompletedTask;
|
||||||
private readonly ConnectionMultiplexer _multi;
|
|
||||||
private readonly string _key;
|
|
||||||
|
|
||||||
private readonly Dictionary<FollowedStream.FType, Provider> _streamProviders;
|
private readonly IReadOnlyDictionary<FollowedStream.FType, Provider> _streamProviders;
|
||||||
private readonly HashSet<(FollowedStream.FType, string)> _offlineBuffer;
|
private readonly HashSet<(FollowedStream.FType, string)> _offlineBuffer;
|
||||||
|
private readonly ConcurrentDictionary<StreamDataKey, StreamData?> _cache = new();
|
||||||
|
|
||||||
public NotifChecker(
|
public NotifChecker(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IBotCredsProvider credsProvider,
|
IBotCredsProvider credsProvider)
|
||||||
ConnectionMultiplexer multi,
|
|
||||||
string uniqueCacheKey,
|
|
||||||
bool isMaster)
|
|
||||||
{
|
{
|
||||||
_multi = multi;
|
_streamProviders = new Dictionary<FollowedStream.FType, Provider>()
|
||||||
_key = $"{uniqueCacheKey}_followed_streams_data";
|
|
||||||
_streamProviders = new()
|
|
||||||
{
|
{
|
||||||
{ FollowedStream.FType.Twitch, new TwitchHelixProvider(httpClientFactory, credsProvider) },
|
{ FollowedStream.FType.Twitch, new TwitchHelixProvider(httpClientFactory, credsProvider) },
|
||||||
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) },
|
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) },
|
||||||
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) }
|
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) }
|
||||||
};
|
};
|
||||||
_offlineBuffer = new();
|
_offlineBuffer = new();
|
||||||
if (isMaster)
|
|
||||||
CacheClearAllData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets all streams which have been failing for more than the provided timespan
|
// gets all streams which have been failing for more than the provided timespan
|
||||||
@@ -61,7 +51,7 @@ public class NotifChecker
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var allStreamData = CacheGetAllData();
|
var allStreamData = GetAllData();
|
||||||
|
|
||||||
var oldStreamDataDict = allStreamData
|
var oldStreamDataDict = allStreamData
|
||||||
// group by type
|
// group by type
|
||||||
@@ -101,7 +91,7 @@ public class NotifChecker
|
|||||||
|| !typeDict.TryGetValue(key.Name, out var oldData)
|
|| !typeDict.TryGetValue(key.Name, out var oldData)
|
||||||
|| oldData is null)
|
|| oldData is null)
|
||||||
{
|
{
|
||||||
CacheAddData(key, newData, true);
|
AddLastData(key, newData, true);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +99,7 @@ public class NotifChecker
|
|||||||
if (string.IsNullOrWhiteSpace(newData.Game))
|
if (string.IsNullOrWhiteSpace(newData.Game))
|
||||||
newData.Game = oldData.Game;
|
newData.Game = oldData.Game;
|
||||||
|
|
||||||
CacheAddData(key, newData, true);
|
AddLastData(key, newData, true);
|
||||||
|
|
||||||
// if the stream is offline, we need to check if it was
|
// if the stream is offline, we need to check if it was
|
||||||
// marked as offline once previously
|
// marked as offline once previously
|
||||||
@@ -158,39 +148,22 @@ public class NotifChecker
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace)
|
public bool AddLastData(StreamDataKey key, StreamData? data, bool replace)
|
||||||
{
|
{
|
||||||
var db = _multi.GetDatabase();
|
if (replace)
|
||||||
return db.HashSet(_key,
|
{
|
||||||
JsonConvert.SerializeObject(key),
|
_cache[key] = data;
|
||||||
JsonConvert.SerializeObject(data),
|
return true;
|
||||||
replace ? When.Always : When.NotExists);
|
}
|
||||||
|
|
||||||
|
return _cache.TryAdd(key, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CacheDeleteData(StreamDataKey key)
|
public void DeleteLastData(StreamDataKey key)
|
||||||
{
|
=> _cache.TryRemove(key, out _);
|
||||||
var db = _multi.GetDatabase();
|
|
||||||
db.HashDelete(_key, JsonConvert.SerializeObject(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CacheClearAllData()
|
public Dictionary<StreamDataKey, StreamData?> GetAllData()
|
||||||
{
|
=> _cache.ToDictionary(x => x.Key, x => x.Value);
|
||||||
var db = _multi.GetDatabase();
|
|
||||||
db.KeyDelete(_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<StreamDataKey, StreamData?> CacheGetAllData()
|
|
||||||
{
|
|
||||||
var db = _multi.GetDatabase();
|
|
||||||
if (!db.KeyExists(_key))
|
|
||||||
return new();
|
|
||||||
|
|
||||||
return db.HashGetAll(_key)
|
|
||||||
.ToDictionary(entry => JsonConvert.DeserializeObject<StreamDataKey>(entry.Name),
|
|
||||||
entry => entry.Value.IsNullOrEmpty
|
|
||||||
? default
|
|
||||||
: JsonConvert.DeserializeObject<StreamData>(entry.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
public async Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||||
{
|
{
|
||||||
@@ -234,9 +207,9 @@ public class NotifChecker
|
|||||||
|
|
||||||
// if stream is found, add it to the cache for tracking only if it doesn't already exist
|
// if stream is found, add it to the cache for tracking only if it doesn't already exist
|
||||||
// because stream will be checked and events will fire in a loop. We don't want to override old state
|
// because stream will be checked and events will fire in a loop. We don't want to override old state
|
||||||
return CacheAddData(data.CreateKey(), data, false);
|
return AddLastData(data.CreateKey(), data, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UntrackStreamByKey(in StreamDataKey key)
|
public void UntrackStreamByKey(in StreamDataKey key)
|
||||||
=> CacheDeleteData(key);
|
=> DeleteLastData(key);
|
||||||
}
|
}
|
@@ -31,9 +31,10 @@ public sealed class PatronageService
|
|||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly ISubscriptionHandler _subsHandler;
|
private readonly ISubscriptionHandler _subsHandler;
|
||||||
private readonly IEmbedBuilderService _eb;
|
private readonly IEmbedBuilderService _eb;
|
||||||
private readonly ConnectionMultiplexer _redis;
|
private static readonly TypedKey<long> _quotaKey
|
||||||
private readonly IBotCredentials _creds;
|
= new($"quota:last_hourly_reset");
|
||||||
private readonly TypedKey<bool> _quotaKey;
|
|
||||||
|
private readonly IBotCache _cache;
|
||||||
|
|
||||||
public PatronageService(
|
public PatronageService(
|
||||||
PatronageConfig pConf,
|
PatronageConfig pConf,
|
||||||
@@ -41,18 +42,14 @@ public sealed class PatronageService
|
|||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
ISubscriptionHandler subsHandler,
|
ISubscriptionHandler subsHandler,
|
||||||
IEmbedBuilderService eb,
|
IEmbedBuilderService eb,
|
||||||
ConnectionMultiplexer redis,
|
IBotCache cache)
|
||||||
IBotCredentials creds)
|
|
||||||
{
|
{
|
||||||
_pConf = pConf;
|
_pConf = pConf;
|
||||||
_db = db;
|
_db = db;
|
||||||
_client = client;
|
_client = client;
|
||||||
_subsHandler = subsHandler;
|
_subsHandler = subsHandler;
|
||||||
_eb = eb;
|
_eb = eb;
|
||||||
_redis = redis;
|
_cache = cache;
|
||||||
_creds = creds;
|
|
||||||
|
|
||||||
_quotaKey = new TypedKey<bool>($"{_creds.RedisKey()}:quota:last_hourly_reset");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task OnReadyAsync()
|
public Task OnReadyAsync()
|
||||||
@@ -101,11 +98,10 @@ public sealed class PatronageService
|
|||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var lastRun = DateTime.MinValue;
|
var lastRun = DateTime.MinValue;
|
||||||
|
|
||||||
var rdb = _redis.GetDatabase();
|
var result = await _cache.GetAsync(_quotaKey);
|
||||||
var lastVal = await rdb.StringGetAsync(_quotaKey.Key);
|
if (result.TryGetValue(out var lastVal) && lastVal != default)
|
||||||
if (lastVal != default)
|
|
||||||
{
|
{
|
||||||
lastRun = DateTime.FromBinary((long)lastVal);
|
lastRun = DateTime.FromBinary(lastVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nowDate = now.ToDateOnly();
|
var nowDate = now.ToDateOnly();
|
||||||
@@ -130,8 +126,6 @@ public sealed class PatronageService
|
|||||||
HourlyCount = 0,
|
HourlyCount = 0,
|
||||||
DailyCount = 0,
|
DailyCount = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await rdb.StringSetAsync(_quotaKey.Key, true);
|
|
||||||
}
|
}
|
||||||
else if (now.Hour != lastRun.Hour) // if it's not, just reset hourly quotas
|
else if (now.Hour != lastRun.Hour) // if it's not, just reset hourly quotas
|
||||||
{
|
{
|
||||||
@@ -143,7 +137,7 @@ public sealed class PatronageService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// assumes that the code above runs in less than an hour
|
// assumes that the code above runs in less than an hour
|
||||||
await rdb.StringSetAsync(_quotaKey.Key, now.ToBinary());
|
await _cache.AddAsync(_quotaKey, now.ToBinary());
|
||||||
await tran.CommitAsync();
|
await tran.CommitAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@@ -1,23 +1,24 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
using NadekoBot.Modules.Utility.Common;
|
using NadekoBot.Modules.Utility.Common;
|
||||||
using Newtonsoft.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Utility.Services;
|
namespace NadekoBot.Modules.Utility.Services;
|
||||||
|
|
||||||
public class ConverterService : INService, IReadyExecutor
|
public class ConverterService : INService, IReadyExecutor
|
||||||
{
|
{
|
||||||
public ConvertUnit[] Units
|
private static readonly TypedKey<List<ConvertUnit>> _convertKey =
|
||||||
=> _cache.Redis.GetDatabase().StringGet("converter_units").ToString().MapJson<ConvertUnit[]>();
|
new("convert:units");
|
||||||
|
|
||||||
private readonly TimeSpan _updateInterval = new(12, 0, 0);
|
private readonly TimeSpan _updateInterval = new(12, 0, 0);
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IDataCache _cache;
|
private readonly IBotCache _cache;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
|
||||||
public ConverterService(
|
public ConverterService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
IDataCache cache,
|
IBotCache cache,
|
||||||
IHttpClientFactory factory)
|
IHttpClientFactory factory)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
@@ -48,7 +49,7 @@ public class ConverterService : INService, IReadyExecutor
|
|||||||
{
|
{
|
||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
var res = await http.GetStringAsync("https://convertapi.nadeko.bot/latest");
|
var res = await http.GetStringAsync("https://convertapi.nadeko.bot/latest");
|
||||||
return JsonConvert.DeserializeObject<Rates>(res);
|
return JsonSerializer.Deserialize<Rates>(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateCurrency()
|
private async Task UpdateCurrency()
|
||||||
@@ -61,29 +62,38 @@ public class ConverterService : INService, IReadyExecutor
|
|||||||
Modifier = decimal.One,
|
Modifier = decimal.One,
|
||||||
UnitType = unitTypeString
|
UnitType = unitTypeString
|
||||||
};
|
};
|
||||||
var range = currencyRates.ConversionRates.Select(u => new ConvertUnit
|
var units = currencyRates.ConversionRates.Select(u => new ConvertUnit
|
||||||
{
|
{
|
||||||
Triggers = new[] { u.Key },
|
Triggers = new[] { u.Key },
|
||||||
Modifier = u.Value,
|
Modifier = u.Value,
|
||||||
UnitType = unitTypeString
|
UnitType = unitTypeString
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToList();
|
||||||
|
|
||||||
var fileData = JsonConvert.DeserializeObject<ConvertUnit[]>(File.ReadAllText("data/units.json"))
|
var stream = File.OpenRead("data/units.json");
|
||||||
?.Where(x => x.UnitType != "currency");
|
var defaultUnits = await JsonSerializer.DeserializeAsync<ConvertUnit[]>(stream);
|
||||||
if (fileData is null)
|
if(defaultUnits is not null)
|
||||||
return;
|
units.AddRange(defaultUnits);
|
||||||
|
|
||||||
var data = JsonConvert.SerializeObject(range.Append(baseType).Concat(fileData).ToList());
|
units.Add(baseType);
|
||||||
_cache.Redis.GetDatabase().StringSet("converter_units", data);
|
|
||||||
|
await _cache.AddAsync(_convertKey, units);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ConvertUnit>> GetUnitsAsync()
|
||||||
|
=> (await _cache.GetAsync(_convertKey)).TryGetValue(out var list)
|
||||||
|
? list
|
||||||
|
: Array.Empty<ConvertUnit>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Rates
|
public class Rates
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("base")]
|
||||||
public string Base { get; set; }
|
public string Base { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("date")]
|
||||||
public DateTime Date { get; set; }
|
public DateTime Date { get; set; }
|
||||||
|
|
||||||
[JsonProperty("rates")]
|
[JsonPropertyName("rates")]
|
||||||
public Dictionary<string, decimal> ConversionRates { get; set; }
|
public Dictionary<string, decimal> ConversionRates { get; set; }
|
||||||
}
|
}
|
@@ -11,7 +11,7 @@ public partial class Utility
|
|||||||
[Cmd]
|
[Cmd]
|
||||||
public async partial Task ConvertList()
|
public async partial Task ConvertList()
|
||||||
{
|
{
|
||||||
var units = _service.Units;
|
var units = await _service.GetUnitsAsync();
|
||||||
|
|
||||||
var embed = _eb.Create().WithTitle(GetText(strs.convertlist)).WithOkColor();
|
var embed = _eb.Create().WithTitle(GetText(strs.convertlist)).WithOkColor();
|
||||||
|
|
||||||
@@ -29,9 +29,10 @@ public partial class Utility
|
|||||||
[Priority(0)]
|
[Priority(0)]
|
||||||
public async partial Task Convert(string origin, string target, decimal value)
|
public async partial Task Convert(string origin, string target, decimal value)
|
||||||
{
|
{
|
||||||
var originUnit = _service.Units.FirstOrDefault(x
|
var units = await _service.GetUnitsAsync();
|
||||||
|
var originUnit = units.FirstOrDefault(x
|
||||||
=> x.Triggers.Select(y => y.ToUpperInvariant()).Contains(origin.ToUpperInvariant()));
|
=> x.Triggers.Select(y => y.ToUpperInvariant()).Contains(origin.ToUpperInvariant()));
|
||||||
var targetUnit = _service.Units.FirstOrDefault(x
|
var targetUnit = units.FirstOrDefault(x
|
||||||
=> x.Triggers.Select(y => y.ToUpperInvariant()).Contains(target.ToUpperInvariant()));
|
=> x.Triggers.Select(y => y.ToUpperInvariant()).Contains(target.ToUpperInvariant()));
|
||||||
if (originUnit is null || targetUnit is null)
|
if (originUnit is null || targetUnit is null)
|
||||||
{
|
{
|
||||||
|
@@ -23,10 +23,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
public const int XP_REQUIRED_LVL_1 = 36;
|
public const int XP_REQUIRED_LVL_1 = 36;
|
||||||
|
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly CommandHandler _cmd;
|
|
||||||
private readonly IImageCache _images;
|
private readonly IImageCache _images;
|
||||||
private readonly IBotStrings _strings;
|
private readonly IBotStrings _strings;
|
||||||
private readonly IDataCache _cache;
|
|
||||||
private readonly FontProvider _fonts;
|
private readonly FontProvider _fonts;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCredentials _creds;
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
@@ -45,14 +43,15 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
|
|
||||||
private readonly TypedKey<bool> _xpTemplateReloadKey;
|
private readonly TypedKey<bool> _xpTemplateReloadKey;
|
||||||
private readonly IPatronageService _ps;
|
private readonly IPatronageService _ps;
|
||||||
|
private readonly IBotCache _c;
|
||||||
|
|
||||||
public XpService(
|
public XpService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
CommandHandler cmd,
|
|
||||||
Bot bot,
|
Bot bot,
|
||||||
DbService db,
|
DbService db,
|
||||||
IBotStrings strings,
|
IBotStrings strings,
|
||||||
IDataCache cache,
|
IImageCache images,
|
||||||
|
IBotCache c,
|
||||||
FontProvider fonts,
|
FontProvider fonts,
|
||||||
IBotCredentials creds,
|
IBotCredentials creds,
|
||||||
ICurrencyService cs,
|
ICurrencyService cs,
|
||||||
@@ -63,10 +62,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
IPatronageService ps)
|
IPatronageService ps)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_cmd = cmd;
|
_images = images;
|
||||||
_images = cache.LocalImages;
|
|
||||||
_strings = strings;
|
_strings = strings;
|
||||||
_cache = cache;
|
|
||||||
_fonts = fonts;
|
_fonts = fonts;
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
_cs = cs;
|
_cs = cs;
|
||||||
@@ -79,6 +76,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
_client = client;
|
_client = client;
|
||||||
_xpTemplateReloadKey = new("xp.template.reload");
|
_xpTemplateReloadKey = new("xp.template.reload");
|
||||||
_ps = ps;
|
_ps = ps;
|
||||||
|
_c = c;
|
||||||
|
|
||||||
InternalReloadXpTemplate();
|
InternalReloadXpTemplate();
|
||||||
|
|
||||||
@@ -453,10 +451,10 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
|
|
||||||
private Task Client_OnGuildAvailable(SocketGuild guild)
|
private Task Client_OnGuildAvailable(SocketGuild guild)
|
||||||
{
|
{
|
||||||
Task.Run(() =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
foreach (var channel in guild.VoiceChannels)
|
foreach (var channel in guild.VoiceChannels)
|
||||||
ScanChannelForVoiceXp(channel);
|
await ScanChannelForVoiceXp(channel);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -467,33 +465,33 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
if (socketUser is not SocketGuildUser user || user.IsBot)
|
if (socketUser is not SocketGuildUser user || user.IsBot)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
if (before.VoiceChannel is not null)
|
if (before.VoiceChannel is not null)
|
||||||
ScanChannelForVoiceXp(before.VoiceChannel);
|
await ScanChannelForVoiceXp(before.VoiceChannel);
|
||||||
|
|
||||||
if (after.VoiceChannel is not null && after.VoiceChannel != before.VoiceChannel)
|
if (after.VoiceChannel is not null && after.VoiceChannel != before.VoiceChannel)
|
||||||
ScanChannelForVoiceXp(after.VoiceChannel);
|
await ScanChannelForVoiceXp(after.VoiceChannel);
|
||||||
else if (after.VoiceChannel is null)
|
else if (after.VoiceChannel is null)
|
||||||
// In this case, the user left the channel and the previous for loops didn't catch
|
// In this case, the user left the channel and the previous for loops didn't catch
|
||||||
// it because it wasn't in any new channel. So we need to get rid of it.
|
// it because it wasn't in any new channel. So we need to get rid of it.
|
||||||
UserLeftVoiceChannel(user, before.VoiceChannel);
|
await UserLeftVoiceChannel(user, before.VoiceChannel);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ScanChannelForVoiceXp(SocketVoiceChannel channel)
|
private async Task ScanChannelForVoiceXp(SocketVoiceChannel channel)
|
||||||
{
|
{
|
||||||
if (ShouldTrackVoiceChannel(channel))
|
if (ShouldTrackVoiceChannel(channel))
|
||||||
{
|
{
|
||||||
foreach (var user in channel.Users)
|
foreach (var user in channel.Users)
|
||||||
ScanUserForVoiceXp(user, channel);
|
await ScanUserForVoiceXp(user, channel);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var user in channel.Users)
|
foreach (var user in channel.Users)
|
||||||
UserLeftVoiceChannel(user, channel);
|
await UserLeftVoiceChannel(user, channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,12 +500,12 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <param name="channel"></param>
|
/// <param name="channel"></param>
|
||||||
private void ScanUserForVoiceXp(SocketGuildUser user, SocketVoiceChannel channel)
|
private async Task ScanUserForVoiceXp(SocketGuildUser user, SocketVoiceChannel channel)
|
||||||
{
|
{
|
||||||
if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel.Id))
|
if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel.Id))
|
||||||
UserJoinedVoiceChannel(user);
|
await UserJoinedVoiceChannel(user);
|
||||||
else
|
else
|
||||||
UserLeftVoiceChannel(user, channel);
|
await UserLeftVoiceChannel(user, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldTrackVoiceChannel(SocketVoiceChannel channel)
|
private bool ShouldTrackVoiceChannel(SocketVoiceChannel channel)
|
||||||
@@ -516,32 +514,31 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
private bool UserParticipatingInVoiceChannel(SocketGuildUser user)
|
private bool UserParticipatingInVoiceChannel(SocketGuildUser user)
|
||||||
=> !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted;
|
=> !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted;
|
||||||
|
|
||||||
private void UserJoinedVoiceChannel(SocketGuildUser user)
|
private TypedKey<long> GetVoiceXpKey(ulong userId)
|
||||||
|
=> new($"xp:vc_join:{userId}");
|
||||||
|
|
||||||
|
private async Task UserJoinedVoiceChannel(SocketGuildUser user)
|
||||||
{
|
{
|
||||||
var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
|
|
||||||
var value = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
var value = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
|
||||||
_cache.Redis.GetDatabase()
|
await _c.AddAsync(GetVoiceXpKey(user.Id),
|
||||||
.StringSet(key,
|
value,
|
||||||
value,
|
TimeSpan.FromMinutes(_xpConfig.Data.VoiceMaxMinutes),
|
||||||
TimeSpan.FromMinutes(_xpConfig.Data.VoiceMaxMinutes),
|
overwrite: false);
|
||||||
when: When.NotExists);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
|
private async Task UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
|
||||||
{
|
{
|
||||||
var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
|
var key = GetVoiceXpKey(user.Id);
|
||||||
var value = _cache.Redis.GetDatabase().StringGet(key);
|
var result = await _c.GetAsync(key);
|
||||||
_cache.Redis.GetDatabase().KeyDelete(key);
|
if (!await _c.RemoveAsync(key))
|
||||||
|
return;
|
||||||
|
|
||||||
// Allow for if this function gets called multiple times when a user leaves a channel.
|
// Allow for if this function gets called multiple times when a user leaves a channel.
|
||||||
if (value.IsNull)
|
if (!result.TryGetValue(out var unixTime))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!value.TryParse(out long startUnixTime))
|
var dateStart = DateTimeOffset.FromUnixTimeSeconds(unixTime);
|
||||||
return;
|
|
||||||
|
|
||||||
var dateStart = DateTimeOffset.FromUnixTimeSeconds(startUnixTime);
|
|
||||||
var dateEnd = DateTimeOffset.UtcNow;
|
var dateEnd = DateTimeOffset.UtcNow;
|
||||||
var minutes = (dateEnd - dateStart).TotalMinutes;
|
var minutes = (dateEnd - dateStart).TotalMinutes;
|
||||||
var xp = _xpConfig.Data.VoiceXpPerMinute * minutes;
|
var xp = _xpConfig.Data.VoiceXpPerMinute * minutes;
|
||||||
@@ -577,7 +574,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
if (arg.Author is not SocketGuildUser user || user.IsBot)
|
if (arg.Author is not SocketGuildUser user || user.IsBot)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
_ = Task.Run(() =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
if (!ShouldTrackXp(user, arg.Channel.Id))
|
if (!ShouldTrackXp(user, arg.Channel.Id))
|
||||||
return;
|
return;
|
||||||
@@ -593,7 +590,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
if (xp <= 0)
|
if (xp <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!SetUserRewarded(user.Id))
|
if (!await SetUserRewardedAsync(user.Id))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_addMessageXp.Enqueue(new()
|
_addMessageXp.Enqueue(new()
|
||||||
@@ -650,16 +647,14 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
return Enumerable.Empty<ulong>();
|
return Enumerable.Empty<ulong>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool SetUserRewarded(ulong userId)
|
private static TypedKey<bool> GetUserRewKey(ulong userId)
|
||||||
{
|
=> new($"xp:user_gain:{userId}");
|
||||||
var r = _cache.Redis.GetDatabase();
|
|
||||||
var key = $"{_creds.RedisKey()}_user_xp_gain_{userId}";
|
|
||||||
|
|
||||||
return r.StringSet(key,
|
private async Task<bool> SetUserRewardedAsync(ulong userId)
|
||||||
|
=> await _c.AddAsync(GetUserRewKey(userId),
|
||||||
true,
|
true,
|
||||||
TimeSpan.FromMinutes(_xpConfig.Data.MessageXpCooldown),
|
expiry: TimeSpan.FromMinutes(_xpConfig.Data.MessageXpCooldown),
|
||||||
when: When.NotExists);
|
overwrite: false);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FullUserStats> GetUserStatsAsync(IGuildUser user)
|
public async Task<FullUserStats> GetUserStatsAsync(IGuildUser user)
|
||||||
{
|
{
|
||||||
@@ -782,7 +777,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
}
|
}
|
||||||
}.WithFallbackFonts(_fonts.FallBackFonts);
|
}.WithFallbackFonts(_fonts.FallBackFonts);
|
||||||
|
|
||||||
using var img = Image.Load<Rgba32>(_images.XpBackground, out var imageFormat);
|
using var img = Image.Load<Rgba32>(await GetXpBackgroundAsync(stats.User.UserId), out var imageFormat);
|
||||||
if (template.User.Name.Show)
|
if (template.User.Name.Show)
|
||||||
{
|
{
|
||||||
var fontSize = (int)(template.User.Name.FontSize * 0.9);
|
var fontSize = (int)(template.User.Name.FontSize * 0.9);
|
||||||
@@ -979,8 +974,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
{
|
{
|
||||||
var avatarUrl = stats.User.RealAvatarUrl();
|
var avatarUrl = stats.User.RealAvatarUrl();
|
||||||
|
|
||||||
var (succ, data) = await _cache.TryGetImageDataAsync(avatarUrl);
|
var result = await _c.GetImageDataAsync(avatarUrl);
|
||||||
if (!succ)
|
if (!result.TryPickT0(out var data, out _))
|
||||||
{
|
{
|
||||||
using (var http = _httpFactory.CreateClient())
|
using (var http = _httpFactory.CreateClient())
|
||||||
{
|
{
|
||||||
@@ -999,7 +994,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cache.SetImageDataAsync(avatarUrl, data);
|
await _c.SetImageDataAsync(avatarUrl, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var toDraw = Image.Load(data);
|
using var toDraw = Image.Load(data);
|
||||||
@@ -1033,7 +1028,13 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
return output;
|
return output;
|
||||||
});
|
});
|
||||||
|
|
||||||
// #if GLOBAL_NADEKO
|
private async Task<byte[]> GetXpBackgroundAsync(ulong userId)
|
||||||
|
{
|
||||||
|
var img = await _images.GetXpBackgroundImageAsync();
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #if GLOBAL_NADEKO
|
||||||
private async Task DrawFrame(Image<Rgba32> img, ulong userId)
|
private async Task DrawFrame(Image<Rgba32> img, ulong userId)
|
||||||
{
|
{
|
||||||
var patron = await _ps.GetPatronAsync(userId);
|
var patron = await _ps.GetPatronAsync(userId);
|
||||||
@@ -1103,8 +1104,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var imgUrl = new Uri(stats.User.Club.ImageUrl);
|
var imgUrl = new Uri(stats.User.Club.ImageUrl);
|
||||||
var (succ, data) = await _cache.TryGetImageDataAsync(imgUrl);
|
var result = await _c.GetImageDataAsync(imgUrl);
|
||||||
if (!succ)
|
if (!result.TryPickT0(out var data, out _))
|
||||||
{
|
{
|
||||||
using (var http = _httpFactory.CreateClient())
|
using (var http = _httpFactory.CreateClient())
|
||||||
using (var temp = await http.GetAsync(imgUrl, HttpCompletionOption.ResponseHeadersRead))
|
using (var temp = await http.GetAsync(imgUrl, HttpCompletionOption.ResponseHeadersRead))
|
||||||
@@ -1127,7 +1128,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _cache.SetImageDataAsync(imgUrl, data);
|
await _c.SetImageDataAsync(imgUrl, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var toDraw = Image.Load(data);
|
using var toDraw = Image.Load(data);
|
||||||
|
@@ -52,6 +52,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
|
<PackageReference Include="NonBlocking" Version="2.0.0" />
|
||||||
<PackageReference Include="OneOf" Version="3.0.216" />
|
<PackageReference Include="OneOf" Version="3.0.216" />
|
||||||
<PackageReference Include="Scrutor" Version="4.1.0" />
|
<PackageReference Include="Scrutor" Version="4.1.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||||
|
@@ -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();
|
|
||||||
}
|
|
@@ -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();
|
|
||||||
}
|
|
@@ -6,8 +6,8 @@ namespace NadekoBot.Services;
|
|||||||
|
|
||||||
public interface ILocalDataCache
|
public interface ILocalDataCache
|
||||||
{
|
{
|
||||||
IReadOnlyDictionary<string, SearchPokemon> Pokemons { get; }
|
Task<IReadOnlyDictionary<string, SearchPokemon>> GetPokemonsAsync();
|
||||||
IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities { get; }
|
Task<IReadOnlyDictionary<string, SearchPokemonAbility>> GetPokemonAbilitiesAsync();
|
||||||
IReadOnlyDictionary<int, string> PokemonMap { get; }
|
Task<TriviaQuestionModel[]> GetTriviaQuestionsAsync();
|
||||||
TriviaQuestion[] TriviaQuestions { get; }
|
Task<PokemonNameId[]> GetPokemonMapAsync();
|
||||||
}
|
}
|
@@ -172,9 +172,10 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
|||||||
if (File.Exists(CREDS_FILE_NAME))
|
if (File.Exists(CREDS_FILE_NAME))
|
||||||
{
|
{
|
||||||
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(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));
|
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
82
src/NadekoBot/Services/Impl/LocalDataCache.cs
Normal file
82
src/NadekoBot/Services/Impl/LocalDataCache.cs
Normal 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);
|
||||||
|
}
|
@@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,297 +1,111 @@
|
|||||||
#nullable disable
|
using NadekoBot.Common.Configs;
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
|
||||||
using NadekoBot.Common.Yml;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using StackExchange.Redis;
|
|
||||||
|
|
||||||
namespace NadekoBot.Services;
|
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,
|
public interface IImageCache
|
||||||
SlotEmojis,
|
{
|
||||||
Currency,
|
Task<byte[]?> GetHeadsImageAsync();
|
||||||
RategirlMatrix,
|
Task<byte[]?> GetTailsImageAsync();
|
||||||
RategirlDot,
|
Task<byte[]?> GetCurrencyImageAsync();
|
||||||
RipOverlay,
|
Task<byte[]?> GetXpBackgroundImageAsync();
|
||||||
RipBg,
|
Task<byte[]?> GetRategirlBgAsync();
|
||||||
XpBg
|
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 static TypedKey<byte[]> GetImageKey(Uri url)
|
||||||
private const string CARDS_PATH = $"{BASE_PATH}images/cards";
|
=> new($"image:{url}");
|
||||||
|
|
||||||
private IDatabase Db
|
private async Task<byte[]?> GetImageDataAsync(Uri url)
|
||||||
=> _con.GetDatabase();
|
=> await _cache.GetOrAddAsync(
|
||||||
|
GetImageKey(url),
|
||||||
public ImageUrls ImageUrls { get; private set; }
|
async () =>
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var newData = new ImageUrls
|
using var http = _httpFactory.CreateClient();
|
||||||
{
|
var bytes = await http.GetByteArrayAsync(url);
|
||||||
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);
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
},
|
||||||
catch (Exception ex)
|
expiry: 48.Hours());
|
||||||
{
|
|
||||||
Log.Warning(ex, "Failed reading image bytes from uri: {Uri}", uri.ToString());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
private async Task<byte[]?> GetRandomImageDataAsync(Uri[] urls)
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
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()
|
public Task<byte[]?> GetHeadsImageAsync()
|
||||||
=> Enum.GetValues<ImageKeys>();
|
=> GetRandomImageDataAsync(_ic.Data.Coins.Heads);
|
||||||
|
|
||||||
private byte[][] GetByteArrayData(ImageKeys key)
|
public Task<byte[]?> GetTailsImageAsync()
|
||||||
=> Db.ListRange(GetRedisKey(key)).Map(x => (byte[])x);
|
=> GetRandomImageDataAsync(_ic.Data.Coins.Tails);
|
||||||
|
|
||||||
private byte[] GetByteData(ImageKeys key)
|
public Task<byte[]?> GetCurrencyImageAsync()
|
||||||
=> Db.StringGet(GetRedisKey(key));
|
=> GetRandomImageDataAsync(_ic.Data.Currency);
|
||||||
|
|
||||||
private RedisKey GetRedisKey(ImageKeys key)
|
public Task<byte[]?> GetXpBackgroundImageAsync()
|
||||||
=> _creds.RedisKey() + "_image_" + key;
|
=> 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);
|
||||||
|
}
|
||||||
|
@@ -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));
|
|
||||||
}
|
|
@@ -1,13 +1,13 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
namespace NadekoBot.Services;
|
namespace NadekoBot.Services;
|
||||||
|
|
||||||
public class LocalBotStringsProvider : IBotStringsProvider
|
public class MemoryBotStringsProvider : IBotStringsProvider
|
||||||
{
|
{
|
||||||
private readonly IStringsSource _source;
|
private readonly IStringsSource _source;
|
||||||
private IReadOnlyDictionary<string, Dictionary<string, string>> responseStrings;
|
private IReadOnlyDictionary<string, Dictionary<string, string>> responseStrings;
|
||||||
private IReadOnlyDictionary<string, Dictionary<string, CommandStrings>> commandStrings;
|
private IReadOnlyDictionary<string, Dictionary<string, CommandStrings>> commandStrings;
|
||||||
|
|
||||||
public LocalBotStringsProvider(IStringsSource source)
|
public MemoryBotStringsProvider(IStringsSource source)
|
||||||
{
|
{
|
||||||
_source = source;
|
_source = source;
|
||||||
Reload();
|
Reload();
|
@@ -32,7 +32,7 @@ public static class ArrayExtensions
|
|||||||
public static TOut[] Map<TIn, TOut>(this TIn[] arr, Func<TIn, TOut> f)
|
public static TOut[] Map<TIn, TOut>(this TIn[] arr, Func<TIn, TOut> f)
|
||||||
=> Array.ConvertAll(arr, x => f(x));
|
=> Array.ConvertAll(arr, x => f(x));
|
||||||
|
|
||||||
public static IReadOnlyCollection<TOut> Map<TIn, TOut>(this IReadOnlyCollection<TIn> col, Func<TIn, TOut> f)
|
public static TOut[] Map<TIn, TOut>(this IReadOnlyCollection<TIn> col, Func<TIn, TOut> f)
|
||||||
{
|
{
|
||||||
var toReturn = new TOut[col.Count];
|
var toReturn = new TOut[col.Count];
|
||||||
|
|
||||||
|
@@ -37,8 +37,9 @@ public static class Extensions
|
|||||||
_ => throw new ArgumentOutOfRangeException(nameof(text))
|
_ => throw new ArgumentOutOfRangeException(nameof(text))
|
||||||
};
|
};
|
||||||
|
|
||||||
public static List<ulong> GetGuildIds(this DiscordSocketClient client)
|
public static ulong[] GetGuildIds(this DiscordSocketClient client)
|
||||||
=> client.Guilds.Select(x => x.Id).ToList();
|
=> client.Guilds
|
||||||
|
.Map(x => x.Id);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a string in the format HHH:mm if timespan is >= 2m.
|
/// Generates a string in the format HHH:mm if timespan is >= 2m.
|
||||||
|
@@ -9,10 +9,10 @@ namespace NadekoBot.Extensions;
|
|||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddBotStringsServices(this IServiceCollection services, int totalShards)
|
public static IServiceCollection AddBotStringsServices(this IServiceCollection services, BotCacheImplemenation botCache)
|
||||||
=> totalShards <= 1
|
=> botCache == BotCacheImplemenation.Memory
|
||||||
? services.AddSingleton<IStringsSource, LocalFileStringsSource>()
|
? services.AddSingleton<IStringsSource, LocalFileStringsSource>()
|
||||||
.AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
|
.AddSingleton<IBotStringsProvider, MemoryBotStringsProvider>()
|
||||||
.AddSingleton<IBotStrings, BotStrings>()
|
.AddSingleton<IBotStrings, BotStrings>()
|
||||||
: services.AddSingleton<IStringsSource, LocalFileStringsSource>()
|
: services.AddSingleton<IStringsSource, LocalFileStringsSource>()
|
||||||
.AddSingleton<IBotStringsProvider, RedisBotStringsProvider>()
|
.AddSingleton<IBotStringsProvider, RedisBotStringsProvider>()
|
||||||
@@ -23,17 +23,6 @@ public static class ServiceCollectionExtensions
|
|||||||
services.Scan(x => x.FromCallingAssembly()
|
services.Scan(x => x.FromCallingAssembly()
|
||||||
.AddClasses(f => f.AssignableTo(typeof(ConfigServiceBase<>)))
|
.AddClasses(f => f.AssignableTo(typeof(ConfigServiceBase<>)))
|
||||||
.AsSelfWithInterfaces());
|
.AsSelfWithInterfaces());
|
||||||
|
|
||||||
// var baseType = typeof(ConfigServiceBase<>);
|
|
||||||
//
|
|
||||||
// foreach (var type in Assembly.GetCallingAssembly().ExportedTypes.Where(x => x.IsSealed))
|
|
||||||
// {
|
|
||||||
// if (type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == baseType)
|
|
||||||
// {
|
|
||||||
// services.AddSingleton(type);
|
|
||||||
// services.AddSingleton(x => (IConfigService)x.GetRequiredService(type));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -48,7 +37,7 @@ public static class ServiceCollectionExtensions
|
|||||||
.AddSingleton<ISoundcloudResolver, SoundcloudResolver>()
|
.AddSingleton<ISoundcloudResolver, SoundcloudResolver>()
|
||||||
.AddSingleton<ILocalTrackResolver, LocalTrackResolver>()
|
.AddSingleton<ILocalTrackResolver, LocalTrackResolver>()
|
||||||
.AddSingleton<IRadioResolver, RadioResolver>()
|
.AddSingleton<IRadioResolver, RadioResolver>()
|
||||||
.AddSingleton<ITrackCacher, RedisTrackCacher>()
|
.AddSingleton<ITrackCacher, TrackCacher>()
|
||||||
.AddSingleton<YtLoader>()
|
.AddSingleton<YtLoader>()
|
||||||
.AddSingleton<IPlaceholderProvider>(svc => svc.GetRequiredService<IMusicService>());
|
.AddSingleton<IPlaceholderProvider>(svc => svc.GetRequiredService<IMusicService>());
|
||||||
|
|
||||||
@@ -65,10 +54,23 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddRedis(this IServiceCollection services, string redisOptions)
|
public static IServiceCollection AddCache(this IServiceCollection services, IBotCredentials creds)
|
||||||
{
|
{
|
||||||
var conf = ConfigurationOptions.Parse(redisOptions);
|
if (creds.BotCache == BotCacheImplemenation.Redis)
|
||||||
services.AddSingleton(ConnectionMultiplexer.Connect(conf));
|
{
|
||||||
return services;
|
var conf = ConfigurationOptions.Parse(creds.RedisOptions);
|
||||||
|
services.AddSingleton(ConnectionMultiplexer.Connect(conf))
|
||||||
|
.AddSingleton<IBotCache, RedisBotCache>()
|
||||||
|
.AddSingleton<IPubSub, RedisPubSub>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddSingleton<IBotCache, MemoryBotCache>()
|
||||||
|
.AddSingleton<IPubSub, EventPubSub>();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return services
|
||||||
|
.AddBotStringsServices(creds.BotCache);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -30,9 +30,9 @@ public static class UserExtensions
|
|||||||
=> usr.AvatarId is null ? new(usr.GetDefaultAvatarUrl()) : new Uri(usr.GetAvatarUrl(ImageFormat.Auto, size));
|
=> usr.AvatarId is null ? new(usr.GetDefaultAvatarUrl()) : new Uri(usr.GetAvatarUrl(ImageFormat.Auto, size));
|
||||||
|
|
||||||
// This method is only used for the xp card
|
// This method is only used for the xp card
|
||||||
public static Uri? RealAvatarUrl(this DiscordUser usr)
|
public static Uri RealAvatarUrl(this DiscordUser usr)
|
||||||
=> usr.AvatarId is null
|
=> usr.AvatarId is null
|
||||||
? null
|
? new(CDN.GetDefaultUserAvatarUrl(ushort.Parse(usr.Discriminator)))
|
||||||
: new Uri(usr.AvatarId.StartsWith("a_", StringComparison.InvariantCulture)
|
: new Uri(usr.AvatarId.StartsWith("a_", StringComparison.InvariantCulture)
|
||||||
? $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.gif"
|
? $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.gif"
|
||||||
: $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.png");
|
: $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.png");
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
# DO NOT CHANGE
|
# DO NOT CHANGE
|
||||||
version: 5
|
version: 6
|
||||||
# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/
|
# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/
|
||||||
token: ''
|
token: ''
|
||||||
# List of Ids of the users who have bot owner permissions
|
# List of Ids of the users who have bot owner permissions
|
||||||
@@ -9,6 +9,9 @@ ownerIds: []
|
|||||||
usePrivilegedIntents: true
|
usePrivilegedIntents: true
|
||||||
# The number of shards that the bot will be running on.
|
# The number of shards that the bot will be running on.
|
||||||
# Leave at 1 if you don't know what you're doing.
|
# 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.
|
||||||
totalShards: 1
|
totalShards: 1
|
||||||
# Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
|
# 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.
|
# Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
|
||||||
@@ -53,7 +56,12 @@ patreon:
|
|||||||
botListToken: ''
|
botListToken: ''
|
||||||
# Official cleverbot api key.
|
# Official cleverbot api key.
|
||||||
cleverbotApiKey: ''
|
cleverbotApiKey: ''
|
||||||
|
# Which cache implementation should bot use.
|
||||||
|
# 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
|
||||||
|
# 'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
|
||||||
|
botCache: Memory
|
||||||
# Redis connection string. Don't change if you don't know what you're doing.
|
# Redis connection string. Don't change if you don't know what you're doing.
|
||||||
|
# Only used if botCache is set to 'redis'
|
||||||
redisOptions: localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=
|
redisOptions: localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=
|
||||||
# Database options. Don't change if you don't know what you're doing. Leave null for default values
|
# Database options. Don't change if you don't know what you're doing. Leave null for default values
|
||||||
db:
|
db:
|
||||||
@@ -99,7 +107,7 @@ twitchClientSecret:
|
|||||||
# args: "NadekoBot.dll -- {0}"
|
# args: "NadekoBot.dll -- {0}"
|
||||||
# Windows default
|
# Windows default
|
||||||
# cmd: NadekoBot.exe
|
# cmd: NadekoBot.exe
|
||||||
# args: {0}
|
# args: "{0}"
|
||||||
restartCommand:
|
restartCommand:
|
||||||
cmd:
|
cmd:
|
||||||
args:
|
args:
|
||||||
|
@@ -862,8 +862,6 @@ unsetmusicchannel:
|
|||||||
musicquality:
|
musicquality:
|
||||||
- musicquality
|
- musicquality
|
||||||
- mquality
|
- mquality
|
||||||
imagesreload:
|
|
||||||
- imagesreload
|
|
||||||
stringsreload:
|
stringsreload:
|
||||||
- stringsreload
|
- stringsreload
|
||||||
shardstats:
|
shardstats:
|
||||||
|
@@ -24,7 +24,7 @@ quotas:
|
|||||||
# Dictionary of commands with their respective quota data
|
# Dictionary of commands with their respective quota data
|
||||||
commands:
|
commands:
|
||||||
cleverbot:
|
cleverbot:
|
||||||
V: null
|
V:
|
||||||
prune:
|
prune:
|
||||||
X:
|
X:
|
||||||
PerHour: 1
|
PerHour: 1
|
||||||
|
@@ -1496,10 +1496,6 @@ musicquality:
|
|||||||
- ""
|
- ""
|
||||||
- "High"
|
- "High"
|
||||||
- "Low"
|
- "Low"
|
||||||
imagesreload:
|
|
||||||
desc: "Reloads images bot is using. Safe to use even when bot is being used heavily."
|
|
||||||
args:
|
|
||||||
- ""
|
|
||||||
stringsreload:
|
stringsreload:
|
||||||
desc: "Reloads localized bot strings."
|
desc: "Reloads localized bot strings."
|
||||||
args:
|
args:
|
||||||
|
Reference in New Issue
Block a user