From ea4d7b0a299dd293e30e0298733b210565cfd51b Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 6 May 2018 13:09:17 -0700 Subject: [PATCH] 'First' commit; add most of the initialization routine --- Kerobot/InstanceConfig.cs | 72 +++++++++++++++ Kerobot/Kerobot.cs | 63 +++++++++++++ Kerobot/Kerobot.csproj | 1 + Kerobot/Options.cs | 38 ++++++++ Kerobot/Program.cs | 90 +++++++++++++++++++ Kerobot/Services/GuildStateManager/Manager.cs | 10 +++ Kerobot/Services/Service.cs | 25 ++++++ 7 files changed, 299 insertions(+) create mode 100644 Kerobot/InstanceConfig.cs create mode 100644 Kerobot/Kerobot.cs create mode 100644 Kerobot/Options.cs create mode 100644 Kerobot/Program.cs create mode 100644 Kerobot/Services/GuildStateManager/Manager.cs create mode 100644 Kerobot/Services/Service.cs diff --git a/Kerobot/InstanceConfig.cs b/Kerobot/InstanceConfig.cs new file mode 100644 index 0000000..6ec1b41 --- /dev/null +++ b/Kerobot/InstanceConfig.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.IO; +using System.Reflection; + +namespace Kerobot +{ + /// + /// Contains instance configuration for this bot, + /// including Discord connection settings and service configuration. + /// + class InstanceConfig + { + const string JBotToken = "BotToken"; + readonly string _botToken; + /// + /// Token used for Discord authentication. + /// + internal string BotToken => _botToken; + + const string JPgSqlConnectionString = "SqlConnectionString"; + readonly string _pgSqlConnectionString; + /// + /// Connection string for accessing the PostgreSQL database. + /// + /// + /// That's right, the user can specify the -entire- thing. + /// Should problems arise, this will be replaced by a full section within configuration. + /// + internal string PostgresConnString => _pgSqlConnectionString; + + // TODO add fields for services to be configurable: DMRelay, InstanceLog + + /// + /// Sets up instance configuration object from file and command line parameters. + /// + /// Path to file from which to load configuration. If null, uses default path. + internal InstanceConfig(Options options) + { + string path = options.ConfigFile; + if (path == null) // default: config.json in working directory + { + path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + + "." + Path.DirectorySeparatorChar + "config.json"; + } + + JObject conf; + try + { + var conftxt = File.ReadAllText(path); + conf = JObject.Parse(conftxt); + } + catch (Exception ex) + { + string pfx; + if (ex is JsonException) pfx = "Unable to parse configuration: "; + else pfx = "Unable to access configuration: "; + + throw new Exception(pfx + ex.Message, ex); + } + + // Input validation - throw exception on errors. Exception messages printed as-is. + _botToken = conf[JBotToken]?.Value(); + if (string.IsNullOrEmpty(_botToken)) + throw new Exception($"'{JBotToken}' was not properly specified in configuration."); + _pgSqlConnectionString = conf[JPgSqlConnectionString]?.Value(); + if (string.IsNullOrEmpty(_pgSqlConnectionString)) + throw new Exception($"'{JPgSqlConnectionString}' was not properly specified in configuration."); + } + } +} diff --git a/Kerobot/Kerobot.cs b/Kerobot/Kerobot.cs new file mode 100644 index 0000000..00945d6 --- /dev/null +++ b/Kerobot/Kerobot.cs @@ -0,0 +1,63 @@ +using Discord; +using Discord.WebSocket; +using Kerobot.Services; +using Npgsql; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Kerobot +{ + /// + /// Kerobot main class, and the most accessible and useful class in the whole program. + /// Provides an interface for any part of the program to call into all existing services. + /// + public partial class Kerobot + { + // Partial class: Services are able to add their own methods and properties to this class. + // This is to prevent this file from having too many references to many different and unrelated features. + + private readonly InstanceConfig _icfg; + private readonly DiscordSocketClient _client; + + /// + /// Gets application instance configuration. + /// + internal InstanceConfig Config => _icfg; + /// + /// Gets the Discord client instance. + /// + public DiscordSocketClient DiscordClient => _client; + + internal Kerobot(InstanceConfig conf, DiscordSocketClient client) + { + _icfg = conf; + _client = client; + + InitializeServices(); + + // and prepare modules here + } + + private void InitializeServices() + { + throw new NotImplementedException(); + } + + /// + /// Returns an open NpgsqlConnection instance. + /// + /// + /// If manipulating guild-specific information, this parameter sets the database connection's search path. + /// + internal async Task GetOpenNpgsqlConnectionAsync(ulong? guild) + { + string cs = _icfg.PostgresConnString; + if (guild.HasValue) cs += ";searchpath=guild_" + guild.Value; + + var db = new NpgsqlConnection(cs); + await db.OpenAsync(); + return db; + } + } +} diff --git a/Kerobot/Kerobot.csproj b/Kerobot/Kerobot.csproj index 9259a34..aa1b6a2 100644 --- a/Kerobot/Kerobot.csproj +++ b/Kerobot/Kerobot.csproj @@ -10,6 +10,7 @@ Advanced and flexible Discord moderation bot. 0.0.1 0.0.1 + 7.2 diff --git a/Kerobot/Options.cs b/Kerobot/Options.cs new file mode 100644 index 0000000..154adcf --- /dev/null +++ b/Kerobot/Options.cs @@ -0,0 +1,38 @@ +using CommandLine; +using CommandLine.Text; +using System; + +namespace Kerobot +{ + /// + /// Command line options + /// + class Options + { + [Option('c', "config", Default = null, + HelpText = "Custom path to instance configuration. Defaults to config.json in bot directory.")] + public string ConfigFile { get; set; } + + /// + /// Command line arguments parsed here. Depending on inputs, the program can exit here. + /// + public static Options ParseOptions(string[] args) + { + // Parser will not write out to console by itself + var parser = new Parser(config => config.HelpWriter = null); + Options opts = null; + + var result = parser.ParseArguments(args); + result.WithParsed(p => opts = p); + result.WithNotParsed(p => + { + // Taking some extra steps to modify the header to make it resemble our welcome message. + var ht = HelpText.AutoBuild(result); + ht.Heading = ht.Heading += " - https://github.com/Noikoio/Kerobot"; + Console.WriteLine(ht.ToString()); + Environment.Exit(1); + }); + return opts; + } + } +} diff --git a/Kerobot/Program.cs b/Kerobot/Program.cs new file mode 100644 index 0000000..f268541 --- /dev/null +++ b/Kerobot/Program.cs @@ -0,0 +1,90 @@ +using Discord; +using Discord.WebSocket; +using System; +using System.Threading.Tasks; + +namespace Kerobot +{ + /// + /// Program startup class. Does initialization before starting the Discord client. + /// + class Program + { + static DateTimeOffset _startTime; + /// + /// Timestamp specifying the date and time that the program began running. + /// + public static DateTimeOffset StartTime => _startTime; + + static Kerobot _main; + + static async Task Main(string[] args) + { + _startTime = DateTimeOffset.UtcNow; + Console.WriteLine("Bot start time: " + _startTime.ToString("u")); + + // Get instance config figured out + var opts = Options.ParseOptions(args); // Program can exit here. + InstanceConfig cfg; + try + { + cfg = new InstanceConfig(opts); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + Environment.ExitCode = 1; + return; + } + + // Quick test if database configuration works + try + { + using (var d = new Npgsql.NpgsqlConnection(cfg.PostgresConnString)) + { + await d.OpenAsync(); + d.Close(); + } + } + catch (Exception ex) + { + Console.WriteLine("Could not establish a database connection! Check your settings and try again."); + Console.WriteLine($"Error: {ex.GetType().FullName}: {ex.Message}"); + Environment.Exit(1); + } + + // Configure Discord client + var client = new DiscordSocketClient(new DiscordSocketConfig() + { + DefaultRetryMode = RetryMode.AlwaysRetry, + MessageCacheSize = 0 // using our own + }); + + // Kerobot class initialization - will set up services and modules + _main = new Kerobot(cfg, client); + + // Set up application close handler + Console.CancelKeyPress += Console_CancelKeyPress; + + // TODO Set up unhandled exception handler + // send error notification to instance log channel, if possible + + // And off we go. + await _main.DiscordClient.LoginAsync(Discord.TokenType.Bot, cfg.BotToken); + await _main.DiscordClient.StartAsync(); + await Task.Delay(-1); + } + + private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + // TODO finish implementation when logging is set up + e.Cancel = true; + // _main.Log("Received Cancel event. Application will shut down..."); + // stop periodic task processing - wait for current run to finish if executing (handled by service?) + // notify services of shutdown + bool success = _main.DiscordClient.LogoutAsync().Wait(10000); + // if (!success) _main.Log("Failed to disconnect cleanly from Discord. Will force shut down."); + Environment.Exit(0); + } + } +} diff --git a/Kerobot/Services/GuildStateManager/Manager.cs b/Kerobot/Services/GuildStateManager/Manager.cs new file mode 100644 index 0000000..f2b456c --- /dev/null +++ b/Kerobot/Services/GuildStateManager/Manager.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Kerobot.Services.GuildStateManager +{ + class Manager + { + } +} diff --git a/Kerobot/Services/Service.cs b/Kerobot/Services/Service.cs new file mode 100644 index 0000000..17a3292 --- /dev/null +++ b/Kerobot/Services/Service.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Kerobot.Services +{ + /// + /// Base class for Kerobot service. + /// + /// + /// Services provide the core functionality of this program. Modules are expected to call into methods + /// provided by services for the times when processor-intensive or shared functionality needs to be utilized. + /// + internal class Service + { + private readonly Kerobot _kb; + + public Kerobot Kerobot => _kb; + + protected internal Service(Kerobot kb) + { + _kb = kb; + } + } +}