using RegexBot.Common;
using System.Text.RegularExpressions;
namespace RegexBot.Modules.AutoResponder;
///
/// Representation of a single configuration definition.
///
class Definition {
private static readonly Random Chance = new();
public string Label { get; }
public IEnumerable Regex { get; }
public IReadOnlyList Response { get; }
public string? Command { get; }
public FilterList Filter { get; }
public RateLimit RateLimit { get; }
public double RandomChance { get; }
///
/// Creates an instance based on JSON configuration.
///
public Definition(JProperty incoming) {
Label = incoming.Name;
if (incoming.Value.Type != JTokenType.Object)
throw new ModuleLoadException($"Value of {nameof(AutoResponder)} definition must be a JSON object.");
var data = (JObject)incoming.Value;
// error message postfix
var errpofx = $" in AutoRespond definition '{Label}'.";
// Parse regex
const RegexOptions rxopts = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline;
var regexes = new List();
var rxconf = data[nameof(Regex)];
// Accepting either array or single string
// TODO this code could be moved into a helper method somehow
if (rxconf?.Type == JTokenType.Array) {
foreach (var input in rxconf.Values()) {
try {
var r = new Regex(input!, rxopts);
regexes.Add(r);
} catch (ArgumentException) {
throw new ModuleLoadException($"Failed to parse regular expression pattern '{input}'{errpofx}");
}
}
} else if (rxconf?.Type == JTokenType.String) {
var rxstr = rxconf.Value()!;
try {
var r = new Regex(rxstr, rxopts);
regexes.Add(r);
} catch (ArgumentException) {
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()!;
Response = new List() { str }.AsReadOnly();
responseDefined = true;
} else if (replyconf?.Type == JTokenType.Array) {
Response = new List(replyconf.Values()!).AsReadOnly();
responseDefined = true;
} else {
Response = Array.Empty();
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();
if (string.IsNullOrWhiteSpace(commstr))
throw new ModuleLoadException("'Command' is defined, but value is blank" + errpofx);
Command = commstr;
responseDefined = true;
}
}
// Got neither...
if (!responseDefined) throw new ModuleLoadException("Neither 'Response' nor 'Command' were defined" + errpofx);
// Filtering
Filter = new FilterList(data);
// Rate limiting
var rlconf = data[nameof(RateLimit)];
if (rlconf?.Type == JTokenType.Integer) {
var rlval = rlconf.Value();
RateLimit = new RateLimit(rlval);
} else if (rlconf != null) {
throw new ModuleLoadException("'RateLimit' must be a non-negative integer" + errpofx);
} else {
RateLimit = new(0);
}
var rlstr = data[nameof(RateLimit)]?.Value();
// Random chance parameter
var randstr = data[nameof(RandomChance)]?.Value();
double randval;
if (string.IsNullOrWhiteSpace(randstr)) {
randval = double.NaN;
} else {
if (!double.TryParse(randstr, out randval)) {
throw new ModuleLoadException("Random value is invalid (unable to parse)" + errpofx);
}
if (randval is > 1 or < 0) {
throw new ModuleLoadException("Random value is invalid (not between 0 and 1)" + errpofx);
}
}
RandomChance = randval;
}
///
/// Checks the given message to determine if it matches this rule's constraints.
/// This method also maintains rate limiting and performs random number generation.
///
/// True if the rule's response(s) should be executed.
public bool Match(SocketMessage m) {
// Filter check
if (Filter.IsFiltered(m, true)) return false;
// Match check
bool matchFound = false;
foreach (var item in Regex) {
if (item.IsMatch(m.Content)) {
matchFound = true;
break;
}
}
if (!matchFound) return false;
// Rate limit check - currently per channel
if (!RateLimit.IsPermitted(m.Channel.Id)) return false;
// Random chance check
if (!double.IsNaN(RandomChance)) {
// Fail if randomly generated value is higher than the parameter
// Example: To fail a 75% chance, the check value must be between 0.75000...001 and 1.0.
var chk = Chance.NextDouble();
if (chk > RandomChance) return false;
}
return true;
}
///
/// Gets a response string to display in the channel.
///
public string GetResponse() {
// TODO feature request: option to show responses in order instead of random
if (Response.Count == 1) return Response[0];
return Response[Chance.Next(0, Response.Count - 1)];
}
}