Lots more stuff

This commit is contained in:
Kwoth
2023-03-14 15:48:59 +01:00
parent 0af8048938
commit 7a60868632
315 changed files with 2482 additions and 2128 deletions

View File

@@ -0,0 +1,66 @@
#nullable disable
using NadekoBot.Common.Configs;
using SixLabors.ImageSharp.PixelFormats;
namespace NadekoBot.Services;
/// <summary>
/// Settings service for bot-wide configuration.
/// </summary>
public sealed class BotConfigService : ConfigServiceBase<BotConfig>
{
private const string FILE_PATH = "data/bot.yml";
private static readonly TypedKey<BotConfig> _changeKey = new("config.bot.updated");
public override string Name { get; } = "bot";
public BotConfigService(IConfigSeria serializer, IPubSub pubSub)
: base(FILE_PATH, serializer, pubSub, _changeKey)
{
AddParsedProp("color.ok", bs => bs.Color.Ok, Rgba32.TryParseHex, ConfigPrinters.Color);
AddParsedProp("color.error", bs => bs.Color.Error, Rgba32.TryParseHex, ConfigPrinters.Color);
AddParsedProp("color.pending", bs => bs.Color.Pending, Rgba32.TryParseHex, ConfigPrinters.Color);
AddParsedProp("help.text", bs => bs.HelpText, ConfigParsers.String, ConfigPrinters.ToString);
AddParsedProp("help.dmtext", bs => bs.DmHelpText, ConfigParsers.String, ConfigPrinters.ToString);
AddParsedProp("console.type", bs => bs.ConsoleOutputType, Enum.TryParse, ConfigPrinters.ToString);
AddParsedProp("locale", bs => bs.DefaultLocale, ConfigParsers.Culture, ConfigPrinters.Culture);
AddParsedProp("prefix", bs => bs.Prefix, ConfigParsers.String, ConfigPrinters.ToString);
AddParsedProp("checkforupdates", bs => bs.CheckForUpdates, bool.TryParse, ConfigPrinters.ToString);
Migrate();
}
private void Migrate()
{
if (data.Version < 2)
ModifyConfig(c => c.Version = 2);
if (data.Version < 3)
{
ModifyConfig(c =>
{
c.Version = 3;
c.Blocked.Modules = c.Blocked.Modules?.Select(static x
=> string.Equals(x,
"ActualCustomReactions",
StringComparison.InvariantCultureIgnoreCase)
? "ACTUALEXPRESSIONS"
: x)
.Distinct()
.ToHashSet();
});
}
if (data.Version < 4)
ModifyConfig(c =>
{
c.Version = 4;
c.CheckForUpdates = true;
});
if(data.Version < 5)
ModifyConfig(c =>
{
c.Version = 5;
});
}
}

View File

@@ -0,0 +1,50 @@
#nullable disable
using SixLabors.ImageSharp.PixelFormats;
using System.Globalization;
namespace NadekoBot.Services;
/// <summary>
/// Custom setting value parsers for types which don't have them by default
/// </summary>
public static class ConfigParsers
{
/// <summary>
/// Default string parser. Passes input to output and returns true.
/// </summary>
public static bool String(string input, out string output)
{
output = input;
return true;
}
public static bool Culture(string input, out CultureInfo output)
{
try
{
output = new(input);
return true;
}
catch
{
output = null;
return false;
}
}
public static bool InsensitiveEnum<T>(string input, out T output)
where T: struct
=> Enum.TryParse(input, true, out output);
}
public static class ConfigPrinters
{
public static string ToString<TAny>(TAny input)
=> input.ToString();
public static string Culture(CultureInfo culture)
=> culture.Name;
public static string Color(Rgba32 color)
=> ((uint)((color.B << 0) | (color.G << 8) | (color.R << 16))).ToString("X6");
}

View File

@@ -0,0 +1,203 @@
using NadekoBot.Common.Configs;
using NadekoBot.Common.Yml;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
namespace NadekoBot.Services;
/// <summary>
/// Base service for all settings services
/// </summary>
/// <typeparam name="TSettings">Type of the settings</typeparam>
public abstract class ConfigServiceBase<TSettings> : IConfigService
where TSettings : ICloneable<TSettings>, new()
{
// FUTURE config arrays are not copied - they're not protected from mutations
public TSettings Data
=> data.Clone();
public abstract string Name { get; }
protected readonly string _filePath;
protected readonly IConfigSeria _serializer;
protected readonly IPubSub _pubSub;
private readonly TypedKey<TSettings> _changeKey;
protected TSettings data;
private readonly Dictionary<string, Func<TSettings, string, bool>> _propSetters = new();
private readonly Dictionary<string, Func<object>> _propSelectors = new();
private readonly Dictionary<string, Func<object, string>> _propPrinters = new();
private readonly Dictionary<string, string?> _propComments = new();
/// <summary>
/// Initialized an instance of <see cref="ConfigServiceBase{TSettings}" />
/// </summary>
/// <param name="filePath">Path to the file where the settings are serialized/deserialized to and from</param>
/// <param name="serializer">Serializer which will be used</param>
/// <param name="pubSub">Pubsub implementation for signaling when settings are updated</param>
/// <param name="changeKey">Key used to signal changed event</param>
protected ConfigServiceBase(
string filePath,
IConfigSeria serializer,
IPubSub pubSub,
TypedKey<TSettings> changeKey)
{
_filePath = filePath;
_serializer = serializer;
_pubSub = pubSub;
_changeKey = changeKey;
data = new();
Load();
_pubSub.Sub(_changeKey, OnChangePublished);
}
private void PublishChange()
=> _pubSub.Pub(_changeKey, data);
private ValueTask OnChangePublished(TSettings newData)
{
data = newData;
OnStateUpdate();
return default;
}
/// <summary>
/// Loads data from disk. If file doesn't exist, it will be created with default values
/// </summary>
protected void Load()
{
// if file is deleted, regenerate it with default values
if (!File.Exists(_filePath))
{
data = new();
Save();
}
data = _serializer.Deserialize<TSettings>(File.ReadAllText(_filePath));
}
/// <summary>
/// Loads new data and publishes the new state
/// </summary>
public void Reload()
{
Load();
_pubSub.Pub(_changeKey, data);
}
/// <summary>
/// Doesn't do anything by default. This method will be executed after
/// <see cref="data" /> is reloaded from <see cref="_filePath" /> or new data is recieved
/// from the publish event
/// </summary>
protected virtual void OnStateUpdate()
{
}
private void Save()
{
var strData = _serializer.Serialize(data);
File.WriteAllText(_filePath, strData);
}
protected void AddParsedProp<TProp>(
string key,
Expression<Func<TSettings, TProp>> selector,
SettingParser<TProp> parser,
Func<TProp, string> printer,
Func<TProp, bool>? checker = null)
{
checker ??= _ => true;
key = key.ToLowerInvariant();
_propPrinters[key] = obj => printer((TProp)obj);
_propSelectors[key] = () => selector.Compile()(data)!;
_propSetters[key] = Magic(selector, parser, checker);
_propComments[key] = ((MemberExpression)selector.Body).Member.GetCustomAttribute<CommentAttribute>()?.Comment;
}
private Func<TSettings, string, bool> Magic<TProp>(
Expression<Func<TSettings, TProp>> selector,
SettingParser<TProp> parser,
Func<TProp, bool> checker)
=> (target, input) =>
{
if (!parser(input, out var value))
return false;
if (!checker(value))
return false;
object targetObject = target;
var expr = (MemberExpression)selector.Body;
var prop = (PropertyInfo)expr.Member;
var expressions = new List<MemberExpression>();
while (true)
{
expr = expr.Expression as MemberExpression;
if (expr is null)
break;
expressions.Add(expr);
}
foreach (var memberExpression in expressions.AsEnumerable().Reverse())
{
var localProp = (PropertyInfo)memberExpression.Member;
targetObject = localProp.GetValue(targetObject)!;
}
prop.SetValue(targetObject, value, null);
return true;
};
public IReadOnlyList<string> GetSettableProps()
=> _propSetters.Keys.ToList();
public string? GetSetting(string prop)
{
prop = prop.ToLowerInvariant();
if (!_propSelectors.TryGetValue(prop, out var selector) || !_propPrinters.TryGetValue(prop, out var printer))
return null;
return printer(selector());
}
public string? GetComment(string prop)
{
if (_propComments.TryGetValue(prop, out var comment))
return comment;
return null;
}
private bool SetProperty(TSettings target, string key, string value)
=> _propSetters.TryGetValue(key.ToLowerInvariant(), out var magic) && magic(target, value);
public bool SetSetting(string prop, string newValue)
{
var success = true;
ModifyConfig(bs =>
{
success = SetProperty(bs, prop, newValue);
});
if (success)
PublishChange();
return success;
}
public void ModifyConfig(Action<TSettings> action)
{
var copy = Data;
action(copy);
data = copy;
Save();
PublishChange();
}
}

View File

@@ -0,0 +1,7 @@
#nullable disable
namespace NadekoBot.Services;
public interface IConfigMigrator
{
public void EnsureMigrated();
}

View File

@@ -0,0 +1,46 @@
#nullable disable
namespace NadekoBot.Services;
/// <summary>
/// Interface that all services which deal with configs should implement
/// </summary>
public interface IConfigService
{
/// <summary>
/// Name of the config
/// </summary>
public string Name { get; }
/// <summary>
/// Loads new data and publishes the new state
/// </summary>
void Reload();
/// <summary>
/// Gets the list of props you can set
/// </summary>
/// <returns>List of props</returns>
IReadOnlyList<string> GetSettableProps();
/// <summary>
/// Gets the value of the specified property
/// </summary>
/// <param name="prop">Prop name</param>
/// <returns>Value of the prop</returns>
string GetSetting(string prop);
/// <summary>
/// Gets the value of the specified property
/// </summary>
/// <param name="prop">Prop name</param>
/// <returns>Value of the prop</returns>
string GetComment(string prop);
/// <summary>
/// Sets the value of the specified property
/// </summary>
/// <param name="prop">Property to set</param>
/// <param name="newValue">Value to set the property to</param>
/// <returns>Success</returns>
bool SetSetting(string prop, string newValue);
}

View File

@@ -0,0 +1,8 @@
#nullable disable
namespace NadekoBot.Services;
/// <summary>
/// Delegate which describes a parser which can convert string input into given data type
/// </summary>
/// <typeparam name="TData">Data type to convert string to</typeparam>
public delegate bool SettingParser<TData>(string input, out TData output);