using Discord;
using Discord.WebSocket;
using Kerobot.Common;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
namespace Kerobot.Modules.RegexModerator
{
///
/// Representation of a single RegexModerator rule for a guild.
/// Data in this class is immutable. Contains various helper methods.
///
[DebuggerDisplay("RM rule '{_label}' for {_guild}")]
class ConfDefinition
{
public string Label { get; }
readonly RegexModerator _module; // TODO is this needed?
readonly ulong _guild; // corresponding guild, for debug purposes. (is this needed?)
// Matching settings
readonly IEnumerable _regex;
readonly FilterList _filter;
readonly bool _ignoreMods;
readonly bool _embedScan;
// Response settings
public string ReplyInChannel { get; }
public string ReplyInDM { get; }
public EntityName RoleAdd { get; } // keep in mind it's possible to have both add and remove role available at once
public EntityName RoleRemove { get; }
//readonly bool _rRolePersist; // TODO use when feature exists
public EntityName ReportingChannel { get; }
public Kerobot.RemovalType RemovalAction { get; } // ban, kick?
public int BanPurgeDays { get; }
public string RemovalReason { get; } // reason to place into audit log, notification
public bool RemovalSendUserNotification; // send ban/kick notification to user?
public bool DeleteMessage { get; }
public ConfDefinition(RegexModerator instance, JObject def, ulong guildId)
{
_module = instance;
Label = def["Label"].Value();
if (string.IsNullOrWhiteSpace(Label))
throw new ModuleLoadException("A rule does not have a label defined.");
string errpostfx = $" in the rule definition for '{Label}'.";
// Regex loading
var opts = RegexOptions.Compiled | RegexOptions.CultureInvariant;
// TODO consider adding an option to specify Singleline and Multiline mode. Defaulting to Singleline.
opts |= RegexOptions.Singleline;
// IgnoreCase is enabled by default; must be explicitly set to false
bool? rxci = def["IgnoreCase"]?.Value();
if (rxci ?? true) opts |= RegexOptions.IgnoreCase;
const string ErrNoRegex = "Regular expression patterns are not defined";
var rxs = new List();
var rxconf = def["regex"];
if (rxconf == null) throw new ModuleLoadException(ErrNoRegex + errpostfx);
if (rxconf.Type == JTokenType.Array)
{
foreach (var input in rxconf.Values())
{
try
{
// TODO HIGH IMPORTANCE: sanitize input regex; don't allow inline editing of options
Regex r = new Regex(input, opts);
rxs.Add(r);
}
catch (ArgumentException)
{
throw new ModuleLoadException("Unable to parse regular expression pattern" + errpostfx);
}
}
}
else
{
string rxstr = rxconf.Value();
try
{
Regex r = new Regex(rxstr, opts);
rxs.Add(r);
}
catch (Exception ex) when (ex is ArgumentException || ex is NullReferenceException)
{
throw new ModuleLoadException("Unable to parse regular expression pattern" + errpostfx);
}
}
if (rxs.Count == 0)
{
throw new ModuleLoadException(ErrNoRegex + errpostfx);
}
_regex = rxs.ToArray();
// Filtering
_filter = new FilterList(def);
// Misc options
// IgnoreMods is enabled by default; must be explicitly set to false
bool? bypass = def["IgnoreMods"]?.Value();
_ignoreMods = bypass ?? true;
bool? embedScan = def["EmbedScanMode"]?.Value();
_embedScan = embedScan ?? false; // false by default
// Response options
var resp = def["Response"] as JObject;
if (resp == null)
throw new ModuleLoadException("Cannot find a valid response section" + errpostfx);
ReplyInChannel = resp[nameof(ReplyInChannel)]?.Value();
ReplyInDM = resp[nameof(ReplyInDM)]?.Value();
const string ErrRole = "The role value specified is not properly defined as a role";
// TODO make this error message nicer
var rolestr = resp[nameof(RoleAdd)]?.Value();
if (!string.IsNullOrWhiteSpace(rolestr))
{
RoleAdd = new EntityName(rolestr);
if (RoleAdd.Type != EntityType.Role) throw new ModuleLoadException(ErrRole + errpostfx);
}
else RoleAdd = null;
rolestr = resp[nameof(RoleRemove)]?.Value();
if (!string.IsNullOrWhiteSpace(rolestr))
{
RoleRemove = new EntityName(rolestr);
if (RoleRemove.Type != EntityType.Role) throw new ModuleLoadException(ErrRole + errpostfx);
}
else RoleRemove = null;
//_rRolePersist = resp["RolePersist"]?.Value() ?? false;
var reportstr = resp[nameof(ReportingChannel)]?.Value();
if (!string.IsNullOrWhiteSpace(reportstr))
{
ReportingChannel = new EntityName(reportstr);
if (ReportingChannel.Type != EntityType.Channel)
throw new ModuleLoadException("The reporting channel specified is not properly defined as a channel" + errpostfx);
}
else ReportingChannel = null;
var removestr = resp[nameof(RemovalAction)]?.Value();
// accept values ban, kick, none
switch (removestr)
{
case "ban": RemovalAction = Kerobot.RemovalType.Ban; break;
case "kick": RemovalAction = Kerobot.RemovalType.Kick; break;
case "none": RemovalAction = Kerobot.RemovalType.None; break;
default:
if (removestr != null)
throw new ModuleLoadException("RemoveAction is not set to a proper value" + errpostfx);
break;
}
// TODO extract BanPurgeDays
int? banpurgeint;
try { banpurgeint = resp[nameof(BanPurgeDays)]?.Value(); }
catch (InvalidCastException) { throw new ModuleLoadException("BanPurgeDays must be a numeric value."); }
if (banpurgeint.HasValue)
{
if (banpurgeint > 7 || banpurgeint < 0)
throw new ModuleLoadException("BanPurgeDays must be a value between 0 and 7 inclusive.");
BanPurgeDays = banpurgeint ?? 0;
}
RemovalReason = resp[nameof(RemovalReason)]?.Value();
RemovalSendUserNotification = resp[nameof(RemovalSendUserNotification)]?.Value() ?? false;
DeleteMessage = resp[nameof(DeleteMessage)]?.Value() ?? false;
}
///
/// Checks the given message to determine if it matches this definition's constraints.
///
/// True if match.
public bool IsMatch(SocketMessage m, bool senderIsModerator)
{
// TODO keep id: true or false?
if (_filter.IsFiltered(m, false)) return false;
if (senderIsModerator && _ignoreMods) return false;
var matchText = _embedScan ? SerializeEmbed(m.Embeds) : m.Content;
foreach (var regex in _regex)
{
// TODO enforce maximum execution time
// TODO multi-processing of multiple regexes?
// TODO metrics: temporary tracking of regex execution times
if (regex.IsMatch(m.Content)) return true;
}
return false;
}
private string SerializeEmbed(IReadOnlyCollection