Implement timeouts
With use of this feature available within ModCommands and RegexModerator
This commit is contained in:
parent
911ae63713
commit
c73bfabc19
8 changed files with 135 additions and 44 deletions
|
@ -70,11 +70,6 @@ public static class Utilities {
|
||||||
/// Builds and returns an embed which displays this log entry.
|
/// Builds and returns an embed which displays this log entry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Embed BuildEmbed(this Data.ModLogEntry entry, RegexbotClient bot) {
|
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;
|
string? issuedDisplay = null;
|
||||||
try {
|
try {
|
||||||
var entityTry = new EntityName(entry.IssuedBy, EntityType.User);
|
var entityTry = new EntityName(entry.IssuedBy, EntityType.User);
|
||||||
|
@ -86,24 +81,30 @@ public static class Utilities {
|
||||||
string targetDisplay;
|
string targetDisplay;
|
||||||
var targetq = bot.EcQueryUser(entry.UserId.ToString());
|
var targetq = bot.EcQueryUser(entry.UserId.ToString());
|
||||||
if (targetq != null) targetDisplay = $"<@{targetq.UserId}> - {targetq.Username}#{targetq.Discriminator} `{targetq.UserId}`";
|
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();
|
var contextStr = new StringBuilder();
|
||||||
contextStr.AppendLine($"Log type: {Enum.GetName(typeof(ModLogType), entry.LogType)}");
|
contextStr.AppendLine($"User: {targetDisplay}");
|
||||||
contextStr.AppendLine($"Regarding user: {targetDisplay}");
|
|
||||||
contextStr.AppendLine($"Logged by: {issuedDisplay}");
|
contextStr.AppendLine($"Logged by: {issuedDisplay}");
|
||||||
|
|
||||||
logEmbed.AddField(new EmbedFieldBuilder() {
|
logEmbed.AddField(new EmbedFieldBuilder() {
|
||||||
Name = "Context",
|
Name = "Context",
|
||||||
Value = contextStr.ToString()
|
Value = contextStr.ToString()
|
||||||
});
|
});
|
||||||
if (entry.Message != null) {
|
|
||||||
logEmbed.AddField(new EmbedFieldBuilder() {
|
|
||||||
Name = "Message",
|
|
||||||
Value = entry.Message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return logEmbed.Build();
|
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}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,14 +13,9 @@ class Ban : BanKick {
|
||||||
ulong targetId, CachedGuildUser? targetQuery, SocketUser? targetUser) {
|
ulong targetId, CachedGuildUser? targetQuery, SocketUser? targetUser) {
|
||||||
// Ban: Unlike kick, the minimum required is just the target ID
|
// 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);
|
var result = await Module.Bot.BanAsync(g, msg.Author.ToString(), targetId, PurgeDays, reason, SendNotify);
|
||||||
if (result.OperationSuccess) {
|
if (result.OperationSuccess && SuccessMessage != null) {
|
||||||
if (SuccessMessage != null) {
|
// TODO string replacement, formatting, etc
|
||||||
// TODO customization
|
|
||||||
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
|
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
|
||||||
} else {
|
|
||||||
// TODO custom fail message?
|
|
||||||
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
|
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
|
||||||
}
|
}
|
||||||
|
@ -39,14 +34,13 @@ class Kick : BanKick {
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await Module.Bot.KickAsync(g, msg.Author.ToString(), targetId, reason, SendNotify);
|
var result = await Module.Bot.KickAsync(g, msg.Author.ToString(), targetId, reason, SendNotify);
|
||||||
if (result.OperationSuccess) {
|
if (result.OperationSuccess && SuccessMessage != null) {
|
||||||
if (SuccessMessage != null) {
|
|
||||||
// TODO string replacement, formatting, etc
|
// TODO string replacement, formatting, etc
|
||||||
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
|
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));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BanKick : CommandConfig {
|
abstract class BanKick : CommandConfig {
|
||||||
|
|
73
Modules/ModCommands/Commands/Timeout.cs
Normal file
73
Modules/ModCommands/Commands/Timeout.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ class ModuleConfig {
|
||||||
{ "note", typeof(Note) },
|
{ "note", typeof(Note) },
|
||||||
{ "addnote", typeof(Note) },
|
{ "addnote", typeof(Note) },
|
||||||
{ "warn", typeof(Warn) },
|
{ "warn", typeof(Warn) },
|
||||||
|
{ "timeout", typeof(Commands.Timeout) },
|
||||||
{ "addrole", typeof(RoleAdd) },
|
{ "addrole", typeof(RoleAdd) },
|
||||||
{ "roleadd", typeof(RoleAdd) },
|
{ "roleadd", typeof(RoleAdd) },
|
||||||
{ "delrole", typeof(RoleDel) },
|
{ "delrole", typeof(RoleDel) },
|
||||||
|
|
|
@ -23,8 +23,8 @@ class ConfDefinition {
|
||||||
public EntityName? ReportingChannel { get; }
|
public EntityName? ReportingChannel { get; }
|
||||||
public IReadOnlyList<string> Response { get; }
|
public IReadOnlyList<string> Response { get; }
|
||||||
public int BanPurgeDays { get; }
|
public int BanPurgeDays { get; }
|
||||||
public bool NotifyChannelOfRemoval { get; }
|
public bool NotifyChannel { get; }
|
||||||
public bool NotifyUserOfRemoval { get; }
|
public bool NotifyUser { get; }
|
||||||
|
|
||||||
public ConfDefinition(JObject def) {
|
public ConfDefinition(JObject def) {
|
||||||
Label = def[nameof(Label)]?.Value<string>()
|
Label = def[nameof(Label)]?.Value<string>()
|
||||||
|
@ -83,8 +83,8 @@ class ConfDefinition {
|
||||||
throw new ModuleLoadException($"'{nameof(Response)}' is not properly defined{errpostfx}");
|
throw new ModuleLoadException($"'{nameof(Response)}' is not properly defined{errpostfx}");
|
||||||
}
|
}
|
||||||
BanPurgeDays = def[nameof(BanPurgeDays)]?.Value<int>() ?? 0;
|
BanPurgeDays = def[nameof(BanPurgeDays)]?.Value<int>() ?? 0;
|
||||||
NotifyChannelOfRemoval = def[nameof(NotifyChannelOfRemoval)]?.Value<bool>() ?? true;
|
NotifyChannel = def[nameof(NotifyChannel)]?.Value<bool>() ?? true;
|
||||||
NotifyUserOfRemoval = def[nameof(NotifyUserOfRemoval)]?.Value<bool>() ?? true;
|
NotifyUser = def[nameof(NotifyUser)]?.Value<bool>() ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -3,13 +3,14 @@ using RegexBot.Common;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace RegexBot.Modules.RegexModerator;
|
namespace RegexBot.Modules.RegexModerator;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Transient helper class which handles response interpreting and execution.
|
/// Transient helper class which handles response interpreting and execution.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class ResponseExecutor {
|
class ResponseExecutor {
|
||||||
private const string ErrParamNeedNone = "This response type does not accept parameters.";
|
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 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);
|
delegate Task<ResponseResult> ResponseHandler(string? parameter);
|
||||||
|
|
||||||
private readonly ConfDefinition _rule;
|
private readonly ConfDefinition _rule;
|
||||||
|
@ -140,14 +141,14 @@ class ResponseExecutor {
|
||||||
BanKickResult result;
|
BanKickResult result;
|
||||||
if (rt == RemovalType.Ban) {
|
if (rt == RemovalType.Ban) {
|
||||||
result = await _bot.BanAsync(_guild, LogSource, _user.Id,
|
result = await _bot.BanAsync(_guild, LogSource, _user.Id,
|
||||||
_rule.BanPurgeDays, parameter, _rule.NotifyUserOfRemoval);
|
_rule.BanPurgeDays, parameter, _rule.NotifyUser);
|
||||||
} else {
|
} else {
|
||||||
result = await _bot.KickAsync(_guild, LogSource, _user.Id,
|
result = await _bot.KickAsync(_guild, LogSource, _user.Id,
|
||||||
parameter, _rule.NotifyUserOfRemoval);
|
parameter, _rule.NotifyUser);
|
||||||
}
|
}
|
||||||
if (result.ErrorForbidden) return FromError(Messages.ForbiddenGenericError);
|
if (result.ErrorForbidden) return FromError(Messages.ForbiddenGenericError);
|
||||||
if (result.ErrorNotFound) return FromError("The target user is no longer in the server.");
|
if (result.ErrorNotFound) return FromError(ErrMissingUser);
|
||||||
if (_rule.NotifyChannelOfRemoval) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot));
|
if (_rule.NotifyChannel) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot));
|
||||||
return FromSuccess(result.MessageSendSuccess ? null : "Unable to send notification DM.");
|
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);
|
var log = await _bot.AddUserNoteAsync(_guild, _user.Id, LogSource, parameter);
|
||||||
return FromSuccess($"Note \\#{log.LogId} logged for {_user}.");
|
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) {
|
private async Task<ResponseResult> CmdWarn(string? parameter) {
|
||||||
if (string.IsNullOrWhiteSpace(parameter)) return FromError(ErrParamWrongAmount);
|
if (string.IsNullOrWhiteSpace(parameter)) return FromError(ErrParamWrongAmount);
|
||||||
var (log, result) = await _bot.AddUserWarnAsync(_guild, _user.Id, LogSource, parameter);
|
var (log, result) = await _bot.AddUserWarnAsync(_guild, _user.Id, LogSource, parameter);
|
||||||
|
@ -248,6 +246,27 @@ class ResponseExecutor {
|
||||||
if (result.Success) return FromSuccess(resultMsg);
|
if (result.Success) return FromSuccess(resultMsg);
|
||||||
else return FromError(resultMsg + " Failed to send DM.");
|
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
|
#endregion
|
||||||
|
|
||||||
#region Response reporting
|
#region Response reporting
|
||||||
|
|
|
@ -29,13 +29,14 @@ internal partial class CommonFunctionsService : Service {
|
||||||
UserId = (long)target.Id,
|
UserId = (long)target.Id,
|
||||||
LogType = ModLogType.Timeout,
|
LogType = ModLogType.Timeout,
|
||||||
IssuedBy = source,
|
IssuedBy = source,
|
||||||
Message = reason
|
Message = $"Duration: {Math.Floor(duration.TotalMinutes)}min{(reason == null ? "." : " - " + reason)}"
|
||||||
};
|
};
|
||||||
using (var db = new BotDatabaseContext()) {
|
using (var db = new BotDatabaseContext()) {
|
||||||
db.Add(entry);
|
db.Add(entry);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
// TODO check if this log entry should be propagated now or if (to be implemented) will do it for us later
|
// 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;
|
bool dmSuccess;
|
||||||
// DM notification
|
// DM notification
|
||||||
|
|
|
@ -37,8 +37,10 @@ public class TimeoutSetResult : IOperationResult {
|
||||||
return msg;
|
return msg;
|
||||||
} else {
|
} else {
|
||||||
var msg = ":x: Failed to set timeout: ";
|
var msg = ":x: Failed to set timeout: ";
|
||||||
if (ErrorNotFound) msg += ": The specified user could not be found.";
|
if (ErrorNotFound) msg += "The specified user could not be found.";
|
||||||
else if (ErrorForbidden) msg += ": " + Messages.ForbiddenGenericError;
|
else if (ErrorForbidden) msg += Messages.ForbiddenGenericError;
|
||||||
|
else if (Error != null) msg += Error.Message;
|
||||||
|
else msg += "Unknown error.";
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue