mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
* Replacement mechanism reworked
* Services can now register their own replacements * Possible bugs and/or backwards-incompatible behavior
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Nadeko.Bot.Common;
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public interface ICurrencyProvider
|
||||
{
|
||||
|
@@ -2,7 +2,7 @@
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Nadeko.Bot.Common;
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public interface IPermissionChecker
|
||||
{
|
||||
|
@@ -39,4 +39,5 @@
|
||||
<Link>responses.en-US.json</Link>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
#nullable disable
|
||||
using System.Globalization;
|
||||
using NadekoBot.Common;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
@@ -18,6 +19,7 @@ public abstract class NadekoModule : ModuleBase
|
||||
public ILocalization _localization { get; set; }
|
||||
public IEmbedBuilderService _eb { get; set; }
|
||||
public INadekoInteractionService _inter { get; set; }
|
||||
public IReplacementService repSvc { get; set; }
|
||||
|
||||
protected string prefix
|
||||
=> _cmdHandler.GetPrefix(ctx.Guild);
|
||||
|
@@ -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/Nadeko.Bot.Common/Replacements/Impl/ReplacementInfo.cs
Normal file
57
src/Nadeko.Bot.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/Nadeko.Bot.Common/Replacements/Impl/ReplacementService.cs
Normal file
137
src/Nadeko.Bot.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/Nadeko.Bot.Common/Replacements/Impl/Replacer.cs
Normal file
138
src/Nadeko.Bot.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;
|
||||
}
|
||||
}
|
@@ -1,164 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public class ReplacementBuilder
|
||||
{
|
||||
private static readonly Regex _rngRegex = new("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private readonly ConcurrentDictionary<Regex, Func<Match, string>> _regex = new();
|
||||
|
||||
private readonly ConcurrentDictionary<string, Func<string>> _reps = new();
|
||||
|
||||
public ReplacementBuilder()
|
||||
=> WithRngRegex();
|
||||
|
||||
public ReplacementBuilder WithDefault(
|
||||
IUser usr,
|
||||
IMessageChannel ch,
|
||||
SocketGuild g,
|
||||
DiscordSocketClient client)
|
||||
=> WithUser(usr).WithChannel(ch).WithServer(client, g).WithClient(client);
|
||||
|
||||
public ReplacementBuilder WithDefault(ICommandContext ctx)
|
||||
=> WithDefault(ctx.User, ctx.Channel, ctx.Guild as SocketGuild, (DiscordSocketClient)ctx.Client);
|
||||
|
||||
public ReplacementBuilder WithMention(DiscordSocketClient client)
|
||||
{
|
||||
_reps.TryAdd("%bot.mention%", () => client.CurrentUser.Mention);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithClient(DiscordSocketClient client)
|
||||
{
|
||||
WithMention(client);
|
||||
|
||||
_reps.TryAdd("%bot.status%", () => client.Status.ToString());
|
||||
_reps.TryAdd("%bot.latency%", () => client.Latency.ToString());
|
||||
_reps.TryAdd("%bot.name%", () => client.CurrentUser.Username);
|
||||
_reps.TryAdd("%bot.fullname%", () => client.CurrentUser.ToString());
|
||||
_reps.TryAdd("%bot.time%",
|
||||
() => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
|
||||
_reps.TryAdd("%bot.discrim%", () => client.CurrentUser.Discriminator);
|
||||
_reps.TryAdd("%bot.id%", () => client.CurrentUser.Id.ToString());
|
||||
_reps.TryAdd("%bot.avatar%", () => client.CurrentUser.RealAvatarUrl().ToString());
|
||||
|
||||
WithStats(client);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithServer(DiscordSocketClient client, SocketGuild g)
|
||||
{
|
||||
_reps.TryAdd("%server%", () => g is null ? "DM" : g.Name);
|
||||
_reps.TryAdd("%server.id%", () => g is null ? "DM" : g.Id.ToString());
|
||||
_reps.TryAdd("%server.name%", () => g is null ? "DM" : g.Name);
|
||||
_reps.TryAdd("%server.icon%", () => g is null ? null : g.IconUrl);
|
||||
_reps.TryAdd("%server.members%", () => g is { } sg ? sg.MemberCount.ToString() : "?");
|
||||
_reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString());
|
||||
_reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString());
|
||||
// todo fix
|
||||
// _reps.TryAdd("%server.time%",
|
||||
// () =>
|
||||
// {
|
||||
// var to = TimeZoneInfo.Local;
|
||||
// if (g is not null)
|
||||
// {
|
||||
// if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
|
||||
// to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
|
||||
// }
|
||||
//
|
||||
// return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ")
|
||||
// + to.StandardName.GetInitials();
|
||||
// });
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithChannel(IMessageChannel ch)
|
||||
{
|
||||
_reps.TryAdd("%channel%", () => ch.Name);
|
||||
_reps.TryAdd("%channel.mention%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name);
|
||||
_reps.TryAdd("%channel.name%", () => ch.Name);
|
||||
_reps.TryAdd("%channel.id%", () => ch.Id.ToString());
|
||||
_reps.TryAdd("%channel.created%", () => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy"));
|
||||
_reps.TryAdd("%channel.nsfw%", () => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-");
|
||||
_reps.TryAdd("%channel.topic%", () => (ch as ITextChannel)?.Topic ?? "-");
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithUser(IUser user)
|
||||
{
|
||||
WithManyUsers(new[] { user });
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithManyUsers(IEnumerable<IUser> users)
|
||||
{
|
||||
_reps.TryAdd("%user%", () => string.Join(" ", users.Select(user => user.Mention)));
|
||||
_reps.TryAdd("%user.mention%", () => string.Join(" ", users.Select(user => user.Mention)));
|
||||
_reps.TryAdd("%user.fullname%", () => string.Join(" ", users.Select(user => user.ToString())));
|
||||
_reps.TryAdd("%user.name%", () => string.Join(" ", users.Select(user => user.Username)));
|
||||
_reps.TryAdd("%user.discrim%", () => string.Join(" ", users.Select(user => user.Discriminator)));
|
||||
_reps.TryAdd("%user.avatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl().ToString())));
|
||||
_reps.TryAdd("%user.id%", () => string.Join(" ", users.Select(user => user.Id.ToString())));
|
||||
_reps.TryAdd("%user.created_time%",
|
||||
() => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm"))));
|
||||
_reps.TryAdd("%user.created_date%",
|
||||
() => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy"))));
|
||||
_reps.TryAdd("%user.joined_time%",
|
||||
() => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-")));
|
||||
_reps.TryAdd("%user.joined_date%",
|
||||
() => string.Join(" ",
|
||||
users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-")));
|
||||
return this;
|
||||
}
|
||||
|
||||
private ReplacementBuilder WithStats(DiscordSocketClient c)
|
||||
{
|
||||
_reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString());
|
||||
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
|
||||
_reps.TryAdd("%shard.id%", () => c.ShardId.ToString());
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithRngRegex()
|
||||
{
|
||||
var rng = new NadekoRandom();
|
||||
_regex.TryAdd(_rngRegex,
|
||||
match =>
|
||||
{
|
||||
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();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithOverride(string key, Func<string> output)
|
||||
{
|
||||
_reps.AddOrUpdate(key, output, delegate { return output; });
|
||||
return this;
|
||||
}
|
||||
|
||||
public Replacer Build()
|
||||
=> new(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray());
|
||||
|
||||
public ReplacementBuilder WithProviders(IEnumerable<IPlaceholderProvider> phProviders)
|
||||
{
|
||||
foreach (var provider in phProviders)
|
||||
foreach (var ovr in provider.GetPlaceholders())
|
||||
_reps.TryAdd(ovr.Name, ovr.Func);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public class Replacer
|
||||
{
|
||||
private readonly IEnumerable<(Regex Regex, Func<Match, string> Replacement)> _regex;
|
||||
private readonly IEnumerable<(string Key, Func<string> Text)> _replacements;
|
||||
|
||||
public Replacer(IEnumerable<(string, Func<string>)> replacements, IEnumerable<(Regex, Func<Match, string>)> regex)
|
||||
{
|
||||
_replacements = replacements;
|
||||
_regex = regex;
|
||||
}
|
||||
|
||||
public string Replace(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return input;
|
||||
|
||||
foreach (var (key, text) in _replacements)
|
||||
{
|
||||
if (input.Contains(key))
|
||||
input = input.Replace(key, text(), StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
foreach (var item in _regex)
|
||||
input = item.Regex.Replace(input, m => item.Replacement(m));
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
public SmartText Replace(SmartText data)
|
||||
=> data switch
|
||||
{
|
||||
SmartEmbedText embedData => Replace(embedData) with
|
||||
{
|
||||
PlainText = Replace(embedData.PlainText),
|
||||
Color = embedData.Color
|
||||
},
|
||||
SmartPlainText plain => Replace(plain),
|
||||
SmartEmbedTextArray arr => Replace(arr),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type")
|
||||
};
|
||||
|
||||
private SmartEmbedTextArray Replace(SmartEmbedTextArray embedArr)
|
||||
=> new()
|
||||
{
|
||||
Embeds = embedArr.Embeds.Map(e => Replace(e) with
|
||||
{
|
||||
Color = e.Color
|
||||
}),
|
||||
Content = Replace(embedArr.Content)
|
||||
};
|
||||
|
||||
private SmartPlainText Replace(SmartPlainText plain)
|
||||
=> Replace(plain.Text);
|
||||
|
||||
private T Replace<T>(T embedData) where T: SmartEmbedTextBase, new()
|
||||
{
|
||||
var newEmbedData = new T
|
||||
{
|
||||
Description = Replace(embedData.Description),
|
||||
Title = Replace(embedData.Title),
|
||||
Thumbnail = Replace(embedData.Thumbnail),
|
||||
Image = Replace(embedData.Image),
|
||||
Url = Replace(embedData.Url),
|
||||
Author = embedData.Author is null
|
||||
? null
|
||||
: new()
|
||||
{
|
||||
Name = Replace(embedData.Author.Name),
|
||||
IconUrl = Replace(embedData.Author.IconUrl)
|
||||
},
|
||||
Fields = embedData.Fields?.Map(f => new SmartTextEmbedField
|
||||
{
|
||||
Name = Replace(f.Name),
|
||||
Value = Replace(f.Value),
|
||||
Inline = f.Inline
|
||||
}),
|
||||
Footer = embedData.Footer is null
|
||||
? null
|
||||
: new()
|
||||
{
|
||||
Text = Replace(embedData.Footer.Text),
|
||||
IconUrl = Replace(embedData.Footer.IconUrl)
|
||||
}
|
||||
};
|
||||
|
||||
return newEmbedData;
|
||||
}
|
||||
}
|
@@ -30,7 +30,7 @@ public sealed record SmartEmbedArrayElementText : SmartEmbedTextBase
|
||||
|
||||
public sealed record SmartEmbedText : SmartEmbedTextBase
|
||||
{
|
||||
public string PlainText { get; init; }
|
||||
public string? PlainText { get; init; }
|
||||
|
||||
public uint Color { get; init; } = 7458112;
|
||||
|
||||
@@ -54,15 +54,15 @@ public sealed record SmartEmbedText : SmartEmbedTextBase
|
||||
|
||||
public abstract record SmartEmbedTextBase : SmartText
|
||||
{
|
||||
public string Title { get; init; }
|
||||
public string Description { get; init; }
|
||||
public string Url { get; init; }
|
||||
public string Thumbnail { get; init; }
|
||||
public string Image { get; init; }
|
||||
public string? Title { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? Url { get; init; }
|
||||
public string? Thumbnail { get; init; }
|
||||
public string? Image { get; init; }
|
||||
|
||||
public SmartTextEmbedAuthor Author { get; init; }
|
||||
public SmartTextEmbedFooter Footer { get; init; }
|
||||
public SmartTextEmbedField[] Fields { get; init; }
|
||||
public SmartTextEmbedAuthor? Author { get; init; }
|
||||
public SmartTextEmbedFooter? Footer { get; init; }
|
||||
public SmartTextEmbedField[]? Fields { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsValid
|
||||
|
Reference in New Issue
Block a user