Added UserCacheItem
This class will be really useful for a feature that is soon to be added. Also moved some commonly used bits from EntityCache over to its own file.
This commit is contained in:
parent
3780a34b0d
commit
25a441fba9
3 changed files with 240 additions and 42 deletions
|
@ -26,7 +26,7 @@ namespace Noikoio.RegexBot.Feature.EntityCache
|
||||||
|
|
||||||
if (_db.Enabled)
|
if (_db.Enabled)
|
||||||
{
|
{
|
||||||
CreateCacheTables();
|
Sql.CreateCacheTables();
|
||||||
|
|
||||||
client.GuildAvailable += Client_GuildAvailable;
|
client.GuildAvailable += Client_GuildAvailable;
|
||||||
client.GuildUpdated += Client_GuildUpdated;
|
client.GuildUpdated += Client_GuildUpdated;
|
||||||
|
@ -67,45 +67,7 @@ namespace Noikoio.RegexBot.Feature.EntityCache
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Table setup
|
#region Table setup
|
||||||
public const string TableGuild = "cache_guild";
|
|
||||||
public const string TableUser = "cache_users";
|
|
||||||
|
|
||||||
private void CreateCacheTables()
|
|
||||||
{
|
|
||||||
using (var db = _db.GetOpenConnectionAsync().GetAwaiter().GetResult())
|
|
||||||
{
|
|
||||||
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"
|
|
||||||
+ ")";
|
|
||||||
// TODO determine if other columns might be needed?
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task UpdateGuild(SocketGuild g)
|
private async Task UpdateGuild(SocketGuild g)
|
||||||
|
@ -116,7 +78,7 @@ namespace Noikoio.RegexBot.Feature.EntityCache
|
||||||
{
|
{
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
c.CommandText = "INSERT INTO " + TableGuild + " (guild_id, current_name) "
|
c.CommandText = "INSERT INTO " + Sql.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";
|
||||||
|
@ -141,7 +103,7 @@ namespace Noikoio.RegexBot.Feature.EntityCache
|
||||||
{
|
{
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
c.CommandText = "INSERT INTO " + TableUser
|
c.CommandText = "INSERT INTO " + Sql.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 "
|
||||||
|
|
56
Feature/EntityCache/Sql.cs
Normal file
56
Feature/EntityCache/Sql.cs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
using Npgsql;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Noikoio.RegexBot.Feature.EntityCache
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains common constants and static methods for cache access.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
180
Feature/EntityCache/UserCacheItem.cs
Normal file
180
Feature/EntityCache/UserCacheItem.cs
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Noikoio.RegexBot.Feature.EntityCache
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a cached user.
|
||||||
|
/// </summary>
|
||||||
|
class UserCacheItem
|
||||||
|
{
|
||||||
|
readonly ulong _userId;
|
||||||
|
readonly ulong _guildId;
|
||||||
|
readonly DateTime _cacheDate;
|
||||||
|
readonly string _username;
|
||||||
|
readonly string _discriminator;
|
||||||
|
readonly string _nickname;
|
||||||
|
readonly string _avatarUrl;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cached user's ID (snowflake) value.
|
||||||
|
/// </summary>
|
||||||
|
public ulong UserId => _userId;
|
||||||
|
/// <summary>
|
||||||
|
/// The guild ID (snowflake) for which this user information corresponds to.
|
||||||
|
/// </summary>
|
||||||
|
public ulong GuildId => _guildId;
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp value for when this cache item was last updated, in universal time.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CacheDate => _cacheDate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display name, including discriminator. Shows the nickname, if available.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName => (_nickname ?? _username) + "#" + _discriminator;
|
||||||
|
/// <summary>
|
||||||
|
/// String useful for tagging the user.
|
||||||
|
/// </summary>
|
||||||
|
public string Mention => $"<@{_userId}>";
|
||||||
|
/// <summary>
|
||||||
|
/// User's cached nickname in the guild, if any.
|
||||||
|
/// </summary>
|
||||||
|
public string Nickname => _nickname;
|
||||||
|
/// <summary>
|
||||||
|
/// User's cached username.
|
||||||
|
/// </summary>
|
||||||
|
public string Username => _username;
|
||||||
|
/// <summary>
|
||||||
|
/// User's cached discriminator value.
|
||||||
|
/// </summary>
|
||||||
|
public string Discriminator => _discriminator;
|
||||||
|
/// <summary>
|
||||||
|
/// URL for user's last known avatar. May be null or invalid.
|
||||||
|
/// </summary>
|
||||||
|
public string AvatarUrl => _avatarUrl;
|
||||||
|
|
||||||
|
|
||||||
|
private UserCacheItem(DbDataReader r)
|
||||||
|
{
|
||||||
|
// Double-check ordinals if making changes to QueryColumns
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
// PostgreSQL does not support unsigned 64-bit numbers. Must convert.
|
||||||
|
_userId = (ulong)r.GetInt64(0);
|
||||||
|
_guildId = (ulong)r.GetInt64(1);
|
||||||
|
}
|
||||||
|
_cacheDate = r.GetDateTime(2).ToUniversalTime();
|
||||||
|
_username = r.GetString(3);
|
||||||
|
_discriminator = r.GetString(4);
|
||||||
|
_nickname = r.GetString(5);
|
||||||
|
_avatarUrl = r.GetString(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => DisplayName;
|
||||||
|
|
||||||
|
#region Queries
|
||||||
|
const string QueryColumns = "user_id, guild_id, cache_date, username, discriminator, nickname, avatar_url";
|
||||||
|
|
||||||
|
/// <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())
|
||||||
|
{
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = $"SELECT {QueryColumns} FROM {Sql.TableUser} WHERE "
|
||||||
|
+ "user_id = @Uid AND guild_id = @Gid";
|
||||||
|
c.Parameters.Add("@Uid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = user;
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = guild;
|
||||||
|
c.Prepare();
|
||||||
|
using (var r = await c.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
if (await r.ReadAsync())
|
||||||
|
{
|
||||||
|
return new UserCacheItem(r);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Regex DiscriminatorSearch = new Regex(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to look up the user given a search string.
|
||||||
|
/// This string looks up case-insensitive, exact matches of nicknames and usernames.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="search"></param>
|
||||||
|
/// <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.
|
||||||
|
if (ulong.TryParse(search, out var presult))
|
||||||
|
{
|
||||||
|
return new UserCacheItem[] { await QueryAsync(guild, presult) };
|
||||||
|
}
|
||||||
|
|
||||||
|
string name;
|
||||||
|
string disc;
|
||||||
|
var split = DiscriminatorSearch.Match(search);
|
||||||
|
if (split.Success)
|
||||||
|
{
|
||||||
|
name = split.Groups[1].Value;
|
||||||
|
disc = split.Groups[2].Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
name = search;
|
||||||
|
disc = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storing in HashSet to enforce uniqueness
|
||||||
|
HashSet<UserCacheItem> result = new HashSet<UserCacheItem>(_uc);
|
||||||
|
|
||||||
|
using (var db = await RegexBot.Config.Database.GetOpenConnectionAsync())
|
||||||
|
{
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = $"SELECT {QueryColumns} FROM {Sql.TableUser} WHERE "
|
||||||
|
+ "( lower(username) = lower(@NameSearch) OR lower(nickname) = lower(@NameSearch) )";
|
||||||
|
c.Parameters.Add("@NameSearch", NpgsqlTypes.NpgsqlDbType.Text).Value = name;
|
||||||
|
if (disc != null)
|
||||||
|
{
|
||||||
|
c.CommandText += " AND discriminator = @DiscSearch";
|
||||||
|
c.Parameters.Add("@DiscSearch", NpgsqlTypes.NpgsqlDbType.Text).Value = disc;
|
||||||
|
}
|
||||||
|
c.Prepare();
|
||||||
|
|
||||||
|
using (var r = await c.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await r.ReadAsync())
|
||||||
|
{
|
||||||
|
result.Add(new UserCacheItem(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue