From 27a25d90fc067c05578078cca101d5fc343c73d9 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Wed, 26 Jul 2017 15:36:59 -0700 Subject: [PATCH] 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 --- BotFeature.cs | 94 +++++++++++++++++++++++ ConfigItem/EntityName.cs | 2 +- ConfigItem/{Server.cs => ServerConfig.cs} | 16 ++-- ConfigLoader.cs => Configuration.cs | 74 +++++++++--------- Feature/RegexResponder/EventProcessor.cs | 73 ++++++++++++++---- Feature/RegexResponder/Responses.cs | 45 ++++++----- Feature/RegexResponder/RuleConfig.cs | 6 +- Program.cs | 13 +--- RegexBot.cs | 52 ++++++++++--- RegexBot.csproj | 2 +- 10 files changed, 264 insertions(+), 113 deletions(-) create mode 100644 BotFeature.cs rename ConfigItem/{Server.cs => ServerConfig.cs} (57%) rename ConfigLoader.cs => Configuration.cs (71%) diff --git a/BotFeature.cs b/BotFeature.cs new file mode 100644 index 0000000..f410afa --- /dev/null +++ b/BotFeature.cs @@ -0,0 +1,94 @@ +using Discord.WebSocket; +using Newtonsoft.Json.Linq; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Noikoio.RegexBot +{ + /// + /// Base class for bot features + /// + /// + /// This may have use in some sort of external plugin system later. + /// + 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); + } + + /// + /// Processes feature-specific configuration. + /// + /// + /// Feature code should not hold on to this data, but instead use 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. + /// + /// + /// Processed configuration data prepared for later use. + /// + /// + /// This method should throw RuleImportException in the event of any error. + /// The exception message will be properly logged. + /// + public abstract Task ProcessConfiguration(JToken configSection); + + /// + /// Gets this feature's relevant configuration data associated with the given Discord guild. + /// + /// + /// The stored configuration data, or null if none exists. + /// + 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(); + } + + /// + /// Indicates which section under an individual Discord guild configuration should be passed to the + /// feature's method during configuration load. + /// + [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; + } + } +} diff --git a/ConfigItem/EntityName.cs b/ConfigItem/EntityName.cs index 9b796b1..3ce6528 100644 --- a/ConfigItem/EntityName.cs +++ b/ConfigItem/EntityName.cs @@ -71,7 +71,7 @@ namespace Noikoio.RegexBot.ConfigItem if (_id.HasValue) return; _id = id; - var log = Logger.GetLogger(ConfigLoader.LogPrefix); + var log = Logger.GetLogger(Configuration.LogPrefix); var thisstr = this.ToString(); log(String.Format( "Suggestion: \"{0}\" may be written in configuration as \"{1}\"", diff --git a/ConfigItem/Server.cs b/ConfigItem/ServerConfig.cs similarity index 57% rename from ConfigItem/Server.cs rename to ConfigItem/ServerConfig.cs index 428a612..d1b52bb 100644 --- a/ConfigItem/Server.cs +++ b/ConfigItem/ServerConfig.cs @@ -1,5 +1,5 @@ -using Noikoio.RegexBot.Feature.RegexResponder; -using System.Collections.Generic; +using System; +using System.Collections.ObjectModel; using System.Diagnostics; namespace Noikoio.RegexBot.ConfigItem @@ -7,27 +7,27 @@ namespace Noikoio.RegexBot.ConfigItem /// /// Represents known information about a Discord guild (server) and other associated data /// - class Server + class ServerConfig { private readonly string _name; private ulong? _id; - private IEnumerable _rules; private EntityList _moderators; + private ReadOnlyDictionary _featureData; public string Name => _name; public ulong? Id { get => _id; set { if (!_id.HasValue) _id = value; } } - public IEnumerable MatchResponseRules => _rules; public EntityList Moderators => _moderators; + public ReadOnlyDictionary FeatureConfigs => _featureData; - public Server(string name, ulong? id, IEnumerable rules, EntityList moderators) + public ServerConfig(string name, ulong? id, EntityList moderators, ReadOnlyDictionary featureconf) { _name = name; _id = id; - _rules = rules; _moderators = moderators; - Debug.Assert(_name != null && _rules != null && _moderators != null); + _featureData = featureconf; + Debug.Assert(_name != null && _moderators != null); } } } diff --git a/ConfigLoader.cs b/Configuration.cs similarity index 71% rename from ConfigLoader.cs rename to Configuration.cs index f9b286d..dc739f4 100644 --- a/ConfigLoader.cs +++ b/Configuration.cs @@ -1,9 +1,9 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Noikoio.RegexBot.ConfigItem; -using Noikoio.RegexBot.Feature.RegexResponder; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; using System.Reflection; using System.Text.RegularExpressions; @@ -14,22 +14,25 @@ namespace Noikoio.RegexBot /// /// Configuration loader /// - class ConfigLoader + class Configuration { public const string LogPrefix = "Config"; + private readonly RegexBot _bot; private readonly string _configPath; - private Server[] _servers; + private ServerConfig[] _servers; + // The following values do not change on reload: private string _botToken; private string _currentGame; public string BotUserToken => _botToken; public string CurrentGame => _currentGame; - public Server[] Servers => _servers; + public ServerConfig[] Servers => _servers; - public ConfigLoader() + public Configuration(RegexBot bot) { + _bot = bot; var dsc = Path.DirectorySeparatorChar; _configPath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + dsc + "settings.json"; @@ -64,7 +67,7 @@ namespace Noikoio.RegexBot } /// - /// Called only on bot startup. Returns false on failure. + /// Loads essential, unchanging values needed for bot startup. Returns false on failure. /// public bool LoadInitialConfig() { @@ -81,7 +84,7 @@ namespace Noikoio.RegexBot } _currentGame = conf["playing"]?.Value(); - return ProcessServerConfig(conf).GetAwaiter().GetResult(); + return true; } /// @@ -90,15 +93,10 @@ namespace Noikoio.RegexBot /// False on failure. Specific reasons will have been sent to log. public async Task ReloadServerConfig() { - await Logger.GetLogger(LogPrefix)("Configuration reload currently not supported."); - return false; - // TODO actually implement this - var lt = LoadFile(); - lt.Wait(); - JObject conf = lt.Result; - if (conf == null) return false; + var config = await LoadFile(); + if (config == null) return false; - return await ProcessServerConfig(conf); + return await ProcessServerConfig(config); } /// @@ -114,7 +112,7 @@ namespace Noikoio.RegexBot return false; } - List newservers = new List(); + List newservers = new List(); await Log("Reading server configurations..."); foreach (JObject sconf in conf["servers"].Children()) { @@ -145,38 +143,44 @@ namespace Noikoio.RegexBot // Load server moderator list EntityList mods = new EntityList(sconf["moderators"]); if (sconf["moderators"] != null) await SLog("Moderator " + mods.ToString()); - - // Read rules - // Also, parsed rules require a server reference. Creating it here. - List rules = new List(); - Server newserver = new Server(sname, sid, rules, mods); - - foreach (JObject ruleconf in sconf["rules"]) + + // Load feature configurations + Dictionary customConfs = new Dictionary(); + foreach (var item in _bot.Features) { - // Try and get at least the name before passing it to RuleItem - string name = ruleconf["name"]?.Value(); - if (name == null) + var attr = item.GetType().GetTypeInfo() + .GetMethod("ProcessConfiguration").GetCustomAttribute(); + if (attr == null) { - await SLog("Display name not defined within a rule section."); - return false; + await SLog("No additional configuration for " + item.Name); + 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 { - rule = new RuleConfig(newserver, ruleconf); - } catch (RuleImportException ex) + result = await item.ProcessConfiguration(section); + } + catch (RuleImportException ex) { - await SLog("-> Error: " + ex.Message); + await SLog($"{item.Name} failed to load configuration: " + ex.Message); return false; } - rules.Add(rule); + + customConfs.Add(item, result); } + // Switch to using new data List> rulesfinal = new List>(); - newservers.Add(newserver); + newservers.Add(new ServerConfig(sname, sid, mods, new ReadOnlyDictionary(customConfs))); } _servers = newservers.ToArray(); diff --git a/Feature/RegexResponder/EventProcessor.cs b/Feature/RegexResponder/EventProcessor.cs index b8c546d..2d4d8c0 100644 --- a/Feature/RegexResponder/EventProcessor.cs +++ b/Feature/RegexResponder/EventProcessor.cs @@ -7,6 +7,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace Noikoio.RegexBot.Feature.RegexResponder { @@ -14,15 +15,15 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// Implements per-message regex matching and executes customizable responses. /// Namesake of this project. /// - partial class EventProcessor + partial class EventProcessor : BotFeature { 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; - _conf = conf; _client.MessageReceived += OnMessageReceived; _client.MessageUpdated += OnMessageUpdated; @@ -58,12 +59,16 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// 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 - SocketGuild g = ((SocketGuildUser)arg.Author).Guild; - Server sd = null; - foreach (var item in _conf.Servers) + SocketGuild g = ch.Guild; + ServerConfig sd = null; + foreach (var item in RegexBot.Config.Servers) { if (item.Id.HasValue) { @@ -81,7 +86,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder { item.Id = g.Id; sd = item; - await Logger.GetLogger(ConfigLoader.LogPrefix) + await Logger.GetLogger(Configuration.LogPrefix) ($"Suggestion: Server \"{item.Name}\" can be defined as \"{item.Id}::{item.Name}\""); break; } @@ -89,9 +94,11 @@ namespace Noikoio.RegexBot.Feature.RegexResponder } 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 sd.MatchResponseRules) + foreach (var rule in rules) 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. /// If it matches, the rule's responses are executed. To be run in the thread pool. /// - private async Task ProcessMessage(Server srv, RuleConfig rule, SocketMessage msg) + private async Task ProcessMessage(ServerConfig srv, RuleConfig rule, SocketMessage msg) { string msgcontent; @@ -134,8 +141,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder if (!success) return; // Prepare to execute responses - var log = Logger.GetLogger(rule.DisplayName); - await log($"Triggered in {srv.Name}/#{msg.Channel} by {msg.Author.ToString()}"); + await Log($"\"{rule.DisplayName}\" triggered in {srv.Name}/#{msg.Channel} by {msg.Author.ToString()}"); foreach (string rcmd in rule.Responses) { @@ -145,19 +151,52 @@ namespace Noikoio.RegexBot.Feature.RegexResponder ResponseProcessor response; if (!_commands.TryGetValue(cmd, out response)) { - await log($"Unknown command \"{cmd}\""); + await Log($"Unknown command defined in response: \"{cmd}\""); continue; } - await response.Invoke(log, rcmd, rule, msg); + await response.Invoke(rcmd, rule, msg); } catch (Exception ex) { - await log($"Encountered an error while processing \"{cmd}\""); - await log(ex.ToString()); + 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 /// diff --git a/Feature/RegexResponder/Responses.cs b/Feature/RegexResponder/Responses.cs index 8710702..ecd4b3e 100644 --- a/Feature/RegexResponder/Responses.cs +++ b/Feature/RegexResponder/Responses.cs @@ -11,7 +11,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder // Contains code for handling each response in a rule. 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 _commands; #if DEBUG @@ -19,9 +19,8 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// Throws an exception. Meant to be a quick error handling test. /// No parameters. /// - 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."); } @@ -30,7 +29,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// The guild info displayed is the one in which the command is invoked. /// No parameters. /// - 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 result = new StringBuilder(); @@ -55,19 +54,19 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// Sends a message to a specified channel. /// Parameters: say (channel) (message) /// - 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); if (@in.Length != 3) { - await l("Error: say: Incorrect number of parameters."); + await Log("Error: say: Incorrect number of parameters."); return; } var target = await GetMessageTargetAsync(@in[1], m); if (target == null) { - await l("Error: say: Unable to resolve given target."); + await Log("Error: say: Unable to resolve given target."); return; } @@ -80,19 +79,19 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// Reports the incoming message to a given channel. /// Parameters: report (channel) /// - 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); if (@in.Length != 2) { - await l("Error: report: Incorrect number of parameters."); + await Log("Error: report: Incorrect number of parameters."); return; } var target = await GetMessageTargetAsync(@in[1], m); if (target == null) { - await l("Error: report: Unable to resolve given target."); + await Log("Error: report: Unable to resolve given target."); return; } @@ -136,7 +135,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// Deletes the incoming message. /// No parameters. /// - 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 await m.DeleteAsync(); @@ -146,19 +145,19 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// Executes an external program and sends standard output to the given channel. /// Parameters: exec (channel) (command line) /// - 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); if (@in.Length < 3) { - await l("exec: Incorrect number of parameters."); + await Log("exec: Incorrect number of parameters."); } string result; var target = await GetMessageTargetAsync(@in[1], m); if (target == null) { - await l("Error: exec: Unable to resolve given channel."); + await Log("Error: exec: Unable to resolve given channel."); return; } @@ -174,7 +173,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder p.WaitForExit(5000); // waiting at most 5 seconds 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) { result = await stdout.ReadToEndAsync(); @@ -182,7 +181,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder } 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(); return; } @@ -197,7 +196,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// No parameters. /// // 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; 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. /// Parameters: grantrole/revokerole (user ID or @_) (role ID) /// - 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); if (@in.Length != 3) { - await l($"Error: {@in[0]}: incorrect number of parameters."); + await Log($"Error: {@in[0]}: incorrect number of parameters."); return; } 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; } @@ -226,7 +225,7 @@ namespace Noikoio.RegexBot.Feature.RegexResponder SocketRole rl = gu.Guild.GetRole(roleID); if (rl == null) { - await l($"Error: {@in[0]}: Specified role not found."); + await Log($"Error: {@in[0]}: Specified role not found."); return; } @@ -240,13 +239,13 @@ namespace Noikoio.RegexBot.Feature.RegexResponder { 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; } target = gu.Guild.GetUser(userID); 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; } } diff --git a/Feature/RegexResponder/RuleConfig.cs b/Feature/RegexResponder/RuleConfig.cs index b626bba..f263d18 100644 --- a/Feature/RegexResponder/RuleConfig.cs +++ b/Feature/RegexResponder/RuleConfig.cs @@ -13,7 +13,6 @@ namespace Noikoio.RegexBot.Feature.RegexResponder internal struct RuleConfig { private string _displayName; - private Server _server; private IEnumerable _regex; private IEnumerable _responses; private FilterType _filtermode; @@ -25,7 +24,6 @@ namespace Noikoio.RegexBot.Feature.RegexResponder private bool _matchEmbeds; public string DisplayName => _displayName; - public Server Server => _server; public IEnumerable Regex => _regex; public IEnumerable Responses => _responses; public FilterType FilterMode => _filtermode; @@ -43,10 +41,8 @@ namespace Noikoio.RegexBot.Feature.RegexResponder /// /// Thrown when encountering a missing or invalid value. /// - public RuleConfig(Server serverref, JObject ruleconf) + public RuleConfig(JObject ruleconf) { - _server = serverref; - // display name - validation should've been done outside this constructor already _displayName = ruleconf["name"]?.Value(); if (_displayName == null) diff --git a/Program.cs b/Program.cs index ddb3a20..f9108d6 100644 --- a/Program.cs +++ b/Program.cs @@ -10,18 +10,7 @@ namespace Noikoio.RegexBot { static void Main(string[] args) { - // Attempt to load basic configuration before setting up the client - 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); + RegexBot rb = new RegexBot(); Console.CancelKeyPress += rb.Console_CancelKeyPress; //AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; diff --git a/RegexBot.cs b/RegexBot.cs index f5df43c..ddb58ed 100644 --- a/RegexBot.cs +++ b/RegexBot.cs @@ -1,6 +1,7 @@ using Discord; using Discord.WebSocket; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Noikoio.RegexBot @@ -10,34 +11,63 @@ namespace Noikoio.RegexBot /// class RegexBot { - private readonly ConfigLoader _config; + private static Configuration _config; private readonly DiscordSocketClient _client; + private BotFeature[] _features; - // Constructor loads all subsystems. Subsystem constructors hook up their event delegates. - internal RegexBot(ConfigLoader conf) + internal static Configuration Config => _config; + internal IEnumerable 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() { LogLevel = LogSeverity.Info, DefaultRetryMode = RetryMode.AlwaysRetry, MessageCacheSize = 50 }); - _config = conf; // Hook up handlers for basic functions _client.Connected += _client_Connected; // 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() { - var dlog = Logger.GetLogger("Discord"); - _client.Log += async (arg) => - 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.LoginAsync(TokenType.Bot, Config.BotUserToken); await _client.StartAsync(); await Task.Delay(-1); @@ -45,7 +75,7 @@ namespace Noikoio.RegexBot 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 } diff --git a/RegexBot.csproj b/RegexBot.csproj index 791a982..53dfc70 100644 --- a/RegexBot.csproj +++ b/RegexBot.csproj @@ -4,7 +4,7 @@ Exe netcoreapp1.1 Noikoio.RegexBot - 0.15.0.0 + 1.0.0.0 Highly configurable Discord moderation bot Noikoio