From 0f3fd350fa24f0723de146e0ff851f33714a3536 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sat, 26 Aug 2017 10:23:02 -0700 Subject: [PATCH] Removed RegexResponder --- Feature/RegexResponder/EventProcessor.cs | 309 ----------------------- Feature/RegexResponder/Responses.cs | 265 ------------------- Feature/RegexResponder/RuleConfig.cs | 141 ----------- 3 files changed, 715 deletions(-) delete mode 100644 Feature/RegexResponder/EventProcessor.cs delete mode 100644 Feature/RegexResponder/Responses.cs delete mode 100644 Feature/RegexResponder/RuleConfig.cs diff --git a/Feature/RegexResponder/EventProcessor.cs b/Feature/RegexResponder/EventProcessor.cs deleted file mode 100644 index e78f50e..0000000 --- a/Feature/RegexResponder/EventProcessor.cs +++ /dev/null @@ -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 -{ - /// - /// Implements per-message regex matching and executes customizable responses. - /// Namesake of this project. - /// - 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( - new Dictionary() { -#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 arg1, SocketMessage arg2, ISocketMessageChannel arg3) - => await ReceiveMessage(arg2); - #endregion - - /// - /// Receives incoming messages and creates tasks to handle them if necessary. - /// - 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; - 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)); - } - - /// - /// 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. - /// - 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 ProcessConfiguration(JToken configSection) - { - List rules = new List(); - foreach (JObject ruleconf in configSection) - { - // Try and get at least the name before passing it to RuleItem - string name = ruleconf["name"]?.Value(); - 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(); - } - - // ------------------------------------- - - /// - /// Turns an embed into a single string for regex matching purposes - /// - 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("@\\_", "@_"); - } - - /// - /// Receives a string (beginning with @ or #) and returns an object - /// suitable for sending out messages - /// - private async Task 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; - } - } -} diff --git a/Feature/RegexResponder/Responses.cs b/Feature/RegexResponder/Responses.cs deleted file mode 100644 index ecd4b3e..0000000 --- a/Feature/RegexResponder/Responses.cs +++ /dev/null @@ -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 _commands; - -#if DEBUG - /// - /// Throws an exception. Meant to be a quick error handling test. - /// No parameters. - /// - private Task RP_Crash(string cmd, RuleConfig r, SocketMessage m) - { - throw new Exception("Requested in response."); - } - - /// - /// 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. - /// - 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 - /// - /// Sends a message to a specified channel. - /// Parameters: say (channel) (message) - /// - 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; - } - - // CHANGE THE SAY - @in[2] = ProcessText(@in[2], m); - await target.SendMessageAsync(@in[2]); - } - - /// - /// Reports the incoming message to a given channel. - /// Parameters: report (channel) - /// - 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() - })); - } - - /// - /// Deletes the incoming message. - /// No parameters. - /// - private async Task RP_Remove(string cmd, RuleConfig r, SocketMessage m) - { - // Parameters are not checked - await m.DeleteAsync(); - } - - /// - /// Executes an external program and sends standard output to the given channel. - /// Parameters: exec (channel) (command line) - /// - 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); - } - - /// - /// Bans the sender of the incoming message. - /// No parameters. - /// - // 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); - } - - /// - /// Grants or revokes a specified role to/from a given user. - /// Parameters: grantrole/revokerole (user ID or @_) (role ID) - /// - 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); - } - } - - - } -} diff --git a/Feature/RegexResponder/RuleConfig.cs b/Feature/RegexResponder/RuleConfig.cs deleted file mode 100644 index 8668c9e..0000000 --- a/Feature/RegexResponder/RuleConfig.cs +++ /dev/null @@ -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 -{ - /// - /// Represents configuration for a single rule. - /// - [System.Diagnostics.DebuggerDisplay("Rule: {DisplayName}")] - internal struct RuleConfig - { - private string _displayName; - private IEnumerable _regex; - private IEnumerable _responses; - private FilterList _filter; - private int? _minLength; - private int? _maxLength; - private bool _modBypass; - private bool _matchEmbeds; - - public string DisplayName => _displayName; - public IEnumerable Regex => _regex; - public IEnumerable Responses => _responses; - public FilterList Filter => _filter; - public int? MinLength => _minLength; - public int? MaxLength => _maxLength; - public bool AllowModBypass => _modBypass; - public bool MatchEmbeds => _matchEmbeds; - - /// - /// Takes the JObject for a single rule and retrieves all data for use as a struct. - /// - /// Rule configuration input - /// - /// Thrown when encountering a missing or invalid value. - /// - public RuleConfig(JObject ruleconf) - { - // display name - validation should've been done outside this constructor already - _displayName = ruleconf["name"]?.Value(); - 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(); - 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(); - var rxconf = ruleconf["regex"]; - if (rxconf == null) - { - throw new RuleImportException(RegexError); - } - 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); - } - } - } - else - { - string rxstr = rxconf.Value(); - 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(); - _maxLength = ruleconf["max"]?.Value(); - } - 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(); - var rsconf = ruleconf["response"]; - if (rsconf == null) - { - throw new RuleImportException(ResponseError); - } - if (rsconf.Type == JTokenType.Array) - { - foreach (var input in rsconf.Values()) responses.Add(input); - } - else - { - responses.Add(rsconf.Value()); - } - // 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(); - _modBypass = modoverride.HasValue ? modoverride.Value : true; - - // embed matching mode - bool? embedmode = ruleconf["MatchEmbeds"]?.Value(); - _matchEmbeds = (embedmode.HasValue && embedmode == true); - } - } -}