2019-06-17 05:37:11 +00:00
|
|
|
|
using Discord;
|
2021-08-26 03:18:45 +00:00
|
|
|
|
using RegexBot.Common;
|
2019-06-17 05:37:11 +00:00
|
|
|
|
using System.Text;
|
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
namespace RegexBot.Modules.RegexModerator;
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Transient helper class which handles response interpreting and execution.
|
|
|
|
|
/// </summary>
|
|
|
|
|
class ResponseExecutor {
|
2022-09-13 21:52:44 +00:00
|
|
|
|
private const string ErrParamNeedNone = "This response type does not accept parameters.";
|
|
|
|
|
private const string ErrParamWrongAmount = "Incorrect number of parameters defined in the response.";
|
2022-09-21 04:50:33 +00:00
|
|
|
|
private const string ErrMissingUser = "The target user is no longer in the server.";
|
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
delegate Task<ResponseResult> ResponseHandler(string? parameter);
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
private readonly ConfDefinition _rule;
|
|
|
|
|
private readonly RegexbotClient _bot;
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
private readonly SocketGuild _guild;
|
|
|
|
|
private readonly SocketGuildUser _user;
|
|
|
|
|
private readonly SocketMessage _msg;
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
private readonly List<(string, ResponseResult)> _reports;
|
|
|
|
|
private Action<string> Log { get; }
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-10-13 18:43:42 +00:00
|
|
|
|
private string LogSource => $"{_rule.Label} ({nameof(RegexModerator)})";
|
2022-09-13 21:52:44 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
public ResponseExecutor(ConfDefinition rule, RegexbotClient bot, SocketMessage msg, Action<string> logger) {
|
|
|
|
|
_rule = rule;
|
|
|
|
|
_bot = bot;
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
_msg = msg;
|
|
|
|
|
_user = (SocketGuildUser)msg.Author;
|
|
|
|
|
_guild = _user.Guild;
|
2021-08-26 03:18:45 +00:00
|
|
|
|
|
2024-06-09 04:07:35 +00:00
|
|
|
|
_reports = [];
|
2022-07-06 03:59:19 +00:00
|
|
|
|
Log = logger;
|
|
|
|
|
}
|
2021-08-26 03:18:45 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
public async Task Execute() {
|
|
|
|
|
var reportTarget = _rule.ReportingChannel?.FindChannelIn(_guild, true);
|
|
|
|
|
if (_rule.ReportingChannel != null && reportTarget == null)
|
|
|
|
|
Log("Could not find target reporting channel.");
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
foreach (var line in _rule.Response) {
|
|
|
|
|
var item = line.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries & StringSplitOptions.TrimEntries);
|
|
|
|
|
var cmd = item[0];
|
|
|
|
|
var param = item.Length >= 2 ? item[1] : null;
|
|
|
|
|
ResponseHandler runLine = cmd.ToLowerInvariant() switch {
|
|
|
|
|
"comment" => CmdComment,
|
|
|
|
|
"rem" => CmdComment,
|
|
|
|
|
"#" => CmdComment,
|
|
|
|
|
"ban" => CmdBan,
|
|
|
|
|
"delete" => CmdDelete,
|
|
|
|
|
"remove" => CmdDelete,
|
|
|
|
|
"kick" => CmdKick,
|
|
|
|
|
"note" => CmdNote,
|
|
|
|
|
"roleadd" => CmdRoleAdd,
|
|
|
|
|
"addrole" => CmdRoleAdd,
|
|
|
|
|
"roledel" => CmdRoleDel,
|
|
|
|
|
"delrole" => CmdRoleDel,
|
|
|
|
|
"say" => CmdSay,
|
|
|
|
|
"send" => CmdSay,
|
|
|
|
|
"reply" => CmdSay,
|
|
|
|
|
"timeout" => CmdTimeout,
|
|
|
|
|
"mute" => CmdTimeout,
|
|
|
|
|
"warn" => CmdWarn,
|
|
|
|
|
_ => delegate (string? p) { return Task.FromResult(FromError($"Unknown command '{cmd}'.")); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
var result = await runLine(param);
|
|
|
|
|
_reports.Add((cmd, result));
|
|
|
|
|
} catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
|
2022-07-21 01:55:08 +00:00
|
|
|
|
_reports.Add((cmd, FromError(Messages.ForbiddenGenericError)));
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
// Handle reporting
|
|
|
|
|
if (reportTarget != null) {
|
|
|
|
|
// Set up report
|
|
|
|
|
var rptOutput = new StringBuilder();
|
|
|
|
|
foreach (var (action, result) in _reports) {
|
|
|
|
|
rptOutput.Append(result.Success ? ":white_check_mark:" : ":x:");
|
|
|
|
|
rptOutput.Append($" `{action}`");
|
|
|
|
|
if (result.LogLine != null) {
|
|
|
|
|
rptOutput.Append(": ");
|
|
|
|
|
rptOutput.Append(result.LogLine);
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
2022-07-06 03:59:19 +00:00
|
|
|
|
rptOutput.AppendLine();
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
// We can only afford to show a preview of the message being reported, due to embeds
|
|
|
|
|
// being constrained to the same 2000 character limit as normal messages.
|
|
|
|
|
const string TruncateWarning = "**Notice: Full message has been truncated.**\n";
|
|
|
|
|
const int TruncateMaxLength = 990;
|
|
|
|
|
var invokingLine = _msg.Content;
|
|
|
|
|
if (invokingLine.Length > TruncateMaxLength) {
|
|
|
|
|
invokingLine = string.Concat(TruncateWarning, invokingLine.AsSpan(0, TruncateMaxLength - TruncateWarning.Length));
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
var resultEmbed = new EmbedBuilder()
|
|
|
|
|
.WithFields(
|
|
|
|
|
new EmbedFieldBuilder() {
|
|
|
|
|
Name = "Context",
|
|
|
|
|
Value =
|
|
|
|
|
$"User: {_user.Mention} `{_user.Id}`\n" +
|
|
|
|
|
$"Channel: <#{_msg.Channel.Id}> `#{_msg.Channel.Name}`"
|
|
|
|
|
},
|
|
|
|
|
new EmbedFieldBuilder() {
|
|
|
|
|
Name = "Response status",
|
|
|
|
|
Value = rptOutput.ToString()
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
.WithAuthor(
|
2023-11-15 05:09:21 +00:00
|
|
|
|
name: $"{_user.GetDisplayableUsername()} said:",
|
|
|
|
|
iconUrl: _user.GetAvatarUrl(),
|
2022-07-06 03:59:19 +00:00
|
|
|
|
url: _msg.GetJumpUrl()
|
|
|
|
|
)
|
|
|
|
|
.WithDescription(invokingLine)
|
|
|
|
|
.WithFooter(
|
2022-09-13 21:52:44 +00:00
|
|
|
|
text: LogSource,
|
2022-07-06 03:59:19 +00:00
|
|
|
|
iconUrl: _bot.DiscordClient.CurrentUser.GetAvatarUrl()
|
|
|
|
|
)
|
|
|
|
|
.WithCurrentTimestamp()
|
|
|
|
|
.Build();
|
|
|
|
|
try {
|
|
|
|
|
await reportTarget.SendMessageAsync(embed: resultEmbed);
|
|
|
|
|
} catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
|
2023-11-15 05:09:21 +00:00
|
|
|
|
Log("Encountered error 403 when attempting to send report.");
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2022-07-06 03:59:19 +00:00
|
|
|
|
}
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
#region Response delegates
|
|
|
|
|
private static Task<ResponseResult> CmdComment(string? parameter) => Task.FromResult(FromSuccess(parameter));
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-12-03 04:28:23 +00:00
|
|
|
|
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) {
|
2022-07-06 03:59:19 +00:00
|
|
|
|
BanKickResult result;
|
2022-12-03 04:28:23 +00:00
|
|
|
|
if (isBan) {
|
2022-09-13 21:52:44 +00:00
|
|
|
|
result = await _bot.BanAsync(_guild, LogSource, _user.Id,
|
2022-09-21 04:50:33 +00:00
|
|
|
|
_rule.BanPurgeDays, parameter, _rule.NotifyUser);
|
2022-07-06 03:59:19 +00:00
|
|
|
|
} else {
|
2022-09-13 21:52:44 +00:00
|
|
|
|
result = await _bot.KickAsync(_guild, LogSource, _user.Id,
|
2022-09-21 04:50:33 +00:00
|
|
|
|
parameter, _rule.NotifyUser);
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
2022-07-21 01:55:08 +00:00
|
|
|
|
if (result.ErrorForbidden) return FromError(Messages.ForbiddenGenericError);
|
2022-09-21 04:50:33 +00:00
|
|
|
|
if (result.ErrorNotFound) return FromError(ErrMissingUser);
|
|
|
|
|
if (_rule.NotifyChannel) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot));
|
2022-07-06 03:59:19 +00:00
|
|
|
|
return FromSuccess(result.MessageSendSuccess ? null : "Unable to send notification DM.");
|
|
|
|
|
}
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
private Task<ResponseResult> CmdRoleAdd(string? parameter) => CmdRoleManipulation(parameter, true);
|
|
|
|
|
private Task<ResponseResult> CmdRoleDel(string? parameter) => CmdRoleManipulation(parameter, false);
|
|
|
|
|
private async Task<ResponseResult> CmdRoleManipulation(string? parameter, bool add) {
|
|
|
|
|
// parameters: @_, &, reason?
|
2022-09-13 21:52:44 +00:00
|
|
|
|
if (string.IsNullOrWhiteSpace(parameter)) return FromError(ErrParamWrongAmount);
|
2022-07-06 03:59:19 +00:00
|
|
|
|
var param = parameter.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
2022-09-13 21:52:44 +00:00
|
|
|
|
if (param.Length != 2) return FromError(ErrParamWrongAmount);
|
2022-07-06 03:59:19 +00:00
|
|
|
|
|
|
|
|
|
// Find targets
|
|
|
|
|
SocketGuildUser? tuser;
|
|
|
|
|
SocketRole? trole;
|
|
|
|
|
try {
|
2022-08-23 02:25:48 +00:00
|
|
|
|
var userName = new EntityName(param[0], EntityType.User);
|
2022-07-06 03:59:19 +00:00
|
|
|
|
if (userName.Id.HasValue) tuser = _guild.GetUser(userName.Id.Value);
|
|
|
|
|
else {
|
|
|
|
|
if (userName.Name == "_") tuser = _user;
|
|
|
|
|
else tuser = userName.FindUserIn(_guild);
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
2022-07-06 03:59:19 +00:00
|
|
|
|
if (tuser == null) return FromError($"Unable to find user '{userName.Name}'.");
|
2022-08-23 02:25:48 +00:00
|
|
|
|
var roleName = new EntityName(param[1], EntityType.Role);
|
2022-07-06 03:59:19 +00:00
|
|
|
|
if (roleName.Id.HasValue) trole = _guild.GetRole(roleName.Id.Value);
|
|
|
|
|
else trole = roleName.FindRoleIn(_guild);
|
|
|
|
|
if (trole == null) return FromError($"Unable to find role '{roleName.Name}'.");
|
|
|
|
|
} catch (ArgumentException) {
|
|
|
|
|
return FromError("User or role were not correctly set in configuration.");
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
2022-07-06 03:59:19 +00:00
|
|
|
|
|
|
|
|
|
// Do action
|
2022-09-13 21:52:44 +00:00
|
|
|
|
var rq = new RequestOptions() { AuditLogReason = LogSource };
|
2022-07-06 03:59:19 +00:00
|
|
|
|
if (add) await tuser.AddRoleAsync(trole, rq);
|
|
|
|
|
else await tuser.RemoveRoleAsync(trole, rq);
|
|
|
|
|
return FromSuccess($"{(add ? "Set" : "Unset")} {trole.Mention}.");
|
|
|
|
|
}
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
private async Task<ResponseResult> CmdDelete(string? parameter) {
|
2022-09-13 21:52:44 +00:00
|
|
|
|
if (!string.IsNullOrWhiteSpace(parameter)) return FromError(ErrParamNeedNone);
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
try {
|
2022-09-13 21:52:44 +00:00
|
|
|
|
await _msg.DeleteAsync(new RequestOptions { AuditLogReason = LogSource });
|
2022-07-06 03:59:19 +00:00
|
|
|
|
return FromSuccess();
|
|
|
|
|
} catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound) {
|
|
|
|
|
return FromError("The message had already been deleted.");
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
private async Task<ResponseResult> CmdSay(string? parameter) {
|
|
|
|
|
// parameters: [#_/@_] message
|
2022-09-13 21:52:44 +00:00
|
|
|
|
if (string.IsNullOrWhiteSpace(parameter)) return FromError(ErrParamWrongAmount);
|
2022-07-06 03:59:19 +00:00
|
|
|
|
var param = parameter.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
2022-09-13 21:52:44 +00:00
|
|
|
|
if (param.Length != 2) return FromError(ErrParamWrongAmount);
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
// Get target
|
|
|
|
|
IMessageChannel? targetCh;
|
|
|
|
|
EntityName name;
|
|
|
|
|
try {
|
|
|
|
|
name = new EntityName(param[0]);
|
|
|
|
|
} catch (ArgumentException) {
|
|
|
|
|
return FromError("Reply target was not correctly set in configuration.");
|
|
|
|
|
}
|
|
|
|
|
bool isUser;
|
|
|
|
|
if (name.Type == EntityType.Channel) {
|
|
|
|
|
if (name.Name == "_") targetCh = _msg.Channel;
|
|
|
|
|
else targetCh = name.FindChannelIn(_guild);
|
|
|
|
|
if (targetCh == null) return FromError($"Unable to find channel '{name.Name}'.");
|
|
|
|
|
isUser = false;
|
|
|
|
|
} else if (name.Type == EntityType.User) {
|
2023-11-15 05:09:21 +00:00
|
|
|
|
if (name.Name == "_") targetCh = await _user.CreateDMChannelAsync();
|
2022-07-06 03:59:19 +00:00
|
|
|
|
else {
|
|
|
|
|
var searchedUser = name.FindUserIn(_guild);
|
|
|
|
|
if (searchedUser == null) return FromError($"Unable to find user '{name.Name}'.");
|
|
|
|
|
targetCh = await searchedUser.CreateDMChannelAsync();
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
2022-07-06 03:59:19 +00:00
|
|
|
|
isUser = true;
|
|
|
|
|
} else {
|
|
|
|
|
return FromError("Channel or user were not correctly set in configuration.");
|
|
|
|
|
}
|
|
|
|
|
if (targetCh == null) return FromError("Could not acquire target channel.");
|
2023-11-16 18:19:10 +00:00
|
|
|
|
await targetCh.SendMessageAsync(Utilities.ProcessTextTokens(param[1], _msg));
|
2022-07-06 03:59:19 +00:00
|
|
|
|
return FromSuccess($"Sent to {(isUser ? "user DM" : $"<#{targetCh.Id}>")}.");
|
|
|
|
|
}
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-09-13 21:52:44 +00:00
|
|
|
|
private async Task<ResponseResult> CmdNote(string? parameter) {
|
|
|
|
|
if (string.IsNullOrWhiteSpace(parameter)) return FromError(ErrParamWrongAmount);
|
|
|
|
|
var log = await _bot.AddUserNoteAsync(_guild, _user.Id, LogSource, parameter);
|
|
|
|
|
return FromSuccess($"Note \\#{log.LogId} logged for {_user}.");
|
2022-07-06 03:59:19 +00:00
|
|
|
|
}
|
2022-09-21 04:50:33 +00:00
|
|
|
|
|
2022-09-13 21:52:44 +00:00
|
|
|
|
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);
|
|
|
|
|
var resultMsg = $"Warning \\#{log.LogId} logged for {_user}.";
|
|
|
|
|
if (result.Success) return FromSuccess(resultMsg);
|
|
|
|
|
else return FromError(resultMsg + " Failed to send DM.");
|
2022-07-06 03:59:19 +00:00
|
|
|
|
}
|
2022-09-21 04:50:33 +00:00
|
|
|
|
|
|
|
|
|
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.");
|
|
|
|
|
}
|
2022-07-06 03:59:19 +00:00
|
|
|
|
#endregion
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
#region Response reporting
|
|
|
|
|
private struct ResponseResult {
|
|
|
|
|
public bool Success;
|
|
|
|
|
public string? LogLine;
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
2022-07-06 03:59:19 +00:00
|
|
|
|
|
|
|
|
|
private static ResponseResult FromSuccess(string? logLine = null) => new() { Success = true, LogLine = logLine };
|
|
|
|
|
private static ResponseResult FromError(string? logLine = null) => new() { Success = false, LogLine = logLine };
|
|
|
|
|
#endregion
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|