diff --git a/Module/EntityCache/UserCacheItem.cs b/EntityCache/CacheUser.cs
similarity index 55%
rename from Module/EntityCache/UserCacheItem.cs
rename to EntityCache/CacheUser.cs
index 1e94e95..56c58f6 100644
--- a/Module/EntityCache/UserCacheItem.cs
+++ b/EntityCache/CacheUser.cs
@@ -1,15 +1,18 @@
-using System;
+using Discord.WebSocket;
+using Npgsql;
+using System;
using System.Collections.Generic;
using System.Data.Common;
+using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
-namespace Noikoio.RegexBot.Module.EntityCache
+namespace Noikoio.RegexBot.EntityCache
{
///
- /// Represents a cached user.
+ /// Representation of a cached user.
///
- class UserCacheItem
+ class CacheUser
{
readonly ulong _userId;
readonly ulong _guildId;
@@ -57,8 +60,20 @@ namespace Noikoio.RegexBot.Module.EntityCache
///
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
unchecked
@@ -77,20 +92,32 @@ namespace Noikoio.RegexBot.Module.EntityCache
public override string ToString() => DisplayName;
#region Queries
- // Double-check constructor if making changes to this constant
- const string QueryColumns = "user_id, guild_id, cache_date, username, discriminator, nickname, avatar_url";
-
- ///
- /// Attempts to query for an exact result with the given parameters.
- ///
- /// Null on no result.
- public static async Task QueryAsync(ulong guild, ulong user)
+ // Accessible by EntityCache. Documentation is there.
+ internal static async Task QueryAsync(DiscordSocketClient c, 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 DbQueryAsync(NpgsqlConnection db, ulong guild, ulong user)
+ {
+ using (db)
{
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";
c.Parameters.Add("@Uid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = user;
c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = guild;
@@ -99,7 +126,7 @@ namespace Noikoio.RegexBot.Module.EntityCache
{
if (await r.ReadAsync())
{
- return new UserCacheItem(r);
+ return new CacheUser(r);
}
else
{
@@ -110,23 +137,21 @@ namespace Noikoio.RegexBot.Module.EntityCache
}
}
- private static Regex DiscriminatorSearch = new Regex(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
+ // -----
- ///
- /// Attempts to look up the user given a search string.
- /// This string looks up case-insensitive, exact matches of nicknames and usernames.
- ///
- /// An containing zero or more query results, sorted by cache date.
- public static async Task> QueryAsync(ulong guild, string search)
+ private static Regex DiscriminatorSearch = new Regex(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
+ // Accessible by EntityCache. Documentation is there.
+ internal static async Task> QueryAsync(DiscordSocketClient c, 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))
{
- var r = await QueryAsync(guild, presult);
- if (r == null) return new UserCacheItem[0];
- else return new UserCacheItem[] { r };
+ var r = await QueryAsync(c, guild, presult);
+ if (r == null) return new CacheUser[0];
+ else return new CacheUser[] { r };
}
+ // Split name/discriminator
string name;
string disc;
var split = DiscriminatorSearch.Match(search);
@@ -141,14 +166,52 @@ namespace Noikoio.RegexBot.Module.EntityCache
disc = null;
}
- // Storing in HashSet to enforce uniqueness
- HashSet result = new HashSet(_uc);
+ // Local cache search
+ 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 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();
+ foreach (var item in qresult)
+ {
+ result.Add(new CacheUser(item));
+ }
+ return result;
+ }
+
+ private static async Task> DbQueryAsync(NpgsqlConnection db, ulong guild, string name, string disc)
+ {
+ var result = new List();
+
+ using (db = await RegexBot.Config.Database.GetOpenConnectionAsync())
{
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) )";
c.Parameters.Add("@NameSearch", NpgsqlTypes.NpgsqlDbType.Text).Value = name;
if (disc != null)
@@ -162,21 +225,13 @@ namespace Noikoio.RegexBot.Module.EntityCache
{
while (await r.ReadAsync())
{
- result.Add(new UserCacheItem(r));
+ result.Add(new CacheUser(r));
}
}
}
}
return result;
}
-
- private static UniqueChecker _uc = new UniqueChecker();
- class UniqueChecker : IEqualityComparer
- {
- 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
}
}
diff --git a/EntityCache/EntityCache.cs b/EntityCache/EntityCache.cs
new file mode 100644
index 0000000..be280b1
--- /dev/null
+++ b/EntityCache/EntityCache.cs
@@ -0,0 +1,38 @@
+using Discord.WebSocket;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Noikoio.RegexBot.EntityCache
+{
+ ///
+ /// Static class for accessing the entity cache.
+ ///
+ 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;
+
+ ///
+ /// Attempts to query for an exact result with the given parameters.
+ /// Does not handle exceptions that may occur.
+ ///
+ /// Null on no result.
+ internal static Task QueryAsync(ulong guild, ulong user)
+ => CacheUser.QueryAsync(_client, guild, user);
+
+ ///
+ /// Attempts to look up the user given a search string.
+ /// This string looks up case-insensitive, exact matches of nicknames and usernames.
+ ///
+ /// An containing zero or more query results, sorted by cache date.
+ internal static Task> QueryAsync(DiscordSocketClient c, ulong guild, string search)
+ => CacheUser.QueryAsync(_client, guild, search);
+ }
+}
diff --git a/EntityCache/SqlHelper.cs b/EntityCache/SqlHelper.cs
index c68a8e7..3c8b6ce 100644
--- a/EntityCache/SqlHelper.cs
+++ b/EntityCache/SqlHelper.cs
@@ -1,5 +1,4 @@
using Discord.WebSocket;
-using Npgsql;
using NpgsqlTypes;
using System;
using System.Collections.Generic;
@@ -17,15 +16,10 @@ namespace Noikoio.RegexBot.EntityCache
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();
- }
-
+ // Reminder: Check Cache query methods if making changes to tables
internal static async Task CreateCacheTablesAsync()
{
- var db = await OpenDB();
+ var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
if (db == null) return;
using (db)
{
@@ -88,13 +82,13 @@ namespace Noikoio.RegexBot.EntityCache
#region Insertions and updates
internal static async Task UpdateGuildAsync(SocketGuild g)
{
- var db = await OpenDB();
+ var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
if (db == null) return;
using (db)
{
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) "
+ "ON CONFLICT (guild_id) DO UPDATE SET "
+ "current_name = EXCLUDED.current_name";
@@ -113,13 +107,13 @@ namespace Noikoio.RegexBot.EntityCache
}
internal static async Task UpdateGuildMemberAsync(IEnumerable users)
{
- var db = await OpenDB();
+ var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
if (db == null) return;
using (db)
{
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)"
+ " VALUES (@Uid, @Gid, @Date, @Uname, @Disc, @Nname, @Url) "
+ "ON CONFLICT (user_id, guild_id) DO UPDATE SET "
diff --git a/RegexBot.cs b/RegexBot.cs
index 019cead..b69db23 100644
--- a/RegexBot.cs
+++ b/RegexBot.cs
@@ -20,7 +20,7 @@ namespace Noikoio.RegexBot
internal RegexBot()
{
- // Load configuration
+ // Load initial configuration
_config = new Configuration(this);
if (!_config.LoadInitialConfig())
{
@@ -31,6 +31,7 @@ namespace Noikoio.RegexBot
Environment.Exit(1);
}
+ // Set Discord client settings
_client = new DiscordSocketClient(new DiscordSocketConfig()
{
LogLevel = LogSeverity.Info,
@@ -39,8 +40,9 @@ namespace Noikoio.RegexBot
MessageCacheSize = 0
});
- // Hook up handlers for basic functions
+ // Hook up basic handlers and other references
_client.Connected += _client_Connected;
+ EntityCache.EntityCache.SetClient(_client);
// Initialize modules
_modules = new BotModule[]
@@ -50,13 +52,15 @@ namespace Noikoio.RegexBot
new Module.AutoRespond.AutoRespond(_client),
new EntityCache.Module(_client) // EntityCache goes before anything else that uses its data
};
+
+ // Set up logging
var dlog = Logger.GetLogger("Discord.Net");
_client.Log += async (arg) =>
await dlog(
String.Format("{0}: {1}{2}", arg.Source, ((int)arg.Severity < 3 ? arg.Severity + ": " : ""),
arg.Message));
- // With modules initialized, finish loading configuration
+ // Finish loading configuration
var conf = _config.ReloadServerConfig().Result;
if (conf == false)
{