Implemented module loading; fixed GuildStateService issues
This commit is contained in:
parent
eae7410dbc
commit
f20a17e914
7 changed files with 160 additions and 36 deletions
|
@ -2,6 +2,7 @@
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Kerobot
|
namespace Kerobot
|
||||||
|
@ -19,6 +20,13 @@ namespace Kerobot
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal string BotToken => _botToken;
|
internal string BotToken => _botToken;
|
||||||
|
|
||||||
|
const string JAssemblies = "Assemblies";
|
||||||
|
readonly string[] _enabledAssemblies;
|
||||||
|
/// <summary>
|
||||||
|
/// List of assemblies to load, by file. Paths are always relative to the bot directory.
|
||||||
|
/// </summary>
|
||||||
|
internal string[] EnabledAssemblies => _enabledAssemblies;
|
||||||
|
|
||||||
const string JPgSqlConnectionString = "SqlConnectionString";
|
const string JPgSqlConnectionString = "SqlConnectionString";
|
||||||
readonly string _pgSqlConnectionString;
|
readonly string _pgSqlConnectionString;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -76,6 +84,13 @@ namespace Kerobot
|
||||||
if (string.IsNullOrEmpty(_pgSqlConnectionString))
|
if (string.IsNullOrEmpty(_pgSqlConnectionString))
|
||||||
throw new Exception($"'{JPgSqlConnectionString}' is not properly specified in configuration.");
|
throw new Exception($"'{JPgSqlConnectionString}' is not properly specified in configuration.");
|
||||||
|
|
||||||
|
var asmList = conf[JAssemblies];
|
||||||
|
if (asmList == null || asmList.Type != JTokenType.Array)
|
||||||
|
{
|
||||||
|
throw new Exception($"'{JAssemblies}' is not properly specified in configuration.");
|
||||||
|
}
|
||||||
|
_enabledAssemblies = asmList.Values<string>().ToArray();
|
||||||
|
|
||||||
var ilInput = conf[JInstanceLogReportTarget]?.Value<string>();
|
var ilInput = conf[JInstanceLogReportTarget]?.Value<string>();
|
||||||
if (!string.IsNullOrWhiteSpace(ilInput))
|
if (!string.IsNullOrWhiteSpace(ilInput))
|
||||||
{
|
{
|
||||||
|
|
|
@ -48,25 +48,29 @@ namespace Kerobot
|
||||||
await InstanceLogAsync(true, "Kerobot", "Connected and ready.");
|
await InstanceLogAsync(true, "Kerobot", "Connected and ready.");
|
||||||
};
|
};
|
||||||
|
|
||||||
InitializeServices();
|
// Get all services started up
|
||||||
|
_services = InitializeServices();
|
||||||
|
|
||||||
// TODO prepare modules here
|
// Load externally defined functionality
|
||||||
|
_modules = ModuleLoader.Load(_icfg, this);
|
||||||
|
|
||||||
// Everything's ready to go by now. Print the welcome message here.
|
// Everything's ready to go. Print the welcome message here.
|
||||||
var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
var ver = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
InstanceLogAsync(false, "Kerobot",
|
InstanceLogAsync(false, "Kerobot",
|
||||||
$"This is Kerobot v{ver.ToString(3)}. https://github.com/Noikoio/Kerobot").Wait();
|
$"This is Kerobot v{ver.ToString(3)}. https://github.com/Noikoio/Kerobot").Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeServices()
|
private IReadOnlyCollection<Service> InitializeServices()
|
||||||
{
|
{
|
||||||
var svcList = new List<Service>();
|
var svcList = new List<Service>();
|
||||||
|
|
||||||
// Put services here as they become usable.
|
// Put services here as they become usable.
|
||||||
_svcLogging = new Services.Logging.LoggingService(this);
|
_svcLogging = new Services.Logging.LoggingService(this);
|
||||||
svcList.Add(_svcLogging);
|
svcList.Add(_svcLogging);
|
||||||
|
_svcGuildState = new Services.GuildState.GuildStateService(this);
|
||||||
|
svcList.Add(_svcGuildState);
|
||||||
|
|
||||||
_services = svcList.AsReadOnly();
|
return svcList.AsReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
12
Kerobot/KerobotModuleAttribute.cs
Normal file
12
Kerobot/KerobotModuleAttribute.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kerobot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies to the Kerobot module loader that the target class should be treated as a module instance.
|
||||||
|
/// When the program scans an assembly which has been specified in its instance configuration to be loaded,
|
||||||
|
/// the program searches for classes implementing <see cref="ModuleBase"/> that also contain this attribute.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
|
public class KerobotModuleAttribute : Attribute { }
|
||||||
|
}
|
|
@ -21,7 +21,11 @@ namespace Kerobot
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Kerobot Kerobot => _kb;
|
public Kerobot Kerobot => _kb;
|
||||||
|
|
||||||
private ModuleBase(Kerobot kb) => _kb = kb;
|
/// <summary>
|
||||||
|
/// When a module is loaded, this constructor is called.
|
||||||
|
/// Services are available at this point. Do not attempt to communicate to Discord within the constructor.
|
||||||
|
/// </summary>
|
||||||
|
public ModuleBase(Kerobot kb) => _kb = kb;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the module name.
|
/// Gets the module name.
|
||||||
|
|
65
Kerobot/ModuleLoader.cs
Normal file
65
Kerobot/ModuleLoader.cs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Kerobot
|
||||||
|
{
|
||||||
|
static class ModuleLoader
|
||||||
|
{
|
||||||
|
private const string LogName = nameof(ModuleLoader);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given the instance configuration, loads all appropriate types from file specified in it.
|
||||||
|
/// </summary>
|
||||||
|
internal static ReadOnlyCollection<ModuleBase> Load(InstanceConfig conf, Kerobot k)
|
||||||
|
{
|
||||||
|
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) + Path.DirectorySeparatorChar;
|
||||||
|
var modules = new List<ModuleBase>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var file in conf.EnabledAssemblies)
|
||||||
|
{
|
||||||
|
var a = Assembly.LoadFile(path + file);
|
||||||
|
modules.AddRange(LoadModulesFromAssembly(a, k));
|
||||||
|
}
|
||||||
|
return modules.AsReadOnly();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// TODO better (not lazy) exception handling
|
||||||
|
// Possible exceptions:
|
||||||
|
// - Errors loading assemblies
|
||||||
|
// - Errors finding module paths
|
||||||
|
// - Errors creating module instances
|
||||||
|
// - Unknown errors
|
||||||
|
Console.WriteLine("Module load failed.");
|
||||||
|
Console.WriteLine(ex.ToString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerable<ModuleBase> LoadModulesFromAssembly(Assembly asm, Kerobot k)
|
||||||
|
{
|
||||||
|
var eligibleTypes = from type in asm.GetTypes()
|
||||||
|
where !type.IsAssignableFrom(typeof(ModuleBase))
|
||||||
|
where type.GetCustomAttribute<KerobotModuleAttribute>() != null
|
||||||
|
select type;
|
||||||
|
k.InstanceLogAsync(false, LogName,
|
||||||
|
$"{asm.FullName} has {eligibleTypes.Count()} usable types:");
|
||||||
|
|
||||||
|
var newmods = new List<ModuleBase>();
|
||||||
|
foreach (var t in eligibleTypes)
|
||||||
|
{
|
||||||
|
var mod = Activator.CreateInstance(t, k);
|
||||||
|
k.InstanceLogAsync(false, LogName,
|
||||||
|
$"---> Instance created: {t.FullName}");
|
||||||
|
newmods.Add((ModuleBase)mod);
|
||||||
|
}
|
||||||
|
return newmods;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Npgsql;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -30,15 +31,29 @@ namespace Kerobot.Services.GuildState
|
||||||
|
|
||||||
// TODO periodic task for refreshing stale configuration
|
// TODO periodic task for refreshing stale configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DiscordClient_GuildAvailable(Discord.WebSocket.SocketGuild arg)
|
private async Task DiscordClient_GuildAvailable(Discord.WebSocket.SocketGuild arg)
|
||||||
{
|
{
|
||||||
// Get this done before any other thing.
|
// Get this done before any other thing.
|
||||||
await CreateSchema(arg.Id);
|
await CreateSchema(arg.Id);
|
||||||
|
|
||||||
// Attempt initialization on the guild.
|
// Attempt initialization on the guild. All services will set up their tables here.
|
||||||
await CreateGuildConfigurationTableAsync(arg.Id);
|
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(arg.Id))
|
||||||
|
{
|
||||||
|
foreach (var svc in Kerobot.Services)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await svc.CreateDatabaseTablesAsync(db);
|
||||||
|
}
|
||||||
|
catch (NpgsqlException ex)
|
||||||
|
{
|
||||||
|
await Log("Database error on CreateDatabaseTablesAsync:\n"
|
||||||
|
+ $"-- Service: {svc.Name}\n-- Guild: {arg.Id}\n-- Error: {ex.Message}" , true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Then start loading guild information
|
// Then start loading guild information
|
||||||
bool success = await LoadGuildConfiguration(arg.Id);
|
bool success = await LoadGuildConfiguration(arg.Id);
|
||||||
if (!success)
|
if (!success)
|
||||||
|
@ -46,6 +61,11 @@ namespace Kerobot.Services.GuildState
|
||||||
await Kerobot.GuildLogAsync(arg.Id, GuildLogSource,
|
await Kerobot.GuildLogAsync(arg.Id, GuildLogSource,
|
||||||
"Configuration was not reloaded due to the previously stated error(s).");
|
"Configuration was not reloaded due to the previously stated error(s).");
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Kerobot.InstanceLogAsync(false, GuildLogSource,
|
||||||
|
$"Configuration successfully refreshed for guild ID {arg.Id}.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private Task DiscordClient_LeftGuild(Discord.WebSocket.SocketGuild arg)
|
private Task DiscordClient_LeftGuild(Discord.WebSocket.SocketGuild arg)
|
||||||
{
|
{
|
||||||
|
@ -91,7 +111,7 @@ namespace Kerobot.Services.GuildState
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tok = JToken.Parse(jstr);
|
var tok = JToken.Parse(jstr);
|
||||||
if (tok.Type != JTokenType.Object)
|
if (tok.Type == JTokenType.Object)
|
||||||
{
|
{
|
||||||
guildConf = (JObject)tok;
|
guildConf = (JObject)tok;
|
||||||
}
|
}
|
||||||
|
@ -153,32 +173,29 @@ namespace Kerobot.Services.GuildState
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the table structures for holding module configuration.
|
/// Creates the table structures for holding module configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CreateGuildConfigurationTableAsync(ulong guildId)
|
public override async Task CreateDatabaseTablesAsync(NpgsqlConnection db)
|
||||||
{
|
{
|
||||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(guildId))
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
using (var c = db.CreateCommand())
|
c.CommandText = $"create table if not exists {DBTableName} ("
|
||||||
{
|
+ $"rev_id SERIAL primary key, "
|
||||||
c.CommandText = $"create table if not exists {DBTableName} ("
|
+ "author bigint not null, "
|
||||||
+ $"rev_id integer not null primary key DEFAULT nextval('{DBTableName}_id_seq') "
|
+ "rev_date timestamptz not null default NOW(), "
|
||||||
+ "author bigint not null, "
|
+ "config_json text not null"
|
||||||
+ "rev_date timestamptz not null default NOW(), "
|
+ ")";
|
||||||
+ "config_json text not null"
|
await c.ExecuteNonQueryAsync();
|
||||||
+ ")";
|
}
|
||||||
await c.ExecuteNonQueryAsync();
|
// Creating default configuration with revision ID 0.
|
||||||
}
|
// This allows us to quickly define rev_id as type SERIAL and not have to configure it so that
|
||||||
// Creating default configuration with revision ID 0.
|
// the serial should start at 2, but rather can easily start at 1. So lazy.
|
||||||
// This allows us to quickly define rev_id as type SERIAL and not have to configure it so that
|
using (var c = db.CreateCommand())
|
||||||
// the serial should start at 2, but rather can easily start at 1. So lazy.
|
{
|
||||||
using (var c = db.CreateCommand())
|
c.CommandText = $"insert into {DBTableName} (rev_id, author, config_json)"
|
||||||
{
|
+ "values (0, 0, @Json) "
|
||||||
c.CommandText = $"insert into {DBTableName} (rev_id, author, config_json)"
|
+ "on conflict (rev_id) do nothing";
|
||||||
+ "values (0, 0, @Json) "
|
c.Parameters.Add("@Json", NpgsqlTypes.NpgsqlDbType.Text).Value = _defaultGuildJson;
|
||||||
+ "on conflict (rev_id) do nothing";
|
c.Prepare();
|
||||||
c.Parameters.Add("@Json", NpgsqlTypes.NpgsqlDbType.Text).Value = _defaultGuildJson;
|
await c.ExecuteNonQueryAsync();
|
||||||
c.Prepare();
|
|
||||||
await c.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +224,7 @@ namespace Kerobot.Services.GuildState
|
||||||
private readonly string _defaultGuildJson;
|
private readonly string _defaultGuildJson;
|
||||||
private string PreloadDefaultGuildJson()
|
private string PreloadDefaultGuildJson()
|
||||||
{
|
{
|
||||||
const string ResourceName = "Kerobot.DefaultGuildJson.json";
|
const string ResourceName = "Kerobot.DefaultGuildConfig.json";
|
||||||
|
|
||||||
var a = System.Reflection.Assembly.GetExecutingAssembly();
|
var a = System.Reflection.Assembly.GetExecutingAssembly();
|
||||||
using (var s = a.GetManifestResourceStream(ResourceName))
|
using (var s = a.GetManifestResourceStream(ResourceName))
|
||||||
|
|
|
@ -21,6 +21,13 @@ namespace Kerobot.Services
|
||||||
_kb = kb;
|
_kb = kb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes database tables per-guild. Called when entering a guild.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="db">An opened database connection with the appropriate schema option set.</param>
|
||||||
|
/// <remarks>If overriding, calling the base method is not necessary.</remarks>
|
||||||
|
public virtual Task CreateDatabaseTablesAsync(Npgsql.NpgsqlConnection db) => Task.CompletedTask;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a log message.
|
/// Creates a log message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
Loading…
Reference in a new issue