Removed RegexResponder

This commit is contained in:
Noikoio 2017-08-26 10:23:02 -07:00
parent 18e1748c4f
commit 0f3fd350fa
3 changed files with 0 additions and 715 deletions

View file

@ -1,309 +0,0 @@
using Discord;
using Discord.WebSocket;
using Newtonsoft.Json.Linq;
using Noikoio.RegexBot.ConfigItem;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Threading.Tasks;
namespace Noikoio.RegexBot.Feature.RegexResponder
{
/// <summary>
/// Implements per-message regex matching and executes customizable responses.
/// Namesake of this project.
/// </summary>
partial class EventProcessor : BotFeature
{
private readonly DiscordSocketClient _client;
public override string Name => "RegexResponder";
public EventProcessor(DiscordSocketClient client) : base(client)
{
_client = client;
_client.MessageReceived += OnMessageReceived;
_client.MessageUpdated += OnMessageUpdated;
_commands = new ReadOnlyDictionary<string, ResponseProcessor>(
new Dictionary<string, ResponseProcessor>() {
#if DEBUG
{ "crash", RP_Crash },
{ "dumpid", RP_DumpID },
#endif
{ "report", RP_Report },
{ "say", RP_Say },
{ "remove", RP_Remove },
{ "delete", RP_Remove },
{ "erase", RP_Remove },
{ "exec", RP_Exec },
{ "ban", RP_Ban },
{ "grantrole", RP_GrantRevokeRole },
{ "revokerole", RP_GrantRevokeRole }
}
);
}
#region Event handlers
private async Task OnMessageReceived(SocketMessage arg)
=> await ReceiveMessage(arg);
private async Task OnMessageUpdated(Cacheable<IMessage, ulong> arg1, SocketMessage arg2, ISocketMessageChannel arg3)
=> await ReceiveMessage(arg2);
#endregion
/// <summary>
/// Receives incoming messages and creates tasks to handle them if necessary.
/// </summary>
private async Task ReceiveMessage(SocketMessage arg)
{
// Determine channel type - if not a guild channel, stop.
var ch = arg.Channel as SocketGuildChannel;
if (ch == null) return;
if (arg.Author == _client.CurrentUser) return; // Don't ever self-trigger
// Looking up server information and extracting settings
SocketGuild g = ch.Guild;
ServerConfig sd = null;
foreach (var item in RegexBot.Config.Servers)
{
if (item.Id.HasValue)
{
// Finding server by ID
if (g.Id == item.Id)
{
sd = item;
break;
}
}
else
{
// Finding server by name and caching ID
if (string.Equals(item.Name, g.Name, StringComparison.OrdinalIgnoreCase))
{
item.Id = g.Id;
sd = item;
await Logger.GetLogger(Configuration.LogPrefix)
($"Suggestion: Server \"{item.Name}\" can be defined as \"{item.Id}::{item.Name}\"");
break;
}
}
}
if (sd == null) return; // No server configuration found
var rules = GetConfig(ch.Guild.Id) as IEnumerable<RuleConfig>;
if (rules == null) return;
// Further processing is sent to the thread pool
foreach (var rule in rules)
await Task.Run(async () => await ProcessMessage(sd, rule, arg));
}
/// <summary>
/// Uses information from a single rule and checks if the incoming message is a match.
/// If it matches, the rule's responses are executed. To be run in the thread pool.
/// </summary>
private async Task ProcessMessage(ServerConfig srv, RuleConfig rule, SocketMessage msg)
{
string msgcontent;
// Embed mode?
if (rule.MatchEmbeds)
{
var embeds = new StringBuilder();
foreach (var e in msg.Embeds) embeds.AppendLine(EmbedToString(e));
msgcontent = embeds.ToString();
}
else
{
msgcontent = msg.Content;
}
// Min/max message length check
if (rule.MinLength.HasValue && msgcontent.Length <= rule.MinLength.Value) return;
if (rule.MaxLength.HasValue && msgcontent.Length >= rule.MaxLength.Value) return;
// Moderator bypass check
if (rule.AllowModBypass == true && srv.Moderators.ExistsInList(msg)) return;
// Individual rule filtering check
if (rule.Filter.IsFiltered(msg)) return;
// And finally, pattern matching checks
bool success = false;
foreach (var regex in rule.Regex)
{
success = regex.Match(msgcontent).Success;
if (success) break;
}
if (!success) return;
// Prepare to execute responses
await Log($"\"{rule.DisplayName}\" triggered in {srv.Name}/#{msg.Channel} by {msg.Author.ToString()}");
foreach (string rcmd in rule.Responses)
{
string cmd = rcmd.TrimStart(' ').Split(' ')[0].ToLower();
try
{
ResponseProcessor response;
if (!_commands.TryGetValue(cmd, out response))
{
await Log($"Unknown command defined in response: \"{cmd}\"");
continue;
}
await response.Invoke(rcmd, rule, msg);
}
catch (Exception ex)
{
await Log($"Encountered an error while processing \"{cmd}\". Details follow:");
await Log(ex.ToString());
}
}
}
[ConfigSection("rules")]
public override async Task<object> ProcessConfiguration(JToken configSection)
{
List<RuleConfig> rules = new List<RuleConfig>();
foreach (JObject ruleconf in configSection)
{
// Try and get at least the name before passing it to RuleItem
string name = ruleconf["name"]?.Value<string>();
if (name == null)
{
await Log("Display name not defined within a rule section.");
return false;
}
await Log($"Adding rule \"{name}\"");
RuleConfig rule;
try
{
rule = new RuleConfig(ruleconf);
}
catch (RuleImportException ex)
{
await Log("-> Error: " + ex.Message);
return false;
}
rules.Add(rule);
}
return rules.AsReadOnly();
}
// -------------------------------------
/// <summary>
/// Turns an embed into a single string for regex matching purposes
/// </summary>
private string EmbedToString(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();
}
private string[] SplitParams(string cmd, int? limit = null)
{
if (limit.HasValue)
{
return cmd.Split(new char[] { ' ' }, limit.Value, StringSplitOptions.RemoveEmptyEntries);
}
else
{
return cmd.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
}
}
private string ProcessText(string input, SocketMessage m)
{
// Maybe in the future this will do more.
// For now, replaces all instances of @_ with the message sender.
return input
.Replace("@_", m.Author.Mention)
.Replace("@\\_", "@_");
}
/// <summary>
/// Receives a string (beginning with @ or #) and returns an object
/// suitable for sending out messages
/// </summary>
private async Task<IMessageChannel> GetMessageTargetAsync(string targetName, SocketMessage m)
{
const string AEShort = "Target name is too short.";
EntityType et;
if (targetName.Length <= 1) throw new ArgumentException(AEShort);
if (targetName[0] == '#') et = EntityType.Channel;
else if (targetName[0] == '@') et = EntityType.User;
else throw new ArgumentException("Target is not specified to be either a channel or user.");
targetName = targetName.Substring(1);
if (targetName == "_")
{
if (et == EntityType.Channel) return m.Channel;
else return await m.Author.GetOrCreateDMChannelAsync();
}
EntityName ei = new EntityName(targetName, et);
SocketGuild g = ((SocketGuildUser)m.Author).Guild;
if (et == EntityType.Channel)
{
if (targetName.Length < 2 || targetName.Length > 100)
throw new ArgumentException(AEShort);
foreach (var ch in g.TextChannels)
{
if (ei.Id.HasValue)
{
if (ei.Id.Value == ch.Id) return ch;
}
else
{
if (string.Equals(ei.Name, ch.Name, StringComparison.OrdinalIgnoreCase)) return ch;
}
}
}
else
{
if (ei.Id.HasValue)
{
// The easy way
return await _client.GetUser(ei.Id.Value).GetOrCreateDMChannelAsync();
}
// The hard way
foreach (var u in g.Users)
{
if (string.Equals(ei.Name, u.Username, StringComparison.OrdinalIgnoreCase) ||
string.Equals(ei.Name, u.Nickname, StringComparison.OrdinalIgnoreCase))
{
return await u.GetOrCreateDMChannelAsync();
}
}
}
return null;
}
}
}

View file

@ -1,265 +0,0 @@
using Discord;
using Discord.WebSocket;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
namespace Noikoio.RegexBot.Feature.RegexResponder
{
// Contains code for handling each response in a rule.
partial class EventProcessor
{
private delegate Task ResponseProcessor(string cmd, RuleConfig r, SocketMessage m);
private readonly ReadOnlyDictionary<string, ResponseProcessor> _commands;
#if DEBUG
/// <summary>
/// Throws an exception. Meant to be a quick error handling test.
/// No parameters.
/// </summary>
private Task RP_Crash(string cmd, RuleConfig r, SocketMessage m)
{
throw new Exception("Requested in response.");
}
/// <summary>
/// Prints all guild values (IDs for users, channels, roles) to console.
/// The guild info displayed is the one in which the command is invoked.
/// No parameters.
/// </summary>
private Task RP_DumpID(string cmd, RuleConfig r, SocketMessage m)
{
var g = ((SocketGuildUser)m.Author).Guild;
var result = new StringBuilder();
result.AppendLine("Users:");
foreach (var item in g.Users)
result.AppendLine($"{item.Id} {item.Username}#{item.Discriminator}");
result.AppendLine();
result.AppendLine("Channels:");
foreach (var item in g.Channels) result.AppendLine($"{item.Id} #{item.Name}");
result.AppendLine();
result.AppendLine("Roles:");
foreach (var item in g.Roles) result.AppendLine($"{item.Id} {item.Name}");
result.AppendLine();
Console.WriteLine(result.ToString());
return Task.CompletedTask;
}
#endif
/// <summary>
/// Sends a message to a specified channel.
/// Parameters: say (channel) (message)
/// </summary>
private async Task RP_Say(string cmd, RuleConfig r, SocketMessage m)
{
string[] @in = SplitParams(cmd, 3);
if (@in.Length != 3)
{
await Log("Error: say: Incorrect number of parameters.");
return;
}
var target = await GetMessageTargetAsync(@in[1], m);
if (target == null)
{
await Log("Error: say: Unable to resolve given target.");
return;
}
// 
@in[2] = ProcessText(@in[2], m);
await target.SendMessageAsync(@in[2]);
}
/// <summary>
/// Reports the incoming message to a given channel.
/// Parameters: report (channel)
/// </summary>
private async Task RP_Report(string cmd, RuleConfig r, SocketMessage m)
{
string[] @in = SplitParams(cmd);
if (@in.Length != 2)
{
await Log("Error: report: Incorrect number of parameters.");
return;
}
var target = await GetMessageTargetAsync(@in[1], m);
if (target == null)
{
await Log("Error: report: Unable to resolve given target.");
return;
}
var responsefield = new StringBuilder();
responsefield.AppendLine("```");
foreach (var line in r.Responses)
responsefield.AppendLine(line.Replace("\r", "").Replace("\n", "\\n"));
responsefield.Append("```");
await target.SendMessageAsync("", embed: new EmbedBuilder()
{
Color = new Color(0xEDCE00), // configurable later?
Author = new EmbedAuthorBuilder()
{
Name = $"{m.Author.Username}#{m.Author.Discriminator} said:",
IconUrl = m.Author.GetAvatarUrl()
},
Description = m.Content,
Footer = new EmbedFooterBuilder()
{
Text = $"Rule '{r.DisplayName}'",
IconUrl = _client.CurrentUser.GetAvatarUrl()
},
Timestamp = m.Timestamp
}.AddField(new EmbedFieldBuilder()
{
Name = "Additional info",
Value = $"Channel: <#{m.Channel.Id}>\n" // NOTE: manually mentioning channel here
+ $"Username: {m.Author.Mention}\n"
+ $"Message ID: {m.Id}"
}).AddField(new EmbedFieldBuilder()
{
Name = "Executing response:",
Value = responsefield.ToString()
}));
}
/// <summary>
/// Deletes the incoming message.
/// No parameters.
/// </summary>
private async Task RP_Remove(string cmd, RuleConfig r, SocketMessage m)
{
// Parameters are not checked
await m.DeleteAsync();
}
/// <summary>
/// Executes an external program and sends standard output to the given channel.
/// Parameters: exec (channel) (command line)
/// </summary>
private async Task RP_Exec(string cmd, RuleConfig r, SocketMessage m)
{
var @in = SplitParams(cmd, 4);
if (@in.Length < 3)
{
await Log("exec: Incorrect number of parameters.");
}
string result;
var target = await GetMessageTargetAsync(@in[1], m);
if (target == null)
{
await Log("Error: exec: Unable to resolve given channel.");
return;
}
ProcessStartInfo ps = new ProcessStartInfo()
{
FileName = @in[2],
Arguments = (@in.Length > 3 ? @in[3] : ""),
UseShellExecute = false,
RedirectStandardOutput = true
};
using (Process p = Process.Start(ps))
{
p.WaitForExit(5000); // waiting at most 5 seconds
if (p.HasExited)
{
if (p.ExitCode != 0) await Log("exec: Process returned exit code " + p.ExitCode);
using (var stdout = p.StandardOutput)
{
result = await stdout.ReadToEndAsync();
}
}
else
{
await Log("exec: Process is taking too long to exit. Killing process.");
p.Kill();
return;
}
}
result = ProcessText(result.Trim(), m);
await target.SendMessageAsync(result);
}
/// <summary>
/// Bans the sender of the incoming message.
/// No parameters.
/// </summary>
// TODO add parameter for message auto-deleting
private async Task RP_Ban(string cmd, RuleConfig r, SocketMessage m)
{
SocketGuild g = ((SocketGuildUser)m.Author).Guild;
await g.AddBanAsync(m.Author);
}
/// <summary>
/// Grants or revokes a specified role to/from a given user.
/// Parameters: grantrole/revokerole (user ID or @_) (role ID)
/// </summary>
private async Task RP_GrantRevokeRole(string cmd, RuleConfig r, SocketMessage m)
{
string[] @in = SplitParams(cmd);
if (@in.Length != 3)
{
await Log($"Error: {@in[0]}: incorrect number of parameters.");
return;
}
if (!ulong.TryParse(@in[2], out var roleID))
{
await Log($"Error: {@in[0]}: Invalid role ID specified.");
return;
}
// Finding role
var gu = (SocketGuildUser)m.Author;
SocketRole rl = gu.Guild.GetRole(roleID);
if (rl == null)
{
await Log($"Error: {@in[0]}: Specified role not found.");
return;
}
// Finding user
SocketGuildUser target;
if (@in[1] == "@_")
{
target = gu;
}
else
{
if (!ulong.TryParse(@in[1], out var userID))
{
await Log($"Error: {@in[0]}: Invalid user ID specified.");
return;
}
target = gu.Guild.GetUser(userID);
if (target == null)
{
await Log($"Error: {@in[0]}: Given user ID does not exist in this server.");
return;
}
}
if (@in[0].ToLower() == "grantrole")
{
await target.AddRoleAsync(rl);
}
else
{
await target.RemoveRoleAsync(rl);
}
}
}
}

View file

@ -1,141 +0,0 @@
using Newtonsoft.Json.Linq;
using Noikoio.RegexBot.ConfigItem;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Noikoio.RegexBot.Feature.RegexResponder
{
/// <summary>
/// Represents configuration for a single rule.
/// </summary>
[System.Diagnostics.DebuggerDisplay("Rule: {DisplayName}")]
internal struct RuleConfig
{
private string _displayName;
private IEnumerable<Regex> _regex;
private IEnumerable<string> _responses;
private FilterList _filter;
private int? _minLength;
private int? _maxLength;
private bool _modBypass;
private bool _matchEmbeds;
public string DisplayName => _displayName;
public IEnumerable<Regex> Regex => _regex;
public IEnumerable<string> Responses => _responses;
public FilterList Filter => _filter;
public int? MinLength => _minLength;
public int? MaxLength => _maxLength;
public bool AllowModBypass => _modBypass;
public bool MatchEmbeds => _matchEmbeds;
/// <summary>
/// Takes the JObject for a single rule and retrieves all data for use as a struct.
/// </summary>
/// <param name="ruleconf">Rule configuration input</param>
/// <exception cref="RuleImportException>">
/// Thrown when encountering a missing or invalid value.
/// </exception>
public RuleConfig(JObject ruleconf)
{
// display name - validation should've been done outside this constructor already
_displayName = ruleconf["name"]?.Value<string>();
if (_displayName == null)
throw new RuleImportException("Display name not defined.");
// 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<bool>();
opts |= RegexOptions.IgnoreCase;
if (regexci.HasValue && regexci.Value == false)
opts &= ~RegexOptions.IgnoreCase;
// regex
const string RegexError = "No regular expression patterns are defined.";
var regexes = new List<Regex>();
var rxconf = ruleconf["regex"];
if (rxconf == null)
{
throw new RuleImportException(RegexError);
}
if (rxconf.Type == JTokenType.Array)
{
foreach (var input in rxconf.Values<string>())
{
try
{
Regex r = new Regex(input, opts);
regexes.Add(r);
}
catch (ArgumentException)
{
throw new RuleImportException("Failed to parse regular expression pattern: " + input);
}
}
}
else
{
string rxstr = rxconf.Value<string>();
try
{
var rxx = new Regex(rxstr, opts);
regexes.Add(rxx);
}
catch (ArgumentException)
{
throw new RuleImportException("Failed to parse regular expression pattern: " + rxstr);
}
}
if (regexes.Count == 0)
{
throw new RuleImportException(RegexError);
}
_regex = regexes.ToArray();
// min/max length
try
{
_minLength = ruleconf["min"]?.Value<int>();
_maxLength = ruleconf["max"]?.Value<int>();
}
catch (FormatException)
{
throw new RuleImportException("Minimum/maximum values must be an integer.");
}
// responses
const string ResponseError = "No responses have been defined for this rule.";
var responses = new List<string>();
var rsconf = ruleconf["response"];
if (rsconf == null)
{
throw new RuleImportException(ResponseError);
}
if (rsconf.Type == JTokenType.Array)
{
foreach (var input in rsconf.Values<string>()) responses.Add(input);
}
else
{
responses.Add(rsconf.Value<string>());
}
// TODO a bit of response validation here. at least check for blanks or something.
_responses = responses.ToArray();
// (white|black)list filtering
_filter = new FilterList(ruleconf);
// moderator bypass toggle - true by default, must be explicitly set to false
bool? modoverride = ruleconf["AllowModBypass"]?.Value<bool>();
_modBypass = modoverride.HasValue ? modoverride.Value : true;
// embed matching mode
bool? embedmode = ruleconf["MatchEmbeds"]?.Value<bool>();
_matchEmbeds = (embedmode.HasValue && embedmode == true);
}
}
}