From 26da617cf15fb27887cf07802c848dc94700cc43 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sat, 10 Feb 2018 19:34:13 -0800 Subject: [PATCH 01/17] 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 + } +} From 683b852de7f66879ba0e2d147936e3311313de52 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Fri, 16 Feb 2018 23:41:12 -0800 Subject: [PATCH 02/17] Removed public access to database config Database connection is now acquired directly via Configuration instead of an object within it. --- ConfigItem/DatabaseConfig.cs | 44 +++++++++++++------------------- Configuration.cs | 49 +++++++++++++++++++++++++++++++----- EntityCache/CacheUser.cs | 6 ++--- 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/ConfigItem/DatabaseConfig.cs b/ConfigItem/DatabaseConfig.cs index 3cee6e3..5afe137 100644 --- a/ConfigItem/DatabaseConfig.cs +++ b/ConfigItem/DatabaseConfig.cs @@ -1,56 +1,43 @@ using Newtonsoft.Json.Linq; using Npgsql; +using System; using System.Threading.Tasks; namespace Noikoio.RegexBot.ConfigItem { class DatabaseConfig { - private readonly bool _enabled; private readonly string _host; private readonly string _user; private readonly string _pass; private readonly string _dbname; - private readonly string _parsemsg; - - /// - /// Gets whether database storage is available. - /// - public bool Available => _enabled; - /// - /// Constructor error message (only if not enabled) - /// - public string ParseMsg => _parsemsg; public DatabaseConfig(JToken ctok) { if (ctok == null || ctok.Type != JTokenType.Object) { - _enabled = false; - _parsemsg = "Database configuration not defined."; - return; + throw new DatabaseConfigLoadException(""); } var conf = (JObject)ctok; _host = conf["hostname"]?.Value() ?? "localhost"; // default to localhost + _user = conf["username"]?.Value(); + if (string.IsNullOrWhiteSpace(_user)) + throw new DatabaseConfigLoadException("Value for username is not defined."); + _pass = conf["password"]?.Value(); + if (string.IsNullOrWhiteSpace(_pass)) + throw new DatabaseConfigLoadException( + $"Value for password is not defined. {nameof(RegexBot)} only supports password authentication."); + _dbname = conf["database"]?.Value(); - - if (string.IsNullOrWhiteSpace(_user) || string.IsNullOrWhiteSpace(_pass) || string.IsNullOrWhiteSpace(_dbname)) - { - _parsemsg = "One or more required values are invalid or not defined."; - _enabled = false; - } - - _parsemsg = null; - _enabled = true; + if (string.IsNullOrWhiteSpace(_dbname)) + throw new DatabaseConfigLoadException("Value for database name is not defined."); } - public async Task GetOpenConnectionAsync() + internal async Task GetOpenConnectionAsync() { - if (!Available) return null; - var cs = new NpgsqlConnectionStringBuilder() { Host = _host, @@ -63,5 +50,10 @@ namespace Noikoio.RegexBot.ConfigItem await db.OpenAsync(); return db; } + + internal class DatabaseConfigLoadException : Exception + { + public DatabaseConfigLoadException(string message) : base(message) { } + } } } diff --git a/Configuration.cs b/Configuration.cs index e49a760..6592ee5 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -29,7 +29,6 @@ namespace Noikoio.RegexBot public string BotUserToken => _botToken; public string CurrentGame => _currentGame; - public DatabaseConfig Database => _dbConfig; public ServerConfig[] Servers => _servers; @@ -72,22 +71,44 @@ namespace Noikoio.RegexBot /// /// Loads essential, unchanging values needed for bot startup. Returns false on failure. /// - public bool LoadInitialConfig() + internal bool LoadInitialConfig() { var lt = LoadFile(); lt.Wait(); JObject conf = lt.Result; if (conf == null) return false; + var log = Logger.GetLogger(LogPrefix); + _botToken = conf["bot-token"]?.Value(); if (String.IsNullOrWhiteSpace(_botToken)) { - Logger.GetLogger(LogPrefix)("Error: Bot token not defined. Cannot continue.").Wait(); + log("Error: Bot token not defined. Cannot continue.").Wait(); return false; } _currentGame = conf["playing"]?.Value(); - _dbConfig = new DatabaseConfig(conf["database"]); + // Database configuration: + // Either it exists or it doesn't. Read config, but also attempt to make a database connection + // right here, or else make it known that database support is disabled for this instance. + try + { + _dbConfig = new DatabaseConfig(conf["database"]); + var conn = _dbConfig.GetOpenConnectionAsync().GetAwaiter().GetResult(); + conn.Dispose(); + } + catch (DatabaseConfig.DatabaseConfigLoadException ex) + { + if (ex.Message == "") log("Database configuration not found.").Wait(); + else log("Error within database config: " + ex.Message).Wait(); + _dbConfig = null; + } + catch (Npgsql.NpgsqlException ex) + { + log("An error occurred while establishing initial database connection: " + ex.Message).Wait(); + _dbConfig = null; + } + // Modules that will not enable due to lack of database access should say so in their constructors. return true; } @@ -179,7 +200,23 @@ namespace Noikoio.RegexBot _servers = newservers.ToArray(); return true; } - } - + /// + /// Gets a value stating if database access is available. + /// Specifically, indicates if will return a non-null value. + /// + /// + /// Ideally, this value remains constant on runtime. It does not take into account + /// the possibility of the database connection failing during the program's run time. + /// + public bool DatabaseAvailable => _dbConfig != null; + /// + /// Gets an opened connection to the SQL database, if available. + /// + /// + /// An in the opened state, + /// or null if an SQL database is not available. + /// + public Task GetOpenDatabaseConnectionAsync() => _dbConfig?.GetOpenConnectionAsync(); + } } diff --git a/EntityCache/CacheUser.cs b/EntityCache/CacheUser.cs index 0d1f024..740ed9c 100644 --- a/EntityCache/CacheUser.cs +++ b/EntityCache/CacheUser.cs @@ -100,7 +100,7 @@ namespace Noikoio.RegexBot.EntityCache if (lresult != null) return lresult; // Database cache search - var db = await RegexBot.Config.Database.GetOpenConnectionAsync(); + var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync(); if (db == null) return null; // Database not available for query. using (db) return await DbQueryAsync(db, guild, user); } @@ -171,7 +171,7 @@ namespace Noikoio.RegexBot.EntityCache if (lresult.Count() != 0) return lresult; // Database cache search - var db = await RegexBot.Config.Database.GetOpenConnectionAsync(); + var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync(); if (db == null) return null; // Database not available for query. using (db) return await DbQueryAsync(db, guild, name, disc); } @@ -207,7 +207,7 @@ namespace Noikoio.RegexBot.EntityCache { var result = new List(); - using (db = await RegexBot.Config.Database.GetOpenConnectionAsync()) + using (db = await RegexBot.Config.GetOpenDatabaseConnectionAsync()) { using (var c = db.CreateCommand()) { From e771e09d08b7cd84b5feb00da9b3bdc235d58427 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sat, 17 Feb 2018 00:45:40 -0800 Subject: [PATCH 03/17] Restructured MessageCache, added notes for implementation --- Module/ModLogs/MessageCache.cs | 85 +++++++++++++++++----------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/Module/ModLogs/MessageCache.cs b/Module/ModLogs/MessageCache.cs index 80cd933..7c67a27 100644 --- a/Module/ModLogs/MessageCache.cs +++ b/Module/ModLogs/MessageCache.cs @@ -1,64 +1,63 @@ using Discord.WebSocket; -using Newtonsoft.Json.Linq; -using Noikoio.RegexBot.ConfigItem; using Npgsql; using NpgsqlTypes; +using System; using System.Threading.Tasks; -namespace Noikoio.RegexBot.Module.DBCache +namespace Noikoio.RegexBot.Module.ModLogs { /// - /// Caches information regarding all incoming messages. - /// The function of this feature should be transparent to the user, and thus no configuration is needed. + /// Helper class for . Keeps a database-backed cache of recent messages and assists + /// in reporting message changes and deletions, if configured to do so. + /// Does not manipulate the moderation log managed by the main class, but rather provides supplemental features. /// - class MessageCache : BotFeature + class MessageCache { - // TODO Something that clears expired cache items - private readonly DatabaseConfig _db; + private readonly DiscordSocketClient _dClient; + private readonly AsyncLogger _outLog; + private readonly Func _outGetConfig; - public override string Name => nameof(MessageCache); - - public MessageCache(DiscordSocketClient client) : base(client) + public MessageCache(DiscordSocketClient client, AsyncLogger logger, Func getConfFunc) { - _db = RegexBot.Config.Database; + _dClient = client; + _outLog = logger; + _outGetConfig = getConfFunc; - if (_db.Enabled) - { - CreateCacheTables(); + CreateCacheTables(); - client.MessageReceived += Client_MessageReceived; - //client.MessageUpdated += Client_MessageUpdated; - } - else - { - Log("No database storage available.").Wait(); - } + client.MessageReceived += Client_MessageReceived; + client.MessageUpdated += Client_MessageUpdated; + client.MessageDeleted += Client_MessageDeleted; } - #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) + private async Task Client_MessageReceived(SocketMessage arg) => await CacheMessage(arg); + + private Task Client_MessageUpdated(Discord.Cacheable arg1, SocketMessage arg2, ISocketMessageChannel arg3) { - await Task.Run(() => CacheMessage(arg)); + /* + * TODO: + * Edited messages seem to retain their ID. Need to look into this. + * In any case, the new message must be stored in case of future edits. + * The change must be sent to the reporting channel (if one exists) as if it were + * a typical log entry (even though it's not). + */ + throw new NotImplementedException(); + } + + private Task Client_MessageDeleted(Discord.Cacheable arg1, ISocketMessageChannel arg2) + { + // TODO report message deletion, if reporting channel exists and message is in cache. + throw new NotImplementedException(); } - - //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 + #region Database manipulation + const string TableMessage = "cache_messages"; + private void CreateCacheTables() { - using (var db = _db.GetOpenConnectionAsync().GetAwaiter().GetResult()) + using (var db = RegexBot.Config.GetOpenDatabaseConnectionAsync().GetAwaiter().GetResult()) { using (var c = db.CreateCommand()) { @@ -70,9 +69,9 @@ namespace Noikoio.RegexBot.Module.DBCache + "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)" + + $"FOREIGN KEY (author_id, guild_id) references {EntityCache.SqlHelper.TableUser} (user_id, guild_id)" + ")"; - // TODO figure out how to store message edits + // TODO are more columns needed for edit info? c.ExecuteNonQuery(); } } @@ -83,7 +82,7 @@ namespace Noikoio.RegexBot.Module.DBCache { try { - using (var db = await _db.GetOpenConnectionAsync()) + using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync()) { using (var c = db.CreateCommand()) { @@ -103,7 +102,7 @@ namespace Noikoio.RegexBot.Module.DBCache } catch (NpgsqlException ex) { - await Log($"SQL error in {nameof(CacheMessage)}: " + ex.Message); + await _outLog($"SQL error in {nameof(CacheMessage)}: " + ex.Message); } } } From c51b89e32e15f45d26c4a188b617d7acacb7eaf4 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sat, 17 Feb 2018 00:46:26 -0800 Subject: [PATCH 04/17] Update changed method name --- Module/ModLogs/Sql.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Module/ModLogs/Sql.cs b/Module/ModLogs/Sql.cs index c500698..238b26f 100644 --- a/Module/ModLogs/Sql.cs +++ b/Module/ModLogs/Sql.cs @@ -15,7 +15,7 @@ namespace Noikoio.RegexBot.Module.ModLogs static void CreateTables() { - using (var db = RegexBot.Config.Database.GetOpenConnectionAsync().GetAwaiter().GetResult()) + using (var db = RegexBot.Config.GetOpenDatabaseConnectionAsync().GetAwaiter().GetResult()) { using (var c = db.CreateCommand()) { @@ -24,11 +24,11 @@ namespace Noikoio.RegexBot.Module.ModLogs + "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, " + + $"invoke_id bigint null references {EntityCache.SqlHelper.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)"; + + $"FOREIGN KEY (target_id, guild_id) REFERENCES {EntityCache.SqlHelper.TableUser} (user_id, guild_id)"; c.ExecuteNonQuery(); } using (var c = db.CreateCommand()) From d21c0c115525101161ae66befcc6e3b0870ac769 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 20 Feb 2018 00:19:39 -0800 Subject: [PATCH 05/17] Removed misleading documentation --- ConfigItem/EntityName.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ConfigItem/EntityName.cs b/ConfigItem/EntityName.cs index 046ded7..b738f82 100644 --- a/ConfigItem/EntityName.cs +++ b/ConfigItem/EntityName.cs @@ -3,9 +3,7 @@ enum EntityType { Channel, Role, User } /// - /// Used to join together an entity ID and its name, particularly when read from configuration. - /// In the event of an unknown ID, the ID is found and cached. The ID should preferably be used - /// over the entity's string-based name, as it can change at any time. + /// Used to join together an entity ID and its name when read from configuration. /// In configuration, entities are fully specified with a prefix (if necessary), an ID, two colons, and a name. /// struct EntityName From 69f9da53132c80fd467b4447e31cefb2f6966899 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 20 Feb 2018 00:48:11 -0800 Subject: [PATCH 06/17] Added mostly complete MessageCache --- Module/ModLogs/MessageCache.cs | 254 +++++++++++++++++++++++++++++---- 1 file changed, 224 insertions(+), 30 deletions(-) diff --git a/Module/ModLogs/MessageCache.cs b/Module/ModLogs/MessageCache.cs index 7c67a27..2969fe6 100644 --- a/Module/ModLogs/MessageCache.cs +++ b/Module/ModLogs/MessageCache.cs @@ -1,4 +1,5 @@ -using Discord.WebSocket; +using Discord; +using Discord.WebSocket; using Npgsql; using NpgsqlTypes; using System; @@ -31,28 +32,178 @@ namespace Noikoio.RegexBot.Module.ModLogs } #region Event handling - private async Task Client_MessageReceived(SocketMessage arg) => await CacheMessage(arg); + private async Task Client_MessageReceived(SocketMessage arg) => await AddOrUpdateCacheItemAsync(arg); - private Task Client_MessageUpdated(Discord.Cacheable arg1, SocketMessage arg2, ISocketMessageChannel arg3) + private async Task Client_MessageUpdated( + Discord.Cacheable before, + SocketMessage after, ISocketMessageChannel channel) { - /* - * TODO: - * Edited messages seem to retain their ID. Need to look into this. - * In any case, the new message must be stored in case of future edits. - * The change must be sent to the reporting channel (if one exists) as if it were - * a typical log entry (even though it's not). - */ - throw new NotImplementedException(); + if (after is SocketUserMessage afterMsg) + { + // We're not interested in all message updates, only those that leave a timestamp. + if (!afterMsg.EditedTimestamp.HasValue) return; + } + else return; // no after??? + + // Once an edited message is cached, the original message contents are discarded. + // This is the only time to report it. + await ProcessReportMessage(false, before.Id, channel, after.Content); + + await AddOrUpdateCacheItemAsync(after); } - private Task Client_MessageDeleted(Discord.Cacheable arg1, ISocketMessageChannel arg2) + private async Task Client_MessageDeleted( + Discord.Cacheable msg, ISocketMessageChannel channel) { - // TODO report message deletion, if reporting channel exists and message is in cache. - throw new NotImplementedException(); + await ProcessReportMessage(true, msg.Id, channel, null); } #endregion - #region Database manipulation + #region Reporting + + // Reports an edited or deleted message as if it were a log entry (even though it's not). + private async Task ProcessReportMessage( + bool isDelete, ulong messageId, ISocketMessageChannel ch, string editMsg) + { + var cht = ch as SocketTextChannel; + if (cht == null) + { + // TODO remove debug print + Console.WriteLine("Incoming message not of a text channel"); + return; + } + ulong guildId = cht.Guild.Id; + + // Check if enabled before doing anything else + var rptTarget = _outGetConfig(guildId) as ConfigItem.EntityName?; + if (!rptTarget.HasValue) return; + + // Regardless of delete or edit, it is necessary to get database information. + EntityCache.CacheUser ucd = null; + ulong userId; + string cacheMsg; + try + { + using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync()) + { + using (var c = db.CreateCommand()) + { + c.CommandText = "SELECT author_id, message FROM " + TableMessage + + " WHERE message_id = @MessageId"; + c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = messageId; + c.Prepare(); + using (var r = await c.ExecuteReaderAsync()) + { + if (await r.ReadAsync()) + { + userId = unchecked((ulong)r.GetInt64(0)); + cacheMsg = r.GetString(1); + } + else + { + userId = 0; + cacheMsg = "*(Message not in cache.)*"; + } + } + } + } + if (userId != 0) ucd = await EntityCache.EntityCache.QueryAsync(guildId, userId); + } + catch (NpgsqlException ex) + { + await _outLog($"SQL error in {nameof(ProcessReportMessage)}: " + ex.Message); + cacheMsg = "**Database error. See log.**"; + } + + // Find target channel, prepare and send out message + var em = CreateReportEmbed(isDelete, ucd, messageId, ch, (cacheMsg, editMsg)); + var rptTargetChannel = _dClient.GetGuild(guildId)?.GetTextChannel(rptTarget.Value.Id.Value); + if (rptTargetChannel == null) + { + await _outLog("Target channel not found."); + // TODO make a more descriptive error message + return; + } + await rptTargetChannel.SendMessageAsync("", embed: em); + } + + const int ReportCutoffLength = 750; + const string ReportCutoffNotify = "**Message length too long; showing first {0} characters.**\n\n"; + private EmbedBuilder CreateReportEmbed( + bool isDelete, + EntityCache.CacheUser ucd, ulong messageId, ISocketMessageChannel chInfo, + (string, string) content) // tuple: Item1 = cached content. Item2 = after-edit message + { + string before = content.Item1; + string after = content.Item2; + if (content.Item1.Length > ReportCutoffLength) + { + before = string.Format(ReportCutoffNotify, ReportCutoffLength) + + content.Item1.Substring(ReportCutoffLength); + } + if (isDelete && content.Item2.Length > ReportCutoffLength) + { + after = string.Format(ReportCutoffNotify, ReportCutoffLength) + + content.Item2.Substring(ReportCutoffLength); + } + + // Note: Value for ucb is null if cached user could not be determined + var eb = new EmbedBuilder + { + Author = new EmbedAuthorBuilder() + { + IconUrl = ucd?.AvatarUrl + }, + Fields = new System.Collections.Generic.List(), + Footer = new EmbedFooterBuilder() + { + Text = (ucd == null ? "" : $"UID {ucd.UserId} - ") + $"MID {messageId}", + IconUrl = _dClient.CurrentUser.GetAvatarUrl() + }, + Timestamp = DateTimeOffset.Now + }; + + if (isDelete) + { + eb.Color = new Color(0x9b9b9b); + eb.Description = content.Item1; + eb.Author.Name = "Message deleted by " + + ucd == null ? "unknown user" : $"{ucd.Username}#{ucd.Discriminator}"; + } + else + { + eb.Color = new Color(8615955); + eb.Fields.Add(new EmbedFieldBuilder() + { + Name = "Before", + Value = before + }); + eb.Fields.Add(new EmbedFieldBuilder() + { + Name = "After", + Value = after + }); + } + + if (ucd != null) eb.Fields.Add(new EmbedFieldBuilder() + { + Name = "Username", + Value = $"<@!{ucd.UserId}>", + IsInline = true + }); + eb.Fields.Add(new EmbedFieldBuilder() + { + Name = "Channel", + Value = $"<#{chInfo.Id}>\n#{chInfo.Name}", + IsInline = true + }); + + return eb; + } + + #endregion + + #region Database storage/retrieval const string TableMessage = "cache_messages"; private void CreateCacheTables() @@ -65,20 +216,61 @@ namespace Noikoio.RegexBot.Module.ModLogs + "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... + + "channel_id bigint not null, " // TODO channel cache fk when that gets implemented + "created_ts timestamptz not null, " + "edited_ts timestamptz null, " + "message text not null, " + $"FOREIGN KEY (author_id, guild_id) references {EntityCache.SqlHelper.TableUser} (user_id, guild_id)" + ")"; - // TODO are more columns needed for edit info? c.ExecuteNonQuery(); } } } - #endregion - private async Task CacheMessage(SocketMessage msg) + private async Task AddOrUpdateCacheItemAsync(SocketMessage msg) + { + try + { + using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync()) + { + // No upsert. Delete, then add. + using (var t = db.BeginTransaction()) + { + using (var c = db.CreateCommand()) + { + c.CommandText = "DELETE FROM " + TableMessage + " WHERE message_id = @MessageId"; + c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = msg.Id; + c.Prepare(); + await c.ExecuteNonQueryAsync(); + } + using (var c = db.CreateCommand()) + { + c.CommandText = "INSERT INTO " + TableMessage + + " (message_id, author_id, guild_id, channel_id, created_ts, edited_ts, message) VALUES " + + "(@MessageId, @UserId, @GuildId, @ChannelId, @Date, @Edit, @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; + if (msg.EditedTimestamp.HasValue) + c.Parameters.Add("@Edit", NpgsqlDbType.TimestampTZ).Value = msg.EditedTimestamp.Value; + else + c.Parameters.Add("@Edit", NpgsqlDbType.TimestampTZ).Value = DBNull.Value; + c.Parameters.Add("@Message", NpgsqlDbType.Text).Value = msg.Content; + c.Prepare(); + await c.ExecuteNonQueryAsync(); + } + } + } + } + catch (NpgsqlException ex) + { + await _outLog($"SQL error in {nameof(AddOrUpdateCacheItemAsync)}: " + ex.Message); + } + } + + private async Task GetCachedMessageAsync(ulong messageId) { try { @@ -86,24 +278,26 @@ namespace Noikoio.RegexBot.Module.ModLogs { 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.CommandText = "SELECT message FROM " + TableMessage + + " WHERE message_id = @MessageId"; + c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = messageId; c.Prepare(); - await c.ExecuteNonQueryAsync(); + using (var r = await c.ExecuteReaderAsync()) + { + if (await r.ReadAsync()) + return r.GetString(0); + else + return null; + } } } } catch (NpgsqlException ex) { - await _outLog($"SQL error in {nameof(CacheMessage)}: " + ex.Message); + await _outLog($"SQL error in {nameof(GetCachedMessageAsync)}: " + ex.Message); + return null; } } + #endregion } } From 6a52a2ba80cbcab683d38b8d15dd39e51b47136a Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 20 Feb 2018 00:58:16 -0800 Subject: [PATCH 07/17] Preparing for testing --- Module/ModLogs/ModLogs.cs | 63 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/Module/ModLogs/ModLogs.cs b/Module/ModLogs/ModLogs.cs index 14fc93b..cabc1fc 100644 --- a/Module/ModLogs/ModLogs.cs +++ b/Module/ModLogs/ModLogs.cs @@ -1,10 +1,71 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; +using Discord.WebSocket; +using Newtonsoft.Json.Linq; +using Noikoio.RegexBot.ConfigItem; namespace Noikoio.RegexBot.Module.ModLogs { - class ModLogs + /// + /// Logs certain events of note to a database for moderators to keep track of user behavior. + /// Makes use of a helper class, . + /// + class ModLogs : BotModule { + public override string Name => "ModLogs"; + + private readonly MessageCache _msgCacheInstance; + + public ModLogs(DiscordSocketClient client) : base(client) + { + if (!RegexBot.Config.DatabaseAvailable) return; // do nothing; warn in ProcessConfiguration + + _msgCacheInstance = new MessageCache(client, Log, GetConfig); + + throw new NotImplementedException(); + } + + [ConfigSection("ModLogs")] + public override async Task ProcessConfiguration(JToken configSection) + { + if (configSection.Type != JTokenType.Object) + throw new RuleImportException("Configuration for this section is invalid."); + var conf = (JObject)configSection; + + if (!RegexBot.Config.DatabaseAvailable) + { + await Log("Database access is not available. This module will not load."); + return null; + } + + try + { + // MessageCache debug: will store an EntityName or die trying + EntityName? mctarget = new EntityName(conf["mctarget"].Value(), EntityType.Channel); + await Log("Enabled MessageCache test on " + mctarget.Value.ToString()); + return mctarget; + } + catch (Exception) + { + // well, not really die + return null; + } + + + /* + * Ideas: + * -Reporting: + * --Reporting channel + * --Types to report + * ---Ignored if no reporting channel has been set + * ---Default to join, quit, kick, ban, ... + * ---Any override will disregard defaults + * -also how will commands work? how to tie into commands mod? + * --modlogs command should also only report a subset of things. custom. + * ---ex: don't report nick changes + */ + } } } \ No newline at end of file From 22af7685ee328d32cb58526515258dc3dc3eab18 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 20 Feb 2018 00:58:55 -0800 Subject: [PATCH 08/17] Fix compilation errors due to database change --- EntityCache/Module.cs | 4 +--- EntityCache/SqlHelper.cs | 6 +++--- Module/ModLogs/Entry.cs | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/EntityCache/Module.cs b/EntityCache/Module.cs index f976632..0a4da61 100644 --- a/EntityCache/Module.cs +++ b/EntityCache/Module.cs @@ -19,9 +19,7 @@ namespace Noikoio.RegexBot.EntityCache public Module(DiscordSocketClient client) : base(client) { - _db = RegexBot.Config.Database; - - if (_db.Available) + if (RegexBot.Config.DatabaseAvailable) { SqlHelper.CreateCacheTablesAsync().Wait(); diff --git a/EntityCache/SqlHelper.cs b/EntityCache/SqlHelper.cs index d19489c..bd666e9 100644 --- a/EntityCache/SqlHelper.cs +++ b/EntityCache/SqlHelper.cs @@ -19,7 +19,7 @@ namespace Noikoio.RegexBot.EntityCache // Reminder: Check Cache query methods if making changes to tables internal static async Task CreateCacheTablesAsync() { - var db = await RegexBot.Config.Database.GetOpenConnectionAsync(); + var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync(); if (db == null) return; using (db) { @@ -83,7 +83,7 @@ namespace Noikoio.RegexBot.EntityCache #region Insertions and updates internal static async Task UpdateGuildAsync(SocketGuild g) { - var db = await RegexBot.Config.Database.GetOpenConnectionAsync(); + var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync(); if (db == null) return; using (db) { @@ -109,7 +109,7 @@ namespace Noikoio.RegexBot.EntityCache } internal static async Task UpdateGuildMemberAsync(IEnumerable users) { - var db = await RegexBot.Config.Database.GetOpenConnectionAsync(); + var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync(); if (db == null) return; using (db) { diff --git a/Module/ModLogs/Entry.cs b/Module/ModLogs/Entry.cs index 4e7918b..d09e1e3 100644 --- a/Module/ModLogs/Entry.cs +++ b/Module/ModLogs/Entry.cs @@ -89,7 +89,7 @@ namespace Noikoio.RegexBot.Module.ModLogs /// Null if no result. public static async Task QueryIdAsync(ulong guild, int id) { - using (var db = await RegexBot.Config.Database.GetOpenConnectionAsync()) + using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync()) { using (var c = db.CreateCommand()) { @@ -121,7 +121,7 @@ namespace Noikoio.RegexBot.Module.ModLogs } var result = new List(); - using (var db = await RegexBot.Config.Database.GetOpenConnectionAsync()) + using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync()) { using (var c = db.CreateCommand()) { From b6f14d5ad4eccd490d6e189bf031ac2ccdc3e5e1 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 20 Feb 2018 01:23:56 -0800 Subject: [PATCH 09/17] This too --- RegexBot.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/RegexBot.cs b/RegexBot.cs index e4e8669..cf540ed 100644 --- a/RegexBot.cs +++ b/RegexBot.cs @@ -57,6 +57,7 @@ namespace Noikoio.RegexBot new Module.DMLogger.DMLogger(_client), new Module.AutoMod.AutoMod(_client), new Module.ModTools.ModTools(_client), + new Module.ModLogs.ModLogs(_client), new Module.AutoRespond.AutoRespond(_client), new EntityCache.Module(_client) // EntityCache goes before anything else that uses its data }; From 3362c8701c4dfd70367e44640421b7fe4b55714f Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 20 Feb 2018 12:25:29 -0800 Subject: [PATCH 10/17] Fix crash on attempting to create tables --- RegexBot.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RegexBot.cs b/RegexBot.cs index cf540ed..ef73229 100644 --- a/RegexBot.cs +++ b/RegexBot.cs @@ -57,9 +57,11 @@ namespace Noikoio.RegexBot new Module.DMLogger.DMLogger(_client), new Module.AutoMod.AutoMod(_client), new Module.ModTools.ModTools(_client), - new Module.ModLogs.ModLogs(_client), new Module.AutoRespond.AutoRespond(_client), - new EntityCache.Module(_client) // EntityCache goes before anything else that uses its data + + // EntityCache loads before anything using it + new EntityCache.Module(_client), + new Module.ModLogs.ModLogs(_client) }; // Set up logging From 7f22da5a4ee3bcc086a69242c599c284e31bb329 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 20 Feb 2018 12:26:10 -0800 Subject: [PATCH 11/17] Fixed several issues; ready for testing --- Module/ModLogs/MessageCache.cs | 131 ++++++++++++++++----------------- Module/ModLogs/ModLogs.cs | 2 +- 2 files changed, 65 insertions(+), 68 deletions(-) diff --git a/Module/ModLogs/MessageCache.cs b/Module/ModLogs/MessageCache.cs index 2969fe6..90bed96 100644 --- a/Module/ModLogs/MessageCache.cs +++ b/Module/ModLogs/MessageCache.cs @@ -3,6 +3,7 @@ using Discord.WebSocket; using Npgsql; using NpgsqlTypes; using System; +using System.Text; using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModLogs @@ -32,18 +33,27 @@ namespace Noikoio.RegexBot.Module.ModLogs } #region Event handling - private async Task Client_MessageReceived(SocketMessage arg) => await AddOrUpdateCacheItemAsync(arg); + private async Task Client_MessageReceived(SocketMessage arg) + { + if (arg.Author.IsBot) return; + + await AddOrUpdateCacheItemAsync(arg); + } private async Task Client_MessageUpdated( - Discord.Cacheable before, - SocketMessage after, ISocketMessageChannel channel) + Cacheable before, SocketMessage after, ISocketMessageChannel channel) { - if (after is SocketUserMessage afterMsg) + if (after.Author.IsBot) return; + + // We only want channel messages + if (after is SocketUserMessage afterMsg && !(afterMsg is IDMChannel)) { + if (after.Author.IsBot) return; + // We're not interested in all message updates, only those that leave a timestamp. if (!afterMsg.EditedTimestamp.HasValue) return; } - else return; // no after??? + else return; // probably unnecessary? // Once an edited message is cached, the original message contents are discarded. // This is the only time to report it. @@ -52,27 +62,24 @@ namespace Noikoio.RegexBot.Module.ModLogs await AddOrUpdateCacheItemAsync(after); } - private async Task Client_MessageDeleted( - Discord.Cacheable msg, ISocketMessageChannel channel) + private async Task Client_MessageDeleted(Cacheable msg, ISocketMessageChannel channel) { await ProcessReportMessage(true, msg.Id, channel, null); } #endregion #region Reporting - // Reports an edited or deleted message as if it were a log entry (even though it's not). private async Task ProcessReportMessage( bool isDelete, ulong messageId, ISocketMessageChannel ch, string editMsg) { - var cht = ch as SocketTextChannel; - if (cht == null) + ulong guildId; + if (ch is SocketTextChannel sch) { - // TODO remove debug print - Console.WriteLine("Incoming message not of a text channel"); - return; + if (sch is IDMChannel) return; + guildId = sch.Guild.Id; } - ulong guildId = cht.Guild.Id; + else return; // Check if enabled before doing anything else var rptTarget = _outGetConfig(guildId) as ConfigItem.EntityName?; @@ -127,24 +134,24 @@ namespace Noikoio.RegexBot.Module.ModLogs await rptTargetChannel.SendMessageAsync("", embed: em); } - const int ReportCutoffLength = 750; + const int ReportCutoffLength = 500; const string ReportCutoffNotify = "**Message length too long; showing first {0} characters.**\n\n"; private EmbedBuilder CreateReportEmbed( bool isDelete, EntityCache.CacheUser ucd, ulong messageId, ISocketMessageChannel chInfo, (string, string) content) // tuple: Item1 = cached content. Item2 = after-edit message { - string before = content.Item1; - string after = content.Item2; + string msgCached = content.Item1; + string msgPostEdit = content.Item2; if (content.Item1.Length > ReportCutoffLength) { - before = string.Format(ReportCutoffNotify, ReportCutoffLength) - + content.Item1.Substring(ReportCutoffLength); + msgCached = string.Format(ReportCutoffNotify, ReportCutoffLength) + + content.Item1.Substring(0, ReportCutoffLength); } - if (isDelete && content.Item2.Length > ReportCutoffLength) + if (!isDelete && content.Item2.Length > ReportCutoffLength) { - after = string.Format(ReportCutoffNotify, ReportCutoffLength) - + content.Item2.Substring(ReportCutoffLength); + msgPostEdit = string.Format(ReportCutoffNotify, ReportCutoffLength) + + content.Item2.Substring(0, ReportCutoffLength); } // Note: Value for ucb is null if cached user could not be determined @@ -157,45 +164,44 @@ namespace Noikoio.RegexBot.Module.ModLogs Fields = new System.Collections.Generic.List(), Footer = new EmbedFooterBuilder() { - Text = (ucd == null ? "" : $"UID {ucd.UserId} - ") + $"MID {messageId}", + Text = (ucd == null ? "UID: Unknown" : $"UID: {ucd.UserId}"), IconUrl = _dClient.CurrentUser.GetAvatarUrl() }, - Timestamp = DateTimeOffset.Now + Timestamp = DateTimeOffset.UtcNow }; if (isDelete) { + eb.Author.Name = "Message deleted by "; eb.Color = new Color(0x9b9b9b); - eb.Description = content.Item1; - eb.Author.Name = "Message deleted by " - + ucd == null ? "unknown user" : $"{ucd.Username}#{ucd.Discriminator}"; + eb.Description = msgCached; } else { - eb.Color = new Color(8615955); + eb.Author.Name = "Message edited by "; + eb.Color = new Color(0x837813); eb.Fields.Add(new EmbedFieldBuilder() { Name = "Before", - Value = before + Value = msgCached }); eb.Fields.Add(new EmbedFieldBuilder() { Name = "After", - Value = after + Value = msgPostEdit }); } - - if (ucd != null) eb.Fields.Add(new EmbedFieldBuilder() - { - Name = "Username", - Value = $"<@!{ucd.UserId}>", - IsInline = true - }); + + eb.Author.Name += ucd == null ? "unknown user" : $"{ucd.Username}#{ucd.Discriminator}"; + + var context = new StringBuilder(); + if (ucd != null) context.AppendLine($"Username: <@!{ucd.UserId}>"); + context.AppendLine($"Channel: <#{chInfo.Id}> #{chInfo.Name}"); + context.Append($"Message ID: {messageId}"); eb.Fields.Add(new EmbedFieldBuilder() { - Name = "Channel", - Value = $"<#{chInfo.Id}>\n#{chInfo.Name}", - IsInline = true + Name = "Context", + Value = context.ToString() }); return eb; @@ -233,34 +239,25 @@ namespace Noikoio.RegexBot.Module.ModLogs { using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync()) { - // No upsert. Delete, then add. - using (var t = db.BeginTransaction()) + using (var c = db.CreateCommand()) { - using (var c = db.CreateCommand()) - { - c.CommandText = "DELETE FROM " + TableMessage + " WHERE message_id = @MessageId"; - c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = msg.Id; - c.Prepare(); - await c.ExecuteNonQueryAsync(); - } - using (var c = db.CreateCommand()) - { - c.CommandText = "INSERT INTO " + TableMessage - + " (message_id, author_id, guild_id, channel_id, created_ts, edited_ts, message) VALUES " - + "(@MessageId, @UserId, @GuildId, @ChannelId, @Date, @Edit, @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; - if (msg.EditedTimestamp.HasValue) - c.Parameters.Add("@Edit", NpgsqlDbType.TimestampTZ).Value = msg.EditedTimestamp.Value; - else - c.Parameters.Add("@Edit", NpgsqlDbType.TimestampTZ).Value = DBNull.Value; - c.Parameters.Add("@Message", NpgsqlDbType.Text).Value = msg.Content; - c.Prepare(); - await c.ExecuteNonQueryAsync(); - } + c.CommandText = "INSERT INTO " + TableMessage + + " (message_id, author_id, guild_id, channel_id, created_ts, edited_ts, message) VALUES" + + " (@MessageId, @UserId, @GuildId, @ChannelId, @Date, @Edit, @Message)" + + " ON CONFLICT (message_id) DO UPDATE" + + " SET message = EXCLUDED.message, edited_ts = EXCLUDED.edited_ts"; + 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; + if (msg.EditedTimestamp.HasValue) + c.Parameters.Add("@Edit", NpgsqlDbType.TimestampTZ).Value = msg.EditedTimestamp.Value; + else + c.Parameters.Add("@Edit", NpgsqlDbType.TimestampTZ).Value = DBNull.Value; + c.Parameters.Add("@Message", NpgsqlDbType.Text).Value = msg.Content; + c.Prepare(); + await c.ExecuteNonQueryAsync(); } } } diff --git a/Module/ModLogs/ModLogs.cs b/Module/ModLogs/ModLogs.cs index cabc1fc..03f4b94 100644 --- a/Module/ModLogs/ModLogs.cs +++ b/Module/ModLogs/ModLogs.cs @@ -24,7 +24,7 @@ namespace Noikoio.RegexBot.Module.ModLogs _msgCacheInstance = new MessageCache(client, Log, GetConfig); - throw new NotImplementedException(); + //throw new NotImplementedException(); } [ConfigSection("ModLogs")] From 2b56dab0e320cebb32a60e651e6cda17baa247e9 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Thu, 22 Feb 2018 18:04:27 -0800 Subject: [PATCH 12/17] Cleaned up MessageCache slightly --- Module/ModLogs/MessageCache.cs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Module/ModLogs/MessageCache.cs b/Module/ModLogs/MessageCache.cs index 90bed96..23cf58b 100644 --- a/Module/ModLogs/MessageCache.cs +++ b/Module/ModLogs/MessageCache.cs @@ -19,6 +19,8 @@ namespace Noikoio.RegexBot.Module.ModLogs private readonly AsyncLogger _outLog; private readonly Func _outGetConfig; + // TODO: How to clear the cache after a time? Can't hold on to this forever. + public MessageCache(DiscordSocketClient client, AsyncLogger logger, Func getConfFunc) { _dClient = client; @@ -55,8 +57,8 @@ namespace Noikoio.RegexBot.Module.ModLogs } else return; // probably unnecessary? - // Once an edited message is cached, the original message contents are discarded. - // This is the only time to report it. + // Once an edited message is cached, the original message contents are lost. + // This is the only time available to report it. await ProcessReportMessage(false, before.Id, channel, after.Content); await AddOrUpdateCacheItemAsync(after); @@ -81,11 +83,14 @@ namespace Noikoio.RegexBot.Module.ModLogs } else return; - // Check if enabled before doing anything else + // Check if this feature is enabled before doing anything else. var rptTarget = _outGetConfig(guildId) as ConfigItem.EntityName?; if (!rptTarget.HasValue) return; - // Regardless of delete or edit, it is necessary to get database information. + // Ignore if it's a message being deleted withing the reporting channel. + if (isDelete && rptTarget.Value.Id.Value == ch.Id) return; + + // Regardless of delete or edit, it is necessary to get the equivalent database information. EntityCache.CacheUser ucd = null; ulong userId; string cacheMsg; @@ -123,7 +128,6 @@ namespace Noikoio.RegexBot.Module.ModLogs } // Find target channel, prepare and send out message - var em = CreateReportEmbed(isDelete, ucd, messageId, ch, (cacheMsg, editMsg)); var rptTargetChannel = _dClient.GetGuild(guildId)?.GetTextChannel(rptTarget.Value.Id.Value); if (rptTargetChannel == null) { @@ -131,6 +135,7 @@ namespace Noikoio.RegexBot.Module.ModLogs // TODO make a more descriptive error message return; } + var em = CreateReportEmbed(isDelete, ucd, messageId, ch, (cacheMsg, editMsg)); await rptTargetChannel.SendMessageAsync("", embed: em); } @@ -139,7 +144,7 @@ namespace Noikoio.RegexBot.Module.ModLogs private EmbedBuilder CreateReportEmbed( bool isDelete, EntityCache.CacheUser ucd, ulong messageId, ISocketMessageChannel chInfo, - (string, string) content) // tuple: Item1 = cached content. Item2 = after-edit message + (string, string) content) // Item1 = cached content. Item2 = after-edit message (null if isDelete) { string msgCached = content.Item1; string msgPostEdit = content.Item2; @@ -154,7 +159,7 @@ namespace Noikoio.RegexBot.Module.ModLogs + content.Item2.Substring(0, ReportCutoffLength); } - // Note: Value for ucb is null if cached user could not be determined + // Note: Value for ucb can be null if cached user could not be determined. var eb = new EmbedBuilder { Author = new EmbedAuthorBuilder() @@ -164,7 +169,7 @@ namespace Noikoio.RegexBot.Module.ModLogs Fields = new System.Collections.Generic.List(), Footer = new EmbedFooterBuilder() { - Text = (ucd == null ? "UID: Unknown" : $"UID: {ucd.UserId}"), + Text = "User ID: " + ucd?.UserId.ToString() ?? "Unknown", IconUrl = _dClient.CurrentUser.GetAvatarUrl() }, Timestamp = DateTimeOffset.UtcNow @@ -172,14 +177,14 @@ namespace Noikoio.RegexBot.Module.ModLogs if (isDelete) { - eb.Author.Name = "Message deleted by "; - eb.Color = new Color(0x9b9b9b); + eb.Author.Name = "Deleted message by "; + eb.Color = new Color(0xff7373); eb.Description = msgCached; } else { - eb.Author.Name = "Message edited by "; - eb.Color = new Color(0x837813); + eb.Author.Name = "Edited message by "; + eb.Color = new Color(0xffcc40); eb.Fields.Add(new EmbedFieldBuilder() { Name = "Before", @@ -206,7 +211,6 @@ namespace Noikoio.RegexBot.Module.ModLogs return eb; } - #endregion #region Database storage/retrieval From 8c0ae8fd4401dbd7402145bab2f09fa81aef6b3b Mon Sep 17 00:00:00 2001 From: Noikoio Date: Thu, 22 Feb 2018 18:05:50 -0800 Subject: [PATCH 13/17] Updated notes --- Module/ModLogs/ModLogs.cs | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/Module/ModLogs/ModLogs.cs b/Module/ModLogs/ModLogs.cs index 03f4b94..acfc8ff 100644 --- a/Module/ModLogs/ModLogs.cs +++ b/Module/ModLogs/ModLogs.cs @@ -20,7 +20,8 @@ namespace Noikoio.RegexBot.Module.ModLogs public ModLogs(DiscordSocketClient client) : base(client) { - if (!RegexBot.Config.DatabaseAvailable) return; // do nothing; warn in ProcessConfiguration + // Do nothing if database unavailable. The user will be informed by ProcessConfiguration. + if (!RegexBot.Config.DatabaseAvailable) return; _msgCacheInstance = new MessageCache(client, Log, GetConfig); @@ -36,13 +37,13 @@ namespace Noikoio.RegexBot.Module.ModLogs if (!RegexBot.Config.DatabaseAvailable) { - await Log("Database access is not available. This module will not load."); + await Log("Database access is not available. This module be unavailable."); return null; } try { - // MessageCache debug: will store an EntityName or die trying + // MessageCache testing: will store an EntityName or die trying EntityName? mctarget = new EntityName(conf["mctarget"].Value(), EntityType.Channel); await Log("Enabled MessageCache test on " + mctarget.Value.ToString()); return mctarget; @@ -52,19 +53,25 @@ namespace Noikoio.RegexBot.Module.ModLogs // well, not really die return null; } - /* - * Ideas: - * -Reporting: - * --Reporting channel - * --Types to report - * ---Ignored if no reporting channel has been set - * ---Default to join, quit, kick, ban, ... - * ---Any override will disregard defaults - * -also how will commands work? how to tie into commands mod? - * --modlogs command should also only report a subset of things. custom. - * ---ex: don't report nick changes + * Concept: + * "ModLogs": { + * "AutoReporting": { + * // behavior for how to output to the reporting channel + * // MessageCache looks for configuration values within here. + * "Channel": "something compatible with EntityName", + * "Events": "perhaps a single string of separated event types" + * }, + * "QueryOptions": { + * // Behavior for the query command (which is defined here rather than ModTools) + * // Need to stress in the documentation that "msgedit" and "msgdelete" events + * // are not kept and cannot be queried + * "QueryCommand": "!modlogs", + * "Permission": "Moderators", // either a string that says "Moderators" or an EntityList + * "DefaultQueryEvents": "another single string of separated event types", + * } + * } */ } } From 419370c379b012d0d6e5e0e6eda0bde5ab79f51f Mon Sep 17 00:00:00 2001 From: Noikoio Date: Tue, 27 Feb 2018 12:32:12 -0800 Subject: [PATCH 14/17] Adding clarification --- BotModule.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BotModule.cs b/BotModule.cs index 959b6e3..e0e5fd5 100644 --- a/BotModule.cs +++ b/BotModule.cs @@ -25,6 +25,7 @@ namespace Noikoio.RegexBot /// /// Processes module-specific configuration. + /// This method is not called if the user did not provide configuration for the module. /// /// /// Module code should not hold on to this data, but instead use to retrieve @@ -36,8 +37,8 @@ namespace Noikoio.RegexBot /// Processed configuration data prepared for later use. /// /// - /// This method should throw RuleImportException in the event of any error. - /// The exception message will be properly logged. + /// This method should throw + /// in the event of configuration errors. The exception message will be properly displayed. /// public abstract Task ProcessConfiguration(JToken configSection); From 6ca73a9b6b9b8577bdf0e5ef7d5f6557d0b4fb4c Mon Sep 17 00:00:00 2001 From: Noikoio Date: Thu, 1 Mar 2018 19:25:08 -0800 Subject: [PATCH 15/17] Added proper MessageCache configuration Though it's now capable of loading configuration for the full ModLogs module, not all features are available yet. --- Module/ModLogs/EventType.cs | 24 +++++++ Module/ModLogs/GuildConfig.cs | 127 +++++++++++++++++++++++++++++++++ Module/ModLogs/MessageCache.cs | 19 ++--- Module/ModLogs/ModLogs.cs | 38 ++-------- 4 files changed, 166 insertions(+), 42 deletions(-) create mode 100644 Module/ModLogs/EventType.cs create mode 100644 Module/ModLogs/GuildConfig.cs diff --git a/Module/ModLogs/EventType.cs b/Module/ModLogs/EventType.cs new file mode 100644 index 0000000..c364bed --- /dev/null +++ b/Module/ModLogs/EventType.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Noikoio.RegexBot.Module.ModLogs +{ + // Types of non-custom events that can be referenced by ModLogs in configuration. + // Enum value names will show themselves to the user in the form of strings valid in configuration, + // so try not to change those without good reason. + [Flags] + enum EventType + { + None = 0x0, + Note = 0x1, + Warn = 0x2, + Kick = 0x4, + Ban = 0x8, + JoinGuild = 0x10, + LeaveGuild = 0x20, + NameChange = 0x40, + MsgEdit = 0x80, + MsgDelete = 0x100 + } +} diff --git a/Module/ModLogs/GuildConfig.cs b/Module/ModLogs/GuildConfig.cs new file mode 100644 index 0000000..f150c5a --- /dev/null +++ b/Module/ModLogs/GuildConfig.cs @@ -0,0 +1,127 @@ +using Newtonsoft.Json.Linq; +using Noikoio.RegexBot.ConfigItem; +using System; + +namespace Noikoio.RegexBot.Module.ModLogs +{ + /// + /// ModLogs guild-specific configuration values. + /// + class GuildConfig + { + // Event reporting + private readonly EntityName _rptTarget; + private EventType _rptTypes; + /// + /// Target reporting channel. + /// + public EntityName? RptTarget => _rptTarget; + /// + /// Event types to send to the reporting channel. + /// + public EventType RptTypes => _rptTypes; + + // Query command + private readonly string _qCmd; // command name + private readonly EntityList _qAccess; // list of those able to issue the command + private readonly EventType _qDefaultAnswer; // default entry types to display + /// + /// Query command. The first word in an incoming message, including prefix, that triggers a query. + /// + public string QrCommand => _qCmd; + /// + /// List of users permitted to invoke the query command. + /// If null, refer to the guild's Moderators list. + /// + public EntityList QrPermittedUsers => _qAccess; + /// + /// Event types to display in a query. + /// + public EventType QrTypes => _qDefaultAnswer; + + public GuildConfig(JObject cfgRoot) + { + // AutoReporting settings + var arcfg = cfgRoot["AutoReporting"]; + if (arcfg == null) + { + _rptTarget = default(EntityName); // NOTE: Change this if EntityName becomes a class later + _rptTypes = EventType.None; + } + else if (arcfg.Type == JTokenType.Object) + { + string chval = arcfg["Channel"]?.Value(); + if (chval == null) throw new RuleImportException("Reporting channel is not defined."); + if (!string.IsNullOrWhiteSpace(chval) && chval[0] == '#') + _rptTarget = new EntityName(chval.Substring(1, chval.Length-1), EntityType.Channel); + else + throw new RuleImportException("Reporting channel is not properly defined."); + // Require the channel's ID for now. + if (!_rptTarget.Id.HasValue) throw new RuleImportException("Reporting channel's ID must be specified."); + + // TODO make optional + string rpval = arcfg["Events"]?.Value(); + _rptTypes = GetTypesFromString(rpval); + } + else + { + throw new RuleImportException("Section for AutoReporting is not correctly defined."); + } + + // QueryCommand settings + var qccfg = cfgRoot["QueryCommand"]; + if (qccfg == null) + { + _qCmd = null; + _qAccess = null; + _qDefaultAnswer = EventType.None; + } + else if (arcfg.Type == JTokenType.Object) + { + _qCmd = arcfg["Command"]?.Value(); + if (string.IsNullOrWhiteSpace(_qCmd)) + throw new RuleImportException("Query command option must have a value."); + if (_qCmd.Contains(" ")) + throw new RuleImportException("Query command must not contain spaces."); + + var acl = arcfg["AllowedUsers"]; + if (acl == null) _qAccess = null; + else _qAccess = new EntityList(acl); + + // TODO make optional + string ansval = arcfg["DefaultEvents"]?.Value(); + _qDefaultAnswer = GetTypesFromString(ansval); + } + else + { + throw new RuleImportException("Section for QueryCommand is not correctly defined."); + } + } + + public static EventType GetTypesFromString(string input) + { + if (string.IsNullOrWhiteSpace(input)) + throw new RuleImportException("Types are not properly defined."); + + var strTypes = input.Split( + new char[] { ' ', ',', '/', '+' }, // and more? + StringSplitOptions.RemoveEmptyEntries); + + EventType endResult = EventType.None; + foreach (var item in strTypes) + { + try + { + var result = Enum.Parse(item, true); + endResult |= result; + } + catch (ArgumentException) + { + throw new RuleImportException($"Unable to determine the given event type \"{item}\""); + } + } + + return endResult; + } + } +} diff --git a/Module/ModLogs/MessageCache.cs b/Module/ModLogs/MessageCache.cs index 23cf58b..6ab2dfb 100644 --- a/Module/ModLogs/MessageCache.cs +++ b/Module/ModLogs/MessageCache.cs @@ -9,9 +9,9 @@ using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModLogs { /// - /// Helper class for . Keeps a database-backed cache of recent messages and assists + /// Helper class for . Keeps a database-backed cache of recent messages for use /// in reporting message changes and deletions, if configured to do so. - /// Does not manipulate the moderation log managed by the main class, but rather provides supplemental features. + /// Despite its place, it does not manipulate moderation logs. It simply pulls from the same configuration. /// class MessageCache { @@ -66,6 +66,7 @@ namespace Noikoio.RegexBot.Module.ModLogs private async Task Client_MessageDeleted(Cacheable msg, ISocketMessageChannel channel) { + if (channel is IDMChannel) return; // No DMs await ProcessReportMessage(true, msg.Id, channel, null); } #endregion @@ -84,11 +85,13 @@ namespace Noikoio.RegexBot.Module.ModLogs else return; // Check if this feature is enabled before doing anything else. - var rptTarget = _outGetConfig(guildId) as ConfigItem.EntityName?; - if (!rptTarget.HasValue) return; + var cfg = _outGetConfig(guildId) as GuildConfig; + if (cfg == null) return; + if (isDelete && (cfg.RptTypes & EventType.MsgDelete) == 0) return; + if (!isDelete && (cfg.RptTypes & EventType.MsgEdit) == 0) return; // Ignore if it's a message being deleted withing the reporting channel. - if (isDelete && rptTarget.Value.Id.Value == ch.Id) return; + if (isDelete && cfg.RptTarget.Value.Id.Value == ch.Id) return; // Regardless of delete or edit, it is necessary to get the equivalent database information. EntityCache.CacheUser ucd = null; @@ -128,11 +131,11 @@ namespace Noikoio.RegexBot.Module.ModLogs } // Find target channel, prepare and send out message - var rptTargetChannel = _dClient.GetGuild(guildId)?.GetTextChannel(rptTarget.Value.Id.Value); + var g = _dClient.GetGuild(guildId); + var rptTargetChannel = g?.GetTextChannel(cfg.RptTarget.Value.Id.Value); if (rptTargetChannel == null) { - await _outLog("Target channel not found."); - // TODO make a more descriptive error message + await _outLog($"WARNING: Reporting channel {cfg.RptTarget.Value.ToString()} could not be determined."); return; } var em = CreateReportEmbed(isDelete, ucd, messageId, ch, (cacheMsg, editMsg)); diff --git a/Module/ModLogs/ModLogs.cs b/Module/ModLogs/ModLogs.cs index acfc8ff..4eeb3e4 100644 --- a/Module/ModLogs/ModLogs.cs +++ b/Module/ModLogs/ModLogs.cs @@ -23,9 +23,11 @@ namespace Noikoio.RegexBot.Module.ModLogs // Do nothing if database unavailable. The user will be informed by ProcessConfiguration. if (!RegexBot.Config.DatabaseAvailable) return; + // MessageCache (reporting of MessageEdit, MessageDelete) handled by helper class _msgCacheInstance = new MessageCache(client, Log, GetConfig); - //throw new NotImplementedException(); + // TODO add handlers for detecting joins, leaves, bans, kicks, user edits (nick/username/discr) + // TODO add handler for processing the log query command } [ConfigSection("ModLogs")] @@ -33,46 +35,14 @@ namespace Noikoio.RegexBot.Module.ModLogs { if (configSection.Type != JTokenType.Object) throw new RuleImportException("Configuration for this section is invalid."); - var conf = (JObject)configSection; if (!RegexBot.Config.DatabaseAvailable) { await Log("Database access is not available. This module be unavailable."); return null; } - - try - { - // MessageCache testing: will store an EntityName or die trying - EntityName? mctarget = new EntityName(conf["mctarget"].Value(), EntityType.Channel); - await Log("Enabled MessageCache test on " + mctarget.Value.ToString()); - return mctarget; - } - catch (Exception) - { - // well, not really die - return null; - } - /* - * Concept: - * "ModLogs": { - * "AutoReporting": { - * // behavior for how to output to the reporting channel - * // MessageCache looks for configuration values within here. - * "Channel": "something compatible with EntityName", - * "Events": "perhaps a single string of separated event types" - * }, - * "QueryOptions": { - * // Behavior for the query command (which is defined here rather than ModTools) - * // Need to stress in the documentation that "msgedit" and "msgdelete" events - * // are not kept and cannot be queried - * "QueryCommand": "!modlogs", - * "Permission": "Moderators", // either a string that says "Moderators" or an EntityList - * "DefaultQueryEvents": "another single string of separated event types", - * } - * } - */ + return new GuildConfig((JObject)configSection); } } } \ No newline at end of file From 681d0a370df079e74a9d87583123ddd74e8ae623 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 4 Mar 2018 15:08:57 -0800 Subject: [PATCH 16/17] Added confirmation message --- Module/ModLogs/EventType.cs | 2 -- Module/ModLogs/ModLogs.cs | 13 +++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Module/ModLogs/EventType.cs b/Module/ModLogs/EventType.cs index c364bed..6a0a964 100644 --- a/Module/ModLogs/EventType.cs +++ b/Module/ModLogs/EventType.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace Noikoio.RegexBot.Module.ModLogs { diff --git a/Module/ModLogs/ModLogs.cs b/Module/ModLogs/ModLogs.cs index 4eeb3e4..2024510 100644 --- a/Module/ModLogs/ModLogs.cs +++ b/Module/ModLogs/ModLogs.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Discord.WebSocket; +using Discord.WebSocket; using Newtonsoft.Json.Linq; using Noikoio.RegexBot.ConfigItem; +using System.Threading.Tasks; namespace Noikoio.RegexBot.Module.ModLogs { @@ -42,7 +39,11 @@ namespace Noikoio.RegexBot.Module.ModLogs return null; } - return new GuildConfig((JObject)configSection); + var conf = new GuildConfig((JObject)configSection); + if (conf.RptTypes != EventType.None) + await Log("Enabled event autoreporting to " + conf.RptTarget); + + return conf; } } } \ No newline at end of file From aa11f9d3134765eb1682c80aff66024283720116 Mon Sep 17 00:00:00 2001 From: Noikoio Date: Sun, 4 Mar 2018 15:58:06 -0800 Subject: [PATCH 17/17] Added ModLogs documentation draft --- docs/modlogs.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/modlogs.md diff --git a/docs/modlogs.md b/docs/modlogs.md new file mode 100644 index 0000000..318918a --- /dev/null +++ b/docs/modlogs.md @@ -0,0 +1,38 @@ +## ModLogs + +ModLogs is a work in progress and not all features are yet available. +When completed, it will be the component that records certain information and notifies moderators of actions on the server deemed important enough to show as they happen. + +Sample within a [server definition](serverdef.html): +``` +"ModLogs": { + "AutoReporting": { + "Channel": "#99999999:mod-events", + "Events": "msgedit,msgdelete" + } +} +``` + +### Definition structure +Behavior of the ModLogs component is defined within a JSON object named `ModLogs`. Omitting this section from a server definition will disable the component for the given server. + +The following values can be defined within the `ModLogs` object: +* AutoReporting (*object*) - See below for details +* QueryCommand (*object*) - Unavailable; Work in progress + +#### AutoReporting +As its name implies, the `AutoReporting` section allows the bot operator to configure automatic reporting of one or more events as they occur to a designated reporting channel. Omitting this section in configuration disables this function. + +The following values are accepted within this object: +* Channel (*string*) - **Required.** The channel name in which to report events. + * The channel ID is currently required to be specified (see [EntityList](entitylist.html)). This limitation will be removed in a future update. +* Events (*string*) - **Required** for now. A comma-separated list of event types to be sent to the reporting channel. + +#### Event types +All events fall into one of a number of categories. +* Custom - The catch-all term for all event types that are not built in, created either by an AutoMod response or an external module. +* (name) - (description) + +Additionally, the following event types are also valid only for `AutoReporting` and are otherwise not logged: +* MsgEdit - Message was edited by the message author. +* MsgDelete - Message was deleted either by the message author or another user. \ No newline at end of file