Added BotFeature base class, implemented in RegexResponder

BotFeature is a new base class that all new individual bot features
will derive from. At least one new feature is planned for this bot,
and in time it may be opened up so external assemblies can be loaded.

Full list of changes:
-Added BotFeature and ConfigSectionAttribute classes
-Renamed ConfigLoader to Configuration
-Removed RegexResponder specific configuration data
-Added per-feature configuration data storage
-LoadInitialConfig() no longer loads all configuration
-ReloadServerConfig() now loads remaining configuration, and allows
 for actual configuration reloading. Live configuration reloading
 is not an exposed feature yet.
-Can now delegate feature-specific configuration loading to feature
 classes that make use of ConfigSectionAttribute

-RegexResponder fully implements BotFeature
-Rule configuration loading moved to RegexResponder
-Logging output has changed slightly in regards to rule triggering
 and execution

-Changed configuration load behavior on startup
-Version pushed up to 1.0.0
This commit is contained in:
Noikoio 2017-07-26 15:36:59 -07:00
parent 0b70dca915
commit 27a25d90fc
10 changed files with 264 additions and 113 deletions

94
BotFeature.cs Normal file
View file

@ -0,0 +1,94 @@
using Discord.WebSocket;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace Noikoio.RegexBot
{
/// <summary>
/// Base class for bot features
/// </summary>
/// <remarks>
/// This may have use in some sort of external plugin system later.
/// </remarks>
abstract class BotFeature
{
private readonly DiscordSocketClient _client;
private readonly AsyncLogger _logger;
public abstract string Name { get; }
protected BotFeature(DiscordSocketClient client)
{
_client = client;
_logger = Logger.GetLogger(this.Name);
}
/// <summary>
/// Processes feature-specific configuration.
/// </summary>
/// <remarks>
/// Feature code <i>should not</i> hold on to this data, but instead use <see cref="GetConfig{T}"/> to retrieve
/// them. This is in the event that configuration is reverted to an earlier state and allows for the
/// bot and all features to revert to previously used configuration values with no effort on the part
/// of individual features.
/// </remarks>
/// <returns>
/// Processed configuration data prepared for later use.
/// </returns>
/// <exception cref="ConfigItem.RuleImportException">
/// This method should throw RuleImportException in the event of any error.
/// The exception message will be properly logged.
/// </exception>
public abstract Task<object> ProcessConfiguration(JToken configSection);
/// <summary>
/// Gets this feature's relevant configuration data associated with the given Discord guild.
/// </summary>
/// <returns>
/// The stored configuration data, or null if none exists.
/// </returns>
protected object GetConfig(ulong guildId)
{
var sc = RegexBot.Config.Servers.FirstOrDefault(g => g.Id == guildId);
if (sc == null)
{
throw new ArgumentException("There is no known configuration associated with the given Guild ID.");
}
if (sc.FeatureConfigs.TryGetValue(this, out var item)) return item;
else return null;
}
protected async Task Log(string text)
{
await _logger(text);
}
public sealed override bool Equals(object obj) => base.Equals(obj);
public sealed override int GetHashCode() => base.GetHashCode();
public sealed override string ToString() => base.ToString();
}
/// <summary>
/// Indicates which section under an individual Discord guild configuration should be passed to the
/// feature's <see cref="BotFeature.ProcessConfiguration(JObject)"/> method during configuration load.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class ConfigSectionAttribute : Attribute
{
private readonly string _sectionName;
public string SectionName => _sectionName;
public ConfigSectionAttribute(string sectionName)
{
if (string.IsNullOrWhiteSpace(sectionName))
{
throw new ArgumentNullException("Configuration section name cannot be blank.");
}
_sectionName = sectionName;
}
}
}

View file

@ -71,7 +71,7 @@ namespace Noikoio.RegexBot.ConfigItem
if (_id.HasValue) return; if (_id.HasValue) return;
_id = id; _id = id;
var log = Logger.GetLogger(ConfigLoader.LogPrefix); var log = Logger.GetLogger(Configuration.LogPrefix);
var thisstr = this.ToString(); var thisstr = this.ToString();
log(String.Format( log(String.Format(
"Suggestion: \"{0}\" may be written in configuration as \"{1}\"", "Suggestion: \"{0}\" may be written in configuration as \"{1}\"",

View file

@ -1,5 +1,5 @@
using Noikoio.RegexBot.Feature.RegexResponder; using System;
using System.Collections.Generic; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
namespace Noikoio.RegexBot.ConfigItem namespace Noikoio.RegexBot.ConfigItem
@ -7,27 +7,27 @@ namespace Noikoio.RegexBot.ConfigItem
/// <summary> /// <summary>
/// Represents known information about a Discord guild (server) and other associated data /// Represents known information about a Discord guild (server) and other associated data
/// </summary> /// </summary>
class Server class ServerConfig
{ {
private readonly string _name; private readonly string _name;
private ulong? _id; private ulong? _id;
private IEnumerable<RuleConfig> _rules;
private EntityList _moderators; private EntityList _moderators;
private ReadOnlyDictionary<BotFeature, object> _featureData;
public string Name => _name; public string Name => _name;
public ulong? Id { public ulong? Id {
get => _id; set { if (!_id.HasValue) _id = value; } get => _id; set { if (!_id.HasValue) _id = value; }
} }
public IEnumerable<RuleConfig> MatchResponseRules => _rules;
public EntityList Moderators => _moderators; public EntityList Moderators => _moderators;
public ReadOnlyDictionary<BotFeature, object> FeatureConfigs => _featureData;
public Server(string name, ulong? id, IEnumerable<RuleConfig> rules, EntityList moderators) public ServerConfig(string name, ulong? id, EntityList moderators, ReadOnlyDictionary<BotFeature, object> featureconf)
{ {
_name = name; _name = name;
_id = id; _id = id;
_rules = rules;
_moderators = moderators; _moderators = moderators;
Debug.Assert(_name != null && _rules != null && _moderators != null); _featureData = featureconf;
Debug.Assert(_name != null && _moderators != null);
} }
} }
} }

View file

@ -1,9 +1,9 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Noikoio.RegexBot.ConfigItem; using Noikoio.RegexBot.ConfigItem;
using Noikoio.RegexBot.Feature.RegexResponder;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -14,22 +14,25 @@ namespace Noikoio.RegexBot
/// <summary> /// <summary>
/// Configuration loader /// Configuration loader
/// </summary> /// </summary>
class ConfigLoader class Configuration
{ {
public const string LogPrefix = "Config"; public const string LogPrefix = "Config";
private readonly RegexBot _bot;
private readonly string _configPath; private readonly string _configPath;
private Server[] _servers; private ServerConfig[] _servers;
// The following values do not change on reload:
private string _botToken; private string _botToken;
private string _currentGame; private string _currentGame;
public string BotUserToken => _botToken; public string BotUserToken => _botToken;
public string CurrentGame => _currentGame; public string CurrentGame => _currentGame;
public Server[] Servers => _servers; public ServerConfig[] Servers => _servers;
public ConfigLoader() public Configuration(RegexBot bot)
{ {
_bot = bot;
var dsc = Path.DirectorySeparatorChar; var dsc = Path.DirectorySeparatorChar;
_configPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) _configPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)
+ dsc + "settings.json"; + dsc + "settings.json";
@ -64,7 +67,7 @@ namespace Noikoio.RegexBot
} }
/// <summary> /// <summary>
/// Called only on bot startup. Returns false on failure. /// Loads essential, unchanging values needed for bot startup. Returns false on failure.
/// </summary> /// </summary>
public bool LoadInitialConfig() public bool LoadInitialConfig()
{ {
@ -81,7 +84,7 @@ namespace Noikoio.RegexBot
} }
_currentGame = conf["playing"]?.Value<string>(); _currentGame = conf["playing"]?.Value<string>();
return ProcessServerConfig(conf).GetAwaiter().GetResult(); return true;
} }
/// <summary> /// <summary>
@ -90,15 +93,10 @@ namespace Noikoio.RegexBot
/// <returns>False on failure. Specific reasons will have been sent to log.</returns> /// <returns>False on failure. Specific reasons will have been sent to log.</returns>
public async Task<bool> ReloadServerConfig() public async Task<bool> ReloadServerConfig()
{ {
await Logger.GetLogger(LogPrefix)("Configuration reload currently not supported."); var config = await LoadFile();
return false; if (config == null) return false;
// TODO actually implement this
var lt = LoadFile();
lt.Wait();
JObject conf = lt.Result;
if (conf == null) return false;
return await ProcessServerConfig(conf); return await ProcessServerConfig(config);
} }
/// <summary> /// <summary>
@ -114,7 +112,7 @@ namespace Noikoio.RegexBot
return false; return false;
} }
List<Server> newservers = new List<Server>(); List<ServerConfig> newservers = new List<ServerConfig>();
await Log("Reading server configurations..."); await Log("Reading server configurations...");
foreach (JObject sconf in conf["servers"].Children<JObject>()) foreach (JObject sconf in conf["servers"].Children<JObject>())
{ {
@ -146,37 +144,43 @@ namespace Noikoio.RegexBot
EntityList mods = new EntityList(sconf["moderators"]); EntityList mods = new EntityList(sconf["moderators"]);
if (sconf["moderators"] != null) await SLog("Moderator " + mods.ToString()); if (sconf["moderators"] != null) await SLog("Moderator " + mods.ToString());
// Read rules // Load feature configurations
// Also, parsed rules require a server reference. Creating it here. Dictionary<BotFeature, object> customConfs = new Dictionary<BotFeature, object>();
List<RuleConfig> rules = new List<RuleConfig>(); foreach (var item in _bot.Features)
Server newserver = new Server(sname, sid, rules, mods);
foreach (JObject ruleconf in sconf["rules"])
{ {
// Try and get at least the name before passing it to RuleItem var attr = item.GetType().GetTypeInfo()
string name = ruleconf["name"]?.Value<string>(); .GetMethod("ProcessConfiguration").GetCustomAttribute<ConfigSectionAttribute>();
if (name == null) if (attr == null)
{ {
await SLog("Display name not defined within a rule section."); await SLog("No additional configuration for " + item.Name);
return false; continue;
}
var section = sconf[attr.SectionName];
if (section == null)
{
await SLog("Additional configuration not defined for " + item.Name);
continue;
} }
await SLog($"Adding rule \"{name}\"");
RuleConfig rule; await SLog("Loading additional configuration for " + item.Name);
object result;
try try
{ {
rule = new RuleConfig(newserver, ruleconf); result = await item.ProcessConfiguration(section);
} catch (RuleImportException ex) }
catch (RuleImportException ex)
{ {
await SLog("-> Error: " + ex.Message); await SLog($"{item.Name} failed to load configuration: " + ex.Message);
return false; return false;
} }
rules.Add(rule);
customConfs.Add(item, result);
} }
// Switch to using new data // Switch to using new data
List<Tuple<Regex, string[]>> rulesfinal = new List<Tuple<Regex, string[]>>(); List<Tuple<Regex, string[]>> rulesfinal = new List<Tuple<Regex, string[]>>();
newservers.Add(newserver); newservers.Add(new ServerConfig(sname, sid, mods, new ReadOnlyDictionary<BotFeature, object>(customConfs)));
} }
_servers = newservers.ToArray(); _servers = newservers.ToArray();

View file

@ -7,6 +7,7 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace Noikoio.RegexBot.Feature.RegexResponder namespace Noikoio.RegexBot.Feature.RegexResponder
{ {
@ -14,15 +15,15 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// Implements per-message regex matching and executes customizable responses. /// Implements per-message regex matching and executes customizable responses.
/// Namesake of this project. /// Namesake of this project.
/// </summary> /// </summary>
partial class EventProcessor partial class EventProcessor : BotFeature
{ {
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly ConfigLoader _conf;
public EventProcessor(DiscordSocketClient client, ConfigLoader conf) public override string Name => "RegexResponder";
public EventProcessor(DiscordSocketClient client) : base(client)
{ {
_client = client; _client = client;
_conf = conf;
_client.MessageReceived += OnMessageReceived; _client.MessageReceived += OnMessageReceived;
_client.MessageUpdated += OnMessageUpdated; _client.MessageUpdated += OnMessageUpdated;
@ -58,12 +59,16 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// </summary> /// </summary>
private async Task ReceiveMessage(SocketMessage arg) private async Task ReceiveMessage(SocketMessage arg)
{ {
if (arg.Author == _client.CurrentUser) return; // 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 // Looking up server information and extracting settings
SocketGuild g = ((SocketGuildUser)arg.Author).Guild; SocketGuild g = ch.Guild;
Server sd = null; ServerConfig sd = null;
foreach (var item in _conf.Servers) foreach (var item in RegexBot.Config.Servers)
{ {
if (item.Id.HasValue) if (item.Id.HasValue)
{ {
@ -81,7 +86,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
{ {
item.Id = g.Id; item.Id = g.Id;
sd = item; sd = item;
await Logger.GetLogger(ConfigLoader.LogPrefix) await Logger.GetLogger(Configuration.LogPrefix)
($"Suggestion: Server \"{item.Name}\" can be defined as \"{item.Id}::{item.Name}\""); ($"Suggestion: Server \"{item.Name}\" can be defined as \"{item.Id}::{item.Name}\"");
break; break;
} }
@ -89,9 +94,11 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
} }
if (sd == null) return; // No server configuration found 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 // Further processing is sent to the thread pool
foreach (var rule in sd.MatchResponseRules) foreach (var rule in rules)
await Task.Run(async () => await ProcessMessage(sd, rule, arg)); await Task.Run(async () => await ProcessMessage(sd, rule, arg));
} }
@ -99,7 +106,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// Uses information from a single rule and checks if the incoming message is a match. /// 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. /// If it matches, the rule's responses are executed. To be run in the thread pool.
/// </summary> /// </summary>
private async Task ProcessMessage(Server srv, RuleConfig rule, SocketMessage msg) private async Task ProcessMessage(ServerConfig srv, RuleConfig rule, SocketMessage msg)
{ {
string msgcontent; string msgcontent;
@ -134,8 +141,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
if (!success) return; if (!success) return;
// Prepare to execute responses // Prepare to execute responses
var log = Logger.GetLogger(rule.DisplayName); await Log($"\"{rule.DisplayName}\" triggered in {srv.Name}/#{msg.Channel} by {msg.Author.ToString()}");
await log($"Triggered in {srv.Name}/#{msg.Channel} by {msg.Author.ToString()}");
foreach (string rcmd in rule.Responses) foreach (string rcmd in rule.Responses)
{ {
@ -145,19 +151,52 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
ResponseProcessor response; ResponseProcessor response;
if (!_commands.TryGetValue(cmd, out response)) if (!_commands.TryGetValue(cmd, out response))
{ {
await log($"Unknown command \"{cmd}\""); await Log($"Unknown command defined in response: \"{cmd}\"");
continue; continue;
} }
await response.Invoke(log, rcmd, rule, msg); await response.Invoke(rcmd, rule, msg);
} }
catch (Exception ex) catch (Exception ex)
{ {
await log($"Encountered an error while processing \"{cmd}\""); await Log($"Encountered an error while processing \"{cmd}\". Details follow:");
await log(ex.ToString()); 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> /// <summary>
/// Turns an embed into a single string for regex matching purposes /// Turns an embed into a single string for regex matching purposes
/// </summary> /// </summary>

View file

@ -11,7 +11,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
// Contains code for handling each response in a rule. // Contains code for handling each response in a rule.
partial class EventProcessor partial class EventProcessor
{ {
private delegate Task ResponseProcessor(AsyncLogger l, string cmd, RuleConfig r, SocketMessage m); private delegate Task ResponseProcessor(string cmd, RuleConfig r, SocketMessage m);
private readonly ReadOnlyDictionary<string, ResponseProcessor> _commands; private readonly ReadOnlyDictionary<string, ResponseProcessor> _commands;
#if DEBUG #if DEBUG
@ -19,9 +19,8 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// Throws an exception. Meant to be a quick error handling test. /// Throws an exception. Meant to be a quick error handling test.
/// No parameters. /// No parameters.
/// </summary> /// </summary>
private async Task RP_Crash(AsyncLogger l, string cmd, RuleConfig r, SocketMessage m) private Task RP_Crash(string cmd, RuleConfig r, SocketMessage m)
{ {
await l("Will throw an exception.");
throw new Exception("Requested in response."); throw new Exception("Requested in response.");
} }
@ -30,7 +29,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// The guild info displayed is the one in which the command is invoked. /// The guild info displayed is the one in which the command is invoked.
/// No parameters. /// No parameters.
/// </summary> /// </summary>
private Task RP_DumpID(AsyncLogger l, string cmd, RuleConfig r, SocketMessage m) private Task RP_DumpID(string cmd, RuleConfig r, SocketMessage m)
{ {
var g = ((SocketGuildUser)m.Author).Guild; var g = ((SocketGuildUser)m.Author).Guild;
var result = new StringBuilder(); var result = new StringBuilder();
@ -55,19 +54,19 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// Sends a message to a specified channel. /// Sends a message to a specified channel.
/// Parameters: say (channel) (message) /// Parameters: say (channel) (message)
/// </summary> /// </summary>
private async Task RP_Say(AsyncLogger l, string cmd, RuleConfig r, SocketMessage m) private async Task RP_Say(string cmd, RuleConfig r, SocketMessage m)
{ {
string[] @in = SplitParams(cmd, 3); string[] @in = SplitParams(cmd, 3);
if (@in.Length != 3) if (@in.Length != 3)
{ {
await l("Error: say: Incorrect number of parameters."); await Log("Error: say: Incorrect number of parameters.");
return; return;
} }
var target = await GetMessageTargetAsync(@in[1], m); var target = await GetMessageTargetAsync(@in[1], m);
if (target == null) if (target == null)
{ {
await l("Error: say: Unable to resolve given target."); await Log("Error: say: Unable to resolve given target.");
return; return;
} }
@ -80,19 +79,19 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// Reports the incoming message to a given channel. /// Reports the incoming message to a given channel.
/// Parameters: report (channel) /// Parameters: report (channel)
/// </summary> /// </summary>
private async Task RP_Report(AsyncLogger l, string cmd, RuleConfig r, SocketMessage m) private async Task RP_Report(string cmd, RuleConfig r, SocketMessage m)
{ {
string[] @in = SplitParams(cmd); string[] @in = SplitParams(cmd);
if (@in.Length != 2) if (@in.Length != 2)
{ {
await l("Error: report: Incorrect number of parameters."); await Log("Error: report: Incorrect number of parameters.");
return; return;
} }
var target = await GetMessageTargetAsync(@in[1], m); var target = await GetMessageTargetAsync(@in[1], m);
if (target == null) if (target == null)
{ {
await l("Error: report: Unable to resolve given target."); await Log("Error: report: Unable to resolve given target.");
return; return;
} }
@ -136,7 +135,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// Deletes the incoming message. /// Deletes the incoming message.
/// No parameters. /// No parameters.
/// </summary> /// </summary>
private async Task RP_Remove(AsyncLogger l, string cmd, RuleConfig r, SocketMessage m) private async Task RP_Remove(string cmd, RuleConfig r, SocketMessage m)
{ {
// Parameters are not checked // Parameters are not checked
await m.DeleteAsync(); await m.DeleteAsync();
@ -146,19 +145,19 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// Executes an external program and sends standard output to the given channel. /// Executes an external program and sends standard output to the given channel.
/// Parameters: exec (channel) (command line) /// Parameters: exec (channel) (command line)
/// </summary> /// </summary>
private async Task RP_Exec(AsyncLogger l, string cmd, RuleConfig r, SocketMessage m) private async Task RP_Exec(string cmd, RuleConfig r, SocketMessage m)
{ {
var @in = SplitParams(cmd, 4); var @in = SplitParams(cmd, 4);
if (@in.Length < 3) if (@in.Length < 3)
{ {
await l("exec: Incorrect number of parameters."); await Log("exec: Incorrect number of parameters.");
} }
string result; string result;
var target = await GetMessageTargetAsync(@in[1], m); var target = await GetMessageTargetAsync(@in[1], m);
if (target == null) if (target == null)
{ {
await l("Error: exec: Unable to resolve given channel."); await Log("Error: exec: Unable to resolve given channel.");
return; return;
} }
@ -174,7 +173,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
p.WaitForExit(5000); // waiting at most 5 seconds p.WaitForExit(5000); // waiting at most 5 seconds
if (p.HasExited) if (p.HasExited)
{ {
if (p.ExitCode != 0) await l("exec: Process returned exit code " + p.ExitCode); if (p.ExitCode != 0) await Log("exec: Process returned exit code " + p.ExitCode);
using (var stdout = p.StandardOutput) using (var stdout = p.StandardOutput)
{ {
result = await stdout.ReadToEndAsync(); result = await stdout.ReadToEndAsync();
@ -182,7 +181,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
} }
else else
{ {
await l("exec: Process is taking too long to exit. Killing process."); await Log("exec: Process is taking too long to exit. Killing process.");
p.Kill(); p.Kill();
return; return;
} }
@ -197,7 +196,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// No parameters. /// No parameters.
/// </summary> /// </summary>
// TODO add parameter for message auto-deleting // TODO add parameter for message auto-deleting
private async Task RP_Ban(AsyncLogger l, string cmd, RuleConfig r, SocketMessage m) private async Task RP_Ban(string cmd, RuleConfig r, SocketMessage m)
{ {
SocketGuild g = ((SocketGuildUser)m.Author).Guild; SocketGuild g = ((SocketGuildUser)m.Author).Guild;
await g.AddBanAsync(m.Author); await g.AddBanAsync(m.Author);
@ -207,17 +206,17 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// Grants or revokes a specified role to/from a given user. /// Grants or revokes a specified role to/from a given user.
/// Parameters: grantrole/revokerole (user ID or @_) (role ID) /// Parameters: grantrole/revokerole (user ID or @_) (role ID)
/// </summary> /// </summary>
private async Task RP_GrantRevokeRole(AsyncLogger l, string cmd, RuleConfig r, SocketMessage m) private async Task RP_GrantRevokeRole(string cmd, RuleConfig r, SocketMessage m)
{ {
string[] @in = SplitParams(cmd); string[] @in = SplitParams(cmd);
if (@in.Length != 3) if (@in.Length != 3)
{ {
await l($"Error: {@in[0]}: incorrect number of parameters."); await Log($"Error: {@in[0]}: incorrect number of parameters.");
return; return;
} }
if (!ulong.TryParse(@in[2], out var roleID)) if (!ulong.TryParse(@in[2], out var roleID))
{ {
await l($"Error: {@in[0]}: Invalid role ID specified."); await Log($"Error: {@in[0]}: Invalid role ID specified.");
return; return;
} }
@ -226,7 +225,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
SocketRole rl = gu.Guild.GetRole(roleID); SocketRole rl = gu.Guild.GetRole(roleID);
if (rl == null) if (rl == null)
{ {
await l($"Error: {@in[0]}: Specified role not found."); await Log($"Error: {@in[0]}: Specified role not found.");
return; return;
} }
@ -240,13 +239,13 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
{ {
if (!ulong.TryParse(@in[1], out var userID)) if (!ulong.TryParse(@in[1], out var userID))
{ {
await l($"Error: {@in[0]}: Invalid user ID specified."); await Log($"Error: {@in[0]}: Invalid user ID specified.");
return; return;
} }
target = gu.Guild.GetUser(userID); target = gu.Guild.GetUser(userID);
if (target == null) if (target == null)
{ {
await l($"Error: {@in[0]}: Given user ID does not exist in this server."); await Log($"Error: {@in[0]}: Given user ID does not exist in this server.");
return; return;
} }
} }

View file

@ -13,7 +13,6 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
internal struct RuleConfig internal struct RuleConfig
{ {
private string _displayName; private string _displayName;
private Server _server;
private IEnumerable<Regex> _regex; private IEnumerable<Regex> _regex;
private IEnumerable<string> _responses; private IEnumerable<string> _responses;
private FilterType _filtermode; private FilterType _filtermode;
@ -25,7 +24,6 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
private bool _matchEmbeds; private bool _matchEmbeds;
public string DisplayName => _displayName; public string DisplayName => _displayName;
public Server Server => _server;
public IEnumerable<Regex> Regex => _regex; public IEnumerable<Regex> Regex => _regex;
public IEnumerable<string> Responses => _responses; public IEnumerable<string> Responses => _responses;
public FilterType FilterMode => _filtermode; public FilterType FilterMode => _filtermode;
@ -43,10 +41,8 @@ namespace Noikoio.RegexBot.Feature.RegexResponder
/// <exception cref="RuleImportException>"> /// <exception cref="RuleImportException>">
/// Thrown when encountering a missing or invalid value. /// Thrown when encountering a missing or invalid value.
/// </exception> /// </exception>
public RuleConfig(Server serverref, JObject ruleconf) public RuleConfig(JObject ruleconf)
{ {
_server = serverref;
// display name - validation should've been done outside this constructor already // display name - validation should've been done outside this constructor already
_displayName = ruleconf["name"]?.Value<string>(); _displayName = ruleconf["name"]?.Value<string>();
if (_displayName == null) if (_displayName == null)

View file

@ -10,18 +10,7 @@ namespace Noikoio.RegexBot
{ {
static void Main(string[] args) static void Main(string[] args)
{ {
// Attempt to load basic configuration before setting up the client RegexBot rb = new RegexBot();
var config = new ConfigLoader();
if (!config.LoadInitialConfig())
{
#if DEBUG
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
#endif
Environment.Exit(1);
}
RegexBot rb = new RegexBot(config);
Console.CancelKeyPress += rb.Console_CancelKeyPress; Console.CancelKeyPress += rb.Console_CancelKeyPress;
//AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; //AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;

View file

@ -1,6 +1,7 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Noikoio.RegexBot namespace Noikoio.RegexBot
@ -10,34 +11,63 @@ namespace Noikoio.RegexBot
/// </summary> /// </summary>
class RegexBot class RegexBot
{ {
private readonly ConfigLoader _config; private static Configuration _config;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private BotFeature[] _features;
// Constructor loads all subsystems. Subsystem constructors hook up their event delegates. internal static Configuration Config => _config;
internal RegexBot(ConfigLoader conf) internal IEnumerable<BotFeature> Features => _features;
internal RegexBot()
{ {
// Load configuration
_config = new Configuration(this);
if (!_config.LoadInitialConfig())
{
#if DEBUG
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
#endif
Environment.Exit(1);
}
_client = new DiscordSocketClient(new DiscordSocketConfig() _client = new DiscordSocketClient(new DiscordSocketConfig()
{ {
LogLevel = LogSeverity.Info, LogLevel = LogSeverity.Info,
DefaultRetryMode = RetryMode.AlwaysRetry, DefaultRetryMode = RetryMode.AlwaysRetry,
MessageCacheSize = 50 MessageCacheSize = 50
}); });
_config = conf;
// Hook up handlers for basic functions // Hook up handlers for basic functions
_client.Connected += _client_Connected; _client.Connected += _client_Connected;
// Initialize features // Initialize features
new Feature.RegexResponder.EventProcessor(_client, _config); _features = new BotFeature[]
{
new Feature.RegexResponder.EventProcessor(_client)
};
var dlog = Logger.GetLogger("Discord.Net");
_client.Log += async (arg) =>
await dlog(
String.Format("{0}: {1}{2}", arg.Source, ((int)arg.Severity < 3 ? arg.Severity + ": " : ""),
arg.Message));
// With features initialized, finish loading configuration
if (!_config.ReloadServerConfig().GetAwaiter().GetResult())
{
Console.WriteLine("Failed to load server configuration.");
#if DEBUG
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
#endif
Environment.Exit(1);
}
} }
internal async Task Start() internal async Task Start()
{ {
var dlog = Logger.GetLogger("Discord");
_client.Log += async (arg) => await _client.LoginAsync(TokenType.Bot, Config.BotUserToken);
await dlog(String.Format("{0}: {1}{2}",
arg.Source, ((int)arg.Severity < 3 ? arg.Severity + ": " : ""), arg.Message));
await _client.LoginAsync(TokenType.Bot, _config.BotUserToken);
await _client.StartAsync(); await _client.StartAsync();
await Task.Delay(-1); await Task.Delay(-1);
@ -45,7 +75,7 @@ namespace Noikoio.RegexBot
private async Task _client_Connected() private async Task _client_Connected()
{ {
await _client.SetGameAsync(_config.CurrentGame); await _client.SetGameAsync(Config.CurrentGame);
// TODO add support for making use of server invites somewhere around here // TODO add support for making use of server invites somewhere around here
} }

View file

@ -4,7 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework> <TargetFramework>netcoreapp1.1</TargetFramework>
<RootNamespace>Noikoio.RegexBot</RootNamespace> <RootNamespace>Noikoio.RegexBot</RootNamespace>
<AssemblyVersion>0.15.0.0</AssemblyVersion> <AssemblyVersion>1.0.0.0</AssemblyVersion>
<Description>Highly configurable Discord moderation bot</Description> <Description>Highly configurable Discord moderation bot</Description>
<Authors>Noikoio</Authors> <Authors>Noikoio</Authors>
<Company /> <Company />