Restructured the project structure back to the way it was, there's no reasonable way to split the modules

This commit is contained in:
Kwoth
2024-04-26 22:26:24 +00:00
parent 6c9c8bf63e
commit e0819f760c
768 changed files with 192 additions and 1047 deletions

View File

@@ -0,0 +1,20 @@
using System.Text.RegularExpressions;
namespace NadekoBot.Common;
public interface IReplacementPatternStore : INService
{
IReadOnlyDictionary<string, ReplacementInfo> Replacements { get; }
IReadOnlyDictionary<string, RegexReplacementInfo> RegexReplacements { get; }
ValueTask<Guid?> Register(string token, Func<ValueTask<string>> repFactory);
ValueTask<Guid?> Register<T1>(string token, Func<T1, ValueTask<string>> repFactory);
ValueTask<Guid?> Register<T1, T2>(string token, Func<T1, T2, ValueTask<string>> repFactory);
ValueTask<Guid?> Register(string token, Func<string> repFactory);
ValueTask<Guid?> Register<T1>(string token, Func<T1, string> repFactory);
ValueTask<Guid?> Register<T1, T2>(string token, Func<T1, T2, string> repFactory);
ValueTask<Guid?> Register(Regex regex, Func<Match, string> repFactory);
ValueTask<Guid?> Register<T1>(Regex regex, Func<Match, T1, string> repFactory);
}

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Common;
public interface IReplacementService
{
ValueTask<string?> ReplaceAsync(string input, ReplacementContext repCtx);
ValueTask<SmartText> ReplaceAsync(SmartText input, ReplacementContext repCtx);
}

View File

@@ -0,0 +1,69 @@
using System.Text.RegularExpressions;
namespace NadekoBot.Common;
public sealed class ReplacementContext
{
public DiscordSocketClient? Client { get; }
public IGuild? Guild { get; }
public IMessageChannel? Channel { get; }
public IUser[]? Users { get; }
private readonly List<ReplacementInfo> _overrides = new();
private readonly HashSet<string> _tokens = new();
public IReadOnlyList<ReplacementInfo> Overrides
=> _overrides.AsReadOnly();
private readonly List<RegexReplacementInfo> _regexOverrides = new();
private readonly HashSet<string> _regexPatterns = new();
public IReadOnlyList<RegexReplacementInfo> RegexOverrides
=> _regexOverrides.AsReadOnly();
public ReplacementContext(ICommandContext cmdContext) : this(cmdContext.Client as DiscordSocketClient,
cmdContext.Guild,
cmdContext.Channel,
cmdContext.User)
{
}
public ReplacementContext(
DiscordSocketClient? client = null,
IGuild? guild = null,
IMessageChannel? channel = null,
params IUser[]? users)
{
Client = client;
Guild = guild;
Channel = channel;
Users = users;
}
public ReplacementContext WithOverride(string key, Func<ValueTask<string>> repFactory)
{
if (_tokens.Add(key))
{
_overrides.Add(new(key, repFactory));
}
return this;
}
public ReplacementContext WithOverride(string key, Func<string> repFactory)
=> WithOverride(key, () => new ValueTask<string>(repFactory()));
public ReplacementContext WithOverride(Regex regex, Func<Match, ValueTask<string>> repFactory)
{
if (_regexPatterns.Add(regex.ToString()))
{
_regexOverrides.Add(new(regex, repFactory));
}
return this;
}
public ReplacementContext WithOverride(Regex regex, Func<Match, string> repFactory)
=> WithOverride(regex, (Match m) => new ValueTask<string>(repFactory(m)));
}

View File

@@ -0,0 +1,57 @@
using System.Reflection;
using System.Text.RegularExpressions;
namespace NadekoBot.Common;
public sealed class ReplacementInfo
{
private readonly Delegate _del;
public IReadOnlyCollection<Type> InputTypes { get; }
public string Token { get; }
private static readonly Func<ValueTask<string?>> _falllbackFunc = static () => default;
public ReplacementInfo(string token, Delegate del)
{
_del = del;
InputTypes = del.GetMethodInfo().GetParameters().Select(x => x.ParameterType).ToArray().AsReadOnly();
Token = token;
}
public async Task<string?> GetValueAsync(params object?[]? objs)
=> await (ValueTask<string?>)(_del.DynamicInvoke(objs) ?? _falllbackFunc);
public override int GetHashCode()
=> Token.GetHashCode();
public override bool Equals(object? obj)
=> obj is ReplacementInfo ri && ri.Token == Token;
}
public sealed class RegexReplacementInfo
{
private readonly Delegate _del;
public IReadOnlyCollection<Type> InputTypes { get; }
public Regex Regex { get; }
public string Pattern { get; }
private static readonly Func<Match, ValueTask<string?>> _falllbackFunc = static _ => default;
public RegexReplacementInfo(Regex regex, Delegate del)
{
_del = del;
InputTypes = del.GetMethodInfo().GetParameters().Select(x => x.ParameterType).ToArray().AsReadOnly();
Regex = regex;
Pattern = Regex.ToString();
}
public async Task<string?> GetValueAsync(Match m, params object?[]? objs)
=> await ((Func<Match, ValueTask<string?>>)(_del.DynamicInvoke(objs) ?? _falllbackFunc))(m);
public override int GetHashCode()
=> Regex.GetHashCode();
public override bool Equals(object? obj)
=> obj is RegexReplacementInfo ri && ri.Pattern == Pattern;
}

View File

@@ -0,0 +1,130 @@
using System.Text.RegularExpressions;
using OneOf;
namespace NadekoBot.Common;
public sealed partial class ReplacementPatternStore : IReplacementPatternStore, INService
{
private readonly ConcurrentDictionary<Guid, OneOf<string, Regex>> _guids = new();
private readonly ConcurrentDictionary<string, ReplacementInfo> _defaultReplacements = new();
private readonly ConcurrentDictionary<string, RegexReplacementInfo> _regexReplacements = new();
public IReadOnlyDictionary<string, ReplacementInfo> Replacements
=> _defaultReplacements.AsReadOnly();
public IReadOnlyDictionary<string, RegexReplacementInfo> RegexReplacements
=> _regexReplacements.AsReadOnly();
public ReplacementPatternStore()
{
WithClient();
WithChannel();
WithServer();
WithUsers();
WithDefault();
WithRegex();
}
// private async ValueTask<string> InternalReplace(string input, ReplacementContexta repCtx)
// {
// // multiple executions vs single execution per replacement
// var minIndex = -1;
// var index = -1;
// foreach (var rep in _replacements)
// {
// while ((index = input.IndexOf(rep.Key, StringComparison.InvariantCulture)) != -1 && index > minIndex)
// {
// var valueToInsert = await rep.Value(repCtx);
// input = input[..index] + valueToInsert +input[(index + rep.Key.Length)..];
// minIndex = (index + valueToInsert.Length);
// }
// }
//
// return input;
// }
private ValueTask<Guid?> InternalRegister(string token, Delegate repFactory)
{
if (!token.StartsWith('%') || !token.EndsWith('%'))
{
Log.Warning(
"""
Invalid replacement token: {Token}
Tokens have to start and end with a '%', ex: %mytoken%
""",
token);
return new(default(Guid?));
}
if (_defaultReplacements.TryAdd(token, new ReplacementInfo(token, repFactory)))
{
var guid = Guid.NewGuid();
_guids[guid] = token;
return new(guid);
}
return new(default(Guid?));
}
public ValueTask<Guid?> Register(string token, Func<ValueTask<string>> repFactory)
=> InternalRegister(token, repFactory);
public ValueTask<Guid?> Register<T1>(string token, Func<T1, ValueTask<string>> repFactory)
=> InternalRegister(token, repFactory);
public ValueTask<Guid?> Register<T1, T2>(string token, Func<T1, T2, ValueTask<string>> repFactory)
=> InternalRegister(token, repFactory);
public ValueTask<Guid?> Register(string token, Func<string> repFactory)
=> InternalRegister(token, () => new ValueTask<string>(repFactory()));
public ValueTask<Guid?> Register<T1>(string token, Func<T1, string> repFactory)
=> InternalRegister(token, (T1 a) => new ValueTask<string>(repFactory(a)));
public ValueTask<Guid?> Register<T1, T2>(string token, Func<T1, T2, string> repFactory)
=> InternalRegister(token, (T1 a, T2 b) => new ValueTask<string>(repFactory(a, b)));
private ValueTask<Guid?> InternalRegexRegister(Regex regex, Delegate repFactory)
{
var regexPattern = regex.ToString();
if (!regexPattern.StartsWith('%') || !regexPattern.EndsWith('%'))
{
Log.Warning(
"""
Invalid replacement pattern: {Token}
Tokens have to start and end with a '%', ex: %mytoken%
""",
regex);
return new(default(Guid?));
}
if (_regexReplacements.TryAdd(regexPattern, new RegexReplacementInfo(regex, repFactory)))
{
var guid = Guid.NewGuid();
_guids[guid] = regex;
return new(guid);
}
return new(default(Guid?));
}
public ValueTask<Guid?> Register(Regex regex, Func<Match, string> repFactory)
=> InternalRegexRegister(regex, () => (Match m) => new ValueTask<string>(repFactory(m)));
public ValueTask<Guid?> Register<T1>(Regex regex, Func<Match, T1, string> repFactory)
=> InternalRegexRegister(regex, (T1 a) => (Match m) => new ValueTask<string>(repFactory(m, a)));
public bool Unregister(Guid guid)
{
if (_guids.TryRemove(guid, out var pattern))
{
return pattern.Match(
token => _defaultReplacements.TryRemove(token, out _),
regex => _regexReplacements.TryRemove(regex.ToString(), out _));
}
return false;
}
}

View File

@@ -0,0 +1,113 @@
using System.Text.RegularExpressions;
namespace NadekoBot.Common;
public sealed partial class ReplacementPatternStore
{
private static readonly Regex _rngRegex = new(@"%rng(?:(?<from>(?:-)?\d+)-(?<to>(?:-)?\d+))?%",
RegexOptions.Compiled);
private void WithDefault()
{
Register("%bot.time%",
static ()
=> DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
}
private void WithClient()
{
Register("%bot.status%", static (DiscordSocketClient client) => client.Status.ToString());
Register("%bot.latency%", static (DiscordSocketClient client) => client.Latency.ToString());
Register("%bot.name%", static (DiscordSocketClient client) => client.CurrentUser.Username);
Register("%bot.fullname%", static (DiscordSocketClient client) => client.CurrentUser.ToString());
Register("%bot.discrim%", static (DiscordSocketClient client) => client.CurrentUser.Discriminator);
Register("%bot.id%", static (DiscordSocketClient client) => client.CurrentUser.Id.ToString());
Register("%bot.avatar%",
static (DiscordSocketClient client) => client.CurrentUser.RealAvatarUrl().ToString());
Register("%bot.mention%", static (DiscordSocketClient client) => client.CurrentUser.Mention);
Register("%shard.servercount%", static (DiscordSocketClient c) => c.Guilds.Count.ToString());
Register("%shard.usercount%",
static (DiscordSocketClient c) => c.Guilds.Sum(g => g.MemberCount).ToString());
Register("%shard.id%", static (DiscordSocketClient c) => c.ShardId.ToString());
}
private void WithServer()
{
Register("%server%", static (IGuild g) => g.Name);
Register("%server.id%", static (IGuild g) => g.Id.ToString());
Register("%server.name%", static (IGuild g) => g.Name);
Register("%server.icon%", static (IGuild g) => g.IconUrl);
Register("%server.members%", static (IGuild g) => g.ApproximateMemberCount?.ToString() ?? "?");
Register("%server.boosters%", static (IGuild g) => g.PremiumSubscriptionCount.ToString());
Register("%server.boost_level%", static (IGuild g) => ((int)g.PremiumTier).ToString());
}
private void WithChannel()
{
Register("%channel%", static (IMessageChannel ch) => ch.Name);
Register("%channel.mention%",
static (IMessageChannel ch) => (ch as ITextChannel)?.Mention ?? "#" + ch.Name);
Register("%channel.name%", static (IMessageChannel ch) => ch.Name);
Register("%channel.id%", static (IMessageChannel ch) => ch.Id.ToString());
Register("%channel.created%",
static (IMessageChannel ch) => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy"));
Register("%channel.nsfw%",
static (IMessageChannel ch) => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-");
Register("%channel.topic%", static (IMessageChannel ch) => (ch as ITextChannel)?.Topic ?? "-");
}
private void WithUsers()
{
Register("%user%",
static (IUser[] users) => string.Join(" ", users.Select(user => user.Mention)));
Register("%user.mention%",
static (IUser[] users) => string.Join(" ", users.Select(user => user.Mention)));
Register("%user.fullname%",
static (IUser[] users) => string.Join(" ", users.Select(user => user.ToString())));
Register("%user.name%",
static (IUser[] users) => string.Join(" ", users.Select(user => user.Username)));
Register("%user.discrim%",
static (IUser[] users) => string.Join(" ", users.Select(user => user.Discriminator)));
Register("%user.avatar%",
static (IUser[] users)
=> string.Join(" ", users.Select(user => user.RealAvatarUrl().ToString())));
Register("%user.id%",
static (IUser[] users) => string.Join(" ", users.Select(user => user.Id.ToString())));
Register("%user.created_time%",
static (IUser[] users)
=> string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm"))));
Register("%user.created_date%",
static (IUser[] users)
=> string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy"))));
Register("%user.joined_time%",
static (IUser[] users) => string.Join(" ",
users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-")));
Register("%user.joined_date%",
static (IUser[] users) => string.Join(" ",
users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-")));
}
private void WithRegex()
{
Register(_rngRegex,
match =>
{
var rng = new NadekoRandom();
if (!int.TryParse(match.Groups["from"].ToString(), out var from))
from = 0;
if (!int.TryParse(match.Groups["to"].ToString(), out var to))
to = 0;
if (from == 0 && to == 0)
return rng.Next(0, 11).ToString();
if (from >= to)
return string.Empty;
return rng.Next(from, to + 1).ToString();
});
}
}

View File

@@ -0,0 +1,137 @@
namespace NadekoBot.Common;
public sealed class ReplacementService : IReplacementService, INService
{
private readonly IReplacementPatternStore _repReg;
public ReplacementService(IReplacementPatternStore repReg)
{
_repReg = repReg;
}
public async ValueTask<SmartText> ReplaceAsync(SmartText input, ReplacementContext repCtx)
{
var reps = GetReplacementsForContext(repCtx);
var regReps = GetRegexReplacementsForContext(repCtx);
var inputData = GetInputData(repCtx);
var rep = new Replacer(reps.Values, regReps.Values, inputData);
return await rep.ReplaceAsync(input);
}
public async ValueTask<string?> ReplaceAsync(string input, ReplacementContext repCtx)
{
var reps = GetReplacementsForContext(repCtx);
var regReps = GetRegexReplacementsForContext(repCtx);
var inputData = GetInputData(repCtx);
var rep = new Replacer(reps.Values, regReps.Values, inputData);
return await rep.ReplaceAsync(input);
}
private object[] GetInputData(ReplacementContext repCtx)
{
var obj = new List<object>();
if (repCtx.Client is not null)
obj.Add(repCtx.Client);
if (repCtx.Guild is not null)
obj.Add(repCtx.Guild);
if (repCtx.Users is not null)
obj.Add(repCtx.Users);
if (repCtx.Channel is not null)
obj.Add(repCtx.Channel);
return obj.ToArray();
}
private IDictionary<string, ReplacementInfo> GetReplacementsForContext(ReplacementContext repCtx)
{
var reps = GetOriginalReplacementsForContext(repCtx);
foreach (var ovrd in repCtx.Overrides)
{
reps.Remove(ovrd.Token);
reps.TryAdd(ovrd.Token, ovrd);
}
return reps;
}
private IDictionary<string, RegexReplacementInfo> GetRegexReplacementsForContext(ReplacementContext repCtx)
{
var reps = GetOriginalRegexReplacementsForContext(repCtx);
foreach (var ovrd in repCtx.RegexOverrides)
{
reps.Remove(ovrd.Pattern);
reps.TryAdd(ovrd.Pattern, ovrd);
}
return reps;
}
private IDictionary<string, ReplacementInfo> GetOriginalReplacementsForContext(ReplacementContext repCtx)
{
var objs = new List<object>();
if (repCtx.Client is not null)
{
objs.Add(repCtx.Client);
}
if (repCtx.Channel is not null)
{
objs.Add(repCtx.Channel);
}
if (repCtx.Users is not null)
{
objs.Add(repCtx.Users);
}
if (repCtx.Guild is not null)
{
objs.Add(repCtx.Guild);
}
var types = objs.Map(x => x.GetType()).OrderBy(x => x.Name).ToHashSet();
return _repReg.Replacements
.Values
.Where(rep => rep.InputTypes.All(t => types.Any(x => x.IsAssignableTo((t)))))
.ToDictionary(rep => rep.Token, rep => rep);
}
private IDictionary<string, RegexReplacementInfo> GetOriginalRegexReplacementsForContext(ReplacementContext repCtx)
{
var objs = new List<object>();
if (repCtx.Client is not null)
{
objs.Add(repCtx.Client);
}
if (repCtx.Channel is not null)
{
objs.Add(repCtx.Channel);
}
if (repCtx.Users is not null)
{
objs.Add(repCtx.Users);
}
if (repCtx.Guild is not null)
{
objs.Add(repCtx.Guild);
}
var types = objs.Map(x => x.GetType()).OrderBy(x => x.Name).ToHashSet();
return _repReg.RegexReplacements
.Values
.Where(rep => rep.InputTypes.All(t => types.Any(x => x.IsAssignableTo((t)))))
.ToDictionary(rep => rep.Pattern, rep => rep);
}
}

View File

@@ -0,0 +1,138 @@
using System.Text;
using System.Text.RegularExpressions;
namespace NadekoBot.Common;
public sealed partial class Replacer
{
private readonly IEnumerable<ReplacementInfo> _reps;
private readonly IEnumerable<RegexReplacementInfo> _regexReps;
private readonly object[] _inputData;
[GeneratedRegex(@"\%[\p{L}\p{N}\._]*[\p{L}\p{N}]+[\p{L}\p{N}\._]*\%")]
private static partial Regex TokenExtractionRegex();
public Replacer(IEnumerable<ReplacementInfo> reps, IEnumerable<RegexReplacementInfo> regexReps, object[] inputData)
{
_reps = reps;
_inputData = inputData;
_regexReps = regexReps;
}
public async ValueTask<string?> ReplaceAsync(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return input;
var matches = TokenExtractionRegex().IsMatch(input);
if (matches)
{
foreach (var rep in _reps)
{
if (input.Contains(rep.Token, StringComparison.InvariantCulture))
{
var objs = GetParams(rep.InputTypes);
input = input.Replace(rep.Token, await rep.GetValueAsync(objs), StringComparison.InvariantCulture);
}
}
}
foreach (var rep in _regexReps)
{
var sb = new StringBuilder();
var objs = GetParams(rep.InputTypes);
var match = rep.Regex.Match(input);
if (match.Success)
{
sb.Append(input, 0, match.Index)
.Append(await rep.GetValueAsync(match, objs));
var lastIndex = match.Index + match.Length;
sb.Append(input, lastIndex, input.Length - lastIndex);
input = sb.ToString();
}
}
return input;
}
private object?[]? GetParams(IReadOnlyCollection<Type> inputTypes)
{
if (inputTypes.Count == 0)
return null;
var objs = new List<object>();
foreach (var t in inputTypes)
{
var datum = _inputData.FirstOrDefault(x => x.GetType().IsAssignableTo(t));
if (datum is not null)
objs.Add(datum);
}
return objs.ToArray();
}
public async ValueTask<SmartText> ReplaceAsync(SmartText data)
=> data switch
{
SmartEmbedText embedData => await ReplaceAsync(embedData) with
{
PlainText = await ReplaceAsync(embedData.PlainText),
Color = embedData.Color
},
SmartPlainText plain => await ReplaceAsync(plain),
SmartEmbedTextArray arr => await ReplaceAsync(arr),
_ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type")
};
private async Task<SmartEmbedTextArray> ReplaceAsync(SmartEmbedTextArray embedArr)
=> new()
{
Embeds = await embedArr.Embeds.Map(async e => await ReplaceAsync(e) with
{
Color = e.Color
}).WhenAll(),
Content = await ReplaceAsync(embedArr.Content)
};
private async ValueTask<SmartPlainText> ReplaceAsync(SmartPlainText plain)
=> await ReplaceAsync(plain.Text);
private async Task<T> ReplaceAsync<T>(T embedData) where T : SmartEmbedTextBase, new()
{
var newEmbedData = new T
{
Description = await ReplaceAsync(embedData.Description),
Title = await ReplaceAsync(embedData.Title),
Thumbnail = await ReplaceAsync(embedData.Thumbnail),
Image = await ReplaceAsync(embedData.Image),
Url = await ReplaceAsync(embedData.Url),
Author = embedData.Author is null
? null
: new()
{
Name = await ReplaceAsync(embedData.Author.Name),
IconUrl = await ReplaceAsync(embedData.Author.IconUrl)
},
Fields = await Task.WhenAll(embedData
.Fields?
.Map(async f => new SmartTextEmbedField
{
Name = await ReplaceAsync(f.Name),
Value = await ReplaceAsync(f.Value),
Inline = f.Inline
}) ?? []),
Footer = embedData.Footer is null
? null
: new()
{
Text = await ReplaceAsync(embedData.Footer.Text),
IconUrl = await ReplaceAsync(embedData.Footer.IconUrl)
}
};
return newEmbedData;
}
}