Merge pull request #3 from NoiTheCat/dev

v3.1.1 updates and fixes
This commit is contained in:
Noi 2022-12-16 21:54:00 -08:00 committed by GitHub
commit fe72cd2cdb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 109 additions and 108 deletions

View file

@ -66,36 +66,6 @@ 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 issuedDisplay = TryFromEntityNameString(entry.IssuedBy, bot);
string targetDisplay;
var targetq = bot.EcQueryUser(entry.UserId.ToString());
if (targetq != null) targetDisplay = $"<@{targetq.UserId}> - {targetq.Username}#{targetq.Discriminator} `{targetq.UserId}`";
else targetDisplay = $"User with ID `{entry.UserId}`";
var logEmbed = new EmbedBuilder()
.WithTitle(Enum.GetName(typeof(ModLogType), entry.LogType) + " logged:")
.WithTimestamp(entry.Timestamp)
.WithFooter($"Log #{entry.LogId}", bot.DiscordClient.CurrentUser.GetAvatarUrl()); // Escaping '#' not necessary here
if (entry.Message != null) {
logEmbed.Description = entry.Message;
}
var contextStr = new StringBuilder();
contextStr.AppendLine($"User: {targetDisplay}");
contextStr.AppendLine($"Logged by: {issuedDisplay}");
logEmbed.AddField(new EmbedFieldBuilder() {
Name = "Context",
Value = contextStr.ToString()
});
return logEmbed.Build();
}
/// <summary>
/// Returns a representation of this entity that can be parsed by the <seealso cref="EntityName"/> constructor.
/// </summary>

View file

@ -12,7 +12,7 @@ internal class AutoResponder : RegexbotModule {
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
}
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken? config) {
if (config == null) return Task.FromResult<object?>(null);
var defs = new List<Definition>();

View file

@ -52,7 +52,7 @@ internal sealed class EntryRole : RegexbotModule, IDisposable {
return Task.CompletedTask;
}
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken? config) {
if (config == null) return Task.FromResult<object?>(null);
if (config.Type != JTokenType.Object)

View file

@ -65,10 +65,10 @@ abstract class NoteWarn : CommandConfig {
var targetUser = g.GetUser(targetId);
// Go to specific action
try {
if (targetUser == null) {
await msg.Channel.SendMessageAsync(":x: Unable to find the specified user.");
} else {
await ContinueInvoke(g, msg, logMessage, targetUser);
} catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
await msg.Channel.SendMessageAsync(":x: " + Messages.ForbiddenGenericError);
}
}

View file

@ -15,7 +15,7 @@ internal class ModCommands : RegexbotModule {
}
}
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken? config) {
if (config == null) return Task.FromResult<object?>(null);
var conf = new ModuleConfig(this, config);

View file

@ -8,6 +8,7 @@ namespace RegexBot.Modules.ModLogs;
[RegexbotModule]
internal partial class ModLogs : RegexbotModule {
// TODO consider resurrecting 2.x idea of logging actions to db, making it searchable?
// TODO more robust channel filtering. define channels in config array, add check to it out here.
public ModLogs(RegexbotClient bot) : base(bot) {
// TODO missing logging features: joins, leaves, user edits (nick/username/discr)
@ -15,7 +16,7 @@ internal partial class ModLogs : RegexbotModule {
bot.SharedEventReceived += HandleReceivedSharedEvent;
}
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken? config) {
if (config == null) return Task.FromResult<object?>(null);
if (config.Type != JTokenType.Object)
throw new ModuleLoadException("Configuration for this section is invalid.");

View file

@ -1,5 +1,7 @@
using Discord;
using RegexBot.Common;
using RegexBot.Data;
using System.Text;
namespace RegexBot.Modules.ModLogs;
// Contains all logic relating to reporting new database mod log entries
@ -12,6 +14,37 @@ internal partial class ModLogs {
var reportChannel = conf?.ReportingChannel?.FindChannelIn(guild, true);
if (reportChannel == null) return;
await reportChannel.SendMessageAsync(embed: entry.BuildEmbed(Bot));
await reportChannel.SendMessageAsync(embed: BuildLogEmbed(entry));
}
/// <summary>
/// Builds and returns an embed which displays this log entry.
/// </summary>
private Embed BuildLogEmbed(ModLogEntry entry) {
var issuedDisplay = Utilities.TryFromEntityNameString(entry.IssuedBy, Bot);
string targetDisplay;
var targetq = Bot.EcQueryUser(entry.UserId.ToString());
if (targetq != null) targetDisplay = $"<@{targetq.UserId}> - {targetq.Username}#{targetq.Discriminator} `{targetq.UserId}`";
else targetDisplay = $"User with ID `{entry.UserId}`";
var logEmbed = new EmbedBuilder()
.WithColor(Color.DarkGrey)
.WithTitle(Enum.GetName(typeof(ModLogType), entry.LogType) + " logged:")
.WithTimestamp(entry.Timestamp)
.WithFooter($"Log #{entry.LogId}", Bot.DiscordClient.CurrentUser.GetAvatarUrl()); // Escaping '#' not necessary here
if (entry.Message != null) {
logEmbed.Description = entry.Message;
}
var contextStr = new StringBuilder();
contextStr.AppendLine($"User: {targetDisplay}");
contextStr.AppendLine($"Logged by: {issuedDisplay}");
logEmbed.AddField(new EmbedFieldBuilder() {
Name = "Context",
Value = contextStr.ToString()
});
return logEmbed.Build();
}
}

View file

@ -29,9 +29,10 @@ internal partial class ModLogs {
.SingleOrDefault();
var reportEmbed = new EmbedBuilder()
.WithColor(Color.Red)
.WithTitle("Message deleted")
.WithCurrentTimestamp()
.WithFooter($"User ID: {(cachedMsg == null ? "Unknown" : cachedMsg.AuthorId)}");
.WithFooter($"Message ID: {argMsg.Id}");
if (cachedMsg != null) {
if (cachedMsg.Content == null) {
@ -60,7 +61,7 @@ internal partial class ModLogs {
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);
SetContextField(reportEmbed, (ulong?)cachedMsg?.AuthorId, channel, editLine);
await reportChannel.SendMessageAsync(embed: reportEmbed.Build());
}
@ -78,9 +79,10 @@ internal partial class ModLogs {
}
var reportEmbed = new EmbedBuilder()
.WithColor(new Color(0xffff00)) // yellow
.WithTitle("Message edited")
.WithCurrentTimestamp()
.WithFooter($"User ID: {newMsg.Author.Id}");
.WithFooter($"Message ID: {newMsg.Id}");
reportEmbed.Author = new EmbedAuthorBuilder() {
Name = $"{newMsg.Author.Username}#{newMsg.Author.Discriminator}",
@ -119,12 +121,12 @@ internal partial class ModLogs {
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);
SetContextField(reportEmbed, newMsg.Author.Id, channel, editLine);
await reportChannel.SendMessageAsync(embed: reportEmbed.Build());
}
private void SetContextField(EmbedBuilder e, ulong? userId, SocketTextChannel channel, string editLine, ulong msgId) {
private void SetContextField(EmbedBuilder e, ulong? userId, SocketTextChannel channel, string editLine) {
string userDisplay;
if (userId.HasValue) {
var q = Bot.EcQueryUser(userId.Value.ToString());
@ -138,7 +140,6 @@ internal partial class ModLogs {
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",

View file

@ -41,7 +41,7 @@ internal class PendingOutRole : RegexbotModule {
}
}
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken? config) {
if (config == null) return Task.FromResult<object?>(null);
if (config.Type != JTokenType.Object)
throw new ModuleLoadException("Configuration for this section is invalid.");

View file

@ -12,7 +12,7 @@ internal class RegexModerator : RegexbotModule {
DiscordClient.MessageUpdated += DiscordClient_MessageUpdated;
}
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken? config) {
if (config == null) return Task.FromResult<object?>(null);
var defs = new List<ConfDefinition>();

View file

@ -135,11 +135,11 @@ class ResponseExecutor {
#region Response delegates
private static Task<ResponseResult> CmdComment(string? parameter) => Task.FromResult(FromSuccess(parameter));
private Task<ResponseResult> CmdBan(string? parameter) => CmdBanKick(RemovalType.Ban, parameter);
private Task<ResponseResult> CmdKick(string? parameter) => CmdBanKick(RemovalType.Kick, parameter);
private async Task<ResponseResult> CmdBanKick(RemovalType rt, string? parameter) {
private Task<ResponseResult> CmdBan(string? parameter) => CmdBanKick(true, parameter);
private Task<ResponseResult> CmdKick(string? parameter) => CmdBanKick(false, parameter);
private async Task<ResponseResult> CmdBanKick(bool isBan, string? parameter) {
BanKickResult result;
if (rt == RemovalType.Ban) {
if (isBan) {
result = await _bot.BanAsync(_guild, LogSource, _user.Id,
_rule.BanPurgeDays, parameter, _rule.NotifyUser);
} else {

View file

@ -43,7 +43,7 @@ internal class VoiceRoleSync : RegexbotModule {
}
}
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken? config) {
if (config == null) return Task.FromResult<object?>(null);
if (config.Type != JTokenType.Object)
throw new ModuleLoadException("Configuration for this section is invalid.");

View file

@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework>
<Authors>NoiTheCat</Authors>
<Description>Advanced and flexible Discord moderation bot.</Description>
<Version>3.1.0</Version>
<Version>3.1.1</Version>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
@ -29,7 +29,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Npgsql" Version="6.0.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.7" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />

View file

@ -43,7 +43,7 @@ public abstract class RegexbotModule {
/// <returns>
/// An object instance containing state and/or configuration information for the guild currently being processed.
/// </returns>
public abstract Task<object?> CreateGuildStateAsync(ulong guildID, JToken config);
public abstract Task<object?> CreateGuildStateAsync(ulong guildID, JToken? config);
/// <summary>
/// Retrieves the state object that corresponds with the given guild.

View file

@ -7,7 +7,6 @@ namespace RegexBot;
/// </summary>
public class BanKickResult {
private readonly bool _userNotFound; // possible to receive this error by means other than exception
private readonly RemovalType _rptRemovalType;
private readonly ulong _rptTargetId;
/// <summary>
@ -59,12 +58,22 @@ public class BanKickResult {
/// </value>
public bool MessageSendSuccess { get; }
/// <summary>
/// Gets a value indicating if this result represents a ban.
/// </summary>
public bool IsBan { get; }
/// <summary>
/// Gets a value indicating if this result represents a kick.
/// </summary>
public bool IsKick { get => !IsBan; }
internal BanKickResult(HttpException? error, bool notificationSuccess, bool errorNotFound,
RemovalType rtype, ulong rtarget) {
bool isBan, ulong rtarget) {
OperationError = error;
MessageSendSuccess = notificationSuccess;
_userNotFound = errorNotFound;
_rptRemovalType = rtype;
IsBan = isBan;
_rptTargetId = rtarget;
}
@ -78,14 +87,12 @@ public class BanKickResult {
if (OperationSuccess) msg = ":white_check_mark: ";
else msg = ":x: Failed to ";
if (_rptRemovalType == RemovalType.Ban) {
if (IsBan) {
if (OperationSuccess) msg += "Banned";
else msg += "ban";
} else if (_rptRemovalType == RemovalType.Kick) {
} else {
if (OperationSuccess) msg += "Kicked";
else msg += "kick";
} else {
throw new InvalidOperationException("Cannot create a message for removal type of None.");
}
if (_rptTargetId != 0) {

View file

@ -18,7 +18,7 @@ partial class RegexbotClient {
int purgeDays,
string? reason,
bool sendDMToTarget)
=> _svcCommonFunctions.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, purgeDays, reason, sendDMToTarget);
=> _svcCommonFunctions.BanOrKickAsync(true, guild, source, targetUser, purgeDays, reason, sendDMToTarget);
/// <summary>
/// Similar to <see cref="BanAsync(SocketGuild, string, ulong, int, string, bool)"/>, but making use of an
@ -37,7 +37,7 @@ partial class RegexbotClient {
string? reason,
bool sendDMToTarget) {
var result = EcQueryGuildUser(guild.Id, targetSearch);
if (result == null) return new BanKickResult(null, false, true, RemovalType.Ban, 0);
if (result == null) return new BanKickResult(null, false, true, true, 0);
return await BanAsync(guild, source, (ulong)result.UserId, purgeDays, reason, sendDMToTarget);
}
@ -60,7 +60,7 @@ partial class RegexbotClient {
ulong targetUser,
string? reason,
bool sendDMToTarget)
=> _svcCommonFunctions.BanOrKickAsync(RemovalType.Kick, guild, source, targetUser, default, reason, sendDMToTarget);
=> _svcCommonFunctions.BanOrKickAsync(false, guild, source, targetUser, default, reason, sendDMToTarget);
/// <summary>
/// Similar to <see cref="KickAsync(SocketGuild, string, ulong, string, bool)"/>, but making use of an
@ -80,7 +80,7 @@ partial class RegexbotClient {
string? reason,
bool sendDMToTarget) {
var result = EcQueryGuildUser(guild.Id, targetSearch);
if (result == null) return new BanKickResult(null, false, true, RemovalType.Kick, 0);
if (result == null) return new BanKickResult(null, false, true, false, 0);
return await KickAsync(guild, source, (ulong)result.UserId, reason, sendDMToTarget);
}
}

View file

@ -4,42 +4,41 @@ using Discord.Net;
namespace RegexBot.Services.CommonFunctions;
internal partial class CommonFunctionsService : Service {
// Hooked (indirectly)
internal async Task<BanKickResult> BanOrKickAsync(RemovalType t, SocketGuild guild, string source, ulong target,
internal async Task<BanKickResult> BanOrKickAsync(bool isBan, SocketGuild guild, string source, ulong target,
int banPurgeDays, string? logReason, bool sendDmToTarget) {
if (t == RemovalType.None) throw new ArgumentException("Removal type must be 'ban' or 'kick'.");
var dmSuccess = true;
SocketGuildUser utarget = guild.GetUser(target);
// Can't kick without obtaining user object. Quit here.
if (t == RemovalType.Kick && utarget == null) return new BanKickResult(null, false, true, RemovalType.Kick, 0);
if (isBan == false && utarget == null) return new BanKickResult(null, false, true, false, 0);
// Send DM notification
// Must be done before removal, or we risk not being able to send a notification afterwards
if (sendDmToTarget) {
if (utarget != null) dmSuccess = await BanKickSendNotificationAsync(utarget, t, logReason);
if (utarget != null) dmSuccess = await BanKickSendNotificationAsync(utarget, isBan, logReason);
else dmSuccess = false;
}
// Perform the action
var auditReason = $"(By: {source}) {logReason}";
try {
if (t == RemovalType.Ban) await guild.AddBanAsync(target, banPurgeDays, auditReason);
if (isBan) await guild.AddBanAsync(target, banPurgeDays, auditReason);
else await utarget!.KickAsync(auditReason);
} catch (HttpException ex) {
return new BanKickResult(ex, dmSuccess, false, t, target);
return new BanKickResult(ex, dmSuccess, false, isBan, target);
}
ModLogsProcessRemoval(guild.Id, target, t == RemovalType.Ban ? ModLogType.Ban : ModLogType.Kick, source, logReason);
ModLogsProcessRemoval(guild.Id, target, isBan ? ModLogType.Ban : ModLogType.Kick, source, logReason);
return new BanKickResult(null, dmSuccess, false, t, target);
return new BanKickResult(null, dmSuccess, false, isBan, target);
}
private async Task<bool> BanKickSendNotificationAsync(SocketGuildUser target, RemovalType action, string? reason) {
private async Task<bool> BanKickSendNotificationAsync(SocketGuildUser target, bool isBan, string? reason) {
const string DMTemplate = "You have been {0} from {1}";
const string DMTemplateReason = " for the following reason:\n{2}";
var outMessage = string.IsNullOrWhiteSpace(reason)
? string.Format(DMTemplate + ".", action == RemovalType.Ban ? "banned" : "kicked", target.Guild.Name)
: string.Format(DMTemplate + DMTemplateReason, action == RemovalType.Ban ? "banned" : "kicked", target.Guild.Name, reason);
? string.Format(DMTemplate + ".", isBan ? "banned" : "kicked", target.Guild.Name)
: string.Format(DMTemplate + DMTemplateReason, isBan ? "banned" : "kicked", target.Guild.Name, reason);
var dch = await target.CreateDMChannelAsync();
try { await dch.SendMessageAsync(outMessage); } catch (HttpException) { return false; }

View file

@ -1,21 +0,0 @@
namespace RegexBot;
/// <summary>
/// Specifies possible outcomes for the removal of a user from a guild.
/// </summary>
// Despite specific to CommonFunctionsService, this enum is meant to be visible by modules too,
// thus it is placed within the root namespace.
// TODO Tends to be unused except internally. Look into removing.
public enum RemovalType {
/// <summary>
/// Default value. Not used in any actual circumstances.
/// </summary>
None,
/// <summary>
/// Specifies that the type of removal includes placing the user on the guild's ban list.
/// </summary>
Ban,
/// <summary>
/// Specifies that the user is removed from the server via kick.
/// </summary>
Kick
}

View file

@ -21,7 +21,11 @@ class MessageCachingSubservice {
}
private async Task AddOrUpdateCacheItemAsync(SocketMessage arg, bool isUpdate) {
if (!Common.Utilities.IsValidUserMessage(arg, out _)) return;
//if (!Common.Utilities.IsValidUserMessage(arg, out _)) return;
if (arg.Channel is not SocketTextChannel) return;
if (arg.Author.IsWebhook) return; // do get bot messages, don't get webhooks
if (((IMessage)arg).Type != MessageType.Default) return;
if (arg is SocketSystemMessage) return;
using var db = new BotDatabaseContext();
CachedGuildMessage? cachedMsg = db.GuildMessageCache.Where(m => m.MessageId == (long)arg.Id).SingleOrDefault();

View file

@ -18,15 +18,22 @@ class UserCachingSubservice {
bot.DiscordClient.GuildMemberUpdated += DiscordClient_GuildMemberUpdated;
bot.DiscordClient.UserUpdated += DiscordClient_UserUpdated;
}
private async Task DiscordClient_GuildMembersDownloaded(SocketGuild arg) {
_log($"{arg.Name}: Full member list obtained. Cache update in progress.");
private Task DiscordClient_GuildMembersDownloaded(SocketGuild arg) {
var userlist = arg.Users.ToList();
_ = Task.Run(async () => {
try {
using var db = new BotDatabaseContext();
foreach (var user in arg.Users) {
foreach (var user in userlist) {
UpdateUser(user, db);
UpdateGuildUser(user, db);
}
var changes = await db.SaveChangesAsync();
_log($"{arg.Name}: Member caches updated ({changes} database writes).");
} catch (Exception ex) {
_log($"{arg.Name}: {ex}");
}
});
return Task.CompletedTask;
}
private async Task DiscordClient_GuildMemberUpdated(Discord.Cacheable<SocketGuildUser, ulong> old, SocketGuildUser current) {

View file

@ -76,7 +76,7 @@ class ModuleStateService : Service {
foreach (var module in BotClient.Modules) {
var t = module.GetType();
try {
var state = await module.CreateGuildStateAsync(guild.Id, guildConf[module.Name]!);
var state = await module.CreateGuildStateAsync(guild.Id, guildConf[module.Name]);
newStates.Add(t, state);
} catch (ModuleLoadException ex) {
Log($"{module.Name} failed to read configuration for {guild.Name}: {ex.Message}");