Fully implemented text channel cache

Additionally, fixed a bug where user cache would quit immediately
after encountering a bot or webhook user.
This commit is contained in:
Noikoio 2018-04-04 17:25:13 -07:00
parent e5758e9aac
commit 84d3b1665d
4 changed files with 79 additions and 24 deletions

View file

@ -13,8 +13,6 @@ namespace Noikoio.RegexBot.EntityCache
/// </summary>
class ECModule : BotModule
{
private readonly DatabaseConfig _db;
public ECModule(DiscordSocketClient client) : base(client)
{
if (RegexBot.Config.DatabaseAvailable)
@ -26,6 +24,8 @@ 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
{
@ -33,6 +33,18 @@ namespace Noikoio.RegexBot.EntityCache
}
}
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)
{

View file

@ -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<SocketGuildUser> 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<SocketGuildChannel> 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
}
}

View file

@ -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;
}

View file

@ -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
/// </summary>
public int Id => _logId;
/// <summary>
/// Gets the timestamp (a <see cref="DateTime"/> with <see cref="DateTimeKind.Utc"/>) of the entry.
/// Gets the UTC timestamp of the entry.
/// </summary>
public DateTime Timestamp => _ts;
public DateTimeOffset Timestamp => _ts;
/// <summary>
/// Gets the ID of the guild to which this log entry corresponds.
/// </summary>
@ -37,9 +37,10 @@ namespace Noikoio.RegexBot.Module.ModLogs
public ulong Target => _targetId;
/// <summary>
/// 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 <see cref="Target"/> if this entry was created through
/// action of another user, such as the issuer of notes and warnings.
/// </summary>
public ulong? Invoker => _invokeId;
public ulong Invoker => _invokeId;
/// <summary>
/// Gets the guild channel ID to which this log entry corresponds, if any.
/// </summary>
@ -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";
/// <summary>
/// Attempts to look up a log entry by its ID.