mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 09:48:26 -04:00
Restructured the project structure back to the way it was, there's no reasonable way to split the modules
This commit is contained in:
@@ -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);
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public interface IReplacementService
|
||||
{
|
||||
ValueTask<string?> ReplaceAsync(string input, ReplacementContext repCtx);
|
||||
ValueTask<SmartText> ReplaceAsync(SmartText input, ReplacementContext repCtx);
|
||||
}
|
@@ -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)));
|
||||
}
|
57
src/NadekoBot/_common/Replacements/Impl/ReplacementInfo.cs
Normal file
57
src/NadekoBot/_common/Replacements/Impl/ReplacementInfo.cs
Normal 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;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
137
src/NadekoBot/_common/Replacements/Impl/ReplacementService.cs
Normal file
137
src/NadekoBot/_common/Replacements/Impl/ReplacementService.cs
Normal 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);
|
||||
}
|
||||
}
|
138
src/NadekoBot/_common/Replacements/Impl/Replacer.cs
Normal file
138
src/NadekoBot/_common/Replacements/Impl/Replacer.cs
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user