Have AutoResponder use similar config format to RegexModerator
This commit is contained in:
parent
6032e4d37b
commit
2160b0fa4e
2 changed files with 92 additions and 93 deletions
|
@ -13,6 +13,22 @@ public class AutoResponder : RegexbotModule {
|
||||||
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
|
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
|
||||||
|
if (config == null) return Task.FromResult<object?>(null);
|
||||||
|
var defs = new List<Definition>();
|
||||||
|
|
||||||
|
if (config.Type != JTokenType.Array)
|
||||||
|
throw new ModuleLoadException(Name + " configuration must be a JSON array.");
|
||||||
|
|
||||||
|
// TODO better error reporting during this process
|
||||||
|
foreach (var def in config.Children<JObject>())
|
||||||
|
defs.Add(new Definition(def));
|
||||||
|
|
||||||
|
if (defs.Count == 0) return Task.FromResult<object?>(null);
|
||||||
|
Log(DiscordClient.GetGuild(guildID), $"Loaded {defs.Count} definition(s).");
|
||||||
|
return Task.FromResult<object?>(defs.AsReadOnly());
|
||||||
|
}
|
||||||
|
|
||||||
private async Task DiscordClient_MessageReceived(SocketMessage arg) {
|
private async Task DiscordClient_MessageReceived(SocketMessage arg) {
|
||||||
if (!Common.Misc.IsValidUserMessage(arg, out var ch)) return;
|
if (!Common.Misc.IsValidUserMessage(arg, out var ch)) return;
|
||||||
|
|
||||||
|
@ -27,20 +43,6 @@ public class AutoResponder : RegexbotModule {
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<object?> CreateGuildStateAsync(ulong guild, JToken config) {
|
|
||||||
// Guild state is a read-only IEnumerable<Definition>
|
|
||||||
if (config == null) return Task.FromResult<object?>(null);
|
|
||||||
var guildDefs = new List<Definition>();
|
|
||||||
foreach (var defconf in config.Children<JProperty>()) {
|
|
||||||
// Validation of data is left to the Definition constructor
|
|
||||||
var def = new Definition(defconf); // ModuleLoadException may be thrown here
|
|
||||||
guildDefs.Add(def);
|
|
||||||
// TODO global options
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult<object?>(guildDefs.AsReadOnly());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcessMessageAsync(SocketMessage msg, Definition def) {
|
private async Task ProcessMessageAsync(SocketMessage msg, Definition def) {
|
||||||
if (!def.Match(msg)) return;
|
if (!def.Match(msg)) return;
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Definition {
|
||||||
|
|
||||||
public string Label { get; }
|
public string Label { get; }
|
||||||
public IEnumerable<Regex> Regex { get; }
|
public IEnumerable<Regex> Regex { get; }
|
||||||
public IReadOnlyList<string> Response { get; }
|
public IReadOnlyList<string> Reply { get; }
|
||||||
public string? Command { get; }
|
public string? Command { get; }
|
||||||
public FilterList Filter { get; }
|
public FilterList Filter { get; }
|
||||||
public RateLimit<ulong> RateLimit { get; }
|
public RateLimit<ulong> RateLimit { get; }
|
||||||
|
@ -20,105 +20,102 @@ class Definition {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instance based on JSON configuration.
|
/// Creates an instance based on JSON configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Definition(JProperty incoming) {
|
public Definition(JObject def) {
|
||||||
Label = incoming.Name;
|
Label = def[nameof(Label)]?.Value<string>()
|
||||||
if (incoming.Value.Type != JTokenType.Object)
|
?? throw new ModuleLoadException($"Encountered a rule without a defined {nameof(Label)}.");
|
||||||
throw new ModuleLoadException($"Value of {nameof(AutoResponder)} definition must be a JSON object.");
|
|
||||||
var data = (JObject)incoming.Value;
|
|
||||||
|
|
||||||
// error message postfix
|
var errpostfx = $" in the rule definition for '{Label}'.";
|
||||||
var errpofx = $" in AutoRespond definition '{Label}'.";
|
|
||||||
|
|
||||||
// Parse regex
|
// Regex
|
||||||
const RegexOptions rxopts = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline;
|
var opts = RegexOptions.Compiled | RegexOptions.CultureInvariant;
|
||||||
var regexes = new List<Regex>();
|
// TODO consider adding an option to specify Singleline and Multiline mode. Defaulting to Singleline.
|
||||||
var rxconf = data[nameof(Regex)];
|
// Reminder: in Singleline mode, all contents are subject to the same regex (useful if e.g. spammer separates words line by line)
|
||||||
// Accepting either array or single string
|
opts |= RegexOptions.Singleline;
|
||||||
// TODO this code could be moved into a helper method somehow
|
// IgnoreCase is enabled by default; must be explicitly set to false
|
||||||
if (rxconf?.Type == JTokenType.Array) {
|
if (def["IgnoreCase"]?.Value<bool>() ?? true) opts |= RegexOptions.IgnoreCase;
|
||||||
|
|
||||||
|
const string ErrNoRegex = $"No patterns were defined under {nameof(Regex)}";
|
||||||
|
var regexRules = new List<Regex>();
|
||||||
|
var rxconf = def[nameof(Regex)];
|
||||||
|
if (rxconf == null) throw new ModuleLoadException(ErrNoRegex + errpostfx);
|
||||||
|
if (rxconf.Type == JTokenType.Array) {
|
||||||
foreach (var input in rxconf.Values<string>()) {
|
foreach (var input in rxconf.Values<string>()) {
|
||||||
try {
|
try {
|
||||||
var r = new Regex(input!, rxopts);
|
regexRules.Add(new Regex(input!, opts));
|
||||||
regexes.Add(r);
|
} catch (Exception ex) when (ex is ArgumentException or NullReferenceException) {
|
||||||
} catch (ArgumentException) {
|
throw new ModuleLoadException("Unable to parse regular expression pattern" + errpostfx);
|
||||||
throw new ModuleLoadException($"Failed to parse regular expression pattern '{input}'{errpofx}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (rxconf?.Type == JTokenType.String) {
|
} else {
|
||||||
var rxstr = rxconf.Value<string>()!;
|
var rxstr = rxconf.Value<string>();
|
||||||
try {
|
try {
|
||||||
var r = new Regex(rxstr, rxopts);
|
regexRules.Add(new Regex(rxstr!, opts));
|
||||||
regexes.Add(r);
|
} catch (Exception ex) when (ex is ArgumentException or NullReferenceException) {
|
||||||
} catch (ArgumentException) {
|
throw new ModuleLoadException("Unable to parse regular expression pattern" + errpostfx);
|
||||||
throw new ModuleLoadException($"Failed to parse regular expression pattern '{rxstr}'{errpofx}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new ModuleLoadException("'Regex' not defined" + errpofx);
|
|
||||||
}
|
|
||||||
Regex = regexes.AsReadOnly();
|
|
||||||
|
|
||||||
bool responseDefined;
|
|
||||||
|
|
||||||
// Get response
|
|
||||||
// TODO this bit could also go into the same aforementioned helper method
|
|
||||||
var replyconf = data["reply"];
|
|
||||||
if (replyconf?.Type == JTokenType.String) {
|
|
||||||
var str = replyconf.Value<string>()!;
|
|
||||||
Response = new List<string>() { str }.AsReadOnly();
|
|
||||||
responseDefined = true;
|
|
||||||
} else if (replyconf?.Type == JTokenType.Array) {
|
|
||||||
Response = new List<string>(replyconf.Values<string>()!).AsReadOnly();
|
|
||||||
responseDefined = true;
|
|
||||||
} else {
|
|
||||||
Response = Array.Empty<string>();
|
|
||||||
responseDefined = false;
|
|
||||||
}
|
|
||||||
// Get command
|
|
||||||
var commconf = data[nameof(Command)];
|
|
||||||
if (commconf != null && responseDefined) {
|
|
||||||
throw new ModuleLoadException("Cannot have 'Response' and 'Command' defined at the same time" + errpofx);
|
|
||||||
}
|
|
||||||
if (!responseDefined) {
|
|
||||||
if (commconf != null) {
|
|
||||||
var commstr = commconf.Value<string>();
|
|
||||||
if (string.IsNullOrWhiteSpace(commstr))
|
|
||||||
throw new ModuleLoadException("'Command' is defined, but value is blank" + errpofx);
|
|
||||||
Command = commstr;
|
|
||||||
responseDefined = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Got neither...
|
if (regexRules.Count == 0) {
|
||||||
if (!responseDefined) throw new ModuleLoadException("Neither 'Response' nor 'Command' were defined" + errpofx);
|
throw new ModuleLoadException(ErrNoRegex + errpostfx);
|
||||||
|
}
|
||||||
|
Regex = regexRules.AsReadOnly();
|
||||||
|
|
||||||
// Filtering
|
// Filtering
|
||||||
Filter = new FilterList(data);
|
Filter = new FilterList(def);
|
||||||
|
|
||||||
|
bool haveResponse;
|
||||||
|
|
||||||
|
// Reply options
|
||||||
|
var replyConf = def[nameof(Reply)];
|
||||||
|
if (replyConf?.Type == JTokenType.String) {
|
||||||
|
// Single string response
|
||||||
|
Reply = new List<string>() { replyConf.Value<string>()! }.AsReadOnly();
|
||||||
|
haveResponse = true;
|
||||||
|
} else if (replyConf?.Type == JTokenType.Array) {
|
||||||
|
// Have multiple responses
|
||||||
|
Reply = new List<string>(replyConf.Values<string>()!).AsReadOnly();
|
||||||
|
haveResponse= true;
|
||||||
|
} else {
|
||||||
|
// Have no response
|
||||||
|
Reply = Array.Empty<string>();
|
||||||
|
haveResponse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command options
|
||||||
|
Command = def[nameof(Command)]?.Value<string>()!;
|
||||||
|
if (Command != null && haveResponse)
|
||||||
|
throw new ModuleLoadException($"Only one of either '{nameof(Reply)}' or '{nameof(Command)}' may be defined{errpostfx}");
|
||||||
|
if (Command != null) {
|
||||||
|
if (string.IsNullOrWhiteSpace(Command))
|
||||||
|
throw new ModuleLoadException($"'{nameof(Command)}' must have a non-blank value{errpostfx}");
|
||||||
|
haveResponse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!haveResponse) throw new ModuleLoadException($"Neither '{nameof(Reply)}' nor '{nameof(Command)}' were defined{errpostfx}");
|
||||||
|
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
var rlconf = data[nameof(RateLimit)];
|
var rlconf = def[nameof(RateLimit)];
|
||||||
if (rlconf?.Type == JTokenType.Integer) {
|
if (rlconf?.Type == JTokenType.Integer) {
|
||||||
var rlval = rlconf.Value<uint>();
|
var rlval = rlconf.Value<uint>();
|
||||||
RateLimit = new RateLimit<ulong>(rlval);
|
RateLimit = new RateLimit<ulong>(rlval);
|
||||||
} else if (rlconf != null) {
|
} else if (rlconf != null) {
|
||||||
throw new ModuleLoadException("'RateLimit' must be a non-negative integer" + errpofx);
|
throw new ModuleLoadException($"'{nameof(RateLimit)}' must be a non-negative integer{errpostfx}");
|
||||||
} else {
|
} else {
|
||||||
RateLimit = new(0);
|
RateLimit = new(0);
|
||||||
}
|
}
|
||||||
var rlstr = data[nameof(RateLimit)]?.Value<ushort>();
|
|
||||||
|
|
||||||
// Random chance parameter
|
// Random chance parameter
|
||||||
var randstr = data[nameof(RandomChance)]?.Value<string>();
|
var randconf = def[nameof(RandomChance)];
|
||||||
double randval;
|
if (randconf?.Type == JTokenType.Float) {
|
||||||
if (string.IsNullOrWhiteSpace(randstr)) {
|
RandomChance = randconf.Value<float>();
|
||||||
randval = double.NaN;
|
if (RandomChance is > 1 or < 0) {
|
||||||
|
throw new ModuleLoadException($"Random value is invalid (not between 0 and 1){errpostfx}");
|
||||||
|
}
|
||||||
|
} else if (randconf != null) {
|
||||||
|
throw new ModuleLoadException($"{nameof(RandomChance)} is not correctly defined{errpostfx}");
|
||||||
} else {
|
} else {
|
||||||
if (!double.TryParse(randstr, out randval)) {
|
// Default to none if undefined
|
||||||
throw new ModuleLoadException("Random value is invalid (unable to parse)" + errpofx);
|
RandomChance = double.NaN;
|
||||||
}
|
|
||||||
if (randval is > 1 or < 0) {
|
|
||||||
throw new ModuleLoadException("Random value is invalid (not between 0 and 1)" + errpofx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
RandomChance = randval;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -131,7 +128,7 @@ class Definition {
|
||||||
if (Filter.IsFiltered(m, true)) return false;
|
if (Filter.IsFiltered(m, true)) return false;
|
||||||
|
|
||||||
// Match check
|
// Match check
|
||||||
bool matchFound = false;
|
var matchFound = false;
|
||||||
foreach (var item in Regex) {
|
foreach (var item in Regex) {
|
||||||
if (item.IsMatch(m.Content)) {
|
if (item.IsMatch(m.Content)) {
|
||||||
matchFound = true;
|
matchFound = true;
|
||||||
|
@ -159,7 +156,7 @@ class Definition {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetResponse() {
|
public string GetResponse() {
|
||||||
// TODO feature request: option to show responses in order instead of random
|
// TODO feature request: option to show responses in order instead of random
|
||||||
if (Response.Count == 1) return Response[0];
|
if (Reply.Count == 1) return Reply[0];
|
||||||
return Response[Chance.Next(0, Response.Count - 1)];
|
return Reply[Chance.Next(0, Reply.Count - 1)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue