'First' commit; add most of the initialization routine

This commit is contained in:
Noikoio 2018-05-06 13:09:17 -07:00
parent 3bf509c5d4
commit ea4d7b0a29
7 changed files with 299 additions and 0 deletions

72
Kerobot/InstanceConfig.cs Normal file
View file

@ -0,0 +1,72 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Reflection;
namespace Kerobot
{
/// <summary>
/// Contains instance configuration for this bot,
/// including Discord connection settings and service configuration.
/// </summary>
class InstanceConfig
{
const string JBotToken = "BotToken";
readonly string _botToken;
/// <summary>
/// Token used for Discord authentication.
/// </summary>
internal string BotToken => _botToken;
const string JPgSqlConnectionString = "SqlConnectionString";
readonly string _pgSqlConnectionString;
/// <summary>
/// Connection string for accessing the PostgreSQL database.
/// </summary>
/// <remarks>
/// That's right, the user can specify the -entire- thing.
/// Should problems arise, this will be replaced by a full section within configuration.
/// </remarks>
internal string PostgresConnString => _pgSqlConnectionString;
// TODO add fields for services to be configurable: DMRelay, InstanceLog
/// <summary>
/// Sets up instance configuration object from file and command line parameters.
/// </summary>
/// <param name="path">Path to file from which to load configuration. If null, uses default path.</param>
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<string>();
if (string.IsNullOrEmpty(_botToken))
throw new Exception($"'{JBotToken}' was not properly specified in configuration.");
_pgSqlConnectionString = conf[JPgSqlConnectionString]?.Value<string>();
if (string.IsNullOrEmpty(_pgSqlConnectionString))
throw new Exception($"'{JPgSqlConnectionString}' was not properly specified in configuration.");
}
}
}

63
Kerobot/Kerobot.cs Normal file
View file

@ -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
{
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// Gets application instance configuration.
/// </summary>
internal InstanceConfig Config => _icfg;
/// <summary>
/// Gets the Discord client instance.
/// </summary>
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();
}
/// <summary>
/// Returns an open NpgsqlConnection instance.
/// </summary>
/// <param name="guild">
/// If manipulating guild-specific information, this parameter sets the database connection's search path.
/// </param>
internal async Task<NpgsqlConnection> 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;
}
}
}

View file

@ -10,6 +10,7 @@
<Description>Advanced and flexible Discord moderation bot.</Description>
<FileVersion>0.0.1</FileVersion>
<Version>0.0.1</Version>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>

38
Kerobot/Options.cs Normal file
View file

@ -0,0 +1,38 @@
using CommandLine;
using CommandLine.Text;
using System;
namespace Kerobot
{
/// <summary>
/// Command line options
/// </summary>
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; }
/// <summary>
/// Command line arguments parsed here. Depending on inputs, the program can exit here.
/// </summary>
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<Options>(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;
}
}
}

90
Kerobot/Program.cs Normal file
View file

@ -0,0 +1,90 @@
using Discord;
using Discord.WebSocket;
using System;
using System.Threading.Tasks;
namespace Kerobot
{
/// <summary>
/// Program startup class. Does initialization before starting the Discord client.
/// </summary>
class Program
{
static DateTimeOffset _startTime;
/// <summary>
/// Timestamp specifying the date and time that the program began running.
/// </summary>
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);
}
}
}

View file

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Kerobot.Services.GuildStateManager
{
class Manager
{
}
}

View file

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Kerobot.Services
{
/// <summary>
/// Base class for Kerobot service.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal class Service
{
private readonly Kerobot _kb;
public Kerobot Kerobot => _kb;
protected internal Service(Kerobot kb)
{
_kb = kb;
}
}
}