diff --git a/Module/EntityCache/EntityCache.cs b/EntityCache/Module.cs similarity index 85% rename from Module/EntityCache/EntityCache.cs rename to EntityCache/Module.cs index 352d436..d2cb8dd 100644 --- a/Module/EntityCache/EntityCache.cs +++ b/EntityCache/Module.cs @@ -7,37 +7,26 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -namespace Noikoio.RegexBot.Module.EntityCache +namespace Noikoio.RegexBot.EntityCache { /// - /// Caches information regarding all known guilds, channels, and users. + /// Bot module portion of the entity cache. Caches information regarding all known guilds, channels, and users. /// The function of this module should be transparent to the user, and thus no configuration is needed. /// This module should be initialized BEFORE any other modules that make use of guild and user cache. /// - class EntityCache : BotModule + class Module : BotModule { - /* - * Future plans: - * Have this, or something connected to this class, be accessible throughout the bot. - * - * There should be a system that holds a small in-memory cache of users (as EntityCache objects) - * for quick lookups by other parts of the bot. - * Without this system, we'll be having future bot features constantly querying the database - * on their own to look up entity cache records, which (among other things) could result in - * a race conditions where an event becomes aware of a user before it has been recorded. - */ - private readonly DatabaseConfig _db; public override string Name => nameof(EntityCache); - - public EntityCache(DiscordSocketClient client) : base(client) + + public Module(DiscordSocketClient client) : base(client) { _db = RegexBot.Config.Database; if (_db.Available) { - Sql.CreateCacheTables(); + SqlHelper.CreateCacheTables(); client.GuildAvailable += Client_GuildAvailable; client.GuildUpdated += Client_GuildUpdated; @@ -49,12 +38,12 @@ namespace Noikoio.RegexBot.Module.EntityCache Log("No database storage available.").Wait(); } } - + public override Task ProcessConfiguration(JToken configSection) => Task.FromResult(null); #region Event handling // Guild and guild member information has become available. - // This is a very expensive operation, when joining larger guilds for the first time. + // This is a very expensive operation, especially when joining larger guilds. private async Task Client_GuildAvailable(SocketGuild arg) { await Task.Run(async () => @@ -124,7 +113,7 @@ namespace Noikoio.RegexBot.Module.EntityCache + "cache_date = EXCLUDED.cache_date, username = EXCLUDED.username, " + "discriminator = EXCLUDED.discriminator, " // I've seen someone's discriminator change this one time... + "nickname = EXCLUDED.nickname, avatar_url = EXCLUDED.avatar_url"; - + var uid = c.Parameters.Add("@Uid", NpgsqlDbType.Bigint); var gid = c.Parameters.Add("@Gid", NpgsqlDbType.Bigint); c.Parameters.Add("@Date", NpgsqlDbType.TimestampTZ).Value = DateTime.Now; diff --git a/EntityCache/SqlHelper.cs b/EntityCache/SqlHelper.cs new file mode 100644 index 0000000..39d3270 --- /dev/null +++ b/EntityCache/SqlHelper.cs @@ -0,0 +1,98 @@ +using Npgsql; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Noikoio.RegexBot.EntityCache +{ + /// + /// Helper methods for database operations. + /// + static class SqlHelper + { + public const string TableGuild = "cache_guild"; + public const string TableTextChannel = "cache_textchannel"; + public const string TableUser = "cache_users"; + + private static async Task OpenDB() + { + if (!RegexBot.Config.Database.Available) return null; + return await RegexBot.Config.Database.GetOpenConnectionAsync(); + } + + public static async Task CreateCacheTables() + { + var db = await OpenDB(); + if (db == null) return; + using (db) + { + // Guild cache + using (var c = db.CreateCommand()) + { + c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableGuild + " (" + + "guild_id bigint primary key, " + + "cache_date timestamptz not null, " + + "current_name text not null, " + + "display_name text null" + + ")"; + await c.ExecuteNonQueryAsync(); + } + // May not require other indexes. Add here if they become necessary. + + // Text channel cache + using (var c = db.CreateCommand()) + { + c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableTextChannel + " (" + + "channel_id bigint not null primary key, " + + $"guild_id bigint not null references {TableGuild}, " + + "cache_date timestamptz not null, " + + "name text not null"; + await c.ExecuteNonQueryAsync(); + } + // As of the time of this commit, Discord doesn't allow any uppercase characters + // in channel names. No lowercase name index needed. + + // User cache + using (var c = db.CreateCommand()) + { + c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableUser + " (" + + "user_id bigint not null, " + + $"guild_id bigint not null references {TableGuild}, " + + "cache_date timestamptz not null, " + + "username text not null, " + + "discriminator text not null, " + + "nickname text null, " + + "avatar_url text null" + + ")"; + await c.ExecuteNonQueryAsync(); + } + using (var c = db.CreateCommand()) + { + // guild_id is a foreign key, and also one half of the primary key here + c.CommandText = "CREATE UNIQUE INDEX IF NOT EXISTS " + + $"{TableUser}_ck_idx on {TableUser} (user_id, guild_id)"; + await c.ExecuteNonQueryAsync(); + } + using (var c = db.CreateCommand()) + { + c.CommandText = "CREATE INDEX IF NOT EXISTS " + + $"{TableUser}_usersearch_idx on {TableUser} LOWER(username)"; + await c.ExecuteNonQueryAsync(); + } + } + } + + #region Insertions and updates + static async Task UpdateGuild() + { + var db = await OpenDB(); + if (db == null) return; + using (db) + { + + } + } + #endregion + } +} diff --git a/Module/EntityCache/Sql.cs b/Module/EntityCache/Sql.cs deleted file mode 100644 index ed55b0b..0000000 --- a/Module/EntityCache/Sql.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Npgsql; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Noikoio.RegexBot.Module.EntityCache -{ - /// - /// Contains common constants and static methods for cache access. - /// - class Sql - { - public const string TableGuild = "cache_guild"; - public const string TableUser = "cache_users"; - - private static NpgsqlConnection OpenDB() => - RegexBot.Config.Database.GetOpenConnectionAsync().GetAwaiter().GetResult(); - - public static void CreateCacheTables() - { - using (var db = OpenDB()) - { - using (var c = db.CreateCommand()) - { - c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableGuild + "(" - + "guild_id bigint primary key, " - + "current_name text not null, " - + "display_name text null" - + ")"; - c.ExecuteNonQuery(); - } - - using (var c = db.CreateCommand()) - { - c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableUser + "(" - + "user_id bigint not null, " - + $"guild_id bigint not null references {TableGuild}, " - + "cache_date timestamptz not null, " - + "username text not null, " - + "discriminator text not null, " - + "nickname text null, " - + "avatar_url text null" - + ")"; - c.ExecuteNonQuery(); - } - using (var c = db.CreateCommand()) - { - c.CommandText = "CREATE UNIQUE INDEX IF NOT EXISTS " - + $"{TableUser}_idx on {TableUser} (user_id, guild_id)"; - c.ExecuteNonQuery(); - } - // TODO create indexes for string-based queries - } - } - } -} diff --git a/RegexBot.cs b/RegexBot.cs index 97e8b8f..019cead 100644 --- a/RegexBot.cs +++ b/RegexBot.cs @@ -48,7 +48,7 @@ namespace Noikoio.RegexBot new Module.AutoMod.AutoMod(_client), new Module.ModTools.ModTools(_client), new Module.AutoRespond.AutoRespond(_client), - new Module.EntityCache.EntityCache(_client) // EntityCache goes before anything else that uses its data + new EntityCache.Module(_client) // EntityCache goes before anything else that uses its data }; var dlog = Logger.GetLogger("Discord.Net"); _client.Log += async (arg) =>