* 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

View File

@@ -1,5 +1,6 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Db;
using Nadeko.Bot.Db.Models;
@@ -11,15 +12,18 @@ public class AdministrationService : INService
public ConcurrentDictionary<ulong, bool> DeleteMessagesOnCommandChannels { get; }
private readonly DbService _db;
private readonly IReplacementService _repSvc;
private readonly ILogCommandService _logService;
public AdministrationService(
IBot bot,
CommandHandler cmdHandler,
DbService db,
IReplacementService repSvc,
ILogCommandService logService)
{
_db = db;
_repSvc = repSvc;
_logService = logService;
DeleteMessagesOnCommand = new(bot.AllGuildConfigs.Where(g => g.DeleteMessageOnCommand).Select(g => g.GuildId));
@@ -148,10 +152,10 @@ public class AdministrationService : INService
if (msg is not IUserMessage umsg || msg.Author.Id != context.Client.CurrentUser.Id)
return;
var rep = new ReplacementBuilder().WithDefault(context).Build();
var repCtx = new ReplacementContext(context);
var text = SmartText.CreateFrom(input);
text = rep.Replace(text);
text = await _repSvc.ReplaceAsync(text, repCtx);
await umsg.EditAsync(text);
}

View File

@@ -2,6 +2,7 @@ using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using Nadeko.Bot.Db.Models;
using System.Threading.Channels;
using NadekoBot.Common;
namespace NadekoBot.Services;
@@ -18,16 +19,19 @@ public class GreetService : INService, IReadyExecutor
private readonly GreetGrouper<IGuildUser> _greets = new();
private readonly GreetGrouper<IUser> _byes = new();
private readonly BotConfigService _bss;
private readonly IReplacementService _repSvc;
public GreetService(
DiscordSocketClient client,
IBot bot,
DbService db,
BotConfigService bss)
BotConfigService bss,
IReplacementService repSvc)
{
_db = db;
_client = client;
_bss = bss;
_repSvc = repSvc;
_guildConfigsCache = new(bot.AllGuildConfigs.ToDictionary(g => g.GuildId, GreetSettings.Create));
@@ -81,11 +85,12 @@ public class GreetService : INService, IReadyExecutor
return;
var toSend = SmartText.CreateFrom(conf.BoostMessage);
var rep = new ReplacementBuilder().WithDefault(user, channel, user.Guild, _client).Build();
try
{
var toDelete = await channel.SendAsync(rep.Replace(toSend));
var newContent = await _repSvc.ReplaceAsync(toSend,
new(client: _client, guild: user.Guild, channel: channel, users: user));
var toDelete = await channel.SendAsync(newContent);
if (conf.BoostMessageDeleteAfter > 0)
toDelete.DeleteAfter(conf.BoostMessageDeleteAfter);
}
@@ -177,23 +182,31 @@ public class GreetService : INService, IReadyExecutor
if (!users.Any())
return;
var rep = new ReplacementBuilder().WithChannel(channel)
.WithClient(_client)
.WithServer(_client, (SocketGuild)channel.Guild)
.WithManyUsers(users)
.Build();
// var rep = new ReplacementBuilder().WithChannel(channel)
// .WithClient(_client)
// .WithServer(_client, (SocketGuild)channel.Guild)
// .WithManyUsers(users)
// .Build();
var repCtx = new ReplacementContext(client: _client,
guild: channel.Guild,
channel: channel,
users: users.ToArray());
var text = SmartText.CreateFrom(conf.ChannelByeMessageText);
text = rep.Replace(text);
text = await _repSvc.ReplaceAsync(text, repCtx);
try
{
var toDelete = await channel.SendAsync(text);
if (conf.AutoDeleteByeMessagesTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer);
}
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions || ex.DiscordCode == DiscordErrorCode.UnknownChannel)
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions ||
ex.DiscordCode == DiscordErrorCode.UnknownChannel)
{
Log.Warning(ex, "Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}", channel.GuildId);
Log.Warning(ex,
"Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}",
channel.GuildId);
await SetBye(channel.GuildId, channel.Id, false);
}
catch (Exception ex)
@@ -210,23 +223,31 @@ public class GreetService : INService, IReadyExecutor
if (users.Count == 0)
return;
var rep = new ReplacementBuilder().WithChannel(channel)
.WithClient(_client)
.WithServer(_client, (SocketGuild)channel.Guild)
.WithManyUsers(users)
.Build();
// var rep = new ReplacementBuilder()
// .WithChannel(channel)
// .WithClient(_client)
// .WithServer(_client, (SocketGuild)channel.Guild)
// .WithManyUsers(users)
// .Build();
var repCtx = new ReplacementContext(client: _client,
guild: channel.Guild,
channel: channel,
users: users.ToArray());
var text = SmartText.CreateFrom(conf.ChannelGreetMessageText);
text = rep.Replace(text);
text = await _repSvc.ReplaceAsync(text, repCtx);
try
{
var toDelete = await channel.SendAsync(text);
if (conf.AutoDeleteGreetMessagesTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer);
}
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions || ex.DiscordCode == DiscordErrorCode.UnknownChannel)
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions ||
ex.DiscordCode == DiscordErrorCode.UnknownChannel)
{
Log.Warning(ex, "Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}", channel.GuildId);
Log.Warning(ex,
"Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}",
channel.GuildId);
await SetGreet(channel.GuildId, channel.Id, false);
}
catch (Exception ex)
@@ -254,13 +275,14 @@ public class GreetService : INService, IReadyExecutor
{
try
{
var rep = new ReplacementBuilder()
.WithUser(user)
.WithServer(_client, (SocketGuild)user.Guild)
.Build();
// var rep = new ReplacementBuilder()
// .WithUser(user)
// .WithServer(_client, (SocketGuild)user.Guild)
// .Build();
var repCtx = new ReplacementContext(client: _client, guild: user.Guild, users: user);
var text = SmartText.CreateFrom(conf.DmGreetMessageText);
text = rep.Replace(text);
text = await _repSvc.ReplaceAsync(text, repCtx);
if (text is SmartPlainText pt)
{
@@ -269,7 +291,7 @@ public class GreetService : INService, IReadyExecutor
Description = pt.Text
};
}
if (text is SmartEmbedText set)
{
text = set with
@@ -616,4 +638,4 @@ public class GreetService : INService, IReadyExecutor
}
#endregion
}
}

View File

@@ -1,5 +1,6 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.ModuleBehaviors;
using Nadeko.Bot.Db.Models;
@@ -9,7 +10,8 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
{
private readonly BotConfigService _bss;
private readonly SelfService _selfService;
private readonly Replacer _rep;
private readonly IReplacementService _repService;
// private readonly Replacer _rep;
private readonly DbService _db;
private readonly DiscordSocketClient _client;
@@ -18,15 +20,15 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
DbService db,
BotConfigService bss,
IEnumerable<IPlaceholderProvider> phProviders,
SelfService selfService)
SelfService selfService,
IReplacementService repService)
{
_db = db;
_bss = bss;
_selfService = selfService;
_repService = repService;
_client = client;
if (client.ShardId == 0)
_rep = new ReplacementBuilder().WithClient(client).WithProviders(phProviders).Build();
}
public async Task OnReadyAsync()
@@ -56,7 +58,7 @@ public sealed class PlayingRotateService : INService, IReadyExecutor
? rotatingStatuses[index = 0]
: rotatingStatuses[index++];
var statusText = _rep.Replace(playingStatus.Status);
var statusText = await _repService.ReplaceAsync(playingStatus.Status, new (client: _client));
await _selfService.SetGameAsync(statusText, (ActivityType)playingStatus.Type);
}
catch (Exception ex)

View File

@@ -1,5 +1,5 @@
#nullable disable
using NadekoBot.Db;
using NadekoBot.Common;
using NadekoBot.Modules.Administration.Services;
using Nadeko.Bot.Db.Models;
using Nadeko.Common.Medusa;
@@ -506,9 +506,10 @@ public partial class Administration
[OwnerOnly]
public async Task SetGame(ActivityType type, [Leftover] string game = null)
{
var rep = new ReplacementBuilder().WithDefault(Context).Build();
// var rep = new ReplacementBuilder().WithDefault(Context).Build();
await _service.SetGameAsync(game is null ? game : rep.Replace(game), type);
var repCtx = new ReplacementContext(ctx);
await _service.SetGameAsync(game is null ? game : await repSvc.ReplaceAsync(game, repCtx), type);
await ReplyConfirmLocalizedAsync(strs.set_game);
}
@@ -538,7 +539,8 @@ public partial class Administration
if (server is null)
return;
var rep = new ReplacementBuilder().WithDefault(Context).Build();
// var repSvc = new ReplacementBuilder().WithDefault(Context).Build();
var repCtx = new ReplacementContext(Context);
if (ids[1].ToUpperInvariant().StartsWith("C:", StringComparison.InvariantCulture))
{
@@ -547,7 +549,7 @@ public partial class Administration
if (ch is null)
return;
text = rep.Replace(text);
text = await repSvc.ReplaceAsync(text, repCtx);
await ch.SendAsync(text);
}
else if (ids[1].ToUpperInvariant().StartsWith("U:", StringComparison.InvariantCulture))
@@ -558,7 +560,7 @@ public partial class Administration
return;
var ch = await user.CreateDMChannelAsync();
text = rep.Replace(text);
text = await repSvc.ReplaceAsync(text, repCtx);
await ch.SendAsync(text);
}
else

View File

@@ -1,26 +1,25 @@
#nullable disable
using NadekoBot.Db;
using Nadeko.Bot.Db.Models;
using NadekoBot.Common.ModuleBehaviors;
namespace NadekoBot.Modules.Administration.Services;
public sealed class GuildTimezoneService : ITimezoneService, INService
public sealed class GuildTimezoneService : ITimezoneService, IReadyExecutor, INService
{
public static ConcurrentDictionary<ulong, GuildTimezoneService> AllServices { get; } = new();
private readonly ConcurrentDictionary<ulong, TimeZoneInfo> _timezones;
private readonly DbService _db;
private readonly IReplacementPatternStore _repStore;
public GuildTimezoneService(DiscordSocketClient client, IBot bot, DbService db)
public GuildTimezoneService(IBot bot, DbService db, IReplacementPatternStore repStore)
{
_timezones = bot.AllGuildConfigs.Select(GetTimzezoneTuple)
.Where(x => x.Timezone is not null)
.ToDictionary(x => x.GuildId, x => x.Timezone)
.ToConcurrent();
.Where(x => x.Timezone is not null)
.ToDictionary(x => x.GuildId, x => x.Timezone)
.ToConcurrent();
var curUser = client.CurrentUser;
if (curUser is not null)
AllServices.TryAdd(curUser.Id, this);
_db = db;
_repStore = repStore;
bot.JoinedGuild += Bot_JoinedGuild;
}
@@ -55,7 +54,7 @@ public sealed class GuildTimezoneService : ITimezoneService, INService
{
if (guildId is ulong gid && _timezones.TryGetValue(gid, out var tz))
return tz;
return null;
}
@@ -75,4 +74,22 @@ public sealed class GuildTimezoneService : ITimezoneService, INService
public TimeZoneInfo GetTimeZoneOrUtc(ulong? guildId)
=> GetTimeZoneOrDefault(guildId) ?? TimeZoneInfo.Utc;
public Task OnReadyAsync()
{
_repStore.Register("%server.time%",
(IGuild g) =>
{
var to = TimeZoneInfo.Local;
if (g is not null)
{
to = GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
}
return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ")
+ to.StandardName.GetInitials();
});
return Task.CompletedTask;
}
}

View File

@@ -425,7 +425,7 @@ public partial class Administration
try
{
var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
var embed = _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time);
var embed = await _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time);
if (embed is not null)
await guildUser.SendAsync(embed);
}
@@ -490,7 +490,7 @@ public partial class Administration
try
{
var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg));
var embed = _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null);
var embed = await _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null);
if (embed is not null)
await user.SendAsync(embed);
}
@@ -586,7 +586,7 @@ public partial class Administration
private async Task InternalBanMessageTest(string reason, TimeSpan? duration)
{
var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), reason));
var embed = _service.GetBanUserDmEmbed(Context, (IGuildUser)ctx.User, defaultMessage, reason, duration);
var embed = await _service.GetBanUserDmEmbed(Context, (IGuildUser)ctx.User, defaultMessage, reason, duration);
if (embed is null)
await ConfirmLocalizedAsync(strs.banmsg_disabled);

View File

@@ -2,6 +2,7 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.TypeReaders.Models;
using NadekoBot.Db;
@@ -18,6 +19,7 @@ public class UserPunishService : INService, IReadyExecutor
private readonly BlacklistService _blacklistService;
private readonly BotConfigService _bcs;
private readonly DiscordSocketClient _client;
private readonly IReplacementService _repSvc;
public event Func<Warning, Task> OnUserWarned = static delegate { return Task.CompletedTask; };
@@ -26,13 +28,15 @@ public class UserPunishService : INService, IReadyExecutor
DbService db,
BlacklistService blacklistService,
BotConfigService bcs,
DiscordSocketClient client)
DiscordSocketClient client,
IReplacementService repSvc)
{
_mute = mute;
_db = db;
_blacklistService = blacklistService;
_bcs = bcs;
_client = client;
_repSvc = repSvc;
}
public async Task OnReadyAsync()
@@ -524,7 +528,7 @@ public class UserPunishService : INService, IReadyExecutor
.FirstOrDefaultAsyncLinqToDB();
}
public SmartText GetBanUserDmEmbed(
public Task<SmartText> GetBanUserDmEmbed(
ICommandContext context,
IGuildUser target,
string defaultMessage,
@@ -538,7 +542,7 @@ public class UserPunishService : INService, IReadyExecutor
banReason,
duration);
public SmartText GetBanUserDmEmbed(
public async Task<SmartText> GetBanUserDmEmbed(
DiscordSocketClient client,
SocketGuild guild,
IGuildUser moderator,
@@ -551,7 +555,7 @@ public class UserPunishService : INService, IReadyExecutor
banReason = string.IsNullOrWhiteSpace(banReason) ? "-" : banReason;
var replacer = new ReplacementBuilder().WithServer(client, guild)
var repCtx = new ReplacementContext(client, guild)
.WithOverride("%ban.mod%", () => moderator.ToString())
.WithOverride("%ban.mod.fullname%", () => moderator.ToString())
.WithOverride("%ban.mod.name%", () => moderator.Username)
@@ -563,8 +567,8 @@ public class UserPunishService : INService, IReadyExecutor
.WithOverride("%reason%", () => banReason)
.WithOverride("%ban.reason%", () => banReason)
.WithOverride("%ban.duration%",
() => duration?.ToString(@"d\.hh\:mm") ?? "perma")
.Build();
() => duration?.ToString(@"d\.hh\:mm") ?? "perma");
// if template isn't set, use the old message style
if (string.IsNullOrWhiteSpace(template))
@@ -590,6 +594,6 @@ public class UserPunishService : INService, IReadyExecutor
}
var output = SmartText.CreateFrom(template);
return replacer.Replace(output);
return await _repSvc.ReplaceAsync(output, repCtx);
}
}

View File

@@ -1,6 +1,7 @@
#nullable disable
using Nadeko.Bot.Db.Models;
using System.Runtime.CompilerServices;
using NadekoBot.Common;
namespace NadekoBot.Modules.NadekoExpressions;
@@ -12,6 +13,7 @@ public static class NadekoExpressionExtensions
public static async Task<IUserMessage> Send(
this NadekoExpression cr,
IUserMessage ctx,
IReplacementService repSvc,
DiscordSocketClient client,
bool sanitize)
{
@@ -32,16 +34,18 @@ public static class NadekoExpressionExtensions
var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
var rep = new ReplacementBuilder()
.WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild as SocketGuild, client)
.WithOverride("%target%",
() => canMentionEveryone
? ctx.Content[substringIndex..].Trim()
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true))
.Build();
var repCtx = new ReplacementContext(client: client,
guild: (ctx.Channel as ITextChannel)?.Guild as SocketGuild,
channel: ctx.Channel,
users: ctx.Author
)
.WithOverride("%target%",
() => canMentionEveryone
? ctx.Content[substringIndex..].Trim()
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true));
var text = SmartText.CreateFrom(cr.Response);
text = rep.Replace(text);
text = await repSvc.ReplaceAsync(text, repCtx);
return await channel.SendAsync(text, sanitize);
}

View File

@@ -6,7 +6,6 @@ using NadekoBot.Db;
using Nadeko.Bot.Db.Models;
using System.Runtime.CompilerServices;
using LinqToDB.EntityFrameworkCore;
using Nadeko.Bot.Common;
using NadekoBot.Services;
using Serilog;
using YamlDotNet.Serialization;
@@ -75,6 +74,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
private readonly IBot _bot;
private readonly IPubSub _pubSub;
private readonly IEmbedBuilderService _eb;
private readonly IReplacementService _repSvc;
private readonly Random _rng;
private bool ready;
@@ -88,6 +88,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
ICommandHandler cmd,
IPubSub pubSub,
IEmbedBuilderService eb,
IReplacementService repSvc,
IPermissionChecker permChecker)
{
_db = db;
@@ -97,6 +98,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
_bot = bot;
_pubSub = pubSub;
_eb = eb;
_repSvc = repSvc;
_permChecker = permChecker;
_rng = new NadekoRandom();
@@ -275,7 +277,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
}
}
var sentMsg = await expr.Send(msg, _client, false);
var sentMsg = await expr.Send(msg, _repSvc, _client, false);
var reactions = expr.GetReactions();
foreach (var reaction in reactions)

View File

@@ -1,7 +1,7 @@
#nullable disable
using NadekoBot.Modules.Gambling.Services;
using System.Numerics;
using Nadeko.Bot.Common;
using NadekoBot.Common;
namespace NadekoBot.Modules.Gambling.Common;

View File

@@ -1,5 +1,5 @@
#nullable disable
using Nadeko.Bot.Common;
using NadekoBot.Common;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Games.Common;

View File

@@ -5,7 +5,7 @@ using NadekoBot.Modules.Help.Services;
using Newtonsoft.Json;
using System.Text;
using System.Text.Json;
using Nadeko.Bot.Common;
using NadekoBot.Common;
using Nadeko.Common.Medusa;
using JsonSerializer = System.Text.Json.JsonSerializer;
@@ -56,15 +56,14 @@ public sealed class Help : NadekoModule<HelpService>
return default;
var clientId = await _lazyClientId.Value;
var r = new ReplacementBuilder().WithDefault(Context)
var repCtx = new ReplacementContext(Context)
.WithOverride("{0}", () => clientId.ToString())
.WithOverride("{1}", () => prefix)
.WithOverride("%prefix%", () => prefix)
.WithOverride("%bot.prefix%", () => prefix)
.Build();
.WithOverride("%bot.prefix%", () => prefix);
var text = SmartText.CreateFrom(botSettings.HelpText);
return r.Replace(text);
return await repSvc.ReplaceAsync(text, repCtx);
}
[Cmd]

View File

@@ -1,42 +1,34 @@
#nullable disable
using NadekoBot.Common;
using NadekoBot.Common.ModuleBehaviors;
namespace NadekoBot.Modules.Help.Services;
public class HelpService : IExecNoCommand, INService
public class HelpService(BotConfigService bss, IReplacementService repSvc) : IExecNoCommand, INService
{
private readonly BotConfigService _bss;
public HelpService(BotConfigService bss)
public async Task ExecOnNoCommandAsync(IGuild? guild, IUserMessage msg)
{
_bss = bss;
}
public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{
var settings = _bss.Data;
var settings = bss.Data;
if (guild is null)
{
if (string.IsNullOrWhiteSpace(settings.DmHelpText) || settings.DmHelpText == "-")
return Task.CompletedTask;
return;
// only send dm help text if it contains one of the keywords, if they're specified
// if they're not, then reply to every DM
if (settings.DmHelpTextKeywords is not null &&
!settings.DmHelpTextKeywords.Any(k => msg.Content.Contains(k)))
return Task.CompletedTask;
{
return;
}
var rep = new ReplacementBuilder().WithOverride("%prefix%", () => _bss.Data.Prefix)
.WithOverride("%bot.prefix%", () => _bss.Data.Prefix)
.WithUser(msg.Author)
.Build();
var repCtx = new ReplacementContext(guild: guild, channel: msg.Channel, users: msg.Author)
.WithOverride("%prefix%", () => bss.Data.Prefix)
.WithOverride("%bot.prefix%", () => bss.Data.Prefix);
var text = SmartText.CreateFrom(settings.DmHelpText);
text = rep.Replace(text);
text = await repSvc.ReplaceAsync(text, repCtx);
return msg.Channel.SendAsync(text);
await msg.Channel.SendAsync(text);
}
return Task.CompletedTask;
}
}

View File

@@ -6,7 +6,7 @@ using System.Text.Json;
namespace NadekoBot.Modules.Searches;
// todo fix stock/crypto
// todo fix stock
public sealed class DefaultStockDataService : IStockDataService, INService
{
private readonly IHttpClientFactory _httpClientFactory;
@@ -83,7 +83,6 @@ public sealed class DefaultStockDataService : IStockDataService, INService
PrepareHeaderForMatch = args => args.Header.Humanize(LetterCasing.Title)
};
// todo this needs testing
public async Task<IReadOnlyCollection<CandleData>> GetCandleDataAsync(string query)
{
using var http = _httpClientFactory.CreateClient();

View File

@@ -1,5 +1,6 @@
#nullable disable
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Db.Models;
@@ -28,6 +29,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
private readonly IPubSub _pubSub;
private readonly IEmbedBuilderService _eb;
private readonly SearchesConfigService _config;
private readonly IReplacementService _repSvc;
public TypedKey<List<StreamData>> StreamsOnlineKey { get; }
public TypedKey<List<StreamData>> StreamsOfflineKey { get; }
@@ -50,7 +52,8 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
IBot bot,
IPubSub pubSub,
IEmbedBuilderService eb,
SearchesConfigService config)
SearchesConfigService config,
IReplacementService repSvc)
{
_db = db;
_client = client;
@@ -58,6 +61,7 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
_pubSub = pubSub;
_eb = eb;
_config = config;
_repSvc = repSvc;
_streamTracker = new(httpFactory, creds);
@@ -275,11 +279,13 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
if (textChannel is null)
return default;
var rep = new ReplacementBuilder().WithOverride("%user%", () => fs.Username)
.WithOverride("%platform%", () => fs.Type.ToString())
.Build();
var repCtx = new ReplacementContext(guild: textChannel.Guild, client: _client)
.WithOverride("%platform%", () => fs.Type.ToString());
var message = string.IsNullOrWhiteSpace(fs.Message) ? "" : rep.Replace(fs.Message);
var message = string.IsNullOrWhiteSpace(fs.Message)
? ""
: await _repSvc.ReplaceAsync(fs.Message, repCtx);
var msg = await textChannel.EmbedAsync(GetEmbed(fs.GuildId, stream, false), message);
@@ -324,8 +330,8 @@ public sealed class StreamNotificationService : INService, IReadyExecutor
using (var uow = _db.GetDbContext())
{
var gc = uow.Set<GuildConfig>().AsQueryable()
.Include(x => x.FollowedStreams)
.FirstOrDefault(x => x.GuildId == guildConfig.GuildId);
.Include(x => x.FollowedStreams)
.FirstOrDefault(x => x.GuildId == guildConfig.GuildId);
if (gc is null)
return Task.CompletedTask;

View File

@@ -1,4 +1,5 @@
#nullable disable warnings
using NadekoBot.Common;
using NadekoBot.Common.Yml;
using NadekoBot.Db;
using Nadeko.Bot.Db.Models;
@@ -99,10 +100,10 @@ public partial class Utility
if (quote is null)
return;
var rep = new ReplacementBuilder().WithDefault(Context).Build();
var repCtx = new ReplacementContext(Context);
var text = SmartText.CreateFrom(quote.Text);
text = rep.Replace(text);
text = await repSvc.ReplaceAsync(text, repCtx);
await ctx.Channel.SendAsync($"`#{quote.Id}` 📣 " + text, true);
}
@@ -193,7 +194,7 @@ public partial class Utility
Quote quote;
var rep = new ReplacementBuilder().WithDefault(Context).Build();
var repCtx = new ReplacementContext(Context);
await using (var uow = _db.GetDbContext())
{
@@ -212,7 +213,7 @@ public partial class Utility
var text = SmartText.CreateFrom(quote.Text);
text = rep.Replace(text);
text = await repSvc.ReplaceAsync(text, repCtx);
await ctx.Channel.SendAsync(infoText + text, true);
}

View File

@@ -1,6 +1,7 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common;
using NadekoBot.Common.ModuleBehaviors;
using Nadeko.Bot.Db.Models;
@@ -11,6 +12,7 @@ public sealed class RepeaterService : IReadyExecutor, INService
private const int MAX_REPEATERS = 5;
private readonly DbService _db;
private readonly IReplacementService _repSvc;
private readonly IBotCredentials _creds;
private readonly DiscordSocketClient _client;
private readonly LinkedList<RunningRepeater> _repeaterQueue;
@@ -22,18 +24,20 @@ public sealed class RepeaterService : IReadyExecutor, INService
public RepeaterService(
DiscordSocketClient client,
DbService db,
IReplacementService repSvc,
IBotCredentials creds)
{
_db = db;
_repSvc = repSvc;
_creds = creds;
_client = client;
using var uow = _db.GetDbContext();
var shardRepeaters = uow.Set<Repeater>()
.Where(x => (int)(x.GuildId / Math.Pow(2, 22)) % _creds.TotalShards
== _client.ShardId)
.AsNoTracking()
.ToList();
.Where(x => (int)(x.GuildId / Math.Pow(2, 22)) % _creds.TotalShards
== _client.ShardId)
.AsNoTracking()
.ToList();
_noRedundant = new(shardRepeaters.Where(x => x.NoRedundant).Select(x => x.Id));
@@ -123,9 +127,9 @@ public sealed class RepeaterService : IReadyExecutor, INService
await using var uow = _db.GetDbContext();
var toTrigger = await uow.Set<Repeater>().AsNoTracking()
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
if (toTrigger is null)
return false;
@@ -195,8 +199,13 @@ public sealed class RepeaterService : IReadyExecutor, INService
var channel = _client.GetChannel(repeater.ChannelId) as ITextChannel;
if (channel is null)
{
try { channel = await _client.Rest.GetChannelAsync(repeater.ChannelId) as ITextChannel; }
catch { }
try
{
channel = await _client.Rest.GetChannelAsync(repeater.ChannelId) as ITextChannel;
}
catch
{
}
}
if (channel is null)
@@ -247,12 +256,15 @@ public sealed class RepeaterService : IReadyExecutor, INService
}
}
var rep = new ReplacementBuilder().WithDefault(guild.CurrentUser, channel, guild, _client).Build();
var repCtx = new ReplacementContext(client: _client,
guild: guild,
channel: channel,
users: guild.CurrentUser);
try
{
var text = SmartText.CreateFrom(repeater.Message);
text = rep.Replace(text);
text = await _repSvc.ReplaceAsync(text, repCtx);
var newMsg = await channel.SendAsync(text);
_ = newMsg.AddReactionAsync(new Emoji("🔄"));
@@ -298,11 +310,11 @@ public sealed class RepeaterService : IReadyExecutor, INService
{
await using var uow = _db.GetDbContext();
await uow.Set<Repeater>().AsQueryable()
.Where(x => x.Id == repeaterId)
.UpdateAsync(rep => new()
{
LastMessageId = lastMsgId
});
.Where(x => x.Id == repeaterId)
.UpdateAsync(rep => new()
{
LastMessageId = lastMsgId
});
}
public async Task<RunningRepeater?> AddRepeaterAsync(
@@ -348,9 +360,9 @@ public sealed class RepeaterService : IReadyExecutor, INService
await using var uow = _db.GetDbContext();
var toRemove = await uow.Set<Repeater>().AsNoTracking()
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
if (toRemove is null)
return null;
@@ -379,9 +391,9 @@ public sealed class RepeaterService : IReadyExecutor, INService
{
await using var uow = _db.GetDbContext();
var toToggle = await uow.Set<Repeater>().AsQueryable()
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
.Where(x => x.GuildId == guildId)
.Skip(index)
.FirstOrDefaultAsyncEF();
if (toToggle is null)
return null;

View File

@@ -1,6 +1,4 @@
#nullable disable
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using NadekoBot.Modules.Utility.Services;
using Newtonsoft.Json;
using System.Diagnostics;
@@ -61,11 +59,12 @@ public partial class Utility : NadekoModule
[Priority(1)]
public async Task Say(ITextChannel channel, [Leftover] SmartText message)
{
var rep = new ReplacementBuilder()
.WithDefault(ctx.User, channel, (SocketGuild)ctx.Guild, (DiscordSocketClient)ctx.Client)
.Build();
// var rep = new ReplacementBuilder()
// .WithDefault(ctx.User, channel, (SocketGuild)ctx.Guild, (DiscordSocketClient)ctx.Client)
// .Build();
message = rep.Replace(message);
var repCtx = new ReplacementContext(Context);
message = await repSvc.ReplaceAsync(message, repCtx);
await channel.SendAsync(message, !((IGuildUser)ctx.User).GuildPermissions.MentionEveryone);
}

View File

@@ -1,7 +1,6 @@
#nullable disable warnings
using NadekoBot.Modules.Xp.Services;
using Nadeko.Bot.Db.Models;
using Nadeko.Bot.Common;
using NadekoBot.Db;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Patronage;

View File

@@ -1,5 +1,4 @@
using Nadeko.Bot.Common;
using NadekoBot.Modules.Xp.Services;
using NadekoBot.Modules.Xp.Services;
namespace NadekoBot.Modules.Xp;

View File

@@ -1,4 +1,4 @@
using Nadeko.Bot.Common;
using NadekoBot.Common;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling;

View File

@@ -1,8 +1,5 @@
using Nadeko.Bot.Common;
using NadekoBot.Modules.Permissions.Common;
using NadekoBot.Modules.Permissions.Common;
using NadekoBot.Modules.Permissions.Services;
using OneOf;
using OneOf.Types;
namespace NadekoBot;