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.EntityCache { /// /// Representation of a cached user. /// class CacheUser { readonly ulong _userId; readonly ulong _guildId; readonly DateTimeOffset _cacheDate; readonly DateTimeOffset _firstSeen; readonly string _username; readonly string _discriminator; readonly string _nickname; readonly string _avatarUrl; /// /// The cached user's ID (snowflake) value. /// public ulong UserId => _userId; /// /// The guild ID (snowflake) for which this user information corresponds to. /// public ulong GuildId => _guildId; /// /// Timestamp value for when this cache item was last updated, in universal time. /// public DateTimeOffset CacheDate => _cacheDate; /// /// Timestamp value for when this user was first seen by the bot, in universal time. /// public DateTimeOffset FirstSeenDate => _firstSeen; /// /// Display name, including discriminator. Shows the nickname, if available. /// public string DisplayName => (_nickname ?? _username) + "#" + _discriminator; /// /// String useful for tagging the user. /// public string Mention => $"<@{_userId}>"; /// /// User's cached nickname in the guild. May be null. /// public string Nickname => _nickname; /// /// User's cached username. /// public string Username => _username; /// /// User's cached discriminator value. /// public string Discriminator => _discriminator; /// /// URL for user's last known avatar. May be null or invalid. /// 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(); } // Double-check SqlHelper if making changes to this constant const string QueryColumns = "user_id, guild_id, first_seen, cache_date, username, discriminator, nickname, avatar_url"; private CacheUser(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); } _firstSeen = r.GetDateTime(2).ToUniversalTime(); _cacheDate = r.GetDateTime(3).ToUniversalTime(); _username = r.GetString(4); _discriminator = r.GetString(5); _nickname = r.IsDBNull(6) ? null : r.GetString(6); _avatarUrl = r.IsDBNull(7) ? null : r.GetString(7); } public override string ToString() => DisplayName; #region Queries // Accessible by EntityCache. Documentation is there. internal static async Task QueryAsync(DiscordSocketClient c, ulong guild, ulong user) { // Local cache search var lresult = LocalQueryAsync(c, guild, user); if (lresult != null) return lresult; // Database cache search var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync(); if (db == null) return null; // Database not available for query. 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 {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; c.Prepare(); using (var r = await c.ExecuteReaderAsync()) { if (await r.ReadAsync()) { return new CacheUser(r); } else { return null; } } } } } // ----- 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? Assume ID, pass it on to the correct place. if (ulong.TryParse(search, out var presult)) { 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); if (split.Success) { name = split.Groups[1].Value; disc = split.Groups[2].Value; } else { name = search; disc = null; } // Strip leading @ from name, if any if (name.Length > 0 && name[0] == '@') name = name.Substring(1); // Local cache search var lresult = LocalQueryAsync(c, guild, name, disc); if (lresult.Count() != 0) return lresult; // Database cache search var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync(); if (db == null) return new CacheUser[0]; // Database not available for query. 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) { using (var c = db.CreateCommand()) { c.CommandText = $"SELECT {QueryColumns} FROM {SqlHelper.TableUser} WHERE" + " ( lower(username) = lower(@NameSearch) OR lower(nickname) = lower(@NameSearch) )"; if (disc != null) { c.CommandText += " AND discriminator = @DiscSearch"; c.Parameters.Add("@DiscSearch", NpgsqlTypes.NpgsqlDbType.Text).Value = disc; } c.CommandText += " ORDER BY cache_date desc, username"; c.Parameters.Add("@NameSearch", NpgsqlTypes.NpgsqlDbType.Text).Value = name; c.Prepare(); using (var r = await c.ExecuteReaderAsync()) { while (await r.ReadAsync()) { result.Add(new CacheUser(r)); } } } } return result; } #endregion } }