Implement timeouts

With use of this feature available within ModCommands and RegexModerator
This commit is contained in:
Noi 2022-09-20 21:50:33 -07:00
parent 911ae63713
commit c73bfabc19
8 changed files with 135 additions and 44 deletions

View file

@ -70,11 +70,6 @@ public static class Utilities {
/// 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);
@ -86,24 +81,30 @@ public static class Utilities {
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}`";
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($"Log type: {Enum.GetName(typeof(ModLogType), entry.LogType)}");
contextStr.AppendLine($"Regarding user: {targetDisplay}");
contextStr.AppendLine($"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();
}
/// <summary>
/// Returns a representation of this entity that can be parsed by the <seealso cref="EntityName"/> constructor.
/// </summary>
public static string AsEntityNameString(this IUser entity) => $"@{entity.Id}::{entity.Username}";
}

View file

@ -13,14 +13,9 @@ class Ban : BanKick {
ulong targetId, CachedGuildUser? targetQuery, SocketUser? targetUser) {
// Ban: Unlike kick, the minimum required is just the target ID
var result = await Module.Bot.BanAsync(g, msg.Author.ToString(), targetId, PurgeDays, reason, SendNotify);
if (result.OperationSuccess) {
if (SuccessMessage != null) {
// TODO customization
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
} else {
// TODO custom fail message?
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
}
if (result.OperationSuccess && SuccessMessage != null) {
// TODO string replacement, formatting, etc
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
} else {
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
}
@ -39,13 +34,12 @@ class Kick : BanKick {
}
var result = await Module.Bot.KickAsync(g, msg.Author.ToString(), targetId, reason, SendNotify);
if (result.OperationSuccess) {
if (SuccessMessage != null) {
// TODO string replacement, formatting, etc
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
}
if (result.OperationSuccess && SuccessMessage != null) {
// TODO string replacement, formatting, etc
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
} else {
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
}
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
}
}

View file

@ -0,0 +1,73 @@
using RegexBot.Common;
namespace RegexBot.Modules.ModCommands.Commands;
class Timeout : CommandConfig {
protected bool ForceReason { get; }
protected bool SendNotify { get; }
protected string? SuccessMessage { get; }
// Configuration:
// "ForceReason" - boolean; Force a reason to be given. Defaults to false.
// "SendNotify" - boolean; Whether to send a notification DM explaining the action. Defaults to true.
// "SuccessMessage" - string; Additional message to display on command success.
// TODO future configuration ideas: max timeout, min timeout, default timeout span...
public Timeout(ModCommands module, JObject config) : base(module, config) {
ForceReason = config[nameof(ForceReason)]?.Value<bool>() ?? false;
SendNotify = config[nameof(SendNotify)]?.Value<bool>() ?? true;
SuccessMessage = config[nameof(SuccessMessage)]?.Value<string>();
_usage = $"{Command} `user ID or tag` `time in minutes` `" + (ForceReason ? "reason" : "[reason]") + "`\n"
+ "Issues a timeout to the given user, preventing them from participating in the server for a set amount of time. "
+ (ForceReason ? "L" : "Optionally l") + "ogs the reason for the timeout to the Audit Log.";
}
private readonly string _usage;
protected override string DefaultUsageMsg => _usage;
// Usage: (command) (user) (duration) (reason)
public override async Task Invoke(SocketGuild g, SocketMessage msg) {
var line = msg.Content.Split(new char[] { ' ' }, 4, StringSplitOptions.RemoveEmptyEntries);
string targetstr;
string? reason;
if (line.Length < 3) {
await SendUsageMessageAsync(msg.Channel, null);
return;
}
targetstr = line[1];
if (line.Length == 4) reason = line[3]; // Reason given - keep it
else {
// No reason given
if (ForceReason) {
await SendUsageMessageAsync(msg.Channel, ":x: **You must specify a reason.**");
return;
}
reason = null;
}
if (!int.TryParse(line[2], out var timeParam)) {
await SendUsageMessageAsync(msg.Channel, ":x: You must specify a duration for the timeout (in minutes).");
return;
}
// Get target user. Required to find for our purposes.
var targetQuery = Module.Bot.EcQueryGuildUser(g.Id, targetstr);
ulong targetId;
if (targetQuery != null) targetId = (ulong)targetQuery.UserId;
else if (ulong.TryParse(targetstr, out var parsed)) targetId = parsed;
else {
await SendUsageMessageAsync(msg.Channel, TargetNotFound);
return;
}
var targetUser = g.GetUser(targetId);
var result = await Module.Bot.SetTimeoutAsync(g, msg.Author.AsEntityNameString(), targetUser,
TimeSpan.FromMinutes(timeParam), reason, SendNotify);
if (result.Success && SuccessMessage != null) {
// TODO string replacement, formatting, etc
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.ToResultString()}");
} else {
await msg.Channel.SendMessageAsync(result.ToResultString());
}
}
}

View file

@ -36,6 +36,7 @@ class ModuleConfig {
{ "note", typeof(Note) },
{ "addnote", typeof(Note) },
{ "warn", typeof(Warn) },
{ "timeout", typeof(Commands.Timeout) },
{ "addrole", typeof(RoleAdd) },
{ "roleadd", typeof(RoleAdd) },
{ "delrole", typeof(RoleDel) },

View file

@ -23,8 +23,8 @@ class ConfDefinition {
public EntityName? ReportingChannel { get; }
public IReadOnlyList<string> Response { get; }
public int BanPurgeDays { get; }
public bool NotifyChannelOfRemoval { get; }
public bool NotifyUserOfRemoval { get; }
public bool NotifyChannel { get; }
public bool NotifyUser { get; }
public ConfDefinition(JObject def) {
Label = def[nameof(Label)]?.Value<string>()
@ -83,8 +83,8 @@ class ConfDefinition {
throw new ModuleLoadException($"'{nameof(Response)}' is not properly defined{errpostfx}");
}
BanPurgeDays = def[nameof(BanPurgeDays)]?.Value<int>() ?? 0;
NotifyChannelOfRemoval = def[nameof(NotifyChannelOfRemoval)]?.Value<bool>() ?? true;
NotifyUserOfRemoval = def[nameof(NotifyUserOfRemoval)]?.Value<bool>() ?? true;
NotifyChannel = def[nameof(NotifyChannel)]?.Value<bool>() ?? true;
NotifyUser = def[nameof(NotifyUser)]?.Value<bool>() ?? true;
}
/// <summary>

View file

@ -3,13 +3,14 @@ using RegexBot.Common;
using System.Text;
namespace RegexBot.Modules.RegexModerator;
/// <summary>
/// Transient helper class which handles response interpreting and execution.
/// </summary>
class ResponseExecutor {
private const string ErrParamNeedNone = "This response type does not accept parameters.";
private const string ErrParamWrongAmount = "Incorrect number of parameters defined in the response.";
private const string ErrMissingUser = "The target user is no longer in the server.";
delegate Task<ResponseResult> ResponseHandler(string? parameter);
private readonly ConfDefinition _rule;
@ -140,14 +141,14 @@ class ResponseExecutor {
BanKickResult result;
if (rt == RemovalType.Ban) {
result = await _bot.BanAsync(_guild, LogSource, _user.Id,
_rule.BanPurgeDays, parameter, _rule.NotifyUserOfRemoval);
_rule.BanPurgeDays, parameter, _rule.NotifyUser);
} else {
result = await _bot.KickAsync(_guild, LogSource, _user.Id,
parameter, _rule.NotifyUserOfRemoval);
parameter, _rule.NotifyUser);
}
if (result.ErrorForbidden) return FromError(Messages.ForbiddenGenericError);
if (result.ErrorNotFound) return FromError("The target user is no longer in the server.");
if (_rule.NotifyChannelOfRemoval) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot));
if (result.ErrorNotFound) return FromError(ErrMissingUser);
if (_rule.NotifyChannel) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot));
return FromSuccess(result.MessageSendSuccess ? null : "Unable to send notification DM.");
}
@ -237,10 +238,7 @@ class ResponseExecutor {
var log = await _bot.AddUserNoteAsync(_guild, _user.Id, LogSource, parameter);
return FromSuccess($"Note \\#{log.LogId} logged for {_user}.");
}
private Task<ResponseResult> CmdTimeout(string? parameter) {
#warning Not implemented
return Task.FromResult(FromError("not implemented"));
}
private async Task<ResponseResult> CmdWarn(string? parameter) {
if (string.IsNullOrWhiteSpace(parameter)) return FromError(ErrParamWrongAmount);
var (log, result) = await _bot.AddUserWarnAsync(_guild, _user.Id, LogSource, parameter);
@ -248,6 +246,27 @@ class ResponseExecutor {
if (result.Success) return FromSuccess(resultMsg);
else return FromError(resultMsg + " Failed to send DM.");
}
private async Task<ResponseResult> CmdTimeout(string? parameter) {
// parameters: (time in minutes) [reason]
if (string.IsNullOrWhiteSpace(parameter)) return FromError(ErrParamWrongAmount);
var param = parameter.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (param.Length < 1) return FromError(ErrParamWrongAmount);
if (!int.TryParse(param[0], out var timemin)) {
return FromError($"Couldn't parse '{param[0]}' as amount of time in minutes.");
}
string? reason = null;
if (param.Length == 2) reason = param[1];
var result = await _bot.SetTimeoutAsync(_guild, LogSource, _user,
TimeSpan.FromMinutes(timemin), reason, _rule.NotifyUser);
if (result.ErrorForbidden) return FromError(Messages.ForbiddenGenericError);
if (result.ErrorNotFound) return FromError(ErrMissingUser);
if (result.Error != null) return FromError(result.Error.Message);
if (_rule.NotifyChannel) await _msg.Channel.SendMessageAsync(result.ToResultString());
return FromSuccess(result.Success ? null : "Unable to send notification DM.");
}
#endregion
#region Response reporting

View file

@ -29,13 +29,14 @@ internal partial class CommonFunctionsService : Service {
UserId = (long)target.Id,
LogType = ModLogType.Timeout,
IssuedBy = source,
Message = reason
Message = $"Duration: {Math.Floor(duration.TotalMinutes)}min{(reason == null ? "." : " - " + reason)}"
};
using (var db = new BotDatabaseContext()) {
db.Add(entry);
await db.SaveChangesAsync();
}
// TODO check if this log entry should be propagated now or if (to be implemented) will do it for us later
await BotClient.PushSharedEventAsync(entry); // Until then, we for sure propagate our own
bool dmSuccess;
// DM notification

View file

@ -37,8 +37,10 @@ public class TimeoutSetResult : IOperationResult {
return msg;
} else {
var msg = ":x: Failed to set timeout: ";
if (ErrorNotFound) msg += ": The specified user could not be found.";
else if (ErrorForbidden) msg += ": " + Messages.ForbiddenGenericError;
if (ErrorNotFound) msg += "The specified user could not be found.";
else if (ErrorForbidden) msg += Messages.ForbiddenGenericError;
else if (Error != null) msg += Error.Message;
else msg += "Unknown error.";
return msg;
}
}