* Replacement mechanism reworked

* Services can now register their own replacements
    * Possible bugs and/or backwards-incompatible behavior
This commit is contained in:
Kwoth
2024-04-01 01:18:26 +00:00
parent 043ad98473
commit 2d3ff83c7c
37 changed files with 892 additions and 415 deletions

View File

@@ -1,7 +1,7 @@
using System.Globalization;
using System.Numerics;
namespace Nadeko.Bot.Common;
namespace NadekoBot.Common;
public interface ICurrencyProvider
{

View File

@@ -2,7 +2,7 @@
using OneOf;
using OneOf.Types;
namespace Nadeko.Bot.Common;
namespace NadekoBot.Common;
public interface IPermissionChecker
{

View File

@@ -39,4 +39,5 @@
<Link>responses.en-US.json</Link>
</AdditionalFiles>
</ItemGroup>
</Project>

View File

@@ -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);

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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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