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