Ported RegexModerator from RegexBot
Changes: -Renamed from AutoMod -Some options have been simplified to reduce complexity -Response exectution has been totally reworked to accomodate new and upcoming features Currently untested.
This commit is contained in:
parent
e87a985067
commit
90feeb47b0
3 changed files with 609 additions and 0 deletions
233
Modules-PublicInstance/RegexModerator/ConfDefinition.cs
Normal file
233
Modules-PublicInstance/RegexModerator/ConfDefinition.cs
Normal file
|
@ -0,0 +1,233 @@
|
|||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Representation of a single RegexModerator rule for a guild.
|
||||
/// Data in this class is immutable. Contains various helper methods.
|
||||
/// </summary>
|
||||
[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> _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<string>();
|
||||
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<bool>();
|
||||
if (rxci ?? true) opts |= RegexOptions.IgnoreCase;
|
||||
|
||||
const string ErrNoRegex = "Regular expression patterns are not defined";
|
||||
var rxs = new List<Regex>();
|
||||
var rxconf = def["regex"];
|
||||
if (rxconf == null) throw new ModuleLoadException(ErrNoRegex + errpostfx);
|
||||
if (rxconf.Type == JTokenType.Array)
|
||||
{
|
||||
foreach (var input in rxconf.Values<string>())
|
||||
{
|
||||
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<string>();
|
||||
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<bool>();
|
||||
_ignoreMods = bypass ?? true;
|
||||
|
||||
bool? embedScan = def["EmbedScanMode"]?.Value<bool>();
|
||||
_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<string>();
|
||||
ReplyInDM = resp[nameof(ReplyInDM)]?.Value<string>();
|
||||
|
||||
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<string>();
|
||||
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<string>();
|
||||
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<bool>() ?? false;
|
||||
|
||||
var reportstr = resp[nameof(ReportingChannel)]?.Value<string>();
|
||||
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<string>();
|
||||
// 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<int>(); }
|
||||
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<string>();
|
||||
|
||||
RemovalSendUserNotification = resp[nameof(RemovalSendUserNotification)]?.Value<bool>() ?? false;
|
||||
|
||||
DeleteMessage = resp[nameof(DeleteMessage)]?.Value<bool>() ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the given message to determine if it matches this definition's constraints.
|
||||
/// </summary>
|
||||
/// <returns>True if match.</returns>
|
||||
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<Embed> e)
|
||||
{
|
||||
var text = new StringBuilder();
|
||||
foreach (var item in e) text.AppendLine(SerializeEmbed(item));
|
||||
return text.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an embed to a plain string for easier matching.
|
||||
/// </summary>
|
||||
private string SerializeEmbed(Embed e)
|
||||
{
|
||||
StringBuilder result = new StringBuilder();
|
||||
if (e.Author.HasValue) result.AppendLine(e.Author.Value.Name ?? "" + e.Author.Value.Url ?? "");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(e.Title)) result.AppendLine(e.Title);
|
||||
if (!string.IsNullOrWhiteSpace(e.Description)) result.AppendLine(e.Description);
|
||||
|
||||
foreach (var f in e.Fields)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(f.Name)) result.AppendLine(f.Name);
|
||||
if (!string.IsNullOrWhiteSpace(f.Value)) result.AppendLine(f.Value);
|
||||
}
|
||||
|
||||
if (e.Footer.HasValue)
|
||||
{
|
||||
result.AppendLine(e.Footer.Value.Text ?? "");
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
77
Modules-PublicInstance/RegexModerator/RegexModerator.cs
Normal file
77
Modules-PublicInstance/RegexModerator/RegexModerator.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Kerobot.Modules.RegexModerator
|
||||
{
|
||||
/// <summary>
|
||||
/// The 'star' feature of Kerobot. Users define pattern-based rules with other constraints.
|
||||
/// When triggered, each rule executes one or more different actions.
|
||||
/// </summary>
|
||||
[KerobotModule]
|
||||
public class RegexModerator : ModuleBase
|
||||
{
|
||||
public RegexModerator(Kerobot kb) : base(kb)
|
||||
{
|
||||
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
|
||||
DiscordClient.MessageUpdated += DiscordClient_MessageUpdated;
|
||||
}
|
||||
|
||||
public override Task<object> CreateGuildStateAsync(ulong guildID, JToken config)
|
||||
{
|
||||
if (config == null) return Task.FromResult<object>(null);
|
||||
var defs = new List<ConfDefinition>();
|
||||
|
||||
foreach (var def in config.Children<JObject>())
|
||||
defs.Add(new ConfDefinition(this, def, guildID));
|
||||
|
||||
if (defs.Count == 0) return Task.FromResult<object>(null);
|
||||
return Task.FromResult<object>(defs.AsReadOnly());
|
||||
}
|
||||
|
||||
private Task DiscordClient_MessageUpdated(Discord.Cacheable<Discord.IMessage, ulong> arg1,
|
||||
SocketMessage arg2, ISocketMessageChannel arg3)
|
||||
=> ReceiveIncomingMessage(arg2);
|
||||
private Task DiscordClient_MessageReceived(SocketMessage arg) => ReceiveIncomingMessage(arg);
|
||||
|
||||
/// <summary>
|
||||
/// Does initial message checking before further processing.
|
||||
/// </summary>
|
||||
private async Task ReceiveIncomingMessage(SocketMessage msg)
|
||||
{
|
||||
// Ignore non-guild channels
|
||||
if (!(msg.Channel is SocketGuildChannel ch)) return;
|
||||
|
||||
// Get config?
|
||||
var defs = GetGuildState<IEnumerable<ConfDefinition>>(ch.Guild.Id);
|
||||
if (defs == null) return;
|
||||
|
||||
// Send further processing to thread pool.
|
||||
// Match checking is a CPU-intensive task, thus very little checking is done here.
|
||||
foreach (var item in defs)
|
||||
{
|
||||
// Need to check sender's moderator status here. Definition can't access mod list.
|
||||
var isMod = GetModerators(ch.Guild.Id).IsListMatch(msg, true);
|
||||
|
||||
var match = item.IsMatch(msg, isMod);
|
||||
await Task.Run(async () => await ProcessMessage(item, msg, isMod));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does further message checking and response execution.
|
||||
/// Invocations of this method are meant to be on the thread pool.
|
||||
/// </summary>
|
||||
private async Task ProcessMessage(ConfDefinition def, SocketMessage msg, bool isMod)
|
||||
{
|
||||
// Reminder: IsMatch handles matching execution time
|
||||
if (!def.IsMatch(msg, isMod)) return;
|
||||
|
||||
// TODO logging options for match result; handle here?
|
||||
|
||||
var executor = new ResponseExecutor(def, Kerobot);
|
||||
await executor.Execute(msg);
|
||||
}
|
||||
}
|
||||
}
|
299
Modules-PublicInstance/RegexModerator/ResponseExecutor.cs
Normal file
299
Modules-PublicInstance/RegexModerator/ResponseExecutor.cs
Normal file
|
@ -0,0 +1,299 @@
|
|||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using Kerobot.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static Kerobot.Kerobot;
|
||||
|
||||
namespace Kerobot.Modules.RegexModerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class to RegexModerator that executes the appropriate actions associated with a triggered rule.
|
||||
/// </summary>
|
||||
class ResponseExecutor
|
||||
{
|
||||
private readonly ConfDefinition _rule;
|
||||
private readonly Kerobot _bot;
|
||||
private List<(ResponseAction, ResponseExecutionResult)> _results;
|
||||
|
||||
public ResponseExecutor(ConfDefinition rule, Kerobot kb)
|
||||
{
|
||||
_rule = rule;
|
||||
_bot = kb;
|
||||
}
|
||||
|
||||
public async Task Execute(SocketMessage msg)
|
||||
{
|
||||
var g = ((SocketGuildUser)msg.Author).Guild;
|
||||
_results = new List<(ResponseAction, ResponseExecutionResult)>();
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
ExecuteAction(DoReplyToChannel, g, msg),
|
||||
ExecuteAction(DoReplyToInvokerDM, g, msg),
|
||||
ExecuteAction(DoRoleAdd, g, msg),
|
||||
ExecuteAction(DoRoleRemove, g, msg),
|
||||
ExecuteAction(DoBan, g, msg),
|
||||
ExecuteAction(DoKick, g, msg),
|
||||
ExecuteAction(DoDelete, g, msg)
|
||||
// TODO role add/remove: add persistence option
|
||||
// TODO add note to user log
|
||||
// TODO add warning to user log
|
||||
};
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
// Report can only run after all previous actions have been performed.
|
||||
await ExecuteAction(DoReport, g, msg);
|
||||
|
||||
// TODO pass any local error messages to guild log
|
||||
}
|
||||
|
||||
#region Response actions
|
||||
/*
|
||||
* For the sake of creating reports and notifying the user of any issues,
|
||||
* every response method should have a signature that conforms to that of the
|
||||
* ResponseAction delegate defined here.
|
||||
* Methods here should attempt to handle their own expected exceptions, and leave the
|
||||
* extraordinary exceptions for the wrapper to deal with.
|
||||
*
|
||||
* Methods may return null, but MUST only do so if they took no action (meaning, they were
|
||||
* not meant to take any action per the input configuration). Data within each
|
||||
* ResponseExecutionResult is then used to build a report (if requested) and/or place
|
||||
* error messages into the guild log.
|
||||
*/
|
||||
delegate Task<ResponseExecutionResult> ResponseAction(SocketGuild g, SocketMessage msg);
|
||||
|
||||
const string ForbiddenGenericError = "Failed to perform the action due to a permissions issue.";
|
||||
|
||||
private Task<ResponseExecutionResult> DoBan(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
if (_rule.RemovalAction != RemovalType.Ban) return Task.FromResult<ResponseExecutionResult>(null);
|
||||
return DoBanOrKick(g, msg, _rule.RemovalAction);
|
||||
}
|
||||
private Task<ResponseExecutionResult> DoKick(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
if (_rule.RemovalAction != RemovalType.Kick) return Task.FromResult<ResponseExecutionResult>(null);
|
||||
return DoBanOrKick(g, msg, _rule.RemovalAction);
|
||||
}
|
||||
private async Task<ResponseExecutionResult> DoBanOrKick(SocketGuild g, SocketMessage msg, RemovalType t)
|
||||
{
|
||||
var result = await _bot.BanOrKickAsync(t, g, $"Rule '{_rule.Label}'",
|
||||
msg.Author.Id, _rule.BanPurgeDays, _rule.RemovalReason, _rule.RemovalSendUserNotification);
|
||||
if (result.ErrorForbidden)
|
||||
{
|
||||
return new ResponseExecutionResult(false, ForbiddenGenericError);
|
||||
}
|
||||
else if (result.ErrorNotFound)
|
||||
{
|
||||
return new ResponseExecutionResult(false, "The target user is no longer in the server.");
|
||||
}
|
||||
else return new ResponseExecutionResult(true, null);
|
||||
|
||||
// TODO option to announce ban/kick result in the trigger channel
|
||||
// ^ implementation: take result, reply to channel. don't alter BanOrKickAsync.
|
||||
}
|
||||
|
||||
private async Task<ResponseExecutionResult> DoDelete(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
if (!_rule.DeleteMessage) return null;
|
||||
try
|
||||
{
|
||||
await msg.DeleteAsync();
|
||||
return new ResponseExecutionResult(true, null);
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
{
|
||||
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
return new ResponseExecutionResult(false, ForbiddenGenericError);
|
||||
}
|
||||
else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return new ResponseExecutionResult(false, "The message has already been deleted.");
|
||||
}
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ResponseExecutionResult> DoReplyToChannel(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_rule.ReplyInChannel)) return null;
|
||||
try
|
||||
{
|
||||
await msg.Channel.SendMessageAsync(_rule.ReplyInChannel);
|
||||
return new ResponseExecutionResult(true, null);
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
{
|
||||
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
return new ResponseExecutionResult(false, ForbiddenGenericError);
|
||||
}
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ResponseExecutionResult> DoReplyToInvokerDM(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_rule.ReplyInDM)) return null;
|
||||
var target = await msg.Author.GetOrCreateDMChannelAsync(); // can this throw an exception?
|
||||
|
||||
try
|
||||
{
|
||||
await target.SendMessageAsync(_rule.ReplyInDM);
|
||||
return new ResponseExecutionResult(true, null);
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
{
|
||||
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
return new ResponseExecutionResult(false, "The target user is not accepting DMs.");
|
||||
}
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
|
||||
private Task<ResponseExecutionResult> DoRoleAdd(SocketGuild g, SocketMessage msg)
|
||||
=> RoleManipulationResponse(g, msg, true);
|
||||
private Task<ResponseExecutionResult> DoRoleRemove(SocketGuild g, SocketMessage msg)
|
||||
=> RoleManipulationResponse(g, msg, false);
|
||||
private async Task<ResponseExecutionResult> RoleManipulationResponse(SocketGuild g, SocketMessage msg, bool add)
|
||||
{
|
||||
EntityName ck;
|
||||
if (add)
|
||||
{
|
||||
if (_rule.RoleAdd == null) return null;
|
||||
ck = _rule.RoleAdd;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_rule.RoleRemove == null) return null;
|
||||
ck = _rule.RoleRemove;
|
||||
}
|
||||
|
||||
SocketRole target = ck.FindRoleIn(g, false);
|
||||
if (target == null)
|
||||
{
|
||||
return new ResponseExecutionResult(false,
|
||||
$"Unable to determine the role to be {(add ? "added" : "removed")}. Does it still exist?");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (add) await ((SocketGuildUser)msg.Author).AddRoleAsync(target);
|
||||
else await ((SocketGuildUser)msg.Author).RemoveRoleAsync(target);
|
||||
|
||||
return new ResponseExecutionResult(true, null);
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
{
|
||||
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
return new ResponseExecutionResult(false, ForbiddenGenericError);
|
||||
}
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Reporting
|
||||
private class ResponseExecutionResult
|
||||
{
|
||||
public bool Success { get; }
|
||||
public string Notice { get; }
|
||||
|
||||
public ResponseExecutionResult(bool success, string log)
|
||||
{
|
||||
Success = success;
|
||||
Notice = log;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteAction(ResponseAction action, SocketGuild g, SocketMessage arg)
|
||||
{
|
||||
ResponseExecutionResult result;
|
||||
try { result = await action(g, arg); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
result = new ResponseExecutionResult(false,
|
||||
"An unknown error occurred. The bot maintainer has been notified.");
|
||||
await _bot.InstanceLogAsync(true, nameof(RegexModerator),
|
||||
"An unexpected error occurred while executing a response. "
|
||||
+ $"Guild: {g.Id} - Rule: '{_rule.Label}' - Exception detail:\n"
|
||||
+ ex.ToString());
|
||||
}
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
lock (_results) _results.Add((action, result));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ResponseExecutionResult> DoReport(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
if (_rule.ReportingChannel == null) return null;
|
||||
|
||||
// Determine channel before anything else
|
||||
var ch = _rule.ReportingChannel.FindChannelIn(g, true);
|
||||
if (ch == null) return new ResponseExecutionResult(false, "Unable to find reporting channel.");
|
||||
|
||||
var rptOutput = new StringBuilder();
|
||||
foreach (var (action, result) in _results) // Locking of _results not necessary at this point
|
||||
{
|
||||
if (result == null) continue;
|
||||
rptOutput.Append(result.Success ? ":white_check_mark:" : ":x:");
|
||||
rptOutput.Append(" " + action.Method.Name);
|
||||
if (result.Notice != null)
|
||||
rptOutput.Append(" - " + result.Notice);
|
||||
rptOutput.AppendLine();
|
||||
}
|
||||
// Report status goes last. It is presumed to succeed. If it fails, the message won't make it anyway.
|
||||
rptOutput.Append($":white_check_mark: {nameof(DoReport)}");
|
||||
|
||||
// We can only afford to show a preview of the message being reported, due to embeds
|
||||
// being constrained to the same 2000 character limit.
|
||||
const string TruncateWarning = "**Notice: Full message has been truncated.**\n";
|
||||
const int TruncateMaxLength = 990;
|
||||
var invokingLine = msg.Content;
|
||||
if (invokingLine.Length > TruncateMaxLength)
|
||||
{
|
||||
invokingLine = TruncateWarning + invokingLine.Substring(0, TruncateMaxLength - TruncateWarning.Length);
|
||||
}
|
||||
|
||||
var resultEm = new EmbedBuilder()
|
||||
{
|
||||
Color = new Color(0xEDCE00), // TODO configurable later?
|
||||
|
||||
Author = new EmbedAuthorBuilder()
|
||||
{
|
||||
Name = $"{msg.Author.Username}#{msg.Author.Discriminator} said:",
|
||||
IconUrl = msg.Author.GetAvatarUrl()
|
||||
},
|
||||
Description = invokingLine,
|
||||
|
||||
Footer = new EmbedFooterBuilder() { Text = $"Rule: {_rule.Label}" },
|
||||
Timestamp = msg.EditedTimestamp ?? msg.Timestamp
|
||||
}.AddField(new EmbedFieldBuilder()
|
||||
{
|
||||
Name = "Actions taken:",
|
||||
Value = rptOutput.ToString()
|
||||
}).Build();
|
||||
|
||||
try
|
||||
{
|
||||
await ch.SendMessageAsync(embed: resultEm);
|
||||
return new ResponseExecutionResult(true, null);
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
{
|
||||
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
return new ResponseExecutionResult(false, ForbiddenGenericError);
|
||||
}
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue