Add log item reporting to ModLogs module
This commit is contained in:
parent
785e69773e
commit
6544d4844b
7 changed files with 114 additions and 49 deletions
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"));
|
||||||
|
|
17
Modules/ModLogs/ModLogs_Logging.cs
Normal file
17
Modules/ModLogs/ModLogs_Logging.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
|
|
||||||
var contextStr = new StringBuilder();
|
string editLine;
|
||||||
contextStr.AppendLine($"User: <@!{newMsg.Author.Id}>");
|
if ((oldMsg?.EditedAt) == null) editLine = $"Posted: {MakeTimestamp(SnowflakeUtils.FromSnowflake(newMsg.Id))}";
|
||||||
contextStr.AppendLine($"Channel: <#{channel.Id}> (#{channel.Name})");
|
else editLine = $"Previous edit: {MakeTimestamp(oldMsg.EditedAt.Value)}";
|
||||||
if ((oldMsg?.EditedAt) == null) contextStr.AppendLine($"Posted: {MakeTimestamp(SnowflakeUtils.FromSnowflake(newMsg.Id))}");
|
SetContextField(reportEmbed, newMsg.Author.Id, channel, editLine, 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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());
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue