Added some common files
This commit is contained in:
parent
06492758ec
commit
8157d92fdf
30 changed files with 1136 additions and 0 deletions
12
src/EllieBot/Common/Attributes/Aliases.cs
Normal file
12
src/EllieBot/Common/Attributes/Aliases.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public sealed class AliasesAttribute : AliasAttribute
|
||||||
|
{
|
||||||
|
public AliasesAttribute([CallerMemberName] string memberName = "")
|
||||||
|
: base(CommandNameLoadHelper.GetAliasesFor(memberName))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
31
src/EllieBot/Common/Attributes/CommandNameLoadHelper.cs
Normal file
31
src/EllieBot/Common/Attributes/CommandNameLoadHelper.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Attributes;
|
||||||
|
|
||||||
|
public static class CommandNameLoadHelper
|
||||||
|
{
|
||||||
|
private static readonly IDeserializer _deserializer = new Deserializer();
|
||||||
|
|
||||||
|
private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases
|
||||||
|
= new(() => LoadAliases());
|
||||||
|
|
||||||
|
public static Dictionary<string, string[]> LoadAliases(string aliasesFilePath = "data/aliases.yml")
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText(aliasesFilePath);
|
||||||
|
return _deserializer.Deserialize<Dictionary<string, string[]>>(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] GetAliasesFor(string methodName)
|
||||||
|
=> _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
|
||||||
|
? aliases.Skip(1).ToArray()
|
||||||
|
: Array.Empty<string>();
|
||||||
|
|
||||||
|
public static string GetCommandNameFor(string methodName)
|
||||||
|
{
|
||||||
|
methodName = methodName.ToLowerInvariant();
|
||||||
|
var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
|
||||||
|
? aliases[0]
|
||||||
|
: methodName;
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
#nullable disable
|
||||||
|
namespace EllieBot.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Classed marked with this attribute will not be added to the service provider
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public class DontAddToIocContainerAttribute : Attribute
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
18
src/EllieBot/Common/Attributes/EllieCommand.cs
Normal file
18
src/EllieBot/Common/Attributes/EllieCommand.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public sealed class CmdAttribute : CommandAttribute
|
||||||
|
{
|
||||||
|
public string MethodName { get; }
|
||||||
|
|
||||||
|
public CmdAttribute([CallerMemberName] string memberName = "")
|
||||||
|
: base(CommandNameLoadHelper.GetCommandNameFor(memberName))
|
||||||
|
{
|
||||||
|
MethodName = memberName.ToLowerInvariant();
|
||||||
|
Aliases = CommandNameLoadHelper.GetAliasesFor(memberName);
|
||||||
|
Remarks = memberName.ToLowerInvariant();
|
||||||
|
Summary = memberName.ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
10
src/EllieBot/Common/Attributes/EllieOptions.cs
Normal file
10
src/EllieBot/Common/Attributes/EllieOptions.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace EllieBot.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public sealed class EllieOptionsAttribute : Attribute
|
||||||
|
{
|
||||||
|
public Type OptionType { get; set; }
|
||||||
|
|
||||||
|
public EllieOptionsAttribute(Type t)
|
||||||
|
=> OptionType = t;
|
||||||
|
}
|
38
src/EllieBot/Common/Attributes/NoPublicBotPrecondition.cs
Normal file
38
src/EllieBot/Common/Attributes/NoPublicBotPrecondition.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#nullable disable
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace EllieBot.Common;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||||
|
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
|
||||||
|
public sealed class NoPublicBotAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
public override Task<PreconditionResult> CheckPermissionsAsync(
|
||||||
|
ICommandContext context,
|
||||||
|
CommandInfo command,
|
||||||
|
IServiceProvider services)
|
||||||
|
{
|
||||||
|
#if GLOBAL_ELLIE
|
||||||
|
return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://docs.elliebot.net/v4/)."));
|
||||||
|
#else
|
||||||
|
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||||
|
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
|
||||||
|
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
public override Task<PreconditionResult> CheckPermissionsAsync(
|
||||||
|
ICommandContext context,
|
||||||
|
CommandInfo command,
|
||||||
|
IServiceProvider services)
|
||||||
|
{
|
||||||
|
#if GLOBAL_ELLIE || DEBUG
|
||||||
|
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
#else
|
||||||
|
return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
19
src/EllieBot/Common/Attributes/OwnerOnlyAttribute.cs
Normal file
19
src/EllieBot/Common/Attributes/OwnerOnlyAttribute.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||||
|
public sealed class OwnerOnlyAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
public override Task<PreconditionResult> CheckPermissionsAsync(
|
||||||
|
ICommandContext context,
|
||||||
|
CommandInfo command,
|
||||||
|
IServiceProvider services)
|
||||||
|
{
|
||||||
|
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
|
||||||
|
|
||||||
|
return Task.FromResult(creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id
|
||||||
|
? PreconditionResult.FromSuccess()
|
||||||
|
: PreconditionResult.FromError("Not owner"));
|
||||||
|
}
|
||||||
|
}
|
38
src/EllieBot/Common/Attributes/Ratelimit.cs
Normal file
38
src/EllieBot/Common/Attributes/Ratelimit.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public sealed class RatelimitAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
public int Seconds { get; }
|
||||||
|
|
||||||
|
public RatelimitAttribute(int seconds)
|
||||||
|
{
|
||||||
|
if (seconds <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(seconds));
|
||||||
|
|
||||||
|
Seconds = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<PreconditionResult> CheckPermissionsAsync(
|
||||||
|
ICommandContext context,
|
||||||
|
CommandInfo command,
|
||||||
|
IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (Seconds == 0)
|
||||||
|
return PreconditionResult.FromSuccess();
|
||||||
|
|
||||||
|
var cache = services.GetRequiredService<IBotCache>();
|
||||||
|
var rem = await cache.GetRatelimitAsync(
|
||||||
|
new($"precondition:{context.User.Id}:{command.Name}"),
|
||||||
|
Seconds.Seconds());
|
||||||
|
|
||||||
|
if (rem is null)
|
||||||
|
return PreconditionResult.FromSuccess();
|
||||||
|
|
||||||
|
var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s.";
|
||||||
|
|
||||||
|
return PreconditionResult.FromError(msgContent);
|
||||||
|
}
|
||||||
|
}
|
30
src/EllieBot/Common/Attributes/UserPerm.cs
Normal file
30
src/EllieBot/Common/Attributes/UserPerm.cs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using EllieBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
namespace Discord;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public class UserPermAttribute : RequireUserPermissionAttribute
|
||||||
|
{
|
||||||
|
public UserPermAttribute(GuildPerm permission)
|
||||||
|
: base(permission)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserPermAttribute(ChannelPerm permission)
|
||||||
|
: base(permission)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<PreconditionResult> CheckPermissionsAsync(
|
||||||
|
ICommandContext context,
|
||||||
|
CommandInfo command,
|
||||||
|
IServiceProvider services)
|
||||||
|
{
|
||||||
|
var permService = services.GetRequiredService<DiscordPermOverrideService>();
|
||||||
|
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out _))
|
||||||
|
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
|
||||||
|
return base.CheckPermissionsAsync(context, command, services);
|
||||||
|
}
|
||||||
|
}
|
46
src/EllieBot/Common/Cache/BotCacheExtensions.cs
Normal file
46
src/EllieBot/Common/Cache/BotCacheExtensions.cs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace EllieBot.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/EllieBot/Common/Cache/IBotCache.cs
Normal file
47
src/EllieBot/Common/Cache/IBotCache.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace EllieBot.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/EllieBot/Common/Cache/MemoryBotCache.cs
Normal file
71
src/EllieBot/Common/Cache/MemoryBotCache.cs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
// ReSharper disable InconsistentlySynchronizedField
|
||||||
|
|
||||||
|
namespace EllieBot.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/EllieBot/Common/Cache/RedisBotCache.cs
Normal file
119
src/EllieBot/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 EllieBot.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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
185
src/EllieBot/Common/Configs/BotConfig.cs
Normal file
185
src/EllieBot/Common/Configs/BotConfig.cs
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
#nullable disable
|
||||||
|
using Cloneable;
|
||||||
|
using EllieBot.Common.Yml;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using System.Globalization;
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Configs;
|
||||||
|
|
||||||
|
[Cloneable]
|
||||||
|
public sealed partial class BotConfig : ICloneable<BotConfig>
|
||||||
|
{
|
||||||
|
[Comment(@"DO NOT CHANGE")]
|
||||||
|
public int Version { get; set; } = 5;
|
||||||
|
|
||||||
|
[Comment(@"Most commands, when executed, have a small colored line
|
||||||
|
next to the response. The color depends whether the command
|
||||||
|
is completed, errored or in progress (pending)
|
||||||
|
Color settings below are for the color of those lines.
|
||||||
|
To get color's hex, you can go here https://htmlcolorcodes.com/
|
||||||
|
and copy the hex code fo your selected color (marked as #)")]
|
||||||
|
public ColorConfig Color { get; set; }
|
||||||
|
|
||||||
|
[Comment("Default bot language. It has to be in the list of supported languages (.langli)")]
|
||||||
|
public CultureInfo DefaultLocale { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Style in which executed commands will show up in the console.
|
||||||
|
Allowed values: Simple, Normal, None")]
|
||||||
|
public ConsoleOutputType ConsoleOutputType { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Whether the bot will check for new releases every hour")]
|
||||||
|
public bool CheckForUpdates { get; set; } = true;
|
||||||
|
|
||||||
|
[Comment(@"Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?")]
|
||||||
|
public bool ForwardMessages { get; set; }
|
||||||
|
|
||||||
|
[Comment(
|
||||||
|
@"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
|
||||||
|
or all owners? (this might cause the bot to lag if there's a lot of owners specified)")]
|
||||||
|
public bool ForwardToAllOwners { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Any messages sent by users in Bot's DM to be forwarded to the specified channel.
|
||||||
|
This option will only work when ForwardToAllOwners is set to false")]
|
||||||
|
public ulong? ForwardToChannel { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"When a user DMs the bot with a message which is not a command
|
||||||
|
they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot.
|
||||||
|
Supports embeds. How it looks: https://puu.sh/B0BLV.png")]
|
||||||
|
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
|
||||||
|
public string DmHelpText { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Only users who send a DM to the bot containing one of the specified words will get a DmHelpText response.
|
||||||
|
Case insensitive.
|
||||||
|
Leave empty to reply with DmHelpText to every DM.")]
|
||||||
|
public List<string> DmHelpTextKeywords { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"This is the response for the .h command")]
|
||||||
|
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
|
||||||
|
public string HelpText { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"List of modules and commands completely blocked on the bot")]
|
||||||
|
public BlockedConfig Blocked { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Which string will be used to recognize the commands")]
|
||||||
|
public string Prefix { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
|
||||||
|
1st user who joins will get greeted immediately
|
||||||
|
If more users join within the next 5 seconds, they will be greeted in groups of 5.
|
||||||
|
This will cause %user.mention% and other placeholders to be replaced with multiple users.
|
||||||
|
Keep in mind this might break some of your embeds - for example if you have %user.avatar% in the thumbnail,
|
||||||
|
it will become invalid, as it will resolve to a list of avatars of grouped users.
|
||||||
|
note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some
|
||||||
|
servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited,
|
||||||
|
and (slightly) reduce the greet spam in those servers.")]
|
||||||
|
public bool GroupGreets { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Whether the bot will rotate through all specified statuses.
|
||||||
|
This setting can be changed via .ropl command.
|
||||||
|
See RotatingStatuses submodule in Administration.")]
|
||||||
|
public bool RotateStatuses { get; set; }
|
||||||
|
|
||||||
|
public BotConfig()
|
||||||
|
{
|
||||||
|
var color = new ColorConfig();
|
||||||
|
Color = color;
|
||||||
|
DefaultLocale = new("en-US");
|
||||||
|
ConsoleOutputType = ConsoleOutputType.Normal;
|
||||||
|
ForwardMessages = false;
|
||||||
|
ForwardToAllOwners = false;
|
||||||
|
DmHelpText = @"{""description"": ""Type `%prefix%h` for help.""}";
|
||||||
|
HelpText = @"{
|
||||||
|
""title"": ""To invite me to your server, use this link"",
|
||||||
|
""description"": ""https://discordapp.com/oauth2/authorize?client_id={0}&scope=bot&permissions=66186303"",
|
||||||
|
""color"": 53380,
|
||||||
|
""thumbnail"": ""https://cdn.elliebot.net/Ellie.png"",
|
||||||
|
""fields"": [
|
||||||
|
{
|
||||||
|
""name"": ""Useful help commands"",
|
||||||
|
""value"": ""`%bot.prefix%modules` Lists all bot modules.
|
||||||
|
`%prefix%h CommandName` Shows some help about a specific command.
|
||||||
|
`%prefix%commands ModuleName` Lists all commands in a module."",
|
||||||
|
""inline"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": ""List of all Commands"",
|
||||||
|
""value"": ""https://commands.elliebot.net"",
|
||||||
|
""inline"": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": ""Ellie Support Server"",
|
||||||
|
""value"": ""https://discord.gg/etQdZxSyEH"",
|
||||||
|
""inline"": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}";
|
||||||
|
var blocked = new BlockedConfig();
|
||||||
|
Blocked = blocked;
|
||||||
|
Prefix = ".";
|
||||||
|
RotateStatuses = false;
|
||||||
|
GroupGreets = false;
|
||||||
|
DmHelpTextKeywords = new()
|
||||||
|
{
|
||||||
|
"help",
|
||||||
|
"commands",
|
||||||
|
"cmds",
|
||||||
|
"module",
|
||||||
|
"can you do"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Comment(@"Whether the prefix will be a suffix, or prefix.
|
||||||
|
// For example, if your prefix is ! you will run a command called 'cash' by typing either
|
||||||
|
// '!cash @Someone' if your prefixIsSuffix: false or
|
||||||
|
// 'cash @Someone!' if your prefixIsSuffix: true")]
|
||||||
|
// public bool PrefixIsSuffix { get; set; }
|
||||||
|
|
||||||
|
// public string Prefixed(string text) => PrefixIsSuffix
|
||||||
|
// ? text + Prefix
|
||||||
|
// : Prefix + text;
|
||||||
|
|
||||||
|
public string Prefixed(string text)
|
||||||
|
=> Prefix + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cloneable]
|
||||||
|
public sealed partial class BlockedConfig
|
||||||
|
{
|
||||||
|
public HashSet<string> Commands { get; set; }
|
||||||
|
public HashSet<string> Modules { get; set; }
|
||||||
|
|
||||||
|
public BlockedConfig()
|
||||||
|
{
|
||||||
|
Modules = new();
|
||||||
|
Commands = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cloneable]
|
||||||
|
public partial class ColorConfig
|
||||||
|
{
|
||||||
|
[Comment(@"Color used for embed responses when command successfully executes")]
|
||||||
|
public Rgba32 Ok { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Color used for embed responses when command has an error")]
|
||||||
|
public Rgba32 Error { get; set; }
|
||||||
|
|
||||||
|
[Comment(@"Color used for embed responses while command is doing work or is in progress")]
|
||||||
|
public Rgba32 Pending { get; set; }
|
||||||
|
|
||||||
|
public ColorConfig()
|
||||||
|
{
|
||||||
|
Ok = Rgba32.ParseHex("00e584");
|
||||||
|
Error = Rgba32.ParseHex("ee281f");
|
||||||
|
Pending = Rgba32.ParseHex("faa61a");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ConsoleOutputType
|
||||||
|
{
|
||||||
|
Normal = 0,
|
||||||
|
Simple = 1,
|
||||||
|
None = 2
|
||||||
|
}
|
18
src/EllieBot/Common/Configs/IConfigSeria.cs
Normal file
18
src/EllieBot/Common/Configs/IConfigSeria.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
namespace EllieBot.Common.Configs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base interface for available config serializers
|
||||||
|
/// </summary>
|
||||||
|
public interface IConfigSeria
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serialize the object to string
|
||||||
|
/// </summary>
|
||||||
|
public string Serialize<T>(T obj)
|
||||||
|
where T : notnull;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize string data into an object of the specified type
|
||||||
|
/// </summary>
|
||||||
|
public T Deserialize<T>(string data);
|
||||||
|
}
|
19
src/EllieBot/Common/ModuleBehaviors/IExecNoCommand.cs
Normal file
19
src/EllieBot/Common/ModuleBehaviors/IExecNoCommand.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
namespace EllieBot.Common.ModuleBehaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executed if no command was found for this message
|
||||||
|
/// </summary>
|
||||||
|
public interface IExecNoCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executed at the end of the lifecycle if no command was found
|
||||||
|
/// <see cref="IExecOnMessage"/> →
|
||||||
|
/// <see cref="IInputTransformer"/> →
|
||||||
|
/// <see cref="IExecPreCommand"/> →
|
||||||
|
/// [<see cref="IExecPostCommand"/> | *<see cref="IExecNoCommand"/>*]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guild"></param>
|
||||||
|
/// <param name="msg"></param>
|
||||||
|
/// <returns>A task representing completion</returns>
|
||||||
|
Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg);
|
||||||
|
}
|
21
src/EllieBot/Common/ModuleBehaviors/IExecOnMessage.cs
Normal file
21
src/EllieBot/Common/ModuleBehaviors/IExecOnMessage.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
namespace EllieBot.Common.ModuleBehaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by modules to handle non-bot messages received
|
||||||
|
/// </summary>
|
||||||
|
public interface IExecOnMessage
|
||||||
|
{
|
||||||
|
int Priority { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ran after a non-bot message was received
|
||||||
|
/// *<see cref="IExecOnMessage"/>* →
|
||||||
|
/// <see cref="IInputTransformer"/> →
|
||||||
|
/// <see cref="IExecPreCommand"/> →
|
||||||
|
/// [<see cref="IExecPostCommand"/> | <see cref="IExecNoCommand"/>]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guild">Guild where the message was sent</param>
|
||||||
|
/// <param name="msg">The message that was received</param>
|
||||||
|
/// <returns>Whether further processing of this message should be blocked</returns>
|
||||||
|
Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg);
|
||||||
|
}
|
22
src/EllieBot/Common/ModuleBehaviors/IExecPostCommand.cs
Normal file
22
src/EllieBot/Common/ModuleBehaviors/IExecPostCommand.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
namespace EllieBot.Common.ModuleBehaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This interface's method is executed after the command successfully finished execution.
|
||||||
|
/// ***There is no support for this method in EllieBot services.***
|
||||||
|
/// It is only meant to be used in marmalade system
|
||||||
|
/// </summary>
|
||||||
|
public interface IExecPostCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executed after a command was successfully executed
|
||||||
|
/// <see cref="IExecOnMessage"/> →
|
||||||
|
/// <see cref="IInputTransformer"/> →
|
||||||
|
/// <see cref="IExecPreCommand"/> →
|
||||||
|
/// [*<see cref="IExecPostCommand"/>* | <see cref="IExecNoCommand"/>]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">Command context</param>
|
||||||
|
/// <param name="moduleName">Module name</param>
|
||||||
|
/// <param name="commandName">Command name</param>
|
||||||
|
/// <returns>A task representing completion</returns>
|
||||||
|
ValueTask ExecPostCommandAsync(ICommandContext ctx, string moduleName, string commandName);
|
||||||
|
}
|
25
src/EllieBot/Common/ModuleBehaviors/IExecPreCommand.cs
Normal file
25
src/EllieBot/Common/ModuleBehaviors/IExecPreCommand.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
namespace EllieBot.Common.ModuleBehaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This interface's method is executed after a command was found but before it was executed.
|
||||||
|
/// Able to block further processing of a command
|
||||||
|
/// </summary>
|
||||||
|
public interface IExecPreCommand
|
||||||
|
{
|
||||||
|
public int Priority { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
|
/// Ran after a command was found but before execution.
|
||||||
|
/// </para>
|
||||||
|
/// <see cref="IExecOnMessage"/> →
|
||||||
|
/// <see cref="IInputTransformer"/> →
|
||||||
|
/// *<see cref="IExecPreCommand"/>* →
|
||||||
|
/// [<see cref="IExecPostCommand"/> | <see cref="IExecNoCommand"/>]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Command context</param>
|
||||||
|
/// <param name="moduleName">Name of the module</param>
|
||||||
|
/// <param name="command">Command info</param>
|
||||||
|
/// <returns>Whether further processing of the command is blocked</returns>
|
||||||
|
Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command);
|
||||||
|
}
|
25
src/EllieBot/Common/ModuleBehaviors/IInputTransformer.cs
Normal file
25
src/EllieBot/Common/ModuleBehaviors/IInputTransformer.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
namespace EllieBot.Common.ModuleBehaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implemented by services which may transform input before a command is searched for
|
||||||
|
/// </summary>
|
||||||
|
public interface IInputTransformer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ran after a non-bot message was received
|
||||||
|
/// <see cref="IExecOnMessage"/> ->
|
||||||
|
/// *<see cref="IInputTransformer"/>* ->
|
||||||
|
/// <see cref="IExecPreCommand"/> ->
|
||||||
|
/// [<see cref="IExecPostCommand"/> OR <see cref="IExecNoCommand"/>]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guild">Guild</param>
|
||||||
|
/// <param name="channel">Channel in which the message was sent</param>
|
||||||
|
/// <param name="user">User who sent the message</param>
|
||||||
|
/// <param name="input">Content of the message</param>
|
||||||
|
/// <returns>New input, if any, otherwise null</returns>
|
||||||
|
Task<string?> TransformInput(
|
||||||
|
IGuild guild,
|
||||||
|
IMessageChannel channel,
|
||||||
|
IUser user,
|
||||||
|
string input);
|
||||||
|
}
|
13
src/EllieBot/Common/ModuleBehaviors/IReadyExecutor.cs
Normal file
13
src/EllieBot/Common/ModuleBehaviors/IReadyExecutor.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
namespace EllieBot.Common.ModuleBehaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All services which need to execute something after
|
||||||
|
/// the bot is ready should implement this interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IReadyExecutor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executed when bot is ready
|
||||||
|
/// </summary>
|
||||||
|
public Task OnReadyAsync();
|
||||||
|
}
|
11
src/EllieBot/Common/Yml/CommentAttribute.cs
Normal file
11
src/EllieBot/Common/Yml/CommentAttribute.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#nullable disable
|
||||||
|
namespace EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class CommentAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Comment { get; }
|
||||||
|
|
||||||
|
public CommentAttribute(string comment)
|
||||||
|
=> Comment = comment;
|
||||||
|
}
|
65
src/EllieBot/Common/Yml/CommentGatheringTypeInspector.cs
Normal file
65
src/EllieBot/Common/Yml/CommentGatheringTypeInspector.cs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
#nullable disable
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.TypeInspectors;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
public class CommentGatheringTypeInspector : TypeInspectorSkeleton
|
||||||
|
{
|
||||||
|
private readonly ITypeInspector _innerTypeDescriptor;
|
||||||
|
|
||||||
|
public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor)
|
||||||
|
=> _innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException(nameof(innerTypeDescriptor));
|
||||||
|
|
||||||
|
public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object container)
|
||||||
|
=> _innerTypeDescriptor.GetProperties(type, container).Select(d => new CommentsPropertyDescriptor(d));
|
||||||
|
|
||||||
|
private sealed class CommentsPropertyDescriptor : IPropertyDescriptor
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public Type Type
|
||||||
|
=> _baseDescriptor.Type;
|
||||||
|
|
||||||
|
public Type TypeOverride
|
||||||
|
{
|
||||||
|
get => _baseDescriptor.TypeOverride;
|
||||||
|
set => _baseDescriptor.TypeOverride = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Order { get; set; }
|
||||||
|
|
||||||
|
public ScalarStyle ScalarStyle
|
||||||
|
{
|
||||||
|
get => _baseDescriptor.ScalarStyle;
|
||||||
|
set => _baseDescriptor.ScalarStyle = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanWrite
|
||||||
|
=> _baseDescriptor.CanWrite;
|
||||||
|
|
||||||
|
private readonly IPropertyDescriptor _baseDescriptor;
|
||||||
|
|
||||||
|
public CommentsPropertyDescriptor(IPropertyDescriptor baseDescriptor)
|
||||||
|
{
|
||||||
|
_baseDescriptor = baseDescriptor;
|
||||||
|
Name = baseDescriptor.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(object target, object value)
|
||||||
|
=> _baseDescriptor.Write(target, value);
|
||||||
|
|
||||||
|
public T GetCustomAttribute<T>()
|
||||||
|
where T : Attribute
|
||||||
|
=> _baseDescriptor.GetCustomAttribute<T>();
|
||||||
|
|
||||||
|
public IObjectDescriptor Read(object target)
|
||||||
|
{
|
||||||
|
var comment = _baseDescriptor.GetCustomAttribute<CommentAttribute>();
|
||||||
|
return comment is not null
|
||||||
|
? new CommentsObjectDescriptor(_baseDescriptor.Read(target), comment.Comment)
|
||||||
|
: _baseDescriptor.Read(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
src/EllieBot/Common/Yml/CommentsObjectDescriptor.cs
Normal file
30
src/EllieBot/Common/Yml/CommentsObjectDescriptor.cs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#nullable disable
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
public sealed class CommentsObjectDescriptor : IObjectDescriptor
|
||||||
|
{
|
||||||
|
public string Comment { get; }
|
||||||
|
|
||||||
|
public object Value
|
||||||
|
=> _innerDescriptor.Value;
|
||||||
|
|
||||||
|
public Type Type
|
||||||
|
=> _innerDescriptor.Type;
|
||||||
|
|
||||||
|
public Type StaticType
|
||||||
|
=> _innerDescriptor.StaticType;
|
||||||
|
|
||||||
|
public ScalarStyle ScalarStyle
|
||||||
|
=> _innerDescriptor.ScalarStyle;
|
||||||
|
|
||||||
|
private readonly IObjectDescriptor _innerDescriptor;
|
||||||
|
|
||||||
|
public CommentsObjectDescriptor(IObjectDescriptor innerDescriptor, string comment)
|
||||||
|
{
|
||||||
|
_innerDescriptor = innerDescriptor;
|
||||||
|
Comment = comment;
|
||||||
|
}
|
||||||
|
}
|
29
src/EllieBot/Common/Yml/CommentsObjectGraphVisitor.cs
Normal file
29
src/EllieBot/Common/Yml/CommentsObjectGraphVisitor.cs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#nullable disable
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
using YamlDotNet.Core.Events;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.ObjectGraphVisitors;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor
|
||||||
|
{
|
||||||
|
public CommentsObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor)
|
||||||
|
: base(nextVisitor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
|
||||||
|
{
|
||||||
|
if (value is CommentsObjectDescriptor commentsDescriptor
|
||||||
|
&& !string.IsNullOrWhiteSpace(commentsDescriptor.Comment))
|
||||||
|
{
|
||||||
|
var parts = commentsDescriptor.Comment.Split('\n');
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
context.Emit(new Comment(part.Trim(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.EnterMapping(key, value, context);
|
||||||
|
}
|
||||||
|
}
|
35
src/EllieBot/Common/Yml/MultilineScalarFlowStyleEmitter.cs
Normal file
35
src/EllieBot/Common/Yml/MultilineScalarFlowStyleEmitter.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
#nullable disable
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.EventEmitters;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
|
||||||
|
{
|
||||||
|
public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter)
|
||||||
|
: base(nextEmitter)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter)
|
||||||
|
{
|
||||||
|
if (typeof(string).IsAssignableFrom(eventInfo.Source.Type))
|
||||||
|
{
|
||||||
|
var value = eventInfo.Source.Value as string;
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
var isMultiLine = value.IndexOfAny(new[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0;
|
||||||
|
if (isMultiLine)
|
||||||
|
{
|
||||||
|
eventInfo = new(eventInfo.Source)
|
||||||
|
{
|
||||||
|
Style = ScalarStyle.Literal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEmitter.Emit(eventInfo, emitter);
|
||||||
|
}
|
||||||
|
}
|
47
src/EllieBot/Common/Yml/Rgba32Converter.cs
Normal file
47
src/EllieBot/Common/Yml/Rgba32Converter.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
#nullable disable
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using System.Globalization;
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
using YamlDotNet.Core.Events;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
public class Rgba32Converter : IYamlTypeConverter
|
||||||
|
{
|
||||||
|
public bool Accepts(Type type)
|
||||||
|
=> type == typeof(Rgba32);
|
||||||
|
|
||||||
|
public object ReadYaml(IParser parser, Type type)
|
||||||
|
{
|
||||||
|
var scalar = parser.Consume<Scalar>();
|
||||||
|
var result = Rgba32.ParseHex(scalar.Value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteYaml(IEmitter emitter, object value, Type type)
|
||||||
|
{
|
||||||
|
var color = (Rgba32)value;
|
||||||
|
var val = (uint)((color.B << 0) | (color.G << 8) | (color.R << 16));
|
||||||
|
emitter.Emit(new Scalar(val.ToString("X6").ToLower()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CultureInfoConverter : IYamlTypeConverter
|
||||||
|
{
|
||||||
|
public bool Accepts(Type type)
|
||||||
|
=> type == typeof(CultureInfo);
|
||||||
|
|
||||||
|
public object ReadYaml(IParser parser, Type type)
|
||||||
|
{
|
||||||
|
var scalar = parser.Consume<Scalar>();
|
||||||
|
var result = new CultureInfo(scalar.Value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteYaml(IEmitter emitter, object value, Type type)
|
||||||
|
{
|
||||||
|
var ci = (CultureInfo)value;
|
||||||
|
emitter.Emit(new Scalar(ci.Name));
|
||||||
|
}
|
||||||
|
}
|
25
src/EllieBot/Common/Yml/UriConverter.cs
Normal file
25
src/EllieBot/Common/Yml/UriConverter.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#nullable disable
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
using YamlDotNet.Core.Events;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
public class UriConverter : IYamlTypeConverter
|
||||||
|
{
|
||||||
|
public bool Accepts(Type type)
|
||||||
|
=> type == typeof(Uri);
|
||||||
|
|
||||||
|
public object ReadYaml(IParser parser, Type type)
|
||||||
|
{
|
||||||
|
var scalar = parser.Consume<Scalar>();
|
||||||
|
var result = new Uri(scalar.Value);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteYaml(IEmitter emitter, object value, Type type)
|
||||||
|
{
|
||||||
|
var uri = (Uri)value;
|
||||||
|
emitter.Emit(new Scalar(uri.ToString()));
|
||||||
|
}
|
||||||
|
}
|
28
src/EllieBot/Common/Yml/Yaml.cs
Normal file
28
src/EllieBot/Common/Yml/Yaml.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#nullable disable
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
|
namespace EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
public class Yaml
|
||||||
|
{
|
||||||
|
public static ISerializer Serializer
|
||||||
|
=> new SerializerBuilder().WithTypeInspector(inner => new CommentGatheringTypeInspector(inner))
|
||||||
|
.WithEmissionPhaseObjectGraphVisitor(args
|
||||||
|
=> new CommentsObjectGraphVisitor(args.InnerVisitor))
|
||||||
|
.WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
|
||||||
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||||
|
.WithIndentedSequences()
|
||||||
|
.WithTypeConverter(new Rgba32Converter())
|
||||||
|
.WithTypeConverter(new CultureInfoConverter())
|
||||||
|
.WithTypeConverter(new UriConverter())
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
public static IDeserializer Deserializer
|
||||||
|
=> new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||||
|
.WithTypeConverter(new Rgba32Converter())
|
||||||
|
.WithTypeConverter(new CultureInfoConverter())
|
||||||
|
.WithTypeConverter(new UriConverter())
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
|
}
|
48
src/EllieBot/Common/Yml/YamlHelper.cs
Normal file
48
src/EllieBot/Common/Yml/YamlHelper.cs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
#nullable disable
|
||||||
|
namespace EllieBot.Common.Yml;
|
||||||
|
|
||||||
|
public class YamlHelper
|
||||||
|
{
|
||||||
|
// https://github.com/aaubry/YamlDotNet/blob/0f4cc205e8b2dd8ef6589d96de32bf608a687c6f/YamlDotNet/Core/Scanner.cs#L1687
|
||||||
|
/// <summary>
|
||||||
|
/// This is modified code from yamldotnet's repo which handles parsing unicode code points
|
||||||
|
/// it is needed as yamldotnet doesn't support unescaped unicode characters
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="point">Unicode code point</param>
|
||||||
|
/// <returns>Actual character</returns>
|
||||||
|
public static string UnescapeUnicodeCodePoint(string point)
|
||||||
|
{
|
||||||
|
var character = 0;
|
||||||
|
|
||||||
|
// Scan the character value.
|
||||||
|
|
||||||
|
foreach (var c in point)
|
||||||
|
{
|
||||||
|
if (!IsHex(c))
|
||||||
|
return point;
|
||||||
|
|
||||||
|
character = (character << 4) + AsHex(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the value and write the character.
|
||||||
|
|
||||||
|
if (character is (>= 0xD800 and <= 0xDFFF) or > 0x10FFFF)
|
||||||
|
return point;
|
||||||
|
|
||||||
|
return char.ConvertFromUtf32(character);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsHex(char c)
|
||||||
|
=> c is (>= '0' and <= '9') or (>= 'A' and <= 'F') or (>= 'a' and <= 'f');
|
||||||
|
|
||||||
|
public static int AsHex(char c)
|
||||||
|
{
|
||||||
|
if (c <= '9')
|
||||||
|
return c - '0';
|
||||||
|
|
||||||
|
if (c <= 'F')
|
||||||
|
return c - 'A' + 10;
|
||||||
|
|
||||||
|
return c - 'a' + 10;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue