Merge branch 'dev/modlogs'
Excluded actual ModLogs files from this merge.
This commit is contained in:
commit
e219cdc542
8 changed files with 421 additions and 254 deletions
|
@ -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,19 +92,32 @@ namespace Noikoio.RegexBot.Module.EntityCache
|
||||||
public override string ToString() => DisplayName;
|
public override string ToString() => DisplayName;
|
||||||
|
|
||||||
#region Queries
|
#region Queries
|
||||||
const string QueryColumns = "user_id, guild_id, cache_date, username, discriminator, nickname, avatar_url";
|
// Accessible by EntityCache. Documentation is there.
|
||||||
|
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;
|
||||||
|
@ -98,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
|
||||||
{
|
{
|
||||||
|
@ -109,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);
|
||||||
|
@ -140,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)
|
||||||
|
@ -161,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
|
||||||
}
|
}
|
||||||
}
|
}
|
38
EntityCache/EntityCache.cs
Normal file
38
EntityCache/EntityCache.cs
Normal 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(ulong guild, string search)
|
||||||
|
=> CacheUser.QueryAsync(_client, guild, search);
|
||||||
|
}
|
||||||
|
}
|
124
EntityCache/Module.cs
Normal file
124
EntityCache/Module.cs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Noikoio.RegexBot.ConfigItem;
|
||||||
|
using Npgsql;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Noikoio.RegexBot.EntityCache
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bot module portion of the entity cache. Caches information regarding all known guilds, channels, and users.
|
||||||
|
/// The function of this module should be transparent to the user, and thus no configuration is needed.
|
||||||
|
/// This module should be initialized BEFORE any other modules that make use of the entity cache.
|
||||||
|
/// </summary>
|
||||||
|
class Module : BotModule
|
||||||
|
{
|
||||||
|
private readonly DatabaseConfig _db;
|
||||||
|
|
||||||
|
public override string Name => nameof(EntityCache);
|
||||||
|
|
||||||
|
public Module(DiscordSocketClient client) : base(client)
|
||||||
|
{
|
||||||
|
_db = RegexBot.Config.Database;
|
||||||
|
|
||||||
|
if (_db.Available)
|
||||||
|
{
|
||||||
|
SqlHelper.CreateCacheTablesAsync().Wait();
|
||||||
|
|
||||||
|
client.GuildAvailable += Client_GuildAvailable;
|
||||||
|
client.GuildUpdated += Client_GuildUpdated;
|
||||||
|
client.GuildMemberUpdated += Client_GuildMemberUpdated;
|
||||||
|
client.UserJoined += Client_UserJoined;
|
||||||
|
client.UserLeft += Client_UserLeft;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log("No database storage available.").Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<object> ProcessConfiguration(JToken configSection) => Task.FromResult<object>(null);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SqlHelper.UpdateGuildAsync(arg);
|
||||||
|
await SqlHelper.UpdateGuildMemberAsync(arg.Users);
|
||||||
|
}
|
||||||
|
catch (NpgsqlException ex)
|
||||||
|
{
|
||||||
|
await Log($"SQL error in {nameof(Client_GuildAvailable)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guild information has changed
|
||||||
|
private async Task Client_GuildUpdated(SocketGuild arg1, SocketGuild arg2)
|
||||||
|
{
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SqlHelper.UpdateGuildAsync(arg2);
|
||||||
|
}
|
||||||
|
catch (NpgsqlException ex)
|
||||||
|
{
|
||||||
|
await Log($"SQL error in {nameof(Client_GuildUpdated)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guild member information has changed
|
||||||
|
private async Task Client_GuildMemberUpdated(SocketGuildUser arg1, SocketGuildUser arg2)
|
||||||
|
{
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SqlHelper.UpdateGuildMemberAsync(arg2);
|
||||||
|
}
|
||||||
|
catch (NpgsqlException ex)
|
||||||
|
{
|
||||||
|
await Log($"SQL error in {nameof(Client_GuildMemberUpdated)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new guild member has appeared
|
||||||
|
private async Task Client_UserJoined(SocketGuildUser arg)
|
||||||
|
{
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SqlHelper.UpdateGuildMemberAsync(arg);
|
||||||
|
}
|
||||||
|
catch (NpgsqlException ex)
|
||||||
|
{
|
||||||
|
await Log($"SQL error in {nameof(Client_UserJoined)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// User left the guild. No new data, but gives an excuse to update the cache date.
|
||||||
|
private async Task Client_UserLeft(SocketGuildUser arg)
|
||||||
|
{
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await SqlHelper.UpdateGuildMemberAsync(arg);
|
||||||
|
}
|
||||||
|
catch (NpgsqlException ex)
|
||||||
|
{
|
||||||
|
await Log($"SQL error in {nameof(Client_UserLeft)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
155
EntityCache/SqlHelper.cs
Normal file
155
EntityCache/SqlHelper.cs
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using NpgsqlTypes;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Noikoio.RegexBot.EntityCache
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper methods for database operations.
|
||||||
|
/// Exceptions are not handled within methods of this class.
|
||||||
|
/// </summary>
|
||||||
|
static class SqlHelper
|
||||||
|
{
|
||||||
|
public const string TableGuild = "cache_guild";
|
||||||
|
public const string TableTextChannel = "cache_textchannel";
|
||||||
|
public const string TableUser = "cache_users";
|
||||||
|
|
||||||
|
// Reminder: Check Cache query methods if making changes to tables
|
||||||
|
internal static async Task CreateCacheTablesAsync()
|
||||||
|
{
|
||||||
|
var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
|
||||||
|
if (db == null) return;
|
||||||
|
using (db)
|
||||||
|
{
|
||||||
|
// Guild cache
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableGuild + " ("
|
||||||
|
+ "guild_id bigint primary key, "
|
||||||
|
+ "cache_date timestamptz not null, "
|
||||||
|
+ "current_name text not null, "
|
||||||
|
+ "display_name text null"
|
||||||
|
+ ")";
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
// May not require other indexes. Add here if they become necessary.
|
||||||
|
|
||||||
|
// Text channel cache
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableTextChannel + " ("
|
||||||
|
+ "channel_id bigint not null primary key, "
|
||||||
|
+ $"guild_id bigint not null references {TableGuild}, "
|
||||||
|
+ "cache_date timestamptz not null, "
|
||||||
|
+ "channel_name text not null"
|
||||||
|
+ ")";
|
||||||
|
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.
|
||||||
|
|
||||||
|
// User cache
|
||||||
|
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"
|
||||||
|
+ ")";
|
||||||
|
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 "
|
||||||
|
+ $"{TableUser}_ck_idx on {TableUser} (user_id, guild_id)";
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = "CREATE INDEX IF NOT EXISTS "
|
||||||
|
+ $"{TableUser}_usersearch_idx on {TableUser} (LOWER(username))";
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Insertions and updates
|
||||||
|
internal static async Task UpdateGuildAsync(SocketGuild g)
|
||||||
|
{
|
||||||
|
var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
|
||||||
|
if (db == null) return;
|
||||||
|
using (db)
|
||||||
|
{
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = "INSERT INTO " + TableGuild + " (guild_id, cache_date, current_name) "
|
||||||
|
+ "VALUES (@GuildId, @Date, @CurrentName) "
|
||||||
|
+ "ON CONFLICT (guild_id) DO UPDATE SET "
|
||||||
|
+ "current_name = EXCLUDED.current_name, cache_date = EXCLUDED.cache_date";
|
||||||
|
c.Parameters.Add("@GuildId", NpgsqlDbType.Bigint).Value = g.Id;
|
||||||
|
c.Parameters.Add("@CurrentName", NpgsqlDbType.Text).Value = g.Name;
|
||||||
|
c.Parameters.Add("@Date", NpgsqlDbType.TimestampTZ).Value = DateTime.Now;
|
||||||
|
c.Prepare();
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Task UpdateGuildMemberAsync(SocketGuildUser user)
|
||||||
|
{
|
||||||
|
var ml = new SocketGuildUser[] { user };
|
||||||
|
return UpdateGuildMemberAsync(ml);
|
||||||
|
}
|
||||||
|
internal static async Task UpdateGuildMemberAsync(IEnumerable<SocketGuildUser> users)
|
||||||
|
{
|
||||||
|
var db = await RegexBot.Config.Database.GetOpenConnectionAsync();
|
||||||
|
if (db == null) return;
|
||||||
|
using (db)
|
||||||
|
{
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
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 "
|
||||||
|
+ "cache_date = EXCLUDED.cache_date, username = EXCLUDED.username, "
|
||||||
|
+ "discriminator = EXCLUDED.discriminator, " // I've seen someone's discriminator change this one time...
|
||||||
|
+ "nickname = EXCLUDED.nickname, avatar_url = EXCLUDED.avatar_url";
|
||||||
|
|
||||||
|
var uid = c.Parameters.Add("@Uid", NpgsqlDbType.Bigint);
|
||||||
|
var gid = c.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
||||||
|
c.Parameters.Add("@Date", NpgsqlDbType.TimestampTZ).Value = DateTime.Now;
|
||||||
|
var uname = c.Parameters.Add("@Uname", NpgsqlDbType.Text);
|
||||||
|
var disc = c.Parameters.Add("@Disc", NpgsqlDbType.Text);
|
||||||
|
var nname = c.Parameters.Add("@Nname", NpgsqlDbType.Text);
|
||||||
|
var url = c.Parameters.Add("@Url", NpgsqlDbType.Text);
|
||||||
|
c.Prepare();
|
||||||
|
|
||||||
|
foreach (var item in users)
|
||||||
|
{
|
||||||
|
if (item.IsBot || item.IsWebhook) continue;
|
||||||
|
|
||||||
|
uid.Value = item.Id;
|
||||||
|
gid.Value = item.Guild.Id;
|
||||||
|
uname.Value = item.Username;
|
||||||
|
disc.Value = item.Discriminator;
|
||||||
|
nname.Value = item.Nickname;
|
||||||
|
if (nname.Value == null) nname.Value = DBNull.Value; // why can't ?? work here?
|
||||||
|
url.Value = item.GetAvatarUrl();
|
||||||
|
if (url.Value == null) url.Value = DBNull.Value;
|
||||||
|
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,154 +0,0 @@
|
||||||
using Discord.WebSocket;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Noikoio.RegexBot.ConfigItem;
|
|
||||||
using Npgsql;
|
|
||||||
using NpgsqlTypes;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Noikoio.RegexBot.Module.EntityCache
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Caches information regarding all known guilds, channels, and users.
|
|
||||||
/// The function of this module should be transparent to the user, and thus no configuration is needed.
|
|
||||||
/// This module should be initialized BEFORE any other modules that make use of guild and user cache.
|
|
||||||
/// </summary>
|
|
||||||
class EntityCache : BotModule
|
|
||||||
{
|
|
||||||
private readonly DatabaseConfig _db;
|
|
||||||
|
|
||||||
public override string Name => nameof(EntityCache);
|
|
||||||
|
|
||||||
public EntityCache(DiscordSocketClient client) : base(client)
|
|
||||||
{
|
|
||||||
_db = RegexBot.Config.Database;
|
|
||||||
|
|
||||||
if (_db.Available)
|
|
||||||
{
|
|
||||||
Sql.CreateCacheTables();
|
|
||||||
|
|
||||||
client.GuildAvailable += Client_GuildAvailable;
|
|
||||||
client.GuildUpdated += Client_GuildUpdated;
|
|
||||||
client.GuildMemberUpdated += Client_GuildMemberUpdated;
|
|
||||||
// it may not be necessary to handle JoinedGuild, as GuildAvailable provides this info
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log("No database storage available.").Wait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task<object> ProcessConfiguration(JToken configSection) => Task.FromResult<object>(null);
|
|
||||||
|
|
||||||
#region Event handling
|
|
||||||
// Guild _and_ guild member information has become available
|
|
||||||
private async Task Client_GuildAvailable(SocketGuild arg)
|
|
||||||
{
|
|
||||||
await Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await UpdateGuild(arg);
|
|
||||||
await UpdateGuildMember(arg.Users);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guild information has changed
|
|
||||||
private async Task Client_GuildUpdated(SocketGuild arg1, SocketGuild arg2)
|
|
||||||
{
|
|
||||||
await Task.Run(() => UpdateGuild(arg2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guild member information has changed
|
|
||||||
private async Task Client_GuildMemberUpdated(SocketGuildUser arg1, SocketGuildUser arg2)
|
|
||||||
{
|
|
||||||
await Task.Run(() => UpdateGuildMember(arg2));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Table setup
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private async Task UpdateGuild(SocketGuild g)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var db = await _db.GetOpenConnectionAsync())
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = "INSERT INTO " + Sql.TableGuild + " (guild_id, current_name) "
|
|
||||||
+ "VALUES (@GuildId, @CurrentName) "
|
|
||||||
+ "ON CONFLICT (guild_id) DO UPDATE SET "
|
|
||||||
+ "current_name = EXCLUDED.current_name";
|
|
||||||
c.Parameters.Add("@GuildId", NpgsqlDbType.Bigint).Value = g.Id;
|
|
||||||
c.Parameters.Add("@CurrentName", NpgsqlDbType.Text).Value = g.Name;
|
|
||||||
c.Prepare();
|
|
||||||
await c.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (NpgsqlException ex)
|
|
||||||
{
|
|
||||||
await Log($"SQL error in {nameof(UpdateGuild)}: " + ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateGuildMember(IEnumerable<SocketGuildUser> users)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var db = await _db.GetOpenConnectionAsync())
|
|
||||||
{
|
|
||||||
using (var c = db.CreateCommand())
|
|
||||||
{
|
|
||||||
c.CommandText = "INSERT INTO " + Sql.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 "
|
|
||||||
+ "cache_date = EXCLUDED.cache_date, username = EXCLUDED.username, "
|
|
||||||
+ "discriminator = EXCLUDED.discriminator, " // I've seen someone's discriminator change this one time...
|
|
||||||
+ "nickname = EXCLUDED.nickname, avatar_url = EXCLUDED.avatar_url";
|
|
||||||
|
|
||||||
var uid = c.Parameters.Add("@Uid", NpgsqlDbType.Bigint);
|
|
||||||
var gid = c.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
|
||||||
c.Parameters.Add("@Date", NpgsqlDbType.TimestampTZ).Value = DateTime.Now;
|
|
||||||
var uname = c.Parameters.Add("@Uname", NpgsqlDbType.Text);
|
|
||||||
var disc = c.Parameters.Add("@Disc", NpgsqlDbType.Text);
|
|
||||||
var nname = c.Parameters.Add("@Nname", NpgsqlDbType.Text);
|
|
||||||
var url = c.Parameters.Add("@Url", NpgsqlDbType.Text);
|
|
||||||
c.Prepare();
|
|
||||||
|
|
||||||
foreach (var item in users)
|
|
||||||
{
|
|
||||||
if (item.IsBot || item.IsWebhook) continue;
|
|
||||||
|
|
||||||
uid.Value = item.Id;
|
|
||||||
gid.Value = item.Guild.Id;
|
|
||||||
uname.Value = item.Username;
|
|
||||||
disc.Value = item.Discriminator;
|
|
||||||
nname.Value = item.Nickname;
|
|
||||||
if (nname.Value == null) nname.Value = DBNull.Value; // why can't ?? work here?
|
|
||||||
url.Value = item.GetAvatarUrl();
|
|
||||||
if (url.Value == null) url.Value = DBNull.Value;
|
|
||||||
|
|
||||||
await c.ExecuteNonQueryAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (NpgsqlException ex)
|
|
||||||
{
|
|
||||||
await Log($"SQL error in {nameof(UpdateGuildMember)}: " + ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task UpdateGuildMember(SocketGuildUser user)
|
|
||||||
{
|
|
||||||
var gid = user.Guild.Id;
|
|
||||||
var ml = new SocketGuildUser[] { user };
|
|
||||||
return UpdateGuildMember(ml);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
using Npgsql;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Noikoio.RegexBot.Module.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
|
This file is used by the publish/package process of your project. You can customize the behavior of this process
|
||||||
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121.
|
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||||
-->
|
-->
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PublishProtocol>FileSystem</PublishProtocol>
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
<Configuration>Debug</Configuration>
|
<Configuration>Debug</Configuration>
|
||||||
<TargetFramework>netcoreapp1.1</TargetFramework>
|
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||||
<PublishDir>bin\Debug\PublishOutput</PublishDir>
|
<PublishDir>bin\Debug\PublishOutput</PublishDir>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
10
RegexBot.cs
10
RegexBot.cs
|
@ -38,6 +38,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,
|
||||||
|
@ -46,8 +47,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[]
|
||||||
|
@ -56,15 +58,17 @@ namespace Noikoio.RegexBot
|
||||||
new Module.AutoMod.AutoMod(_client),
|
new Module.AutoMod.AutoMod(_client),
|
||||||
new Module.ModTools.ModTools(_client),
|
new Module.ModTools.ModTools(_client),
|
||||||
new Module.AutoRespond.AutoRespond(_client),
|
new Module.AutoRespond.AutoRespond(_client),
|
||||||
new Module.EntityCache.EntityCache(_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)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue