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 System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.RegularExpressions;
namespace RegexBot.Common;
@ -64,4 +65,45 @@ public static class Utilities {
}
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?
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<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
@ -24,6 +24,11 @@ internal partial class ModLogs : RegexbotModule {
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) {
var result = new StringBuilder();
//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;
if (argChannel.Value is not SocketTextChannel channel) return;
var conf = GetGuildState<ModuleConfig>(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<ModuleConfig>(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<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()) {
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;
}
}

View file

@ -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<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 RegexBot.Common;
using RegexBot.Data;
@ -22,7 +22,7 @@ partial class RegexbotClient {
/// <returns>
/// The resulting <see cref="ModLogEntry"/> from the creation of this note.
/// </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() {
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());

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 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<HttpException?> SendUserWarningAsync(SocketGuildUser target, string? reason) {