Added EntityCache class with CacheUser

Its purpose is documented in the comments within EntityCache.cs.
CacheUser has been implemented but is not yet tested.
This commit is contained in:
Noikoio 2017-12-22 22:20:06 -08:00
parent 2e0b408946
commit 1fbe4d5e52
4 changed files with 146 additions and 55 deletions

View file

@ -1,15 +1,18 @@
using System; using Discord.WebSocket;
using Npgsql;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.Common; using System.Data.Common;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Noikoio.RegexBot.Module.EntityCache namespace Noikoio.RegexBot.EntityCache
{ {
/// <summary> /// <summary>
/// Represents a cached user. /// Representation of a cached user.
/// </summary> /// </summary>
class UserCacheItem class CacheUser
{ {
readonly ulong _userId; readonly ulong _userId;
readonly ulong _guildId; readonly ulong _guildId;
@ -57,8 +60,20 @@ namespace Noikoio.RegexBot.Module.EntityCache
/// </summary> /// </summary>
public string AvatarUrl => _avatarUrl; public string AvatarUrl => _avatarUrl;
private CacheUser(SocketGuildUser u)
{
_userId = u.Id;
_guildId = u.Guild.Id;
_cacheDate = DateTime.UtcNow;
_username = u.Username;
_discriminator = u.Discriminator;
_nickname = u.Nickname;
_avatarUrl = u.GetAvatarUrl();
}
private UserCacheItem(DbDataReader r) // Double-check SqlHelper if making changes to this constant
const string QueryColumns = "user_id, guild_id, cache_date, username, discriminator, nickname, avatar_url";
private CacheUser(DbDataReader r)
{ {
// Double-check ordinals if making changes to QueryColumns // Double-check ordinals if making changes to QueryColumns
unchecked unchecked
@ -77,20 +92,32 @@ namespace Noikoio.RegexBot.Module.EntityCache
public override string ToString() => DisplayName; public override string ToString() => DisplayName;
#region Queries #region Queries
// Double-check constructor if making changes to this constant // Accessible by EntityCache. Documentation is there.
const string QueryColumns = "user_id, guild_id, cache_date, username, discriminator, nickname, avatar_url"; internal static async Task<CacheUser> QueryAsync(DiscordSocketClient c, ulong guild, ulong user)
/// <summary>
/// Attempts to query for an exact result with the given parameters.
/// </summary>
/// <returns>Null on no result.</returns>
public static async Task<UserCacheItem> QueryAsync(ulong guild, ulong user)
{ {
using (var db = await RegexBot.Config.Database.GetOpenConnectionAsync()) // Local cache search
var lresult = LocalQueryAsync(c, guild, user);
if (lresult != null) return lresult;
// Database cache search
var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
if (db == null) return null; // Database not available for query.
using (db) return await DbQueryAsync(db, guild, user);
}
private static CacheUser LocalQueryAsync(DiscordSocketClient c, ulong guild, ulong user)
{
var u = c.GetGuild(guild)?.GetUser(user);
if (u == null) return null;
return new CacheUser(u);
}
private static async Task<CacheUser> DbQueryAsync(NpgsqlConnection db, ulong guild, ulong user)
{
using (db)
{ {
using (var c = db.CreateCommand()) using (var c = db.CreateCommand())
{ {
c.CommandText = $"SELECT {QueryColumns} FROM {Sql.TableUser} WHERE " c.CommandText = $"SELECT {QueryColumns} FROM {SqlHelper.TableUser} WHERE "
+ "user_id = @Uid AND guild_id = @Gid"; + "user_id = @Uid AND guild_id = @Gid";
c.Parameters.Add("@Uid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = user; c.Parameters.Add("@Uid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = user;
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = guild; c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = guild;
@ -99,7 +126,7 @@ namespace Noikoio.RegexBot.Module.EntityCache
{ {
if (await r.ReadAsync()) if (await r.ReadAsync())
{ {
return new UserCacheItem(r); return new CacheUser(r);
} }
else else
{ {
@ -110,23 +137,21 @@ namespace Noikoio.RegexBot.Module.EntityCache
} }
} }
private static Regex DiscriminatorSearch = new Regex(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled); // -----
/// <summary> private static Regex DiscriminatorSearch = new Regex(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
/// Attempts to look up the user given a search string. // Accessible by EntityCache. Documentation is there.
/// This string looks up case-insensitive, exact matches of nicknames and usernames. internal static async Task<IEnumerable<CacheUser>> QueryAsync(DiscordSocketClient c, ulong guild, string search)
/// </summary>
/// <returns>An <see cref="IEnumerable{T}"/> containing zero or more query results, sorted by cache date.</returns>
public static async Task<IEnumerable<UserCacheItem>> QueryAsync(ulong guild, string search)
{ {
// Is search just a number? It's an ID. // Is search just a number? Assume ID, pass it on to the correct place.
if (ulong.TryParse(search, out var presult)) if (ulong.TryParse(search, out var presult))
{ {
var r = await QueryAsync(guild, presult); var r = await QueryAsync(c, guild, presult);
if (r == null) return new UserCacheItem[0]; if (r == null) return new CacheUser[0];
else return new UserCacheItem[] { r }; else return new CacheUser[] { r };
} }
// Split name/discriminator
string name; string name;
string disc; string disc;
var split = DiscriminatorSearch.Match(search); var split = DiscriminatorSearch.Match(search);
@ -141,14 +166,52 @@ namespace Noikoio.RegexBot.Module.EntityCache
disc = null; disc = null;
} }
// Storing in HashSet to enforce uniqueness // Local cache search
HashSet<UserCacheItem> result = new HashSet<UserCacheItem>(_uc); var lresult = LocalQueryAsync(c, guild, name, disc);
if (lresult.Count() != 0) return lresult;
using (var db = await RegexBot.Config.Database.GetOpenConnectionAsync()) // Database cache search
var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
if (db == null) return null; // Database not available for query.
using (db) return await DbQueryAsync(db, guild, name, disc);
}
private static IEnumerable<CacheUser> LocalQueryAsync(DiscordSocketClient c, ulong guild, string name, string disc)
{
var g = c.GetGuild(guild);
if (g == null) return new CacheUser[] { };
bool Filter(string iun, string inn, string idc)
{
// Same logic as in the SQL query in the method below this one
bool match =
string.Equals(iun, name, StringComparison.InvariantCultureIgnoreCase)
|| string.Equals(inn, name, StringComparison.InvariantCultureIgnoreCase);
if (match && disc != null)
match = idc.Equals(disc);
return match;
}
var qresult = g.Users.Where(i => Filter(i.Username, i.Nickname, i.Discriminator));
var result = new List<CacheUser>();
foreach (var item in qresult)
{
result.Add(new CacheUser(item));
}
return result;
}
private static async Task<IEnumerable<CacheUser>> DbQueryAsync(NpgsqlConnection db, ulong guild, string name, string disc)
{
var result = new List<CacheUser>();
using (db = await RegexBot.Config.Database.GetOpenConnectionAsync())
{ {
using (var c = db.CreateCommand()) using (var c = db.CreateCommand())
{ {
c.CommandText = $"SELECT {QueryColumns} FROM {Sql.TableUser} WHERE " c.CommandText = $"SELECT {QueryColumns} FROM {SqlHelper.TableUser} WHERE "
+ "( lower(username) = lower(@NameSearch) OR lower(nickname) = lower(@NameSearch) )"; + "( lower(username) = lower(@NameSearch) OR lower(nickname) = lower(@NameSearch) )";
c.Parameters.Add("@NameSearch", NpgsqlTypes.NpgsqlDbType.Text).Value = name; c.Parameters.Add("@NameSearch", NpgsqlTypes.NpgsqlDbType.Text).Value = name;
if (disc != null) if (disc != null)
@ -162,21 +225,13 @@ namespace Noikoio.RegexBot.Module.EntityCache
{ {
while (await r.ReadAsync()) while (await r.ReadAsync())
{ {
result.Add(new UserCacheItem(r)); result.Add(new CacheUser(r));
} }
} }
} }
} }
return result; return result;
} }
private static UniqueChecker _uc = new UniqueChecker();
class UniqueChecker : IEqualityComparer<UserCacheItem>
{
public bool Equals(UserCacheItem x, UserCacheItem y) => x.UserId == y.UserId && x.GuildId == y.GuildId;
public int GetHashCode(UserCacheItem obj) => unchecked((int)(obj.UserId ^ obj.GuildId));
}
#endregion #endregion
} }
} }

View file

@ -0,0 +1,38 @@
using Discord.WebSocket;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Noikoio.RegexBot.EntityCache
{
/// <summary>
/// Static class for accessing the entity cache.
/// </summary>
static class EntityCache
{
/*
* The entity cache works by combining data known/cached by Discord.Net in addition to
* what has been stored in the database. If data does not exist in the former, it is
* retrieved from the latter.
* In either case, the resulting data is placed within a cache item object.
*/
static DiscordSocketClient _client;
internal static void SetClient(DiscordSocketClient c) => _client = _client ?? c;
/// <summary>
/// Attempts to query for an exact result with the given parameters.
/// Does not handle exceptions that may occur.
/// </summary>
/// <returns>Null on no result.</returns>
internal static Task<CacheUser> QueryAsync(ulong guild, ulong user)
=> CacheUser.QueryAsync(_client, guild, user);
/// <summary>
/// Attempts to look up the user given a search string.
/// This string looks up case-insensitive, exact matches of nicknames and usernames.
/// </summary>
/// <returns>An <see cref="IEnumerable{T}"/> containing zero or more query results, sorted by cache date.</returns>
internal static Task<IEnumerable<CacheUser>> QueryAsync(DiscordSocketClient c, ulong guild, string search)
=> CacheUser.QueryAsync(_client, guild, search);
}
}

View file

@ -1,5 +1,4 @@
using Discord.WebSocket; using Discord.WebSocket;
using Npgsql;
using NpgsqlTypes; using NpgsqlTypes;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -17,15 +16,10 @@ namespace Noikoio.RegexBot.EntityCache
public const string TableTextChannel = "cache_textchannel"; public const string TableTextChannel = "cache_textchannel";
public const string TableUser = "cache_users"; public const string TableUser = "cache_users";
private static async Task<NpgsqlConnection> OpenDB() // Reminder: Check Cache query methods if making changes to tables
{
if (!RegexBot.Config.Database.Available) return null;
return await RegexBot.Config.Database.GetOpenConnectionAsync();
}
internal static async Task CreateCacheTablesAsync() internal static async Task CreateCacheTablesAsync()
{ {
var db = await OpenDB(); var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
if (db == null) return; if (db == null) return;
using (db) using (db)
{ {
@ -88,13 +82,13 @@ namespace Noikoio.RegexBot.EntityCache
#region Insertions and updates #region Insertions and updates
internal static async Task UpdateGuildAsync(SocketGuild g) internal static async Task UpdateGuildAsync(SocketGuild g)
{ {
var db = await OpenDB(); var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
if (db == null) return; if (db == null) return;
using (db) using (db)
{ {
using (var c = db.CreateCommand()) using (var c = db.CreateCommand())
{ {
c.CommandText = "INSERT INTO " + Sql.TableGuild + " (guild_id, current_name) " c.CommandText = "INSERT INTO " + TableGuild + " (guild_id, current_name) "
+ "VALUES (@GuildId, @CurrentName) " + "VALUES (@GuildId, @CurrentName) "
+ "ON CONFLICT (guild_id) DO UPDATE SET " + "ON CONFLICT (guild_id) DO UPDATE SET "
+ "current_name = EXCLUDED.current_name"; + "current_name = EXCLUDED.current_name";
@ -113,13 +107,13 @@ namespace Noikoio.RegexBot.EntityCache
} }
internal static async Task UpdateGuildMemberAsync(IEnumerable<SocketGuildUser> users) internal static async Task UpdateGuildMemberAsync(IEnumerable<SocketGuildUser> users)
{ {
var db = await OpenDB(); var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
if (db == null) return; if (db == null) return;
using (db) using (db)
{ {
using (var c = db.CreateCommand()) using (var c = db.CreateCommand())
{ {
c.CommandText = "INSERT INTO " + Sql.TableUser c.CommandText = "INSERT INTO " + TableUser
+ " (user_id, guild_id, cache_date, username, discriminator, nickname, avatar_url)" + " (user_id, guild_id, cache_date, username, discriminator, nickname, avatar_url)"
+ " VALUES (@Uid, @Gid, @Date, @Uname, @Disc, @Nname, @Url) " + " VALUES (@Uid, @Gid, @Date, @Uname, @Disc, @Nname, @Url) "
+ "ON CONFLICT (user_id, guild_id) DO UPDATE SET " + "ON CONFLICT (user_id, guild_id) DO UPDATE SET "

View file

@ -20,7 +20,7 @@ namespace Noikoio.RegexBot
internal RegexBot() internal RegexBot()
{ {
// Load configuration // Load initial configuration
_config = new Configuration(this); _config = new Configuration(this);
if (!_config.LoadInitialConfig()) if (!_config.LoadInitialConfig())
{ {
@ -31,6 +31,7 @@ namespace Noikoio.RegexBot
Environment.Exit(1); Environment.Exit(1);
} }
// Set Discord client settings
_client = new DiscordSocketClient(new DiscordSocketConfig() _client = new DiscordSocketClient(new DiscordSocketConfig()
{ {
LogLevel = LogSeverity.Info, LogLevel = LogSeverity.Info,
@ -39,8 +40,9 @@ namespace Noikoio.RegexBot
MessageCacheSize = 0 MessageCacheSize = 0
}); });
// Hook up handlers for basic functions // Hook up basic handlers and other references
_client.Connected += _client_Connected; _client.Connected += _client_Connected;
EntityCache.EntityCache.SetClient(_client);
// Initialize modules // Initialize modules
_modules = new BotModule[] _modules = new BotModule[]
@ -50,13 +52,15 @@ namespace Noikoio.RegexBot
new Module.AutoRespond.AutoRespond(_client), new Module.AutoRespond.AutoRespond(_client),
new EntityCache.Module(_client) // EntityCache goes before anything else that uses its data new EntityCache.Module(_client) // EntityCache goes before anything else that uses its data
}; };
// Set up logging
var dlog = Logger.GetLogger("Discord.Net"); var dlog = Logger.GetLogger("Discord.Net");
_client.Log += async (arg) => _client.Log += async (arg) =>
await dlog( await dlog(
String.Format("{0}: {1}{2}", arg.Source, ((int)arg.Severity < 3 ? arg.Severity + ": " : ""), String.Format("{0}: {1}{2}", arg.Source, ((int)arg.Severity < 3 ? arg.Severity + ": " : ""),
arg.Message)); arg.Message));
// With modules initialized, finish loading configuration // Finish loading configuration
var conf = _config.ReloadServerConfig().Result; var conf = _config.ReloadServerConfig().Result;
if (conf == false) if (conf == false)
{ {