mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 09:18:27 -04:00
* Replacement mechanism reworked
* Services can now register their own replacements * Possible bugs and/or backwards-incompatible behavior
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Nadeko.Bot.Common;
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public interface ICurrencyProvider
|
||||
{
|
||||
|
@@ -2,7 +2,7 @@
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Nadeko.Bot.Common;
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public interface IPermissionChecker
|
||||
{
|
||||
|
@@ -39,4 +39,5 @@
|
||||
<Link>responses.en-US.json</Link>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
#nullable disable
|
||||
using System.Globalization;
|
||||
using NadekoBot.Common;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
@@ -18,6 +19,7 @@ public abstract class NadekoModule : ModuleBase
|
||||
public ILocalization _localization { get; set; }
|
||||
public IEmbedBuilderService _eb { get; set; }
|
||||
public INadekoInteractionService _inter { get; set; }
|
||||
public IReplacementService repSvc { get; set; }
|
||||
|
||||
protected string prefix
|
||||
=> _cmdHandler.GetPrefix(ctx.Guild);
|
||||
|
@@ -0,0 +1,20 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public interface IReplacementPatternStore : INService
|
||||
{
|
||||
IReadOnlyDictionary<string, ReplacementInfo> Replacements { get; }
|
||||
IReadOnlyDictionary<string, RegexReplacementInfo> RegexReplacements { get; }
|
||||
|
||||
ValueTask<Guid?> Register(string token, Func<ValueTask<string>> repFactory);
|
||||
ValueTask<Guid?> Register<T1>(string token, Func<T1, ValueTask<string>> repFactory);
|
||||
ValueTask<Guid?> Register<T1, T2>(string token, Func<T1, T2, ValueTask<string>> repFactory);
|
||||
|
||||
ValueTask<Guid?> Register(string token, Func<string> repFactory);
|
||||
ValueTask<Guid?> Register<T1>(string token, Func<T1, string> repFactory);
|
||||
ValueTask<Guid?> Register<T1, T2>(string token, Func<T1, T2, string> repFactory);
|
||||
|
||||
ValueTask<Guid?> Register(Regex regex, Func<Match, string> repFactory);
|
||||
ValueTask<Guid?> Register<T1>(Regex regex, Func<Match, T1, string> repFactory);
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public interface IReplacementService
|
||||
{
|
||||
ValueTask<string?> ReplaceAsync(string input, ReplacementContext repCtx);
|
||||
ValueTask<SmartText> ReplaceAsync(SmartText input, ReplacementContext repCtx);
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public sealed class ReplacementContext
|
||||
{
|
||||
public DiscordSocketClient? Client { get; }
|
||||
public IGuild? Guild { get; }
|
||||
public IMessageChannel? Channel { get; }
|
||||
public IUser[]? Users { get; }
|
||||
|
||||
private readonly List<ReplacementInfo> _overrides = new();
|
||||
private readonly HashSet<string> _tokens = new();
|
||||
|
||||
public IReadOnlyList<ReplacementInfo> Overrides
|
||||
=> _overrides.AsReadOnly();
|
||||
|
||||
private readonly List<RegexReplacementInfo> _regexOverrides = new();
|
||||
private readonly HashSet<string> _regexPatterns = new();
|
||||
|
||||
public IReadOnlyList<RegexReplacementInfo> RegexOverrides
|
||||
=> _regexOverrides.AsReadOnly();
|
||||
|
||||
public ReplacementContext(ICommandContext cmdContext) : this(cmdContext.Client as DiscordSocketClient,
|
||||
cmdContext.Guild,
|
||||
cmdContext.Channel,
|
||||
cmdContext.User)
|
||||
{
|
||||
}
|
||||
|
||||
public ReplacementContext(
|
||||
DiscordSocketClient? client = null,
|
||||
IGuild? guild = null,
|
||||
IMessageChannel? channel = null,
|
||||
params IUser[]? users)
|
||||
{
|
||||
Client = client;
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
Users = users;
|
||||
}
|
||||
|
||||
public ReplacementContext WithOverride(string key, Func<ValueTask<string>> repFactory)
|
||||
{
|
||||
if (_tokens.Add(key))
|
||||
{
|
||||
_overrides.Add(new(key, repFactory));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementContext WithOverride(string key, Func<string> repFactory)
|
||||
=> WithOverride(key, () => new ValueTask<string>(repFactory()));
|
||||
|
||||
|
||||
public ReplacementContext WithOverride(Regex regex, Func<Match, ValueTask<string>> repFactory)
|
||||
{
|
||||
if (_regexPatterns.Add(regex.ToString()))
|
||||
{
|
||||
_regexOverrides.Add(new(regex, repFactory));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementContext WithOverride(Regex regex, Func<Match, string> repFactory)
|
||||
=> WithOverride(regex, (Match m) => new ValueTask<string>(repFactory(m)));
|
||||
}
|
57
src/Nadeko.Bot.Common/Replacements/Impl/ReplacementInfo.cs
Normal file
57
src/Nadeko.Bot.Common/Replacements/Impl/ReplacementInfo.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public sealed class ReplacementInfo
|
||||
{
|
||||
private readonly Delegate _del;
|
||||
public IReadOnlyCollection<Type> InputTypes { get; }
|
||||
public string Token { get; }
|
||||
|
||||
private static readonly Func<ValueTask<string?>> _falllbackFunc = static () => default;
|
||||
|
||||
public ReplacementInfo(string token, Delegate del)
|
||||
{
|
||||
_del = del;
|
||||
InputTypes = del.GetMethodInfo().GetParameters().Select(x => x.ParameterType).ToArray().AsReadOnly();
|
||||
Token = token;
|
||||
}
|
||||
|
||||
public async Task<string?> GetValueAsync(params object?[]? objs)
|
||||
=> await (ValueTask<string?>)(_del.DynamicInvoke(objs) ?? _falllbackFunc);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Token.GetHashCode();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is ReplacementInfo ri && ri.Token == Token;
|
||||
}
|
||||
|
||||
public sealed class RegexReplacementInfo
|
||||
{
|
||||
private readonly Delegate _del;
|
||||
public IReadOnlyCollection<Type> InputTypes { get; }
|
||||
|
||||
public Regex Regex { get; }
|
||||
public string Pattern { get; }
|
||||
|
||||
private static readonly Func<Match, ValueTask<string?>> _falllbackFunc = static _ => default;
|
||||
|
||||
public RegexReplacementInfo(Regex regex, Delegate del)
|
||||
{
|
||||
_del = del;
|
||||
InputTypes = del.GetMethodInfo().GetParameters().Select(x => x.ParameterType).ToArray().AsReadOnly();
|
||||
Regex = regex;
|
||||
Pattern = Regex.ToString();
|
||||
}
|
||||
|
||||
public async Task<string?> GetValueAsync(Match m, params object?[]? objs)
|
||||
=> await ((Func<Match, ValueTask<string?>>)(_del.DynamicInvoke(objs) ?? _falllbackFunc))(m);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Regex.GetHashCode();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is RegexReplacementInfo ri && ri.Pattern == Pattern;
|
||||
}
|
@@ -0,0 +1,130 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using OneOf;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public sealed partial class ReplacementPatternStore : IReplacementPatternStore, INService
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, OneOf<string, Regex>> _guids = new();
|
||||
|
||||
private readonly ConcurrentDictionary<string, ReplacementInfo> _defaultReplacements = new();
|
||||
private readonly ConcurrentDictionary<string, RegexReplacementInfo> _regexReplacements = new();
|
||||
|
||||
public IReadOnlyDictionary<string, ReplacementInfo> Replacements
|
||||
=> _defaultReplacements.AsReadOnly();
|
||||
|
||||
public IReadOnlyDictionary<string, RegexReplacementInfo> RegexReplacements
|
||||
=> _regexReplacements.AsReadOnly();
|
||||
|
||||
public ReplacementPatternStore()
|
||||
{
|
||||
WithClient();
|
||||
WithChannel();
|
||||
WithServer();
|
||||
WithUsers();
|
||||
WithDefault();
|
||||
WithRegex();
|
||||
}
|
||||
|
||||
// private async ValueTask<string> InternalReplace(string input, ReplacementContexta repCtx)
|
||||
// {
|
||||
// // multiple executions vs single execution per replacement
|
||||
// var minIndex = -1;
|
||||
// var index = -1;
|
||||
// foreach (var rep in _replacements)
|
||||
// {
|
||||
// while ((index = input.IndexOf(rep.Key, StringComparison.InvariantCulture)) != -1 && index > minIndex)
|
||||
// {
|
||||
// var valueToInsert = await rep.Value(repCtx);
|
||||
// input = input[..index] + valueToInsert +input[(index + rep.Key.Length)..];
|
||||
// minIndex = (index + valueToInsert.Length);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return input;
|
||||
// }
|
||||
|
||||
private ValueTask<Guid?> InternalRegister(string token, Delegate repFactory)
|
||||
{
|
||||
if (!token.StartsWith('%') || !token.EndsWith('%'))
|
||||
{
|
||||
Log.Warning(
|
||||
"""
|
||||
Invalid replacement token: {Token}
|
||||
Tokens have to start and end with a '%', ex: %mytoken%
|
||||
""",
|
||||
token);
|
||||
return new(default(Guid?));
|
||||
}
|
||||
|
||||
if (_defaultReplacements.TryAdd(token, new ReplacementInfo(token, repFactory)))
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
_guids[guid] = token;
|
||||
return new(guid);
|
||||
}
|
||||
|
||||
return new(default(Guid?));
|
||||
}
|
||||
|
||||
public ValueTask<Guid?> Register(string token, Func<ValueTask<string>> repFactory)
|
||||
=> InternalRegister(token, repFactory);
|
||||
|
||||
public ValueTask<Guid?> Register<T1>(string token, Func<T1, ValueTask<string>> repFactory)
|
||||
=> InternalRegister(token, repFactory);
|
||||
|
||||
public ValueTask<Guid?> Register<T1, T2>(string token, Func<T1, T2, ValueTask<string>> repFactory)
|
||||
=> InternalRegister(token, repFactory);
|
||||
|
||||
public ValueTask<Guid?> Register(string token, Func<string> repFactory)
|
||||
=> InternalRegister(token, () => new ValueTask<string>(repFactory()));
|
||||
|
||||
public ValueTask<Guid?> Register<T1>(string token, Func<T1, string> repFactory)
|
||||
=> InternalRegister(token, (T1 a) => new ValueTask<string>(repFactory(a)));
|
||||
|
||||
public ValueTask<Guid?> Register<T1, T2>(string token, Func<T1, T2, string> repFactory)
|
||||
=> InternalRegister(token, (T1 a, T2 b) => new ValueTask<string>(repFactory(a, b)));
|
||||
|
||||
|
||||
private ValueTask<Guid?> InternalRegexRegister(Regex regex, Delegate repFactory)
|
||||
{
|
||||
var regexPattern = regex.ToString();
|
||||
if (!regexPattern.StartsWith('%') || !regexPattern.EndsWith('%'))
|
||||
{
|
||||
Log.Warning(
|
||||
"""
|
||||
Invalid replacement pattern: {Token}
|
||||
Tokens have to start and end with a '%', ex: %mytoken%
|
||||
""",
|
||||
regex);
|
||||
return new(default(Guid?));
|
||||
}
|
||||
|
||||
if (_regexReplacements.TryAdd(regexPattern, new RegexReplacementInfo(regex, repFactory)))
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
_guids[guid] = regex;
|
||||
return new(guid);
|
||||
}
|
||||
|
||||
return new(default(Guid?));
|
||||
}
|
||||
|
||||
public ValueTask<Guid?> Register(Regex regex, Func<Match, string> repFactory)
|
||||
=> InternalRegexRegister(regex, () => (Match m) => new ValueTask<string>(repFactory(m)));
|
||||
|
||||
public ValueTask<Guid?> Register<T1>(Regex regex, Func<Match, T1, string> repFactory)
|
||||
=> InternalRegexRegister(regex, (T1 a) => (Match m) => new ValueTask<string>(repFactory(m, a)));
|
||||
|
||||
public bool Unregister(Guid guid)
|
||||
{
|
||||
if (_guids.TryRemove(guid, out var pattern))
|
||||
{
|
||||
return pattern.Match(
|
||||
token => _defaultReplacements.TryRemove(token, out _),
|
||||
regex => _regexReplacements.TryRemove(regex.ToString(), out _));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,113 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public sealed partial class ReplacementPatternStore
|
||||
{
|
||||
private static readonly Regex _rngRegex = new(@"%rng(?:(?<from>(?:-)?\d+)-(?<to>(?:-)?\d+))?%",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
|
||||
private void WithDefault()
|
||||
{
|
||||
Register("%bot.time%",
|
||||
static ()
|
||||
=> DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
|
||||
}
|
||||
|
||||
private void WithClient()
|
||||
{
|
||||
Register("%bot.status%", static (DiscordSocketClient client) => client.Status.ToString());
|
||||
Register("%bot.latency%", static (DiscordSocketClient client) => client.Latency.ToString());
|
||||
Register("%bot.name%", static (DiscordSocketClient client) => client.CurrentUser.Username);
|
||||
Register("%bot.fullname%", static (DiscordSocketClient client) => client.CurrentUser.ToString());
|
||||
Register("%bot.discrim%", static (DiscordSocketClient client) => client.CurrentUser.Discriminator);
|
||||
Register("%bot.id%", static (DiscordSocketClient client) => client.CurrentUser.Id.ToString());
|
||||
Register("%bot.avatar%",
|
||||
static (DiscordSocketClient client) => client.CurrentUser.RealAvatarUrl().ToString());
|
||||
|
||||
Register("%bot.mention%", static (DiscordSocketClient client) => client.CurrentUser.Mention);
|
||||
|
||||
Register("%shard.servercount%", static (DiscordSocketClient c) => c.Guilds.Count.ToString());
|
||||
Register("%shard.usercount%",
|
||||
static (DiscordSocketClient c) => c.Guilds.Sum(g => g.MemberCount).ToString());
|
||||
Register("%shard.id%", static (DiscordSocketClient c) => c.ShardId.ToString());
|
||||
}
|
||||
|
||||
private void WithServer()
|
||||
{
|
||||
Register("%server%", static (IGuild g) => g.Name);
|
||||
Register("%server.id%", static (IGuild g) => g.Id.ToString());
|
||||
Register("%server.name%", static (IGuild g) => g.Name);
|
||||
Register("%server.icon%", static (IGuild g) => g.IconUrl);
|
||||
Register("%server.members%", static (IGuild g) => g.ApproximateMemberCount?.ToString() ?? "?");
|
||||
Register("%server.boosters%", static (IGuild g) => g.PremiumSubscriptionCount.ToString());
|
||||
Register("%server.boost_level%", static (IGuild g) => ((int)g.PremiumTier).ToString());
|
||||
}
|
||||
|
||||
private void WithChannel()
|
||||
{
|
||||
Register("%channel%", static (IMessageChannel ch) => ch.Name);
|
||||
Register("%channel.mention%",
|
||||
static (IMessageChannel ch) => (ch as ITextChannel)?.Mention ?? "#" + ch.Name);
|
||||
Register("%channel.name%", static (IMessageChannel ch) => ch.Name);
|
||||
Register("%channel.id%", static (IMessageChannel ch) => ch.Id.ToString());
|
||||
Register("%channel.created%",
|
||||
static (IMessageChannel ch) => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy"));
|
||||
Register("%channel.nsfw%",
|
||||
static (IMessageChannel ch) => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-");
|
||||
Register("%channel.topic%", static (IMessageChannel ch) => (ch as ITextChannel)?.Topic ?? "-");
|
||||
}
|
||||
|
||||
private void WithUsers()
|
||||
{
|
||||
Register("%user%",
|
||||
static (IUser[] users) => string.Join(" ", users.Select(user => user.Mention)));
|
||||
Register("%user.mention%",
|
||||
static (IUser[] users) => string.Join(" ", users.Select(user => user.Mention)));
|
||||
Register("%user.fullname%",
|
||||
static (IUser[] users) => string.Join(" ", users.Select(user => user.ToString())));
|
||||
Register("%user.name%",
|
||||
static (IUser[] users) => string.Join(" ", users.Select(user => user.Username)));
|
||||
Register("%user.discrim%",
|
||||
static (IUser[] users) => string.Join(" ", users.Select(user => user.Discriminator)));
|
||||
Register("%user.avatar%",
|
||||
static (IUser[] users)
|
||||
=> string.Join(" ", users.Select(user => user.RealAvatarUrl().ToString())));
|
||||
Register("%user.id%",
|
||||
static (IUser[] users) => string.Join(" ", users.Select(user => user.Id.ToString())));
|
||||
Register("%user.created_time%",
|
||||
static (IUser[] users)
|
||||
=> string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm"))));
|
||||
Register("%user.created_date%",
|
||||
static (IUser[] users)
|
||||
=> string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy"))));
|
||||
Register("%user.joined_time%",
|
||||
static (IUser[] users) => string.Join(" ",
|
||||
users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-")));
|
||||
Register("%user.joined_date%",
|
||||
static (IUser[] users) => string.Join(" ",
|
||||
users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-")));
|
||||
}
|
||||
|
||||
private void WithRegex()
|
||||
{
|
||||
Register(_rngRegex,
|
||||
match =>
|
||||
{
|
||||
var rng = new NadekoRandom();
|
||||
if (!int.TryParse(match.Groups["from"].ToString(), out var from))
|
||||
from = 0;
|
||||
if (!int.TryParse(match.Groups["to"].ToString(), out var to))
|
||||
to = 0;
|
||||
|
||||
if (from == 0 && to == 0)
|
||||
return rng.Next(0, 11).ToString();
|
||||
|
||||
if (from >= to)
|
||||
return string.Empty;
|
||||
|
||||
return rng.Next(from, to + 1).ToString();
|
||||
});
|
||||
}
|
||||
}
|
137
src/Nadeko.Bot.Common/Replacements/Impl/ReplacementService.cs
Normal file
137
src/Nadeko.Bot.Common/Replacements/Impl/ReplacementService.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public sealed class ReplacementService : IReplacementService, INService
|
||||
{
|
||||
private readonly IReplacementPatternStore _repReg;
|
||||
|
||||
public ReplacementService(IReplacementPatternStore repReg)
|
||||
{
|
||||
_repReg = repReg;
|
||||
}
|
||||
|
||||
public async ValueTask<SmartText> ReplaceAsync(SmartText input, ReplacementContext repCtx)
|
||||
{
|
||||
var reps = GetReplacementsForContext(repCtx);
|
||||
var regReps = GetRegexReplacementsForContext(repCtx);
|
||||
|
||||
var inputData = GetInputData(repCtx);
|
||||
var rep = new Replacer(reps.Values, regReps.Values, inputData);
|
||||
|
||||
return await rep.ReplaceAsync(input);
|
||||
}
|
||||
|
||||
public async ValueTask<string?> ReplaceAsync(string input, ReplacementContext repCtx)
|
||||
{
|
||||
var reps = GetReplacementsForContext(repCtx);
|
||||
var regReps = GetRegexReplacementsForContext(repCtx);
|
||||
|
||||
var inputData = GetInputData(repCtx);
|
||||
var rep = new Replacer(reps.Values, regReps.Values, inputData);
|
||||
|
||||
return await rep.ReplaceAsync(input);
|
||||
}
|
||||
|
||||
private object[] GetInputData(ReplacementContext repCtx)
|
||||
{
|
||||
var obj = new List<object>();
|
||||
if (repCtx.Client is not null)
|
||||
obj.Add(repCtx.Client);
|
||||
|
||||
if (repCtx.Guild is not null)
|
||||
obj.Add(repCtx.Guild);
|
||||
|
||||
if (repCtx.Users is not null)
|
||||
obj.Add(repCtx.Users);
|
||||
|
||||
if (repCtx.Channel is not null)
|
||||
obj.Add(repCtx.Channel);
|
||||
|
||||
return obj.ToArray();
|
||||
}
|
||||
|
||||
private IDictionary<string, ReplacementInfo> GetReplacementsForContext(ReplacementContext repCtx)
|
||||
{
|
||||
var reps = GetOriginalReplacementsForContext(repCtx);
|
||||
foreach (var ovrd in repCtx.Overrides)
|
||||
{
|
||||
reps.Remove(ovrd.Token);
|
||||
reps.TryAdd(ovrd.Token, ovrd);
|
||||
}
|
||||
|
||||
return reps;
|
||||
}
|
||||
|
||||
private IDictionary<string, RegexReplacementInfo> GetRegexReplacementsForContext(ReplacementContext repCtx)
|
||||
{
|
||||
var reps = GetOriginalRegexReplacementsForContext(repCtx);
|
||||
foreach (var ovrd in repCtx.RegexOverrides)
|
||||
{
|
||||
reps.Remove(ovrd.Pattern);
|
||||
reps.TryAdd(ovrd.Pattern, ovrd);
|
||||
}
|
||||
|
||||
return reps;
|
||||
}
|
||||
|
||||
private IDictionary<string, ReplacementInfo> GetOriginalReplacementsForContext(ReplacementContext repCtx)
|
||||
{
|
||||
var objs = new List<object>();
|
||||
if (repCtx.Client is not null)
|
||||
{
|
||||
objs.Add(repCtx.Client);
|
||||
}
|
||||
|
||||
if (repCtx.Channel is not null)
|
||||
{
|
||||
objs.Add(repCtx.Channel);
|
||||
}
|
||||
|
||||
if (repCtx.Users is not null)
|
||||
{
|
||||
objs.Add(repCtx.Users);
|
||||
}
|
||||
|
||||
if (repCtx.Guild is not null)
|
||||
{
|
||||
objs.Add(repCtx.Guild);
|
||||
}
|
||||
|
||||
var types = objs.Map(x => x.GetType()).OrderBy(x => x.Name).ToHashSet();
|
||||
|
||||
return _repReg.Replacements
|
||||
.Values
|
||||
.Where(rep => rep.InputTypes.All(t => types.Any(x => x.IsAssignableTo((t)))))
|
||||
.ToDictionary(rep => rep.Token, rep => rep);
|
||||
}
|
||||
|
||||
private IDictionary<string, RegexReplacementInfo> GetOriginalRegexReplacementsForContext(ReplacementContext repCtx)
|
||||
{
|
||||
var objs = new List<object>();
|
||||
if (repCtx.Client is not null)
|
||||
{
|
||||
objs.Add(repCtx.Client);
|
||||
}
|
||||
|
||||
if (repCtx.Channel is not null)
|
||||
{
|
||||
objs.Add(repCtx.Channel);
|
||||
}
|
||||
|
||||
if (repCtx.Users is not null)
|
||||
{
|
||||
objs.Add(repCtx.Users);
|
||||
}
|
||||
|
||||
if (repCtx.Guild is not null)
|
||||
{
|
||||
objs.Add(repCtx.Guild);
|
||||
}
|
||||
|
||||
var types = objs.Map(x => x.GetType()).OrderBy(x => x.Name).ToHashSet();
|
||||
|
||||
return _repReg.RegexReplacements
|
||||
.Values
|
||||
.Where(rep => rep.InputTypes.All(t => types.Any(x => x.IsAssignableTo((t)))))
|
||||
.ToDictionary(rep => rep.Pattern, rep => rep);
|
||||
}
|
||||
}
|
138
src/Nadeko.Bot.Common/Replacements/Impl/Replacer.cs
Normal file
138
src/Nadeko.Bot.Common/Replacements/Impl/Replacer.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public sealed partial class Replacer
|
||||
{
|
||||
private readonly IEnumerable<ReplacementInfo> _reps;
|
||||
private readonly IEnumerable<RegexReplacementInfo> _regexReps;
|
||||
private readonly object[] _inputData;
|
||||
|
||||
[GeneratedRegex(@"\%[\p{L}\p{N}\._]*[\p{L}\p{N}]+[\p{L}\p{N}\._]*\%")]
|
||||
private static partial Regex TokenExtractionRegex();
|
||||
|
||||
public Replacer(IEnumerable<ReplacementInfo> reps, IEnumerable<RegexReplacementInfo> regexReps, object[] inputData)
|
||||
{
|
||||
_reps = reps;
|
||||
_inputData = inputData;
|
||||
_regexReps = regexReps;
|
||||
}
|
||||
|
||||
public async ValueTask<string?> ReplaceAsync(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return input;
|
||||
|
||||
var matches = TokenExtractionRegex().IsMatch(input);
|
||||
|
||||
if (matches)
|
||||
{
|
||||
foreach (var rep in _reps)
|
||||
{
|
||||
if (input.Contains(rep.Token, StringComparison.InvariantCulture))
|
||||
{
|
||||
var objs = GetParams(rep.InputTypes);
|
||||
input = input.Replace(rep.Token, await rep.GetValueAsync(objs), StringComparison.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var rep in _regexReps)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var objs = GetParams(rep.InputTypes);
|
||||
var match = rep.Regex.Match(input);
|
||||
if (match.Success)
|
||||
{
|
||||
sb.Append(input, 0, match.Index)
|
||||
.Append(await rep.GetValueAsync(match, objs));
|
||||
|
||||
var lastIndex = match.Index + match.Length;
|
||||
sb.Append(input, lastIndex, input.Length - lastIndex);
|
||||
input = sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private object?[]? GetParams(IReadOnlyCollection<Type> inputTypes)
|
||||
{
|
||||
if (inputTypes.Count == 0)
|
||||
return null;
|
||||
|
||||
var objs = new List<object>();
|
||||
foreach (var t in inputTypes)
|
||||
{
|
||||
var datum = _inputData.FirstOrDefault(x => x.GetType().IsAssignableTo(t));
|
||||
if (datum is not null)
|
||||
objs.Add(datum);
|
||||
}
|
||||
|
||||
return objs.ToArray();
|
||||
}
|
||||
|
||||
public async ValueTask<SmartText> ReplaceAsync(SmartText data)
|
||||
=> data switch
|
||||
{
|
||||
SmartEmbedText embedData => await ReplaceAsync(embedData) with
|
||||
{
|
||||
PlainText = await ReplaceAsync(embedData.PlainText),
|
||||
Color = embedData.Color
|
||||
},
|
||||
SmartPlainText plain => await ReplaceAsync(plain),
|
||||
SmartEmbedTextArray arr => await ReplaceAsync(arr),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type")
|
||||
};
|
||||
|
||||
private async Task<SmartEmbedTextArray> ReplaceAsync(SmartEmbedTextArray embedArr)
|
||||
=> new()
|
||||
{
|
||||
Embeds = await embedArr.Embeds.Map(async e => await ReplaceAsync(e) with
|
||||
{
|
||||
Color = e.Color
|
||||
}).WhenAll(),
|
||||
Content = await ReplaceAsync(embedArr.Content)
|
||||
};
|
||||
|
||||
private async ValueTask<SmartPlainText> ReplaceAsync(SmartPlainText plain)
|
||||
=> await ReplaceAsync(plain.Text);
|
||||
|
||||
private async Task<T> ReplaceAsync<T>(T embedData) where T : SmartEmbedTextBase, new()
|
||||
{
|
||||
var newEmbedData = new T
|
||||
{
|
||||
Description = await ReplaceAsync(embedData.Description),
|
||||
Title = await ReplaceAsync(embedData.Title),
|
||||
Thumbnail = await ReplaceAsync(embedData.Thumbnail),
|
||||
Image = await ReplaceAsync(embedData.Image),
|
||||
Url = await ReplaceAsync(embedData.Url),
|
||||
Author = embedData.Author is null
|
||||
? null
|
||||
: new()
|
||||
{
|
||||
Name = await ReplaceAsync(embedData.Author.Name),
|
||||
IconUrl = await ReplaceAsync(embedData.Author.IconUrl)
|
||||
},
|
||||
Fields = await Task.WhenAll(embedData
|
||||
.Fields?
|
||||
.Map(async f => new SmartTextEmbedField
|
||||
{
|
||||
Name = await ReplaceAsync(f.Name),
|
||||
Value = await ReplaceAsync(f.Value),
|
||||
Inline = f.Inline
|
||||
}) ?? []),
|
||||
Footer = embedData.Footer is null
|
||||
? null
|
||||
: new()
|
||||
{
|
||||
Text = await ReplaceAsync(embedData.Footer.Text),
|
||||
IconUrl = await ReplaceAsync(embedData.Footer.IconUrl)
|
||||
}
|
||||
};
|
||||
|
||||
return newEmbedData;
|
||||
}
|
||||
}
|
@@ -1,164 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public class ReplacementBuilder
|
||||
{
|
||||
private static readonly Regex _rngRegex = new("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private readonly ConcurrentDictionary<Regex, Func<Match, string>> _regex = new();
|
||||
|
||||
private readonly ConcurrentDictionary<string, Func<string>> _reps = new();
|
||||
|
||||
public ReplacementBuilder()
|
||||
=> WithRngRegex();
|
||||
|
||||
public ReplacementBuilder WithDefault(
|
||||
IUser usr,
|
||||
IMessageChannel ch,
|
||||
SocketGuild g,
|
||||
DiscordSocketClient client)
|
||||
=> WithUser(usr).WithChannel(ch).WithServer(client, g).WithClient(client);
|
||||
|
||||
public ReplacementBuilder WithDefault(ICommandContext ctx)
|
||||
=> WithDefault(ctx.User, ctx.Channel, ctx.Guild as SocketGuild, (DiscordSocketClient)ctx.Client);
|
||||
|
||||
public ReplacementBuilder WithMention(DiscordSocketClient client)
|
||||
{
|
||||
_reps.TryAdd("%bot.mention%", () => client.CurrentUser.Mention);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithClient(DiscordSocketClient client)
|
||||
{
|
||||
WithMention(client);
|
||||
|
||||
_reps.TryAdd("%bot.status%", () => client.Status.ToString());
|
||||
_reps.TryAdd("%bot.latency%", () => client.Latency.ToString());
|
||||
_reps.TryAdd("%bot.name%", () => client.CurrentUser.Username);
|
||||
_reps.TryAdd("%bot.fullname%", () => client.CurrentUser.ToString());
|
||||
_reps.TryAdd("%bot.time%",
|
||||
() => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
|
||||
_reps.TryAdd("%bot.discrim%", () => client.CurrentUser.Discriminator);
|
||||
_reps.TryAdd("%bot.id%", () => client.CurrentUser.Id.ToString());
|
||||
_reps.TryAdd("%bot.avatar%", () => client.CurrentUser.RealAvatarUrl().ToString());
|
||||
|
||||
WithStats(client);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithServer(DiscordSocketClient client, SocketGuild g)
|
||||
{
|
||||
_reps.TryAdd("%server%", () => g is null ? "DM" : g.Name);
|
||||
_reps.TryAdd("%server.id%", () => g is null ? "DM" : g.Id.ToString());
|
||||
_reps.TryAdd("%server.name%", () => g is null ? "DM" : g.Name);
|
||||
_reps.TryAdd("%server.icon%", () => g is null ? null : g.IconUrl);
|
||||
_reps.TryAdd("%server.members%", () => g is { } sg ? sg.MemberCount.ToString() : "?");
|
||||
_reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString());
|
||||
_reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString());
|
||||
// todo fix
|
||||
// _reps.TryAdd("%server.time%",
|
||||
// () =>
|
||||
// {
|
||||
// var to = TimeZoneInfo.Local;
|
||||
// if (g is not null)
|
||||
// {
|
||||
// if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
|
||||
// to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
|
||||
// }
|
||||
//
|
||||
// return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ")
|
||||
// + to.StandardName.GetInitials();
|
||||
// });
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithChannel(IMessageChannel ch)
|
||||
{
|
||||
_reps.TryAdd("%channel%", () => ch.Name);
|
||||
_reps.TryAdd("%channel.mention%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name);
|
||||
_reps.TryAdd("%channel.name%", () => ch.Name);
|
||||
_reps.TryAdd("%channel.id%", () => ch.Id.ToString());
|
||||
_reps.TryAdd("%channel.created%", () => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy"));
|
||||
_reps.TryAdd("%channel.nsfw%", () => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-");
|
||||
_reps.TryAdd("%channel.topic%", () => (ch as ITextChannel)?.Topic ?? "-");
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithUser(IUser user)
|
||||
{
|
||||
WithManyUsers(new[] { user });
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithManyUsers(IEnumerable<IUser> users)
|
||||
{
|
||||
_reps.TryAdd("%user%", () => string.Join(" ", users.Select(user => user.Mention)));
|
||||
_reps.TryAdd("%user.mention%", () => string.Join(" ", users.Select(user => user.Mention)));
|
||||
_reps.TryAdd("%user.fullname%", () => string.Join(" ", users.Select(user => user.ToString())));
|
||||
_reps.TryAdd("%user.name%", () => string.Join(" ", users.Select(user => user.Username)));
|
||||
_reps.TryAdd("%user.discrim%", () => string.Join(" ", users.Select(user => user.Discriminator)));
|
||||
_reps.TryAdd("%user.avatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl().ToString())));
|
||||
_reps.TryAdd("%user.id%", () => string.Join(" ", users.Select(user => user.Id.ToString())));
|
||||
_reps.TryAdd("%user.created_time%",
|
||||
() => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm"))));
|
||||
_reps.TryAdd("%user.created_date%",
|
||||
() => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy"))));
|
||||
_reps.TryAdd("%user.joined_time%",
|
||||
() => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-")));
|
||||
_reps.TryAdd("%user.joined_date%",
|
||||
() => string.Join(" ",
|
||||
users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-")));
|
||||
return this;
|
||||
}
|
||||
|
||||
private ReplacementBuilder WithStats(DiscordSocketClient c)
|
||||
{
|
||||
_reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString());
|
||||
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
|
||||
_reps.TryAdd("%shard.id%", () => c.ShardId.ToString());
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithRngRegex()
|
||||
{
|
||||
var rng = new NadekoRandom();
|
||||
_regex.TryAdd(_rngRegex,
|
||||
match =>
|
||||
{
|
||||
if (!int.TryParse(match.Groups["from"].ToString(), out var from))
|
||||
from = 0;
|
||||
if (!int.TryParse(match.Groups["to"].ToString(), out var to))
|
||||
to = 0;
|
||||
|
||||
if (from == 0 && to == 0)
|
||||
return rng.Next(0, 11).ToString();
|
||||
|
||||
if (from >= to)
|
||||
return string.Empty;
|
||||
|
||||
return rng.Next(from, to + 1).ToString();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithOverride(string key, Func<string> output)
|
||||
{
|
||||
_reps.AddOrUpdate(key, output, delegate { return output; });
|
||||
return this;
|
||||
}
|
||||
|
||||
public Replacer Build()
|
||||
=> new(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray());
|
||||
|
||||
public ReplacementBuilder WithProviders(IEnumerable<IPlaceholderProvider> phProviders)
|
||||
{
|
||||
foreach (var provider in phProviders)
|
||||
foreach (var ovr in provider.GetPlaceholders())
|
||||
_reps.TryAdd(ovr.Name, ovr.Func);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public class Replacer
|
||||
{
|
||||
private readonly IEnumerable<(Regex Regex, Func<Match, string> Replacement)> _regex;
|
||||
private readonly IEnumerable<(string Key, Func<string> Text)> _replacements;
|
||||
|
||||
public Replacer(IEnumerable<(string, Func<string>)> replacements, IEnumerable<(Regex, Func<Match, string>)> regex)
|
||||
{
|
||||
_replacements = replacements;
|
||||
_regex = regex;
|
||||
}
|
||||
|
||||
public string Replace(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return input;
|
||||
|
||||
foreach (var (key, text) in _replacements)
|
||||
{
|
||||
if (input.Contains(key))
|
||||
input = input.Replace(key, text(), StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
foreach (var item in _regex)
|
||||
input = item.Regex.Replace(input, m => item.Replacement(m));
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
public SmartText Replace(SmartText data)
|
||||
=> data switch
|
||||
{
|
||||
SmartEmbedText embedData => Replace(embedData) with
|
||||
{
|
||||
PlainText = Replace(embedData.PlainText),
|
||||
Color = embedData.Color
|
||||
},
|
||||
SmartPlainText plain => Replace(plain),
|
||||
SmartEmbedTextArray arr => Replace(arr),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type")
|
||||
};
|
||||
|
||||
private SmartEmbedTextArray Replace(SmartEmbedTextArray embedArr)
|
||||
=> new()
|
||||
{
|
||||
Embeds = embedArr.Embeds.Map(e => Replace(e) with
|
||||
{
|
||||
Color = e.Color
|
||||
}),
|
||||
Content = Replace(embedArr.Content)
|
||||
};
|
||||
|
||||
private SmartPlainText Replace(SmartPlainText plain)
|
||||
=> Replace(plain.Text);
|
||||
|
||||
private T Replace<T>(T embedData) where T: SmartEmbedTextBase, new()
|
||||
{
|
||||
var newEmbedData = new T
|
||||
{
|
||||
Description = Replace(embedData.Description),
|
||||
Title = Replace(embedData.Title),
|
||||
Thumbnail = Replace(embedData.Thumbnail),
|
||||
Image = Replace(embedData.Image),
|
||||
Url = Replace(embedData.Url),
|
||||
Author = embedData.Author is null
|
||||
? null
|
||||
: new()
|
||||
{
|
||||
Name = Replace(embedData.Author.Name),
|
||||
IconUrl = Replace(embedData.Author.IconUrl)
|
||||
},
|
||||
Fields = embedData.Fields?.Map(f => new SmartTextEmbedField
|
||||
{
|
||||
Name = Replace(f.Name),
|
||||
Value = Replace(f.Value),
|
||||
Inline = f.Inline
|
||||
}),
|
||||
Footer = embedData.Footer is null
|
||||
? null
|
||||
: new()
|
||||
{
|
||||
Text = Replace(embedData.Footer.Text),
|
||||
IconUrl = Replace(embedData.Footer.IconUrl)
|
||||
}
|
||||
};
|
||||
|
||||
return newEmbedData;
|
||||
}
|
||||
}
|
@@ -30,7 +30,7 @@ public sealed record SmartEmbedArrayElementText : SmartEmbedTextBase
|
||||
|
||||
public sealed record SmartEmbedText : SmartEmbedTextBase
|
||||
{
|
||||
public string PlainText { get; init; }
|
||||
public string? PlainText { get; init; }
|
||||
|
||||
public uint Color { get; init; } = 7458112;
|
||||
|
||||
@@ -54,15 +54,15 @@ public sealed record SmartEmbedText : SmartEmbedTextBase
|
||||
|
||||
public abstract record SmartEmbedTextBase : SmartText
|
||||
{
|
||||
public string Title { get; init; }
|
||||
public string Description { get; init; }
|
||||
public string Url { get; init; }
|
||||
public string Thumbnail { get; init; }
|
||||
public string Image { get; init; }
|
||||
public string? Title { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? Url { get; init; }
|
||||
public string? Thumbnail { get; init; }
|
||||
public string? Image { get; init; }
|
||||
|
||||
public SmartTextEmbedAuthor Author { get; init; }
|
||||
public SmartTextEmbedFooter Footer { get; init; }
|
||||
public SmartTextEmbedField[] Fields { get; init; }
|
||||
public SmartTextEmbedAuthor? Author { get; init; }
|
||||
public SmartTextEmbedFooter? Footer { get; init; }
|
||||
public SmartTextEmbedField[]? Fields { get; init; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsValid
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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]
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -1,5 +1,4 @@
|
||||
using Nadeko.Bot.Common;
|
||||
using NadekoBot.Modules.Xp.Services;
|
||||
using NadekoBot.Modules.Xp.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Xp;
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
using Nadeko.Bot.Common;
|
||||
using NadekoBot.Common;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
@@ -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;
|
||||
|
||||
|
Reference in New Issue
Block a user