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">
|
||||
/// If manipulating guild-specific information, this parameter sets the database connection's search path.
|
||||
/// </param>
|
||||
internal async Task<NpgsqlConnection> GetOpenNpgsqlConnectionAsync(ulong? guild)
|
||||
internal async Task<NpgsqlConnection> GetOpenNpgsqlConnectionAsync()
|
||||
{
|
||||
string cs = Config.PostgresConnString;
|
||||
if (guild.HasValue) cs += ";searchpath=guild_" + guild.Value;
|
||||
|
||||
var db = new NpgsqlConnection(cs);
|
||||
var db = new NpgsqlConnection(Config.PostgresConnString);
|
||||
await db.OpenAsync();
|
||||
return db;
|
||||
}
|
||||
|
|
|
@ -59,7 +59,8 @@ namespace Kerobot
|
|||
var client = new DiscordSocketClient(new DiscordSocketConfig()
|
||||
{
|
||||
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
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Npgsql;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Kerobot.Services.GuildState
|
||||
{
|
||||
|
@ -14,8 +13,6 @@ namespace Kerobot.Services.GuildState
|
|||
/// </summary>
|
||||
class GuildStateService : Service
|
||||
{
|
||||
// SCHEMAS ARE CREATED HERE, SOMEWHERE BELOW. MAKE SURE THIS CONSTRUCTOR IS CALLED EARLY.
|
||||
|
||||
private readonly object _storageLock = new object();
|
||||
private readonly Dictionary<ulong, Dictionary<Type, StateInfo>> _storage;
|
||||
|
||||
|
@ -24,8 +21,7 @@ namespace Kerobot.Services.GuildState
|
|||
public GuildStateService(Kerobot kb) : base(kb)
|
||||
{
|
||||
_storage = new Dictionary<ulong, Dictionary<Type, StateInfo>>();
|
||||
|
||||
_defaultGuildJson = PreloadDefaultGuildJson();
|
||||
CreateDatabaseTablesAsync().Wait();
|
||||
|
||||
kb.DiscordClient.GuildAvailable += DiscordClient_GuildAvailable;
|
||||
kb.DiscordClient.JoinedGuild += DiscordClient_JoinedGuild;
|
||||
|
@ -48,31 +44,11 @@ namespace Kerobot.Services.GuildState
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes guild in-memory and database structures, then attempts to load configuration.
|
||||
/// Initializes guild in-memory structures and attempts to load configuration.
|
||||
/// </summary>
|
||||
private async Task InitializeGuild(SocketGuild arg)
|
||||
{
|
||||
// Get this done before any other thing.
|
||||
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
|
||||
// We're only loading config here now.
|
||||
bool success = await LoadGuildConfiguration(arg.Id);
|
||||
if (!success)
|
||||
{
|
||||
|
@ -118,6 +94,7 @@ namespace Kerobot.Services.GuildState
|
|||
{
|
||||
|
||||
var jstr = await RetrieveConfiguration(guildId);
|
||||
if (jstr == null) jstr = await RetrieveDefaultConfiguration();
|
||||
int jstrHash = jstr.GetHashCode();
|
||||
JObject guildConf;
|
||||
try
|
||||
|
@ -180,59 +157,47 @@ namespace Kerobot.Services.GuildState
|
|||
}
|
||||
|
||||
#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";
|
||||
/// <summary>
|
||||
/// Creates the table structures for holding module configuration.
|
||||
/// Creates the table structures for holding guild configuration.
|
||||
/// </summary>
|
||||
public override async Task CreateDatabaseTablesAsync(NpgsqlConnection db)
|
||||
private async Task CreateDatabaseTablesAsync()
|
||||
{
|
||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
|
||||
{
|
||||
using (var c = db.CreateCommand())
|
||||
{
|
||||
c.CommandText = $"create table if not exists {DBTableName} ("
|
||||
+ $"rev_id SERIAL primary key, "
|
||||
+ "guild_id bigint not null, "
|
||||
+ "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.
|
||||
// Config ID 0 is used when no other configurations can be loaded for a guild.
|
||||
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, guild_id, author, config_json) "
|
||||
+ "values (0, 0, 0, @Json) "
|
||||
+ "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();
|
||||
await c.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
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";
|
||||
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
|
||||
|
||||
// Default guild configuration JSON is embedded in assembly. Retrieving and caching it here.
|
||||
private readonly string _defaultGuildJson;
|
||||
// Default guild configuration JSON is embedded in assembly.
|
||||
private string PreloadDefaultGuildJson()
|
||||
{
|
||||
const string ResourceName = "Kerobot.DefaultGuildConfig.json";
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
using Discord;
|
||||
using Npgsql;
|
||||
using NpgsqlTypes;
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Discord;
|
||||
using NpgsqlTypes;
|
||||
|
||||
namespace Kerobot.Services.Logging
|
||||
{
|
||||
/// <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>
|
||||
class LoggingService : Service
|
||||
{
|
||||
|
@ -16,13 +18,8 @@ namespace Kerobot.Services.Logging
|
|||
|
||||
internal LoggingService(Kerobot kb) : base(kb)
|
||||
{
|
||||
// Create global instance log table
|
||||
async Task CreateGlobalTable()
|
||||
{
|
||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
|
||||
await CreateDatabaseTablesAsync(db);
|
||||
}
|
||||
CreateGlobalTable().Wait();
|
||||
// Create logging table
|
||||
CreateDatabaseTablesAsync().Wait();
|
||||
|
||||
// Discord.Net log handling (client logging option is specified in Program.cs)
|
||||
kb.DiscordClient.Log += DiscordClient_Log;
|
||||
|
@ -34,7 +31,7 @@ namespace Kerobot.Services.Logging
|
|||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
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}";
|
||||
const string logSource = "Discord.Net";
|
||||
|
||||
if (important)
|
||||
{
|
||||
// Note: Using external method here!
|
||||
await Kerobot.InstanceLogAsync(true, logSource, msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
FormatToConsole(DateTimeOffset.UtcNow, logSource, msg);
|
||||
}
|
||||
if (important) await DoInstanceLogAsync(true, logSource, msg);
|
||||
else FormatToConsole(DateTimeOffset.UtcNow, logSource, msg);
|
||||
}
|
||||
|
||||
#region Database
|
||||
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())
|
||||
{
|
||||
c.CommandText = $"create table if not exists {TableLog} ("
|
||||
+ "log_id serial primary key, "
|
||||
+ "guild_id bigint not null, "
|
||||
+ "log_timestamp timestamptz not null, "
|
||||
+ "log_source text not null, "
|
||||
+ "message text not null"
|
||||
+ ")";
|
||||
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())
|
||||
{
|
||||
c.CommandText = $"insert into {TableLog} (log_timestamp, log_source, message) values"
|
||||
+ "(@Ts, @Src, @Msg)";
|
||||
c.CommandText = $"insert into {TableLog} (guild_id, log_timestamp, log_source, message) values"
|
||||
+ "(@Gid, @Ts, @Src, @Msg)";
|
||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = guildId;
|
||||
c.Parameters.Add("@Ts", NpgsqlDbType.TimestampTZ).Value = timestamp;
|
||||
c.Parameters.Add("@Src", NpgsqlDbType.Text).Value = source;
|
||||
c.Parameters.Add("@Msg", NpgsqlDbType.Text).Value = message;
|
||||
|
@ -81,6 +86,8 @@ namespace Kerobot.Services.Logging
|
|||
await c.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// All console writes originate here.
|
||||
|
@ -106,17 +113,15 @@ namespace Kerobot.Services.Logging
|
|||
Exception insertException = null;
|
||||
try
|
||||
{
|
||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
|
||||
{
|
||||
await TableInsertAsync(db, DateTimeOffset.UtcNow, source, message);
|
||||
}
|
||||
await TableInsertAsync(0, DateTimeOffset.UtcNow, source, message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This is not good. Resorting to plain console write to report the issue.
|
||||
// Let's hope a warning reaches the reporting channel.
|
||||
// Not good. Resorting to plain console write to report the error.
|
||||
Console.WriteLine("!!! Error during recording to instance log: " + ex.Message);
|
||||
Console.WriteLine(ex.StackTrace);
|
||||
|
||||
// Attempt to pass this error to the reporting channel.
|
||||
insertException = ex;
|
||||
}
|
||||
|
||||
|
@ -175,14 +180,11 @@ namespace Kerobot.Services.Logging
|
|||
{
|
||||
try
|
||||
{
|
||||
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(guild))
|
||||
{
|
||||
await TableInsertAsync(db, DateTimeOffset.UtcNow, source, message);
|
||||
}
|
||||
await TableInsertAsync(guild, DateTimeOffset.UtcNow, source, message);
|
||||
}
|
||||
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);
|
||||
// Stack trace goes to console only.
|
||||
FormatToConsole(DateTime.UtcNow, this.Name, ex.StackTrace);
|
||||
|
|
|
@ -17,14 +17,6 @@ namespace Kerobot.Services
|
|||
|
||||
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>
|
||||
/// Creates a log message.
|
||||
/// </summary>
|
||||
|
|
Loading…
Reference in a new issue