Add log item reporting to ModLogs module

This commit is contained in:
Noi 2022-08-23 20:39:44 -07:00
parent 785e69773e
commit 6544d4844b
7 changed files with 114 additions and 49 deletions

View file

@ -1,5 +1,6 @@
using Discord; using Discord;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace RegexBot.Common; namespace RegexBot.Common;
@ -64,4 +65,45 @@ public static class Utilities {
} }
return results; return results;
} }
/// <summary>
/// Builds and returns an embed which displays this log entry.
/// </summary>
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();
}
} }

View file

@ -10,9 +10,9 @@ internal partial class ModLogs : RegexbotModule {
// TODO consider resurrecting 2.x idea of logging actions to db, making it searchable? // TODO consider resurrecting 2.x idea of logging actions to db, making it searchable?
public ModLogs(RegexbotClient bot) : base(bot) { 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; DiscordClient.MessageDeleted += HandleDelete;
bot.SharedEventReceived += FilterIncomingEvents; bot.SharedEventReceived += HandleReceivedSharedEvent;
} }
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) { public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
@ -24,6 +24,11 @@ internal partial class ModLogs : RegexbotModule {
return Task.FromResult<object?>(new ModuleConfig((JObject)config)); return Task.FromResult<object?>(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) { private static string MakeTimestamp(DateTimeOffset time) {
var result = new StringBuilder(); var result = new StringBuilder();
//result.Append(time.ToString("yyyy-MM-dd hh:mm:ss")); //result.Append(time.ToString("yyyy-MM-dd hh:mm:ss"));

View file

@ -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<ModuleConfig>(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));
}
}

View file

@ -14,11 +14,11 @@ internal partial class ModLogs {
const int MaxPreviewLength = 750; const int MaxPreviewLength = 750;
if (argChannel.Value is not SocketTextChannel channel) return; if (argChannel.Value is not SocketTextChannel channel) return;
var conf = GetGuildState<ModuleConfig>(channel.Guild.Id); var conf = GetGuildState<ModuleConfig>(channel.Guild.Id);
if ((conf?.LogMessageDeletions ?? false) == false) return;
var reportChannel = conf?.ReportingChannel?.FindChannelIn(channel.Guild, true); var reportChannel = conf?.ReportingChannel?.FindChannelIn(channel.Guild, true);
if (reportChannel == null) return; if (reportChannel == null) return;
if ((conf?.LogMessageDeletions ?? false) == false) return;
if (reportChannel.Id == channel.Id) { 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; return;
} }
@ -53,41 +53,27 @@ internal partial class ModLogs {
IconUrl = cachedMsg.Author.AvatarUrl ?? GetDefaultAvatarUrl(cachedMsg.Author.Discriminator) IconUrl = cachedMsg.Author.AvatarUrl ?? GetDefaultAvatarUrl(cachedMsg.Author.Discriminator)
}; };
} }
var attach = CheckAttachments(cachedMsg.AttachmentNames); SetAttachmentsField(reportEmbed, cachedMsg.AttachmentNames);
if (attach != null) reportEmbed.AddField(attach);
} else { } else {
reportEmbed.Description = NotCached; reportEmbed.Description = NotCached;
} }
var contextStr = new StringBuilder(); var editLine = $"Posted: {MakeTimestamp(SnowflakeUtils.FromSnowflake(argMsg.Id))}";
contextStr.AppendLine($"User: {(cachedMsg != null ? $"<@!{cachedMsg.AuthorId}>" : "Unknown")}"); if (cachedMsg?.EditedAt != null) editLine += $"\nLast edit: {MakeTimestamp(cachedMsg.EditedAt.Value)}";
contextStr.AppendLine($"Channel: <#{channel.Id}> (#{channel.Name})"); SetContextField(reportEmbed, (ulong?)cachedMsg?.AuthorId, channel, editLine, argMsg.Id);
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()
});
await reportChannel.SendMessageAsync(embed: reportEmbed.Build()); 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) { private async Task HandleUpdate(CachedGuildMessage? oldMsg, SocketMessage newMsg) {
const int MaxPreviewLength = 500; const int MaxPreviewLength = 500;
var channel = (SocketTextChannel)newMsg.Channel; var channel = (SocketTextChannel)newMsg.Channel;
var conf = GetGuildState<ModuleConfig>(channel.Guild.Id); var conf = GetGuildState<ModuleConfig>(channel.Guild.Id);
var reportChannel = conf?.ReportingChannel?.FindChannelIn(channel.Guild, true); var reportChannel = conf?.ReportingChannel?.FindChannelIn(channel.Guild, true);
if (reportChannel == null) return; if (reportChannel == null) return;
if ((conf?.LogMessageEdits ?? false) == false) return;
if (reportChannel.Id == channel.Id) { 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; return;
} }
@ -128,25 +114,39 @@ internal partial class ModLogs {
} }
reportEmbed.AddField(newField); reportEmbed.AddField(newField);
var attach = CheckAttachments(newMsg.Attachments.Select(a => a.Filename)); SetAttachmentsField(reportEmbed, newMsg.Attachments.Select(a => a.Filename));
if (attach != null) reportEmbed.AddField(attach);
string editLine;
var contextStr = new StringBuilder(); if ((oldMsg?.EditedAt) == null) editLine = $"Posted: {MakeTimestamp(SnowflakeUtils.FromSnowflake(newMsg.Id))}";
contextStr.AppendLine($"User: <@!{newMsg.Author.Id}>"); else editLine = $"Previous edit: {MakeTimestamp(oldMsg.EditedAt.Value)}";
contextStr.AppendLine($"Channel: <#{channel.Id}> (#{channel.Name})"); SetContextField(reportEmbed, newMsg.Author.Id, channel, editLine, newMsg.Id);
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);
await reportChannel.SendMessageAsync(embed: reportEmbed.Build()); await reportChannel.SendMessageAsync(embed: reportEmbed.Build());
} }
private static EmbedFieldBuilder? CheckAttachments(IEnumerable<string> 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<string> attachments) {
if (attachments.Any()) { if (attachments.Any()) {
var field = new EmbedFieldBuilder { Name = "Attachments" }; var field = new EmbedFieldBuilder { Name = "Attachments" };
var attachNames = new StringBuilder(); var attachNames = new StringBuilder();
@ -154,8 +154,7 @@ internal partial class ModLogs {
attachNames.AppendLine($"`{name}`"); attachNames.AppendLine($"`{name}`");
} }
field.Value = attachNames.ToString().TrimEnd(); field.Value = attachNames.ToString().TrimEnd();
return field; e.AddField(field);
} }
return null;
} }
} }

View file

@ -6,6 +6,7 @@ class ModuleConfig {
public bool LogMessageDeletions { get; } public bool LogMessageDeletions { get; }
public bool LogMessageEdits { get; } public bool LogMessageEdits { get; }
public bool LogModLogs { get; }
public ModuleConfig(JObject config) { public ModuleConfig(JObject config) {
const string RptChError = $"'{nameof(ReportingChannel)}' must be set to a valid channel name."; 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 // Individual logging settings - all default to false
LogMessageDeletions = config[nameof(LogMessageDeletions)]?.Value<bool>() ?? false; LogMessageDeletions = config[nameof(LogMessageDeletions)]?.Value<bool>() ?? false;
LogMessageEdits = config[nameof(LogMessageEdits)]?.Value<bool>() ?? false; LogMessageEdits = config[nameof(LogMessageEdits)]?.Value<bool>() ?? false;
LogModLogs = config[nameof(LogModLogs)]?.Value<bool>() ?? false;
} }
} }

View file

@ -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 Discord.Net;
using RegexBot.Common; using RegexBot.Common;
using RegexBot.Data; using RegexBot.Data;
@ -22,7 +22,7 @@ partial class RegexbotClient {
/// <returns> /// <returns>
/// The resulting <see cref="ModLogEntry"/> from the creation of this note. /// The resulting <see cref="ModLogEntry"/> from the creation of this note.
/// </returns> /// </returns>
public ModLogEntry AddUserNote(SocketGuild guild, ulong targetUser, string source, string? message) { public async Task<ModLogEntry> AddUserNote(SocketGuild guild, ulong targetUser, string source, string? message) {
var entry = new ModLogEntry() { var entry = new ModLogEntry() {
GuildId = (long)guild.Id, GuildId = (long)guild.Id,
UserId = (long)targetUser, UserId = (long)targetUser,
@ -32,9 +32,9 @@ partial class RegexbotClient {
}; };
using (var db = new BotDatabaseContext()) { using (var db = new BotDatabaseContext()) {
db.Add(entry); db.Add(entry);
db.SaveChanges(); await db.SaveChangesAsync();
} }
// TODO notify await PushSharedEventAsync(entry);
return entry; return entry;
} }
@ -63,7 +63,7 @@ partial class RegexbotClient {
db.Add(entry); db.Add(entry);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
// TODO notify await PushSharedEventAsync(entry);
// Attempt warning message // Attempt warning message
var userSearch = _svcEntityCache.QueryUserCache(targetUser.ToString()); var userSearch = _svcEntityCache.QueryUserCache(targetUser.ToString());

View file

@ -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 Discord.Net;
using RegexBot.Data; using RegexBot.Data;
@ -24,7 +24,7 @@ internal partial class CommonFunctionsService : Service {
db.Add(entry); db.Add(entry);
db.SaveChanges(); db.SaveChanges();
} }
// TODO notify entry BotClient.PushSharedEventAsync(entry);
} }
internal async Task<HttpException?> SendUserWarningAsync(SocketGuildUser target, string? reason) { internal async Task<HttpException?> SendUserWarningAsync(SocketGuildUser target, string? reason) {