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 System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Kerobot
|
||||
|
@ -19,6 +20,13 @@ namespace Kerobot
|
|||
/// </summary>
|
||||
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";
|
||||
readonly string _pgSqlConnectionString;
|
||||
/// <summary>
|
||||
|
@ -76,6 +84,13 @@ namespace Kerobot
|
|||
if (string.IsNullOrEmpty(_pgSqlConnectionString))
|
||||
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>();
|
||||
if (!string.IsNullOrWhiteSpace(ilInput))
|
||||
{
|
||||
|
|
|
@ -48,25 +48,29 @@ namespace Kerobot
|
|||
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;
|
||||
InstanceLogAsync(false, "Kerobot",
|
||||
$"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>();
|
||||
|
||||
// Put services here as they become usable.
|
||||
_svcLogging = new Services.Logging.LoggingService(this);
|
||||
svcList.Add(_svcLogging);
|
||||
_svcGuildState = new Services.GuildState.GuildStateService(this);
|
||||
svcList.Add(_svcGuildState);
|
||||
|
||||
_services = svcList.AsReadOnly();
|
||||
return svcList.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
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>
|
||||
/// 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.Linq;
|
||||
using Npgsql;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -30,15 +31,29 @@ namespace Kerobot.Services.GuildState
|
|||
|
||||
// TODO periodic task for refreshing stale configuration
|
||||
}
|
||||
|
||||
|
||||
private async Task DiscordClient_GuildAvailable(Discord.WebSocket.SocketGuild arg)
|
||||
{
|
||||
// Get this done before any other thing.
|
||||
await CreateSchema(arg.Id);
|
||||
|
||||
// Attempt initialization on the guild.
|
||||
await CreateGuildConfigurationTableAsync(arg.Id);
|
||||
|
||||
// Attempt initialization on the guild. All services will set up their tables here.
|
||||
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
|
||||
bool success = await LoadGuildConfiguration(arg.Id);
|
||||
if (!success)
|
||||
|
@ -46,6 +61,11 @@ namespace Kerobot.Services.GuildState
|
|||
await Kerobot.GuildLogAsync(arg.Id, GuildLogSource,
|
||||
"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)
|
||||
{
|
||||
|
@ -91,7 +111,7 @@ namespace Kerobot.Services.GuildState
|
|||
try
|
||||
{
|
||||
var tok = JToken.Parse(jstr);
|
||||
if (tok.Type != JTokenType.Object)
|
||||
if (tok.Type == JTokenType.Object)
|
||||
{
|
||||
guildConf = (JObject)tok;
|
||||
}
|
||||
|
@ -153,32 +173,29 @@ namespace Kerobot.Services.GuildState
|
|||
/// <summary>
|
||||
/// Creates the table structures for holding module configuration.
|
||||
/// </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 integer not null primary key DEFAULT nextval('{DBTableName}_id_seq') "
|
||||
+ "author bigint not null, "
|
||||
+ "rev_date timestamptz not null default NOW(), "
|
||||
+ "config_json text not null"
|
||||
+ ")";
|
||||
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
|
||||
// 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) "
|
||||
+ "on conflict (rev_id) do nothing";
|
||||
c.Parameters.Add("@Json", NpgsqlTypes.NpgsqlDbType.Text).Value = _defaultGuildJson;
|
||||
c.Prepare();
|
||||
await c.ExecuteNonQueryAsync();
|
||||
}
|
||||
c.CommandText = $"create table if not exists {DBTableName} ("
|
||||
+ $"rev_id SERIAL primary key, "
|
||||
+ "author bigint not null, "
|
||||
+ "rev_date timestamptz not null default NOW(), "
|
||||
+ "config_json text not null"
|
||||
+ ")";
|
||||
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
|
||||
// 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) "
|
||||
+ "on conflict (rev_id) do nothing";
|
||||
c.Parameters.Add("@Json", NpgsqlTypes.NpgsqlDbType.Text).Value = _defaultGuildJson;
|
||||
c.Prepare();
|
||||
await c.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,7 +224,7 @@ namespace Kerobot.Services.GuildState
|
|||
private readonly string _defaultGuildJson;
|
||||
private string PreloadDefaultGuildJson()
|
||||
{
|
||||
const string ResourceName = "Kerobot.DefaultGuildJson.json";
|
||||
const string ResourceName = "Kerobot.DefaultGuildConfig.json";
|
||||
|
||||
var a = System.Reflection.Assembly.GetExecutingAssembly();
|
||||
using (var s = a.GetManifestResourceStream(ResourceName))
|
||||
|
|
|
@ -21,6 +21,13 @@ namespace Kerobot.Services
|
|||
_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>
|
||||
/// Creates a log message.
|
||||
/// </summary>
|
||||
|
|
Loading…
Reference in a new issue