using Discord;
using Discord.WebSocket;
using Newtonsoft.Json.Linq;
using Noikoio.RegexBot.ConfigItem;
using Noikoio.RegexBot.Feature.AutoMod.Responses;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Noikoio.RegexBot.Feature.AutoMod
{
///
/// Representation of a single AutoMod rule.
/// Data stored within cannot be edited.
///
class Rule
{
readonly AutoMod _instance;
readonly string _label;
readonly IEnumerable _regex;
readonly ICollection _responses;
readonly FilterList _filter;
readonly int _msgMinLength;
readonly int _msgMaxLength;
readonly bool _modBypass;
readonly bool _embedMode;
public string Label => _label;
public IEnumerable Regex => _regex;
public ICollection Response => _responses;
public FilterList Filter => _filter;
public (int?, int?) MatchLengthMinMaxLimit => (_msgMinLength, _msgMaxLength);
public bool AllowsModBypass => _modBypass;
public bool MatchEmbed => _embedMode;
public DiscordSocketClient Discord => _instance.Client;
public Func Logger => _instance.Log;
///
/// Creates a new Rule instance to represent the given configuration.
///
public Rule(AutoMod instance, JProperty definition)
{
_instance = instance;
_label = definition.Name;
var ruleconf = (JObject)definition.Value;
// TODO validation. does the above line even throw an exception in the right cases?
// and what about the label? does it make for a good name?
string errpfx = $" in definition for rule '{_label}'.";
// regex options
RegexOptions opts = RegexOptions.Compiled | RegexOptions.CultureInvariant;
// TODO consider adding an option to specify Singleline and Multiline matching
opts |= RegexOptions.Singleline;
// case sensitivity must be explicitly defined, else not case sensitive by default
bool? regexci = ruleconf["ignorecase"]?.Value();
opts |= RegexOptions.IgnoreCase;
if (regexci.HasValue && regexci.Value == false)
opts &= ~RegexOptions.IgnoreCase;
// regex
const string NoRegexError = "No regular expression patterns are defined";
var regexes = new List();
var rxconf = ruleconf["regex"];
if (rxconf == null) throw new RuleImportException(NoRegexError + errpfx);
if (rxconf.Type == JTokenType.Array)
{
foreach (var input in rxconf.Values())
{
try
{
Regex r = new Regex(input, opts);
regexes.Add(r);
}
catch (ArgumentException)
{
throw new RuleImportException(
$"Failed to parse regular expression pattern '{input}'{errpfx}");
}
}
}
else
{
string rxstr = rxconf.Value();
try
{
Regex r = new Regex(rxstr, opts);
regexes.Add(r);
}
catch (Exception ex) when (ex is ArgumentException || ex is NullReferenceException)
{
throw new RuleImportException(
$"Failed to parse regular expression pattern '{rxstr}'{errpfx}");
}
}
if (regexes.Count == 0)
{
throw new RuleImportException(NoRegexError + errpfx);
}
_regex = regexes.ToArray();
// min/max length
try
{
_msgMinLength = ruleconf["min"]?.Value() ?? -1;
_msgMaxLength = ruleconf["max"]?.Value() ?? -1;
}
catch (FormatException)
{
throw new RuleImportException("Minimum/maximum values must be an integer.");
}
// responses
const string NoResponseError = "No responses have been defined";
var responsestrs = new List();
var rsconf = ruleconf["response"];
if (rsconf == null) throw new RuleImportException(NoResponseError + errpfx);
try
{
if (rsconf.Type == JTokenType.Array)
{
_responses = Responses.Response.ReadConfiguration(this, rsconf.Values());
}
else
{
_responses = Responses.Response.ReadConfiguration(this, new string[] { rsconf.Value() });
}
}
catch (RuleImportException ex)
{
throw new RuleImportException(ex.Message + errpfx);
}
// whitelist/blacklist filtering
_filter = new FilterList(ruleconf);
// moderator bypass toggle - true by default, must be explicitly set to false
bool? bypass = ruleconf["AllowModBypass"]?.Value();
_modBypass = bypass.HasValue ? bypass.Value : true;
// embed matching mode
bool? embed = ruleconf["MatchEmbeds"]?.Value();
_embedMode = (embed.HasValue && embed == true);
}
///
/// Checks given message to see if it matches this rule's constraints.
///
/// If true, the rule's response(s) should be executed.
public bool Match(SocketMessage m, bool isMod)
{
// Regular or embed mode?
string msgcontent;
if (MatchEmbed) msgcontent = SerializeEmbed(m.Embeds);
else msgcontent = m.Content;
// Min/max length check
if (_msgMinLength != -1 && msgcontent.Length <= _msgMinLength) return false;
if (_msgMaxLength != -1 && msgcontent.Length >= _msgMaxLength) return false;
// Filter check
if (Filter.IsFiltered(m)) return false;
// Mod bypass check
if (AllowsModBypass && isMod) return false;
// Finally, regex checks
foreach (var regex in Regex)
{
if (regex.IsMatch(msgcontent)) return true;
}
return false;
}
private string SerializeEmbed(IReadOnlyCollection