Have AutoResponder use similar config format to RegexModerator

This commit is contained in:
Noi 2022-06-13 22:38:58 -07:00
parent 6032e4d37b
commit 2160b0fa4e
2 changed files with 92 additions and 93 deletions

View file

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

View file

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