From 26da617cf15fb27887cf07802c848dc94700cc43 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sat, 10 Feb 2018 19:34:13 -0800 Subject: [PATCH] Adding files from previously deleted branch --- Module/ModLogs/Entry.cs | 168 ++++++++++++++++++++++++++++++++ Module/ModLogs/EventListener.cs | 28 ++++++ Module/ModLogs/MessageCache.cs | 110 +++++++++++++++++++++ Module/ModLogs/ModLogs.cs | 10 ++ Module/ModLogs/Sql.cs | 47 +++++++++ 5 files changed, 363 insertions(+) create mode 100644 Module/ModLogs/Entry.cs create mode 100644 Module/ModLogs/EventListener.cs create mode 100644 Module/ModLogs/MessageCache.cs create mode 100644 Module/ModLogs/ModLogs.cs create mode 100644 Module/ModLogs/Sql.cs diff --git a/Module/ModLogs/Entry.cs b/Module/ModLogs/Entry.cs new file mode 100644 index 0000000..4e7918b --- /dev/null +++ b/Module/ModLogs/Entry.cs @@ -0,0 +1,168 @@ +using Npgsql; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Text; +using System.Threading.Tasks; + +namespace Noikoio.RegexBot.Module.ModLogs +{ + /// + /// Represents a log entry in the database. + /// + class Entry + { + readonly int _logId; + readonly DateTime _ts; + readonly ulong _guildId; + readonly ulong? _invokeId; + readonly ulong _targetId; + readonly ulong? _channelId; + readonly string _type; + readonly string _message; + + /// + /// Gets the ID value of this log entry. + /// + public int Id => _logId; + /// + /// Gets the timestamp (a with ) of the entry. + /// + public DateTime Timestamp => _ts; + /// + /// Gets the ID of the guild to which this log entry corresponds. + /// + public ulong Guild => _guildId; + /// + /// Gets the ID of the user to which this log entry corresponds. + /// + public ulong Target => _targetId; + /// + /// Gets the ID of the invoking user. + /// This value exists only if this entry was created through action of another user that is not the target. + /// + public ulong? Invoker => _invokeId; + /// + /// Gets the guild channel ID to which this log entry corresponds, if any. + /// + public ulong? TargetChannel => _channelId; + /// + /// Gets this log entry's category. + /// + public string Category => _type; + /// + /// Gets the content of this log entry. + /// + public string Message => _message; + + public Entry(DbDataReader r) + { + // Double-check ordinals if making changes to QueryColumns + + _logId = r.GetInt32(0); + _ts = r.GetDateTime(1).ToUniversalTime(); + unchecked + { + _guildId = (ulong)r.GetInt64(2); + _targetId = (ulong)r.GetInt64(3); + if (r.IsDBNull(4)) _invokeId = null; + else _invokeId = (ulong)r.GetInt64(4); + if (r.IsDBNull(5)) _channelId = null; + else _channelId = (ulong)r.GetInt64(5); + } + _type = r.GetString(6); + _message = r.GetString(7); + } + + // TODO lazy loading of channel, user, etc from caches + // TODO methods for updating this log entry(?) + + // TODO figure out some helper methods to retrieve data of other entities by ID, if it becomes necessary + + #region Queries + // Double-check constructor if making changes to this constant + const string QueryColumns = "id, entry_ts, guild_id, target_id, invoke_id, target_channel_id, category, message"; + + /// + /// Attempts to look up a log entry with the given ID. + /// + /// Null if no result. + public static async Task QueryIdAsync(ulong guild, int id) + { + using (var db = await RegexBot.Config.Database.GetOpenConnectionAsync()) + { + using (var c = db.CreateCommand()) + { + c.CommandText = $"SELECT {QueryColumns} FROM {Sql.TableLog} " + + "WHERE guild_id = @Guild and id = @Id"; + c.Parameters.Add("@Guild", NpgsqlTypes.NpgsqlDbType.Bigint).Value = guild; + c.Parameters.Add("@Id", NpgsqlTypes.NpgsqlDbType.Numeric).Value = id; + c.Prepare(); + using (var r = await c.ExecuteReaderAsync()) + { + if (r.Read()) return new Entry(r); + else return null; + } + } + } + } + + public static async Task> QueryLogAsync + (ulong guild, + ulong? target = null, + ulong? invoker = null, + ulong? channel = null, + IEnumerable category = null) + { + // Enforce some limits - can't search too broadly here. Requires this at a minimum: + if (target.HasValue == false && invoker.HasValue == false) + { + throw new ArgumentNullException("Query requires at minimum searching of a target or invoker."); + } + + var result = new List(); + using (var db = await RegexBot.Config.Database.GetOpenConnectionAsync()) + { + using (var c = db.CreateCommand()) + { + c.CommandText = $"SELECT {QueryColumns} FROM {Sql.TableLog} WHERE"; + + bool and = false; + if (target.HasValue) + { + if (and) c.CommandText += " AND"; + else and = true; + c.CommandText += " target_id = @TargetId"; + c.Parameters.Add("@TargetId", NpgsqlTypes.NpgsqlDbType.Bigint).Value = target.Value; + } + if (invoker.HasValue) + { + if (and) c.CommandText += " AND"; + else and = true; + c.CommandText += " invoke_id = @InvokeId"; + c.Parameters.Add("@InvokeId", NpgsqlTypes.NpgsqlDbType.Bigint).Value = invoker.Value; + } + if (channel.HasValue) + { + if (and) c.CommandText += " AND"; + else and = true; + c.CommandText += " target_channel_id = @ChannelId"; + c.Parameters.Add("@ChannelId", NpgsqlTypes.NpgsqlDbType.Bigint).Value = channel.Value; + } + c.Prepare(); + + using (var r = await c.ExecuteReaderAsync()) + { + while (r.Read()) + { + result.Add(new Entry(r)); + } + } + } + } + + return result; + } + #endregion + } +} diff --git a/Module/ModLogs/EventListener.cs b/Module/ModLogs/EventListener.cs new file mode 100644 index 0000000..2ed1932 --- /dev/null +++ b/Module/ModLogs/EventListener.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Discord.WebSocket; +using Newtonsoft.Json.Linq; + +namespace Noikoio.RegexBot.Module.ModLogs +{ + /// + /// Listens for Discord-based events and writes them to the log (database). + /// Additionally writes certain messages to a designated logging channel if configured. + /// + class EventListener : BotModule + { + public override string Name => "ModLogs"; + public EventListener(DiscordSocketClient client) : base(client) + { + + } + + [ConfigSection("modlogs")] + public override Task ProcessConfiguration(JToken configSection) + { + throw new NotImplementedException(); + } + } +} diff --git a/Module/ModLogs/MessageCache.cs b/Module/ModLogs/MessageCache.cs new file mode 100644 index 0000000..80cd933 --- /dev/null +++ b/Module/ModLogs/MessageCache.cs @@ -0,0 +1,110 @@ +using Discord.WebSocket; +using Newtonsoft.Json.Linq; +using Noikoio.RegexBot.ConfigItem; +using Npgsql; +using NpgsqlTypes; +using System.Threading.Tasks; + +namespace Noikoio.RegexBot.Module.DBCache +{ + /// + /// Caches information regarding all incoming messages. + /// The function of this feature should be transparent to the user, and thus no configuration is needed. + /// + class MessageCache : BotFeature + { + // TODO Something that clears expired cache items + private readonly DatabaseConfig _db; + + public override string Name => nameof(MessageCache); + + public MessageCache(DiscordSocketClient client) : base(client) + { + _db = RegexBot.Config.Database; + + if (_db.Enabled) + { + CreateCacheTables(); + + client.MessageReceived += Client_MessageReceived; + //client.MessageUpdated += Client_MessageUpdated; + } + else + { + Log("No database storage available.").Wait(); + } + } + + #region Table setup + const string TableMessage = "cache_messages"; + + public override Task ProcessConfiguration(JToken configSection) => Task.FromResult(null); + + #region Event handling + // A new message has been created + private async Task Client_MessageReceived(SocketMessage arg) + { + await Task.Run(() => CacheMessage(arg)); + } + + //private Task Client_MessageUpdated(Discord.Cacheable arg1, SocketMessage arg2, ISocketMessageChannel arg3) + /* + * Edited messages seem to retain their ID. This is a problem. + * The point of this message cache was to have another feature be able to relay + * both the previous and current message at once. + * For now: Do nothing on updated messages. + */ + #endregion + + private void CreateCacheTables() + { + using (var db = _db.GetOpenConnectionAsync().GetAwaiter().GetResult()) + { + using (var c = db.CreateCommand()) + { + c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableMessage + " (" + + "message_id bigint primary key, " + + "author_id bigint not null, " + + "guild_id bigint not null, " + + "channel_id bigint not null, " // channel cache later? something to think about... + + "created_ts timestamptz not null, " + + "edited_ts timestamptz null, " + + "message text not null, " + + $"FOREIGN KEY (author_id, guild_id) references {EntityCache.Sql.TableUser} (user_id, guild_id)" + + ")"; + // TODO figure out how to store message edits + c.ExecuteNonQuery(); + } + } + } + #endregion + + private async Task CacheMessage(SocketMessage msg) + { + try + { + using (var db = await _db.GetOpenConnectionAsync()) + { + using (var c = db.CreateCommand()) + { + c.CommandText = "INSERT INTO " + TableMessage + + " (message_id, author_id, guild_id, channel_id, created_ts, message) VALUES " + + "(@MessageId, @UserId, @GuildId, @ChannelId, @Date, @Message)"; + c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = msg.Id; + c.Parameters.Add("@UserId", NpgsqlDbType.Bigint).Value = msg.Author.Id; + c.Parameters.Add("@GuildId", NpgsqlDbType.Bigint).Value = ((SocketGuildUser)msg.Author).Guild.Id; + c.Parameters.Add("@ChannelId", NpgsqlDbType.Bigint).Value = msg.Channel.Id; + c.Parameters.Add("@Date", NpgsqlDbType.TimestampTZ).Value = msg.Timestamp; + c.Parameters.Add("@Message", NpgsqlDbType.Text).Value = msg.Content; + c.Prepare(); + await c.ExecuteNonQueryAsync(); + } + } + } + catch (NpgsqlException ex) + { + await Log($"SQL error in {nameof(CacheMessage)}: " + ex.Message); + } + } + } +} diff --git a/Module/ModLogs/ModLogs.cs b/Module/ModLogs/ModLogs.cs new file mode 100644 index 0000000..14fc93b --- /dev/null +++ b/Module/ModLogs/ModLogs.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Noikoio.RegexBot.Module.ModLogs +{ + class ModLogs + { + } +} \ No newline at end of file diff --git a/Module/ModLogs/Sql.cs b/Module/ModLogs/Sql.cs new file mode 100644 index 0000000..c500698 --- /dev/null +++ b/Module/ModLogs/Sql.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Noikoio.RegexBot.Module.ModLogs +{ + /// + /// Contains common constants and static methods used for accessing the log database. + /// + class Sql + { + public const string TableLog = "modlogs_entries"; + public const string TableLogIncr = TableLog + "_id"; + public const string TableMsgCache = "modlogs_msgcache"; + + static void CreateTables() + { + using (var db = RegexBot.Config.Database.GetOpenConnectionAsync().GetAwaiter().GetResult()) + { + using (var c = db.CreateCommand()) + { + c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableLog + " (" + + "id int primary key, " + + "entry_ts timestamptz not null, " + + "guild_id bigint not null, " + + "target_id bigint not null, " + + $"invoke_id bigint null references {EntityCache.Sql.TableUser}.user_id, " + + "target_channel_id bigint null, " // TODO channel cache reference? + + "category text not null, " + + "message text not null, " + + $"FOREIGN KEY (target_id, guild_id) REFERENCES {EntityCache.Sql.TableUser} (user_id, guild_id)"; + c.ExecuteNonQuery(); + } + using (var c = db.CreateCommand()) + { + c.CommandText = $"CREATE SEQUENCE IF NOT EXISTS {TableLogIncr} " + + $"START 100 MAXVALUE {int.MaxValue}"; + c.ExecuteNonQuery(); + } + } + } + + #region Log entry manipulation + // what was I doing again + #endregion + } +}