mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 08:34:27 -05:00 
			
		
		
		
	Abstract away cache. 2 implementations: redis and memory
This commit is contained in:
		@@ -15,22 +15,24 @@ public sealed class RatelimitAttribute : PreconditionAttribute
 | 
			
		||||
        Seconds = seconds;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override Task<PreconditionResult> CheckPermissionsAsync(
 | 
			
		||||
    public override async Task<PreconditionResult> CheckPermissionsAsync(
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        CommandInfo command,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
        if (Seconds == 0)
 | 
			
		||||
            return Task.FromResult(PreconditionResult.FromSuccess());
 | 
			
		||||
            return PreconditionResult.FromSuccess();
 | 
			
		||||
 | 
			
		||||
        var cache = services.GetRequiredService<IDataCache>();
 | 
			
		||||
        var rem = cache.TryAddRatelimit(context.User.Id, command.Name, Seconds);
 | 
			
		||||
        var cache = services.GetRequiredService<IBotCache>();
 | 
			
		||||
        var rem = await cache.GetRatelimitAsync(
 | 
			
		||||
            new($"precondition:{context.User.Id}:{command.Name}"),
 | 
			
		||||
            Seconds.Seconds());
 | 
			
		||||
 | 
			
		||||
        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.";
 | 
			
		||||
 | 
			
		||||
        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>
 | 
			
		||||
{
 | 
			
		||||
    [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
 | 
			
		||||
next to the response. The color depends whether the command
 | 
			
		||||
@@ -182,4 +182,4 @@ public enum ConsoleOutputType
 | 
			
		||||
    Normal = 0,
 | 
			
		||||
    Simple = 1,
 | 
			
		||||
    None = 2
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,10 @@ public sealed class Creds : IBotCredentials
 | 
			
		||||
    public bool UsePrivilegedIntents { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(@"The number of shards that the bot will be running on.
 | 
			
		||||
Leave at 1 if you don't know what you're doing.")]
 | 
			
		||||
Leave at 1 if you don't know what you're doing.
 | 
			
		||||
 | 
			
		||||
note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
 | 
			
		||||
      Also, in that case you should be using NadekoBot.Coordinator to start the bot, and it will correctly override this value.")]
 | 
			
		||||
    public int TotalShards { get; set; }
 | 
			
		||||
 | 
			
		||||
    [Comment(   
 | 
			
		||||
@@ -50,8 +53,14 @@ go to https://www.patreon.com/portal -> my clients -> create client")]
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Official cleverbot api key.")]
 | 
			
		||||
    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; }
 | 
			
		||||
 | 
			
		||||
    [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}""
 | 
			
		||||
Windows default
 | 
			
		||||
    cmd: NadekoBot.exe
 | 
			
		||||
    args: {0}")]
 | 
			
		||||
    args: ""{0}""")]
 | 
			
		||||
    public RestartConfig RestartCommand { get; set; }
 | 
			
		||||
 | 
			
		||||
    public Creds()
 | 
			
		||||
    {
 | 
			
		||||
        Version = 5;
 | 
			
		||||
        Version = 6;
 | 
			
		||||
        Token = string.Empty;
 | 
			
		||||
        UsePrivilegedIntents = true;
 | 
			
		||||
        OwnerIds = new List<ulong>();
 | 
			
		||||
@@ -119,6 +128,7 @@ Windows default
 | 
			
		||||
        Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty);
 | 
			
		||||
        BotListToken = string.Empty;
 | 
			
		||||
        CleverbotApiKey = string.Empty;
 | 
			
		||||
        BotCache = BotCacheImplemenation.Memory;
 | 
			
		||||
        RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
 | 
			
		||||
        Db = new()
 | 
			
		||||
        {
 | 
			
		||||
@@ -217,4 +227,10 @@ public class GoogleApiConfig
 | 
			
		||||
{
 | 
			
		||||
    public string SearchId { get; init; }
 | 
			
		||||
    public string ImageSearchId { get; init; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum BotCacheImplemenation
 | 
			
		||||
{
 | 
			
		||||
    Memory,
 | 
			
		||||
    Redis
 | 
			
		||||
}
 | 
			
		||||
@@ -26,6 +26,7 @@ public interface IBotCredentials
 | 
			
		||||
    string TwitchClientId { get; set; }
 | 
			
		||||
    string TwitchClientSecret { get; set; }
 | 
			
		||||
    GoogleApiConfig Google { get; set; }
 | 
			
		||||
    BotCacheImplemenation BotCache { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class RestartConfig
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.Yml;
 | 
			
		||||
using Cloneable;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public class ImageUrls
 | 
			
		||||
[Cloneable]
 | 
			
		||||
public partial class ImageUrls : ICloneable<ImageUrls> 
 | 
			
		||||
{
 | 
			
		||||
    [Comment("DO NOT CHANGE")]
 | 
			
		||||
    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
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.Pokemon;
 | 
			
		||||
 | 
			
		||||
public class SearchPokemon
 | 
			
		||||
{
 | 
			
		||||
    [JsonProperty("num")]
 | 
			
		||||
    [JsonPropertyName("num")]
 | 
			
		||||
    public int Id { get; set; }
 | 
			
		||||
 | 
			
		||||
    public string Species { get; set; }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user