diff --git a/src/Nadeko.Bot.Common/ICurrencyProvider.cs b/src/Nadeko.Bot.Common/ICurrencyProvider.cs index 5e916b2c8..7947c075f 100644 --- a/src/Nadeko.Bot.Common/ICurrencyProvider.cs +++ b/src/Nadeko.Bot.Common/ICurrencyProvider.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Numerics; -namespace Nadeko.Bot.Common; +namespace NadekoBot.Common; public interface ICurrencyProvider { diff --git a/src/Nadeko.Bot.Common/IPermissionChecker.cs b/src/Nadeko.Bot.Common/IPermissionChecker.cs index 76a21a4e1..e60c3c4c2 100644 --- a/src/Nadeko.Bot.Common/IPermissionChecker.cs +++ b/src/Nadeko.Bot.Common/IPermissionChecker.cs @@ -2,7 +2,7 @@ using OneOf; using OneOf.Types; -namespace Nadeko.Bot.Common; +namespace NadekoBot.Common; public interface IPermissionChecker { diff --git a/src/Nadeko.Bot.Common/Nadeko.Bot.Common.csproj b/src/Nadeko.Bot.Common/Nadeko.Bot.Common.csproj index cbf6974ae..dbafa63fd 100644 --- a/src/Nadeko.Bot.Common/Nadeko.Bot.Common.csproj +++ b/src/Nadeko.Bot.Common/Nadeko.Bot.Common.csproj @@ -39,4 +39,5 @@ responses.en-US.json + diff --git a/src/Nadeko.Bot.Common/NadekoModule.cs b/src/Nadeko.Bot.Common/NadekoModule.cs index d026fd756..411558980 100644 --- a/src/Nadeko.Bot.Common/NadekoModule.cs +++ b/src/Nadeko.Bot.Common/NadekoModule.cs @@ -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); diff --git a/src/Nadeko.Bot.Common/Replacements/IReplacementPatternStore.cs b/src/Nadeko.Bot.Common/Replacements/IReplacementPatternStore.cs new file mode 100644 index 000000000..ef6faae82 --- /dev/null +++ b/src/Nadeko.Bot.Common/Replacements/IReplacementPatternStore.cs @@ -0,0 +1,20 @@ +using System.Text.RegularExpressions; + +namespace NadekoBot.Common; + +public interface IReplacementPatternStore : INService +{ + IReadOnlyDictionary Replacements { get; } + IReadOnlyDictionary RegexReplacements { get; } + + ValueTask Register(string token, Func> repFactory); + ValueTask Register(string token, Func> repFactory); + ValueTask Register(string token, Func> repFactory); + + ValueTask Register(string token, Func repFactory); + ValueTask Register(string token, Func repFactory); + ValueTask Register(string token, Func repFactory); + + ValueTask Register(Regex regex, Func repFactory); + ValueTask Register(Regex regex, Func repFactory); +} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/Replacements/IReplacementService.cs b/src/Nadeko.Bot.Common/Replacements/IReplacementService.cs new file mode 100644 index 000000000..110ce836a --- /dev/null +++ b/src/Nadeko.Bot.Common/Replacements/IReplacementService.cs @@ -0,0 +1,7 @@ +namespace NadekoBot.Common; + +public interface IReplacementService +{ + ValueTask ReplaceAsync(string input, ReplacementContext repCtx); + ValueTask ReplaceAsync(SmartText input, ReplacementContext repCtx); +} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementContext.cs b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementContext.cs new file mode 100644 index 000000000..e0bdf7b09 --- /dev/null +++ b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementContext.cs @@ -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 _overrides = new(); + private readonly HashSet _tokens = new(); + + public IReadOnlyList Overrides + => _overrides.AsReadOnly(); + + private readonly List _regexOverrides = new(); + private readonly HashSet _regexPatterns = new(); + + public IReadOnlyList 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> repFactory) + { + if (_tokens.Add(key)) + { + _overrides.Add(new(key, repFactory)); + } + + return this; + } + + public ReplacementContext WithOverride(string key, Func repFactory) + => WithOverride(key, () => new ValueTask(repFactory())); + + + public ReplacementContext WithOverride(Regex regex, Func> repFactory) + { + if (_regexPatterns.Add(regex.ToString())) + { + _regexOverrides.Add(new(regex, repFactory)); + } + + return this; + } + + public ReplacementContext WithOverride(Regex regex, Func repFactory) + => WithOverride(regex, (Match m) => new ValueTask(repFactory(m))); +} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementInfo.cs b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementInfo.cs new file mode 100644 index 000000000..b50665fe1 --- /dev/null +++ b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementInfo.cs @@ -0,0 +1,57 @@ +using System.Reflection; +using System.Text.RegularExpressions; + +namespace NadekoBot.Common; + +public sealed class ReplacementInfo +{ + private readonly Delegate _del; + public IReadOnlyCollection InputTypes { get; } + public string Token { get; } + + private static readonly Func> _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 GetValueAsync(params object?[]? objs) + => await (ValueTask)(_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 InputTypes { get; } + + public Regex Regex { get; } + public string Pattern { get; } + + private static readonly Func> _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 GetValueAsync(Match m, params object?[]? objs) + => await ((Func>)(_del.DynamicInvoke(objs) ?? _falllbackFunc))(m); + + public override int GetHashCode() + => Regex.GetHashCode(); + + public override bool Equals(object? obj) + => obj is RegexReplacementInfo ri && ri.Pattern == Pattern; +} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementPatternStore.cs b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementPatternStore.cs new file mode 100644 index 000000000..44bb10b24 --- /dev/null +++ b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementPatternStore.cs @@ -0,0 +1,130 @@ +using System.Text.RegularExpressions; +using OneOf; + +namespace NadekoBot.Common; + +public sealed partial class ReplacementPatternStore : IReplacementPatternStore, INService +{ + private readonly ConcurrentDictionary> _guids = new(); + + private readonly ConcurrentDictionary _defaultReplacements = new(); + private readonly ConcurrentDictionary _regexReplacements = new(); + + public IReadOnlyDictionary Replacements + => _defaultReplacements.AsReadOnly(); + + public IReadOnlyDictionary RegexReplacements + => _regexReplacements.AsReadOnly(); + + public ReplacementPatternStore() + { + WithClient(); + WithChannel(); + WithServer(); + WithUsers(); + WithDefault(); + WithRegex(); + } + + // private async ValueTask 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 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 Register(string token, Func> repFactory) + => InternalRegister(token, repFactory); + + public ValueTask Register(string token, Func> repFactory) + => InternalRegister(token, repFactory); + + public ValueTask Register(string token, Func> repFactory) + => InternalRegister(token, repFactory); + + public ValueTask Register(string token, Func repFactory) + => InternalRegister(token, () => new ValueTask(repFactory())); + + public ValueTask Register(string token, Func repFactory) + => InternalRegister(token, (T1 a) => new ValueTask(repFactory(a))); + + public ValueTask Register(string token, Func repFactory) + => InternalRegister(token, (T1 a, T2 b) => new ValueTask(repFactory(a, b))); + + + private ValueTask 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 Register(Regex regex, Func repFactory) + => InternalRegexRegister(regex, () => (Match m) => new ValueTask(repFactory(m))); + + public ValueTask Register(Regex regex, Func repFactory) + => InternalRegexRegister(regex, (T1 a) => (Match m) => new ValueTask(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; + } +} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementRegistrator.default.cs b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementRegistrator.default.cs new file mode 100644 index 000000000..306617901 --- /dev/null +++ b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementRegistrator.default.cs @@ -0,0 +1,113 @@ +using System.Text.RegularExpressions; + +namespace NadekoBot.Common; + +public sealed partial class ReplacementPatternStore +{ + private static readonly Regex _rngRegex = new(@"%rng(?:(?(?:-)?\d+)-(?(?:-)?\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(); + }); + } +} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementService.cs b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementService.cs new file mode 100644 index 000000000..65f3bdeea --- /dev/null +++ b/src/Nadeko.Bot.Common/Replacements/Impl/ReplacementService.cs @@ -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 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 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(); + 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 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 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 GetOriginalReplacementsForContext(ReplacementContext repCtx) + { + var objs = new List(); + 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 GetOriginalRegexReplacementsForContext(ReplacementContext repCtx) + { + var objs = new List(); + 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); + } +} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/Replacements/Impl/Replacer.cs b/src/Nadeko.Bot.Common/Replacements/Impl/Replacer.cs new file mode 100644 index 000000000..68dcbf73b --- /dev/null +++ b/src/Nadeko.Bot.Common/Replacements/Impl/Replacer.cs @@ -0,0 +1,138 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace NadekoBot.Common; + +public sealed partial class Replacer +{ + private readonly IEnumerable _reps; + private readonly IEnumerable _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 reps, IEnumerable regexReps, object[] inputData) + { + _reps = reps; + _inputData = inputData; + _regexReps = regexReps; + } + + public async ValueTask 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 inputTypes) + { + if (inputTypes.Count == 0) + return null; + + var objs = new List(); + 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 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 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 ReplaceAsync(SmartPlainText plain) + => await ReplaceAsync(plain.Text); + + private async Task ReplaceAsync(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; + } +} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/Replacements/ReplacementBuilder.cs b/src/Nadeko.Bot.Common/Replacements/ReplacementBuilder.cs deleted file mode 100644 index d08f15090..000000000 --- a/src/Nadeko.Bot.Common/Replacements/ReplacementBuilder.cs +++ /dev/null @@ -1,164 +0,0 @@ -#nullable disable -using System.Text.RegularExpressions; - -namespace NadekoBot.Common; - -public class ReplacementBuilder -{ - private static readonly Regex _rngRegex = new("%rng(?:(?(?:-)?\\d+)-(?(?:-)?\\d+))?%", - RegexOptions.Compiled); - - private readonly ConcurrentDictionary> _regex = new(); - - private readonly ConcurrentDictionary> _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 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 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 phProviders) - { - foreach (var provider in phProviders) - foreach (var ovr in provider.GetPlaceholders()) - _reps.TryAdd(ovr.Name, ovr.Func); - - return this; - } -} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/Replacements/Replacer.cs b/src/Nadeko.Bot.Common/Replacements/Replacer.cs deleted file mode 100644 index 8c0c84d9f..000000000 --- a/src/Nadeko.Bot.Common/Replacements/Replacer.cs +++ /dev/null @@ -1,93 +0,0 @@ -#nullable disable -using System.Text.RegularExpressions; - -namespace NadekoBot.Common; - -public class Replacer -{ - private readonly IEnumerable<(Regex Regex, Func Replacement)> _regex; - private readonly IEnumerable<(string Key, Func Text)> _replacements; - - public Replacer(IEnumerable<(string, Func)> replacements, IEnumerable<(Regex, Func)> 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 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; - } -} \ No newline at end of file diff --git a/src/Nadeko.Bot.Common/SmartText/SmartEmbedText.cs b/src/Nadeko.Bot.Common/SmartText/SmartEmbedText.cs index ce1f765fc..f2e83206d 100644 --- a/src/Nadeko.Bot.Common/SmartText/SmartEmbedText.cs +++ b/src/Nadeko.Bot.Common/SmartText/SmartEmbedText.cs @@ -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 diff --git a/src/Nadeko.Bot.Modules.Administration/AdministrationService.cs b/src/Nadeko.Bot.Modules.Administration/AdministrationService.cs index 823f2ba95..d48ee32e4 100644 --- a/src/Nadeko.Bot.Modules.Administration/AdministrationService.cs +++ b/src/Nadeko.Bot.Modules.Administration/AdministrationService.cs @@ -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 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); } diff --git a/src/Nadeko.Bot.Modules.Administration/GreetBye/GreetService.cs b/src/Nadeko.Bot.Modules.Administration/GreetBye/GreetService.cs index 7a52ded84..9db490c10 100644 --- a/src/Nadeko.Bot.Modules.Administration/GreetBye/GreetService.cs +++ b/src/Nadeko.Bot.Modules.Administration/GreetBye/GreetService.cs @@ -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 _greets = new(); private readonly GreetGrouper _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 -} +} \ No newline at end of file diff --git a/src/Nadeko.Bot.Modules.Administration/PlayingRotate/PlayingRotateService.cs b/src/Nadeko.Bot.Modules.Administration/PlayingRotate/PlayingRotateService.cs index dc53645b3..eef82e388 100644 --- a/src/Nadeko.Bot.Modules.Administration/PlayingRotate/PlayingRotateService.cs +++ b/src/Nadeko.Bot.Modules.Administration/PlayingRotate/PlayingRotateService.cs @@ -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 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) diff --git a/src/Nadeko.Bot.Modules.Administration/Self/SelfCommands.cs b/src/Nadeko.Bot.Modules.Administration/Self/SelfCommands.cs index a4d5f6120..b16f1db23 100644 --- a/src/Nadeko.Bot.Modules.Administration/Self/SelfCommands.cs +++ b/src/Nadeko.Bot.Modules.Administration/Self/SelfCommands.cs @@ -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 diff --git a/src/Nadeko.Bot.Modules.Administration/Timezone/GuildTimezoneService.cs b/src/Nadeko.Bot.Modules.Administration/Timezone/GuildTimezoneService.cs index f7dd83a5d..67c778f43 100644 --- a/src/Nadeko.Bot.Modules.Administration/Timezone/GuildTimezoneService.cs +++ b/src/Nadeko.Bot.Modules.Administration/Timezone/GuildTimezoneService.cs @@ -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 AllServices { get; } = new(); private readonly ConcurrentDictionary _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; + } } \ No newline at end of file diff --git a/src/Nadeko.Bot.Modules.Administration/UserPunish/UserPunishCommands.cs b/src/Nadeko.Bot.Modules.Administration/UserPunish/UserPunishCommands.cs index c9e05d242..478d0339b 100644 --- a/src/Nadeko.Bot.Modules.Administration/UserPunish/UserPunishCommands.cs +++ b/src/Nadeko.Bot.Modules.Administration/UserPunish/UserPunishCommands.cs @@ -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); diff --git a/src/Nadeko.Bot.Modules.Administration/UserPunish/UserPunishService.cs b/src/Nadeko.Bot.Modules.Administration/UserPunish/UserPunishService.cs index ffa3b99a6..967652ce4 100644 --- a/src/Nadeko.Bot.Modules.Administration/UserPunish/UserPunishService.cs +++ b/src/Nadeko.Bot.Modules.Administration/UserPunish/UserPunishService.cs @@ -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 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 GetBanUserDmEmbed( ICommandContext context, IGuildUser target, string defaultMessage, @@ -538,7 +542,7 @@ public class UserPunishService : INService, IReadyExecutor banReason, duration); - public SmartText GetBanUserDmEmbed( + public async Task 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); } } \ No newline at end of file diff --git a/src/Nadeko.Bot.Modules.Expresssions/NadekoExpressionExtensions.cs b/src/Nadeko.Bot.Modules.Expresssions/NadekoExpressionExtensions.cs index d1f3d5d26..f15412676 100644 --- a/src/Nadeko.Bot.Modules.Expresssions/NadekoExpressionExtensions.cs +++ b/src/Nadeko.Bot.Modules.Expresssions/NadekoExpressionExtensions.cs @@ -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 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); } diff --git a/src/Nadeko.Bot.Modules.Expresssions/NadekoExpressionsService.cs b/src/Nadeko.Bot.Modules.Expresssions/NadekoExpressionsService.cs index 5cabc76fc..a330e47db 100644 --- a/src/Nadeko.Bot.Modules.Expresssions/NadekoExpressionsService.cs +++ b/src/Nadeko.Bot.Modules.Expresssions/NadekoExpressionsService.cs @@ -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) diff --git a/src/Nadeko.Bot.Modules.Gambling/Gambling/GamblingTopLevelModule.cs b/src/Nadeko.Bot.Modules.Gambling/Gambling/GamblingTopLevelModule.cs index 7fd6699cb..6620102cb 100644 --- a/src/Nadeko.Bot.Modules.Gambling/Gambling/GamblingTopLevelModule.cs +++ b/src/Nadeko.Bot.Modules.Gambling/Gambling/GamblingTopLevelModule.cs @@ -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; diff --git a/src/Nadeko.Bot.Modules.Gambling/Games/ChatterBot/ChatterbotService.cs b/src/Nadeko.Bot.Modules.Gambling/Games/ChatterBot/ChatterbotService.cs index ac32a69bb..8195041ef 100644 --- a/src/Nadeko.Bot.Modules.Gambling/Games/ChatterBot/ChatterbotService.cs +++ b/src/Nadeko.Bot.Modules.Gambling/Games/ChatterBot/ChatterbotService.cs @@ -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; diff --git a/src/Nadeko.Bot.Modules.Help/Help.cs b/src/Nadeko.Bot.Modules.Help/Help.cs index 14049e189..67587a572 100644 --- a/src/Nadeko.Bot.Modules.Help/Help.cs +++ b/src/Nadeko.Bot.Modules.Help/Help.cs @@ -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 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] diff --git a/src/Nadeko.Bot.Modules.Help/HelpService.cs b/src/Nadeko.Bot.Modules.Help/HelpService.cs index 96bc87fac..8700e4b65 100644 --- a/src/Nadeko.Bot.Modules.Help/HelpService.cs +++ b/src/Nadeko.Bot.Modules.Help/HelpService.cs @@ -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; } } \ No newline at end of file diff --git a/src/Nadeko.Bot.Modules.Searches/Crypto/DefaultStockDataService.cs b/src/Nadeko.Bot.Modules.Searches/Crypto/DefaultStockDataService.cs index 717b43caf..9ae2766f5 100644 --- a/src/Nadeko.Bot.Modules.Searches/Crypto/DefaultStockDataService.cs +++ b/src/Nadeko.Bot.Modules.Searches/Crypto/DefaultStockDataService.cs @@ -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> GetCandleDataAsync(string query) { using var http = _httpClientFactory.CreateClient(); diff --git a/src/Nadeko.Bot.Modules.Searches/StreamNotification/StreamNotificationService.cs b/src/Nadeko.Bot.Modules.Searches/StreamNotification/StreamNotificationService.cs index 8a515c9e9..38c7edf51 100644 --- a/src/Nadeko.Bot.Modules.Searches/StreamNotification/StreamNotificationService.cs +++ b/src/Nadeko.Bot.Modules.Searches/StreamNotification/StreamNotificationService.cs @@ -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> StreamsOnlineKey { get; } public TypedKey> 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().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; diff --git a/src/Nadeko.Bot.Modules.Utility/Quote/QuoteCommands.cs b/src/Nadeko.Bot.Modules.Utility/Quote/QuoteCommands.cs index 973a9705d..bc83601c2 100644 --- a/src/Nadeko.Bot.Modules.Utility/Quote/QuoteCommands.cs +++ b/src/Nadeko.Bot.Modules.Utility/Quote/QuoteCommands.cs @@ -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); } diff --git a/src/Nadeko.Bot.Modules.Utility/Repeater/RepeaterService.cs b/src/Nadeko.Bot.Modules.Utility/Repeater/RepeaterService.cs index 956be7c91..8c7f90351 100644 --- a/src/Nadeko.Bot.Modules.Utility/Repeater/RepeaterService.cs +++ b/src/Nadeko.Bot.Modules.Utility/Repeater/RepeaterService.cs @@ -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 _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() - .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().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().AsQueryable() - .Where(x => x.Id == repeaterId) - .UpdateAsync(rep => new() - { - LastMessageId = lastMsgId - }); + .Where(x => x.Id == repeaterId) + .UpdateAsync(rep => new() + { + LastMessageId = lastMsgId + }); } public async Task AddRepeaterAsync( @@ -348,9 +360,9 @@ public sealed class RepeaterService : IReadyExecutor, INService await using var uow = _db.GetDbContext(); var toRemove = await uow.Set().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().AsQueryable() - .Where(x => x.GuildId == guildId) - .Skip(index) - .FirstOrDefaultAsyncEF(); + .Where(x => x.GuildId == guildId) + .Skip(index) + .FirstOrDefaultAsyncEF(); if (toToggle is null) return null; diff --git a/src/Nadeko.Bot.Modules.Utility/Utility.cs b/src/Nadeko.Bot.Modules.Utility/Utility.cs index f24184cc2..cafaab877 100644 --- a/src/Nadeko.Bot.Modules.Utility/Utility.cs +++ b/src/Nadeko.Bot.Modules.Utility/Utility.cs @@ -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); } diff --git a/src/Nadeko.Bot.Modules.Xp/Xp.cs b/src/Nadeko.Bot.Modules.Xp/Xp.cs index 193adcd52..60d29c1b3 100644 --- a/src/Nadeko.Bot.Modules.Xp/Xp.cs +++ b/src/Nadeko.Bot.Modules.Xp/Xp.cs @@ -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; diff --git a/src/Nadeko.Bot.Modules.Xp/XpRewards.cs b/src/Nadeko.Bot.Modules.Xp/XpRewards.cs index fd919be71..d98983749 100644 --- a/src/Nadeko.Bot.Modules.Xp/XpRewards.cs +++ b/src/Nadeko.Bot.Modules.Xp/XpRewards.cs @@ -1,5 +1,4 @@ -using Nadeko.Bot.Common; -using NadekoBot.Modules.Xp.Services; +using NadekoBot.Modules.Xp.Services; namespace NadekoBot.Modules.Xp; diff --git a/src/NadekoBot/Modules/Gambling/CurrencyProvider.cs b/src/NadekoBot/Modules/Gambling/CurrencyProvider.cs index 9739e3716..956c8dc13 100644 --- a/src/NadekoBot/Modules/Gambling/CurrencyProvider.cs +++ b/src/NadekoBot/Modules/Gambling/CurrencyProvider.cs @@ -1,4 +1,4 @@ -using Nadeko.Bot.Common; +using NadekoBot.Common; using NadekoBot.Modules.Gambling.Services; namespace NadekoBot.Modules.Gambling; diff --git a/src/NadekoBot/PermissionChecker.cs b/src/NadekoBot/PermissionChecker.cs index 3d9a3e010..c49c88c86 100644 --- a/src/NadekoBot/PermissionChecker.cs +++ b/src/NadekoBot/PermissionChecker.cs @@ -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;