diff --git a/EntityCache/ECModule.cs b/EntityCache/ECModule.cs index 7223139..af813c1 100644 --- a/EntityCache/ECModule.cs +++ b/EntityCache/ECModule.cs @@ -13,8 +13,6 @@ namespace Noikoio.RegexBot.EntityCache /// class ECModule : BotModule { - private readonly DatabaseConfig _db; - public ECModule(DiscordSocketClient client) : base(client) { if (RegexBot.Config.DatabaseAvailable) @@ -26,13 +24,27 @@ namespace Noikoio.RegexBot.EntityCache client.GuildMemberUpdated += Client_GuildMemberUpdated; client.UserJoined += Client_UserJoined; client.UserLeft += Client_UserLeft; + client.ChannelCreated += Client_ChannelCreated; + client.ChannelUpdated += Client_ChannelUpdated; } else { Log("No database storage available.").Wait(); } } - + + private async Task Client_ChannelUpdated(SocketChannel arg1, SocketChannel arg2) + { + if (arg2 is SocketGuildChannel ch) + await SqlHelper.UpdateGuildChannelAsync(ch); + } + + private async Task Client_ChannelCreated(SocketChannel arg) + { + if (arg is SocketGuildChannel ch) + await SqlHelper.UpdateGuildChannelAsync(ch); + } + // Guild and guild member information has become available. // This is a very expensive operation, especially when joining larger guilds. private async Task Client_GuildAvailable(SocketGuild arg) @@ -43,6 +55,7 @@ namespace Noikoio.RegexBot.EntityCache { await SqlHelper.UpdateGuildAsync(arg); await SqlHelper.UpdateGuildMemberAsync(arg.Users); + await SqlHelper.UpdateGuildChannelAsync(arg.Channels); } catch (NpgsqlException ex) { diff --git a/EntityCache/SqlHelper.cs b/EntityCache/SqlHelper.cs index bd666e9..bd932bb 100644 --- a/EntityCache/SqlHelper.cs +++ b/EntityCache/SqlHelper.cs @@ -1,4 +1,5 @@ -using Discord.WebSocket; +using Discord; +using Discord.WebSocket; using NpgsqlTypes; using System; using System.Collections.Generic; @@ -40,13 +41,20 @@ namespace Noikoio.RegexBot.EntityCache using (var c = db.CreateCommand()) { c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableTextChannel + " (" - + "channel_id bigint not null primary key, " + + "channel_id bigint not null, " + $"guild_id bigint not null references {TableGuild}, " + "cache_date timestamptz not null, " + "channel_name text not 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 " + + $"{TableTextChannel}_ck_idx on {TableTextChannel} (channel_id, guild_id)"; + 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. @@ -66,7 +74,7 @@ namespace Noikoio.RegexBot.EntityCache } using (var c = db.CreateCommand()) { - // guild_id is a foreign key, and also one half of the primary key here + // compound primary key c.CommandText = "CREATE UNIQUE INDEX IF NOT EXISTS " + $"{TableUser}_ck_idx on {TableUser} (user_id, guild_id)"; await c.ExecuteNonQueryAsync(); @@ -103,10 +111,7 @@ namespace Noikoio.RegexBot.EntityCache } internal static Task UpdateGuildMemberAsync(SocketGuildUser user) - { - var ml = new SocketGuildUser[] { user }; - return UpdateGuildMemberAsync(ml); - } + => UpdateGuildMemberAsync(new SocketGuildUser[] { user }); internal static async Task UpdateGuildMemberAsync(IEnumerable users) { var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync(); @@ -150,6 +155,41 @@ namespace Noikoio.RegexBot.EntityCache } } } + + internal static Task UpdateGuildChannelAsync(SocketGuildChannel channel) + => UpdateGuildChannelAsync(new SocketGuildChannel[] { channel }); + internal static async Task UpdateGuildChannelAsync(IEnumerable channels) + { + var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync(); + if (db == null) return; + using (db) + { + using (var c = db.CreateCommand()) + { + c.CommandText = "INSERT INTO " + TableTextChannel + + " (channel_id, guild_id, cache_date, channel_name)" + + " VALUES (@Cid, @Gid, @Date, @Name) " + + "ON CONFLICT (channel_id, guild_id) DO UPDATE SET " + + "cache_date = EXCLUDED.cache_date, channel_name = EXCLUDED.channel_name"; + + var cid = c.Parameters.Add("@Cid", NpgsqlDbType.Bigint); + var gid = c.Parameters.Add("@Gid", NpgsqlDbType.Bigint); + c.Parameters.Add("@Date", NpgsqlDbType.TimestampTZ).Value = DateTime.Now; + var cname = c.Parameters.Add("@Name", NpgsqlDbType.Text); + c.Prepare(); + + foreach (var item in channels) + { + if (!(item is ITextChannel ich)) continue; + + cid.Value = item.Id; + gid.Value = item.Guild.Id; + cname.Value = item.Name; + await c.ExecuteNonQueryAsync(); + } + } + } + } #endregion } } diff --git a/Module/ModCommands/Commands/_CommandBase.cs b/Module/ModCommands/Commands/_CommandBase.cs index 9525ebf..92e02ac 100644 --- a/Module/ModCommands/Commands/_CommandBase.cs +++ b/Module/ModCommands/Commands/_CommandBase.cs @@ -142,7 +142,7 @@ namespace Noikoio.RegexBot.Module.ModCommands.Commands try { - cdata = (await EntityCache.EntityCache.QueryAsync(guild, input)) + cdata = (await EntityCache.EntityCache.QueryUserAsync(guild, input)) .FirstOrDefault(); if (cdata != null) uid = cdata.UserId; } diff --git a/Module/ModLogs/LogEntry.cs b/Module/ModLogs/LogEntry.cs index 4e96364..c47d05d 100644 --- a/Module/ModLogs/LogEntry.cs +++ b/Module/ModLogs/LogEntry.cs @@ -11,9 +11,9 @@ namespace Noikoio.RegexBot.Module.ModLogs class LogEntry { readonly int _logId; - readonly DateTime _ts; + readonly DateTimeOffset _ts; readonly ulong _guildId; - readonly ulong? _invokeId; + readonly ulong _invokeId; readonly ulong _targetId; readonly ulong? _channelId; readonly LogType _type; @@ -24,9 +24,9 @@ namespace Noikoio.RegexBot.Module.ModLogs /// public int Id => _logId; /// - /// Gets the timestamp (a with ) of the entry. + /// Gets the UTC timestamp of the entry. /// - public DateTime Timestamp => _ts; + public DateTimeOffset Timestamp => _ts; /// /// Gets the ID of the guild to which this log entry corresponds. /// @@ -37,9 +37,10 @@ namespace Noikoio.RegexBot.Module.ModLogs public ulong Target => _targetId; /// /// Gets the ID of the invoking user. - /// This value exists only if this entry was created through action of another user that is not the target. + /// This value differs from if this entry was created through + /// action of another user, such as the issuer of notes and warnings. /// - public ulong? Invoker => _invokeId; + public ulong Invoker => _invokeId; /// /// Gets the guild channel ID to which this log entry corresponds, if any. /// @@ -63,8 +64,7 @@ namespace Noikoio.RegexBot.Module.ModLogs { _guildId = (ulong)r.GetInt64(2); _targetId = (ulong)r.GetInt64(3); - if (r.IsDBNull(4)) _invokeId = null; - else _invokeId = (ulong)r.GetInt64(4); + _invokeId = (ulong)r.GetInt64(4); if (r.IsDBNull(5)) _channelId = null; else _channelId = (ulong)r.GetInt64(5); } @@ -144,12 +144,14 @@ namespace Noikoio.RegexBot.Module.ModLogs + "id int primary key, " + "entry_ts timestamptz not null, " + "guild_id bigint not null, " - + "target_id bigint not null, " - + $"invoke_id bigint null references {EntityCache.SqlHelper.TableUser}.user_id, " - + "target_channel_id bigint null, " // TODO channel cache reference? + + "target_id bigint not null, " // No foreign constraint: some targets may not be cached + + "invoker_id bigint not null, " + + "target_channel_id bigint null, " + "entry_type integer not null, " + "message text not null, " - + $"FOREIGN KEY (target_id, guild_id) REFERENCES {EntityCache.SqlHelper.TableUser} (user_id, guild_id)"; + + $"FOREIGN KEY (invoker_id, guild_id) REFERENCES {EntityCache.SqlHelper.TableUser} (user_id, guild_id), " + + $"FOREIGN KEY (target_channel_id, guild_id) REFERENCES {EntityCache.SqlHelper.TableTextChannel} (channel_id, guild_id)" + + ")"; c.ExecuteNonQuery(); } using (var c = db.CreateCommand()) @@ -162,7 +164,7 @@ namespace Noikoio.RegexBot.Module.ModLogs } // Double-check constructor if making changes to this constant - const string QueryColumns = "id, entry_ts, guild_id, target_id, invoke_id, target_channel_id, entry_type, message"; + const string QueryColumns = "id, entry_ts, guild_id, target_id, invoker_id, target_channel_id, entry_type, message"; /// /// Attempts to look up a log entry by its ID.