From 6544d4844b8f82e92a572dc03840ee1f8f83f435 Mon Sep 17 00:00:00 2001 From: Noi Date: Tue, 23 Aug 2022 20:39:44 -0700 Subject: [PATCH] Add log item reporting to ModLogs module --- Common/Utilities.cs | 42 +++++++++++ Modules/ModLogs/ModLogs.cs | 9 ++- Modules/ModLogs/ModLogs_Logging.cs | 17 +++++ Modules/ModLogs/ModLogs_Messages.cs | 79 ++++++++++---------- Modules/ModLogs/ModuleConfig.cs | 2 + Services/CommonFunctions/CF_ModLogs.Hooks.cs | 10 +-- Services/CommonFunctions/CF_ModLogs.cs | 4 +- 7 files changed, 114 insertions(+), 49 deletions(-) create mode 100644 Modules/ModLogs/ModLogs_Logging.cs diff --git a/Common/Utilities.cs b/Common/Utilities.cs index 55578eb..012841c 100644 --- a/Common/Utilities.cs +++ b/Common/Utilities.cs @@ -1,5 +1,6 @@ using Discord; using System.Diagnostics.CodeAnalysis; +using System.Text; using System.Text.RegularExpressions; namespace RegexBot.Common; @@ -64,4 +65,45 @@ public static class Utilities { } return results; } + + /// + /// Builds and returns an embed which displays this log entry. + /// + public static Embed BuildEmbed(this Data.ModLogEntry entry, RegexbotClient bot) { + var logEmbed = new EmbedBuilder() + .WithTitle("Moderation log entry") + .WithTimestamp(entry.Timestamp) + .WithFooter($"Log ID {entry.LogId}"); + + string? issuedDisplay = null; + try { + var entityTry = new EntityName(entry.IssuedBy, EntityType.User); + var issueq = bot.EcQueryUser(entityTry.Id!.Value.ToString()); + if (issueq != null) issuedDisplay = $"<@{issueq.UserId}> - {issueq.Username}#{issueq.Discriminator} `{issueq.UserId}`"; + else issuedDisplay = $"Unknown user with ID `{entityTry.Id!.Value}`"; + } catch (Exception) { } + issuedDisplay ??= entry.IssuedBy; + string targetDisplay; + var targetq = bot.EcQueryUser(entry.UserId.ToString()); + if (targetq != null) targetDisplay = $"<@{targetq.UserId}> - {targetq.Username}#{targetq.Discriminator} `{targetq.UserId}`"; + else targetDisplay = $"Unknown user with ID `{entry.UserId}`"; + + var contextStr = new StringBuilder(); + contextStr.AppendLine($"Log type: {Enum.GetName(typeof(ModLogType), entry.LogType)}"); + contextStr.AppendLine($"Regarding user: {targetDisplay}"); + contextStr.AppendLine($"Logged by: {issuedDisplay}"); + + logEmbed.AddField(new EmbedFieldBuilder() { + Name = "Context", + Value = contextStr.ToString() + }); + if (entry.Message != null) { + logEmbed.AddField(new EmbedFieldBuilder() { + Name = "Message", + Value = entry.Message + }); + } + + return logEmbed.Build(); + } } diff --git a/Modules/ModLogs/ModLogs.cs b/Modules/ModLogs/ModLogs.cs index d2dafee..5eb3a0d 100644 --- a/Modules/ModLogs/ModLogs.cs +++ b/Modules/ModLogs/ModLogs.cs @@ -10,9 +10,9 @@ internal partial class ModLogs : RegexbotModule { // TODO consider resurrecting 2.x idea of logging actions to db, making it searchable? public ModLogs(RegexbotClient bot) : base(bot) { - // TODO missing logging features: joins, leaves, bans, kicks, user edits (nick/username/discr) + // TODO missing logging features: joins, leaves, user edits (nick/username/discr) DiscordClient.MessageDeleted += HandleDelete; - bot.SharedEventReceived += FilterIncomingEvents; + bot.SharedEventReceived += HandleReceivedSharedEvent; } public override Task CreateGuildStateAsync(ulong guildID, JToken config) { @@ -24,6 +24,11 @@ internal partial class ModLogs : RegexbotModule { return Task.FromResult(new ModuleConfig((JObject)config)); } + private async Task HandleReceivedSharedEvent(ISharedEvent ev) { + if (ev is MessageCacheUpdateEvent upd) await HandleUpdate(upd.OldMessage, upd.NewMessage); + else if (ev is Data.ModLogEntry log) await HandleLog(log); + } + private static string MakeTimestamp(DateTimeOffset time) { var result = new StringBuilder(); //result.Append(time.ToString("yyyy-MM-dd hh:mm:ss")); diff --git a/Modules/ModLogs/ModLogs_Logging.cs b/Modules/ModLogs/ModLogs_Logging.cs new file mode 100644 index 0000000..147078a --- /dev/null +++ b/Modules/ModLogs/ModLogs_Logging.cs @@ -0,0 +1,17 @@ +using RegexBot.Common; +using RegexBot.Data; + +namespace RegexBot.Modules.ModLogs; +// Contains all logic relating to reporting new database mod log entries +internal partial class ModLogs { + public async Task HandleLog(ModLogEntry entry) { + var guild = Bot.DiscordClient.GetGuild((ulong)entry.GuildId); + if (guild == null) return; + var conf = GetGuildState(guild.Id); + if ((conf?.LogModLogs ?? false) == false) return; + var reportChannel = conf?.ReportingChannel?.FindChannelIn(guild, true); + if (reportChannel == null) return; + + await reportChannel.SendMessageAsync(embed: entry.BuildEmbed(Bot)); + } +} \ No newline at end of file diff --git a/Modules/ModLogs/ModLogs_Messages.cs b/Modules/ModLogs/ModLogs_Messages.cs index 5c38331..1c07284 100644 --- a/Modules/ModLogs/ModLogs_Messages.cs +++ b/Modules/ModLogs/ModLogs_Messages.cs @@ -14,11 +14,11 @@ internal partial class ModLogs { const int MaxPreviewLength = 750; if (argChannel.Value is not SocketTextChannel channel) return; var conf = GetGuildState(channel.Guild.Id); + if ((conf?.LogMessageDeletions ?? false) == false) return; var reportChannel = conf?.ReportingChannel?.FindChannelIn(channel.Guild, true); if (reportChannel == null) return; - if ((conf?.LogMessageDeletions ?? false) == false) return; if (reportChannel.Id == channel.Id) { - Log($"[{channel.Guild.Name}] Message deletion detected in the reporting channel. Regular report has been suppressed."); + Log(channel.Guild, "Message deleted in the reporting channel. Suppressing report."); return; } @@ -53,41 +53,27 @@ internal partial class ModLogs { IconUrl = cachedMsg.Author.AvatarUrl ?? GetDefaultAvatarUrl(cachedMsg.Author.Discriminator) }; } - var attach = CheckAttachments(cachedMsg.AttachmentNames); - if (attach != null) reportEmbed.AddField(attach); + SetAttachmentsField(reportEmbed, cachedMsg.AttachmentNames); } else { reportEmbed.Description = NotCached; } - - var contextStr = new StringBuilder(); - contextStr.AppendLine($"User: {(cachedMsg != null ? $"<@!{cachedMsg.AuthorId}>" : "Unknown")}"); - contextStr.AppendLine($"Channel: <#{channel.Id}> (#{channel.Name})"); - contextStr.AppendLine($"Posted: {MakeTimestamp(SnowflakeUtils.FromSnowflake(argMsg.Id))}"); - if (cachedMsg?.EditedAt != null) contextStr.AppendLine($"Last edit: {MakeTimestamp(cachedMsg.EditedAt.Value)}"); - contextStr.AppendLine($"Message ID: {argMsg.Id}"); - reportEmbed.AddField(new EmbedFieldBuilder() { - Name = "Context", - Value = contextStr.ToString() - }); + + var editLine = $"Posted: {MakeTimestamp(SnowflakeUtils.FromSnowflake(argMsg.Id))}"; + if (cachedMsg?.EditedAt != null) editLine += $"\nLast edit: {MakeTimestamp(cachedMsg.EditedAt.Value)}"; + SetContextField(reportEmbed, (ulong?)cachedMsg?.AuthorId, channel, editLine, argMsg.Id); await reportChannel.SendMessageAsync(embed: reportEmbed.Build()); } - private async Task FilterIncomingEvents(ISharedEvent ev) { - if (ev is MessageCacheUpdateEvent upd) { - await HandleUpdate(upd.OldMessage, upd.NewMessage); - } - } - private async Task HandleUpdate(CachedGuildMessage? oldMsg, SocketMessage newMsg) { const int MaxPreviewLength = 500; var channel = (SocketTextChannel)newMsg.Channel; var conf = GetGuildState(channel.Guild.Id); + var reportChannel = conf?.ReportingChannel?.FindChannelIn(channel.Guild, true); if (reportChannel == null) return; - if ((conf?.LogMessageEdits ?? false) == false) return; if (reportChannel.Id == channel.Id) { - Log($"[{channel.Guild.Name}] Message edit detected in the reporting channel. Regular report has been suppressed."); + Log(channel.Guild, "Message edited in the reporting channel. Suppressing report."); return; } @@ -128,25 +114,39 @@ internal partial class ModLogs { } reportEmbed.AddField(newField); - var attach = CheckAttachments(newMsg.Attachments.Select(a => a.Filename)); - if (attach != null) reportEmbed.AddField(attach); - - var contextStr = new StringBuilder(); - contextStr.AppendLine($"User: <@!{newMsg.Author.Id}>"); - contextStr.AppendLine($"Channel: <#{channel.Id}> (#{channel.Name})"); - if ((oldMsg?.EditedAt) == null) contextStr.AppendLine($"Posted: {MakeTimestamp(SnowflakeUtils.FromSnowflake(newMsg.Id))}"); - else contextStr.AppendLine($"Previous edit: {MakeTimestamp(oldMsg.EditedAt.Value)}"); - contextStr.AppendLine($"Message ID: {newMsg.Id}"); - var contextField = new EmbedFieldBuilder() { - Name = "Context", - Value = contextStr.ToString() - }; - reportEmbed.AddField(contextField); + SetAttachmentsField(reportEmbed, newMsg.Attachments.Select(a => a.Filename)); + + string editLine; + if ((oldMsg?.EditedAt) == null) editLine = $"Posted: {MakeTimestamp(SnowflakeUtils.FromSnowflake(newMsg.Id))}"; + else editLine = $"Previous edit: {MakeTimestamp(oldMsg.EditedAt.Value)}"; + SetContextField(reportEmbed, newMsg.Author.Id, channel, editLine, newMsg.Id); await reportChannel.SendMessageAsync(embed: reportEmbed.Build()); } - private static EmbedFieldBuilder? CheckAttachments(IEnumerable attachments) { + private void SetContextField(EmbedBuilder e, ulong? userId, SocketTextChannel channel, string editLine, ulong msgId) { + string userDisplay; + if (userId.HasValue) { + var q = Bot.EcQueryUser(userId.Value.ToString()); + if (q != null) userDisplay = $"<@{q.UserId}> - {q.Username}#{q.Discriminator} `{q.UserId}`"; + else userDisplay = $"Unknown user with ID `{userId}`"; + } else { + userDisplay = "Unknown"; + } + + var contextStr = new StringBuilder(); + contextStr.AppendLine($"User: {userDisplay}"); + contextStr.AppendLine($"Channel: <#{channel.Id}> (#{channel.Name})"); + contextStr.AppendLine(editLine); + contextStr.AppendLine($"Message ID: {msgId}"); + + e.AddField(new EmbedFieldBuilder() { + Name = "Context", + Value = contextStr.ToString() + }); + } + + private static void SetAttachmentsField(EmbedBuilder e, IEnumerable attachments) { if (attachments.Any()) { var field = new EmbedFieldBuilder { Name = "Attachments" }; var attachNames = new StringBuilder(); @@ -154,8 +154,7 @@ internal partial class ModLogs { attachNames.AppendLine($"`{name}`"); } field.Value = attachNames.ToString().TrimEnd(); - return field; + e.AddField(field); } - return null; } } \ No newline at end of file diff --git a/Modules/ModLogs/ModuleConfig.cs b/Modules/ModLogs/ModuleConfig.cs index 8b83eb5..2f134c3 100644 --- a/Modules/ModLogs/ModuleConfig.cs +++ b/Modules/ModLogs/ModuleConfig.cs @@ -6,6 +6,7 @@ class ModuleConfig { public bool LogMessageDeletions { get; } public bool LogMessageEdits { get; } + public bool LogModLogs { get; } public ModuleConfig(JObject config) { const string RptChError = $"'{nameof(ReportingChannel)}' must be set to a valid channel name."; @@ -18,5 +19,6 @@ class ModuleConfig { // Individual logging settings - all default to false LogMessageDeletions = config[nameof(LogMessageDeletions)]?.Value() ?? false; LogMessageEdits = config[nameof(LogMessageEdits)]?.Value() ?? false; + LogModLogs = config[nameof(LogModLogs)]?.Value() ?? false; } } \ No newline at end of file diff --git a/Services/CommonFunctions/CF_ModLogs.Hooks.cs b/Services/CommonFunctions/CF_ModLogs.Hooks.cs index 3eaf29f..57ecf8a 100644 --- a/Services/CommonFunctions/CF_ModLogs.Hooks.cs +++ b/Services/CommonFunctions/CF_ModLogs.Hooks.cs @@ -1,4 +1,4 @@ -#pragma warning disable CA1822 // "Mark members as static" - will not make static to encourage better structure +#pragma warning disable CA1822 // "Mark members as static" - members should only be callable by code with access to this instance using Discord.Net; using RegexBot.Common; using RegexBot.Data; @@ -22,7 +22,7 @@ partial class RegexbotClient { /// /// The resulting from the creation of this note. /// - public ModLogEntry AddUserNote(SocketGuild guild, ulong targetUser, string source, string? message) { + public async Task AddUserNote(SocketGuild guild, ulong targetUser, string source, string? message) { var entry = new ModLogEntry() { GuildId = (long)guild.Id, UserId = (long)targetUser, @@ -32,9 +32,9 @@ partial class RegexbotClient { }; using (var db = new BotDatabaseContext()) { db.Add(entry); - db.SaveChanges(); + await db.SaveChangesAsync(); } - // TODO notify + await PushSharedEventAsync(entry); return entry; } @@ -63,7 +63,7 @@ partial class RegexbotClient { db.Add(entry); await db.SaveChangesAsync(); } - // TODO notify + await PushSharedEventAsync(entry); // Attempt warning message var userSearch = _svcEntityCache.QueryUserCache(targetUser.ToString()); diff --git a/Services/CommonFunctions/CF_ModLogs.cs b/Services/CommonFunctions/CF_ModLogs.cs index 8f5bee1..6a8453f 100644 --- a/Services/CommonFunctions/CF_ModLogs.cs +++ b/Services/CommonFunctions/CF_ModLogs.cs @@ -1,4 +1,4 @@ -#pragma warning disable CA1822 // "Mark members as static" - will not make static to encourage better structure +#pragma warning disable CA1822 // "Mark members as static" - members should only be callable by code with access to this instance using Discord.Net; using RegexBot.Data; @@ -24,7 +24,7 @@ internal partial class CommonFunctionsService : Service { db.Add(entry); db.SaveChanges(); } - // TODO notify entry + BotClient.PushSharedEventAsync(entry); } internal async Task SendUserWarningAsync(SocketGuildUser target, string? reason) {