Remove per-guild schema
All guild data will now be stored within the same table, instead of a new schema being created for each guild. This avoids issues with missing schemas. This likely wasn't a good idea to begin with.
This commit is contained in:
parent
de8660d913
commit
47a738ddbc
5 changed files with 111 additions and 139 deletions
|
@ -69,12 +69,9 @@ namespace Kerobot
|
||||||
/// <param name="guild">
|
/// <param name="guild">
|
||||||
/// If manipulating guild-specific information, this parameter sets the database connection's search path.
|
/// If manipulating guild-specific information, this parameter sets the database connection's search path.
|
||||||
/// </param>
|
/// </param>
|
||||||
internal async Task<NpgsqlConnection> GetOpenNpgsqlConnectionAsync(ulong? guild)
|
internal async Task<NpgsqlConnection> GetOpenNpgsqlConnectionAsync()
|
||||||
{
|
{
|
||||||
string cs = Config.PostgresConnString;
|
var db = new NpgsqlConnection(Config.PostgresConnString);
|
||||||
if (guild.HasValue) cs += ";searchpath=guild_" + guild.Value;
|
|
||||||
|
|
||||||
var db = new NpgsqlConnection(cs);
|
|
||||||
await db.OpenAsync();
|
await db.OpenAsync();
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,8 @@ namespace Kerobot
|
||||||
var client = new DiscordSocketClient(new DiscordSocketConfig()
|
var client = new DiscordSocketClient(new DiscordSocketConfig()
|
||||||
{
|
{
|
||||||
DefaultRetryMode = RetryMode.AlwaysRetry,
|
DefaultRetryMode = RetryMode.AlwaysRetry,
|
||||||
MessageCacheSize = 0 // using our own
|
MessageCacheSize = 0, // using our own
|
||||||
|
LogLevel = LogSeverity.Info
|
||||||
});
|
});
|
||||||
|
|
||||||
// Kerobot class initialization - will set up services and modules
|
// Kerobot class initialization - will set up services and modules
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
using Discord.WebSocket;
|
using System;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Npgsql;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Kerobot.Services.GuildState
|
namespace Kerobot.Services.GuildState
|
||||||
{
|
{
|
||||||
|
@ -14,8 +13,6 @@ namespace Kerobot.Services.GuildState
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class GuildStateService : Service
|
class GuildStateService : Service
|
||||||
{
|
{
|
||||||
// SCHEMAS ARE CREATED HERE, SOMEWHERE BELOW. MAKE SURE THIS CONSTRUCTOR IS CALLED EARLY.
|
|
||||||
|
|
||||||
private readonly object _storageLock = new object();
|
private readonly object _storageLock = new object();
|
||||||
private readonly Dictionary<ulong, Dictionary<Type, StateInfo>> _storage;
|
private readonly Dictionary<ulong, Dictionary<Type, StateInfo>> _storage;
|
||||||
|
|
||||||
|
@ -24,8 +21,7 @@ namespace Kerobot.Services.GuildState
|
||||||
public GuildStateService(Kerobot kb) : base(kb)
|
public GuildStateService(Kerobot kb) : base(kb)
|
||||||
{
|
{
|
||||||
_storage = new Dictionary<ulong, Dictionary<Type, StateInfo>>();
|
_storage = new Dictionary<ulong, Dictionary<Type, StateInfo>>();
|
||||||
|
CreateDatabaseTablesAsync().Wait();
|
||||||
_defaultGuildJson = PreloadDefaultGuildJson();
|
|
||||||
|
|
||||||
kb.DiscordClient.GuildAvailable += DiscordClient_GuildAvailable;
|
kb.DiscordClient.GuildAvailable += DiscordClient_GuildAvailable;
|
||||||
kb.DiscordClient.JoinedGuild += DiscordClient_JoinedGuild;
|
kb.DiscordClient.JoinedGuild += DiscordClient_JoinedGuild;
|
||||||
|
@ -48,31 +44,11 @@ namespace Kerobot.Services.GuildState
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes guild in-memory and database structures, then attempts to load configuration.
|
/// Initializes guild in-memory structures and attempts to load configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task InitializeGuild(SocketGuild arg)
|
private async Task InitializeGuild(SocketGuild arg)
|
||||||
{
|
{
|
||||||
// Get this done before any other thing.
|
// We're only loading config here now.
|
||||||
await CreateSchema(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);
|
bool success = await LoadGuildConfiguration(arg.Id);
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
|
@ -118,6 +94,7 @@ namespace Kerobot.Services.GuildState
|
||||||
{
|
{
|
||||||
|
|
||||||
var jstr = await RetrieveConfiguration(guildId);
|
var jstr = await RetrieveConfiguration(guildId);
|
||||||
|
if (jstr == null) jstr = await RetrieveDefaultConfiguration();
|
||||||
int jstrHash = jstr.GetHashCode();
|
int jstrHash = jstr.GetHashCode();
|
||||||
JObject guildConf;
|
JObject guildConf;
|
||||||
try
|
try
|
||||||
|
@ -180,59 +157,47 @@ namespace Kerobot.Services.GuildState
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Database
|
#region Database
|
||||||
/// <summary>
|
|
||||||
/// Creates a schema for holding all guild data.
|
|
||||||
/// Ensure that this runs first before any other database call to a guild.
|
|
||||||
/// </summary>
|
|
||||||
private async Task CreateSchema(ulong guildId)
|
|
||||||
{
|
|
||||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = $"create schema if not exists guild_{guildId}";
|
|
||||||
await c.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const string DBTableName = "guild_configuration";
|
const string DBTableName = "guild_configuration";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the table structures for holding module configuration.
|
/// Creates the table structures for holding guild configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override async Task CreateDatabaseTablesAsync(NpgsqlConnection db)
|
private async Task CreateDatabaseTablesAsync()
|
||||||
|
{
|
||||||
|
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
|
||||||
{
|
{
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
c.CommandText = $"create table if not exists {DBTableName} ("
|
c.CommandText = $"create table if not exists {DBTableName} ("
|
||||||
+ $"rev_id SERIAL primary key, "
|
+ $"rev_id SERIAL primary key, "
|
||||||
|
+ "guild_id bigint not null, "
|
||||||
+ "author bigint not null, "
|
+ "author bigint not null, "
|
||||||
+ "rev_date timestamptz not null default NOW(), "
|
+ "rev_date timestamptz not null default NOW(), "
|
||||||
+ "config_json text not null"
|
+ "config_json text not null"
|
||||||
+ ")";
|
+ ")";
|
||||||
await c.ExecuteNonQueryAsync();
|
await c.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creating default configuration with revision ID 0.
|
// 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
|
// Config ID 0 is used when no other configurations can be loaded for a guild.
|
||||||
// the serial should start at 2, but rather can easily start at 1. So lazy.
|
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
c.CommandText = $"insert into {DBTableName} (rev_id, author, config_json)"
|
c.CommandText = $"insert into {DBTableName} (rev_id, guild_id, author, config_json) "
|
||||||
+ "values (0, 0, @Json) "
|
+ "values (0, 0, 0, @Json) "
|
||||||
+ "on conflict (rev_id) do nothing";
|
+ "on conflict (rev_id) do nothing";
|
||||||
c.Parameters.Add("@Json", NpgsqlTypes.NpgsqlDbType.Text).Value = _defaultGuildJson;
|
c.Parameters.Add("@Json", NpgsqlTypes.NpgsqlDbType.Text).Value = PreloadDefaultGuildJson();
|
||||||
c.Prepare();
|
c.Prepare();
|
||||||
await c.ExecuteNonQueryAsync();
|
await c.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string> RetrieveConfiguration(ulong guildId)
|
private async Task<string> RetrieveConfiguration(ulong guildId)
|
||||||
{
|
{
|
||||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(guildId))
|
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
|
||||||
{
|
{
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
c.CommandText = $"select config_json from {DBTableName} "
|
c.CommandText = $"select config_json from {DBTableName} where guild_id = {guildId} "
|
||||||
+ "order by rev_id desc limit 1";
|
+ "order by rev_id desc limit 1";
|
||||||
using (var r = await c.ExecuteReaderAsync())
|
using (var r = await c.ExecuteReaderAsync())
|
||||||
{
|
{
|
||||||
|
@ -245,10 +210,25 @@ namespace Kerobot.Services.GuildState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string> RetrieveDefaultConfiguration()
|
||||||
|
{
|
||||||
|
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
|
||||||
|
{
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = $"select config_json from {DBTableName} where rev_id = 0";
|
||||||
|
using (var r = await c.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
if (await r.ReadAsync()) return r.GetString(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Exception("Unable to retrieve fallback configuration.");
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
// Default guild configuration JSON is embedded in assembly. Retrieving and caching it here.
|
// Default guild configuration JSON is embedded in assembly.
|
||||||
private readonly string _defaultGuildJson;
|
|
||||||
private string PreloadDefaultGuildJson()
|
private string PreloadDefaultGuildJson()
|
||||||
{
|
{
|
||||||
const string ResourceName = "Kerobot.DefaultGuildConfig.json";
|
const string ResourceName = "Kerobot.DefaultGuildConfig.json";
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
using Discord;
|
using System;
|
||||||
using Npgsql;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Discord;
|
||||||
|
using NpgsqlTypes;
|
||||||
|
|
||||||
namespace Kerobot.Services.Logging
|
namespace Kerobot.Services.Logging
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements logging for the whole program.
|
/// Implements logging. Logging is distinguished into two types: Instance and per-guild.
|
||||||
|
/// Instance logs are messages of varying importance to the bot operator. Guild logs are messages that can be seen
|
||||||
|
/// by moderators of a particular guild. All log messages are backed by database.
|
||||||
|
/// Instance logs are stored as guild ID 0.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class LoggingService : Service
|
class LoggingService : Service
|
||||||
{
|
{
|
||||||
|
@ -16,13 +18,8 @@ namespace Kerobot.Services.Logging
|
||||||
|
|
||||||
internal LoggingService(Kerobot kb) : base(kb)
|
internal LoggingService(Kerobot kb) : base(kb)
|
||||||
{
|
{
|
||||||
// Create global instance log table
|
// Create logging table
|
||||||
async Task CreateGlobalTable()
|
CreateDatabaseTablesAsync().Wait();
|
||||||
{
|
|
||||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
|
|
||||||
await CreateDatabaseTablesAsync(db);
|
|
||||||
}
|
|
||||||
CreateGlobalTable().Wait();
|
|
||||||
|
|
||||||
// Discord.Net log handling (client logging option is specified in Program.cs)
|
// Discord.Net log handling (client logging option is specified in Program.cs)
|
||||||
kb.DiscordClient.Log += DiscordClient_Log;
|
kb.DiscordClient.Log += DiscordClient_Log;
|
||||||
|
@ -34,7 +31,7 @@ namespace Kerobot.Services.Logging
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Discord.Net logging events handled here.
|
/// Discord.Net logging events handled here.
|
||||||
/// Only events with high severity are placed in the log. Others are just printed to console.
|
/// Only events with high importance are kept. Others are just printed to console.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task DiscordClient_Log(LogMessage arg)
|
private async Task DiscordClient_Log(LogMessage arg)
|
||||||
{
|
{
|
||||||
|
@ -43,37 +40,45 @@ namespace Kerobot.Services.Logging
|
||||||
string msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
|
string msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
|
||||||
const string logSource = "Discord.Net";
|
const string logSource = "Discord.Net";
|
||||||
|
|
||||||
if (important)
|
if (important) await DoInstanceLogAsync(true, logSource, msg);
|
||||||
{
|
else FormatToConsole(DateTimeOffset.UtcNow, logSource, msg);
|
||||||
// Note: Using external method here!
|
|
||||||
await Kerobot.InstanceLogAsync(true, logSource, msg);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
FormatToConsole(DateTimeOffset.UtcNow, logSource, msg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Database
|
||||||
const string TableLog = "program_log";
|
const string TableLog = "program_log";
|
||||||
public override async Task CreateDatabaseTablesAsync(NpgsqlConnection db)
|
private async Task CreateDatabaseTablesAsync()
|
||||||
|
{
|
||||||
|
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
|
||||||
{
|
{
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
c.CommandText = $"create table if not exists {TableLog} ("
|
c.CommandText = $"create table if not exists {TableLog} ("
|
||||||
+ "log_id serial primary key, "
|
+ "log_id serial primary key, "
|
||||||
|
+ "guild_id bigint not null, "
|
||||||
+ "log_timestamp timestamptz not null, "
|
+ "log_timestamp timestamptz not null, "
|
||||||
+ "log_source text not null, "
|
+ "log_source text not null, "
|
||||||
+ "message text not null"
|
+ "message text not null"
|
||||||
+ ")";
|
+ ")";
|
||||||
await c.ExecuteNonQueryAsync();
|
await c.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = "create index if not exists " +
|
||||||
|
$"{TableLog}_guildid_idx on {TableLog} guild_id";
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
private async Task TableInsertAsync(NpgsqlConnection db, DateTimeOffset timestamp, string source, string message)
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
private async Task TableInsertAsync(ulong guildId, DateTimeOffset timestamp, string source, string message)
|
||||||
|
{
|
||||||
|
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
|
||||||
{
|
{
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
c.CommandText = $"insert into {TableLog} (log_timestamp, log_source, message) values"
|
c.CommandText = $"insert into {TableLog} (guild_id, log_timestamp, log_source, message) values"
|
||||||
+ "(@Ts, @Src, @Msg)";
|
+ "(@Gid, @Ts, @Src, @Msg)";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = guildId;
|
||||||
c.Parameters.Add("@Ts", NpgsqlDbType.TimestampTZ).Value = timestamp;
|
c.Parameters.Add("@Ts", NpgsqlDbType.TimestampTZ).Value = timestamp;
|
||||||
c.Parameters.Add("@Src", NpgsqlDbType.Text).Value = source;
|
c.Parameters.Add("@Src", NpgsqlDbType.Text).Value = source;
|
||||||
c.Parameters.Add("@Msg", NpgsqlDbType.Text).Value = message;
|
c.Parameters.Add("@Msg", NpgsqlDbType.Text).Value = message;
|
||||||
|
@ -81,6 +86,8 @@ namespace Kerobot.Services.Logging
|
||||||
await c.ExecuteNonQueryAsync();
|
await c.ExecuteNonQueryAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All console writes originate here.
|
/// All console writes originate here.
|
||||||
|
@ -106,17 +113,15 @@ namespace Kerobot.Services.Logging
|
||||||
Exception insertException = null;
|
Exception insertException = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
|
await TableInsertAsync(0, DateTimeOffset.UtcNow, source, message);
|
||||||
{
|
|
||||||
await TableInsertAsync(db, DateTimeOffset.UtcNow, source, message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// This is not good. Resorting to plain console write to report the issue.
|
// Not good. Resorting to plain console write to report the error.
|
||||||
// Let's hope a warning reaches the reporting channel.
|
|
||||||
Console.WriteLine("!!! Error during recording to instance log: " + ex.Message);
|
Console.WriteLine("!!! Error during recording to instance log: " + ex.Message);
|
||||||
Console.WriteLine(ex.StackTrace);
|
Console.WriteLine(ex.StackTrace);
|
||||||
|
|
||||||
|
// Attempt to pass this error to the reporting channel.
|
||||||
insertException = ex;
|
insertException = ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,14 +180,11 @@ namespace Kerobot.Services.Logging
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(guild))
|
await TableInsertAsync(guild, DateTimeOffset.UtcNow, source, message);
|
||||||
{
|
|
||||||
await TableInsertAsync(db, DateTimeOffset.UtcNow, source, message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Probably a bad idea, but...
|
// This is probably a terrible idea, but...
|
||||||
await DoInstanceLogAsync(true, this.Name, "Failed to store guild log item: " + ex.Message);
|
await DoInstanceLogAsync(true, this.Name, "Failed to store guild log item: " + ex.Message);
|
||||||
// Stack trace goes to console only.
|
// Stack trace goes to console only.
|
||||||
FormatToConsole(DateTime.UtcNow, this.Name, ex.StackTrace);
|
FormatToConsole(DateTime.UtcNow, this.Name, ex.StackTrace);
|
||||||
|
|
|
@ -17,14 +17,6 @@ namespace Kerobot.Services
|
||||||
|
|
||||||
public Service(Kerobot kb) => Kerobot = kb;
|
public Service(Kerobot kb) => Kerobot = kb;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes database tables per-guild.
|
|
||||||
/// This method is called by GuildStateService 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