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.
|
||||
/// </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}";
|
||||
}
|
||||
|
|
|
@ -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
|
||||
if (result.OperationSuccess && SuccessMessage != null) {
|
||||
// TODO string replacement, formatting, etc
|
||||
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
|
||||
} else {
|
||||
// TODO custom fail message?
|
||||
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
|
||||
}
|
||||
} else {
|
||||
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
|
||||
}
|
||||
|
@ -39,15 +34,14 @@ class Kick : BanKick {
|
|||
}
|
||||
|
||||
var result = await Module.Bot.KickAsync(g, msg.Author.ToString(), targetId, reason, SendNotify);
|
||||
if (result.OperationSuccess) {
|
||||
if (SuccessMessage != null) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BanKick : CommandConfig {
|
||||
protected bool ForceReason { get; }
|
||||
|
|
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) },
|
||||
{ "addnote", typeof(Note) },
|
||||
{ "warn", typeof(Warn) },
|
||||
{ "timeout", typeof(Commands.Timeout) },
|
||||
{ "addrole", typeof(RoleAdd) },
|
||||
{ "roleadd", typeof(RoleAdd) },
|
||||
{ "delrole", typeof(RoleDel) },
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue