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;
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Transient helper class which handles response interpreting and execution.
|
|
|
|
|
/// </summary>
|
|
|
|
|
class ResponseExecutor {
|
|
|
|
|
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-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
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
_reports = new();
|
|
|
|
|
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-09 20:22:39 +00:00
|
|
|
|
_reports.Add((cmd, FromError(Strings.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(
|
|
|
|
|
name: $"{_msg.Author.Username}#{_msg.Author.Discriminator} said:",
|
|
|
|
|
iconUrl: _msg.Author.GetAvatarUrl(),
|
|
|
|
|
url: _msg.GetJumpUrl()
|
|
|
|
|
)
|
|
|
|
|
.WithDescription(invokingLine)
|
|
|
|
|
.WithFooter(
|
|
|
|
|
text: $"Rule: {_rule.Label}",
|
|
|
|
|
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) {
|
|
|
|
|
Log("Encountered 403 error 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-07-06 03:59:19 +00:00
|
|
|
|
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) {
|
|
|
|
|
BanKickResult result;
|
|
|
|
|
if (rt == RemovalType.Ban) {
|
|
|
|
|
result = await _bot.BanAsync(_guild, $"Rule '{_rule.Label}'", _user.Id,
|
|
|
|
|
_rule.BanPurgeDays, parameter, _rule.NotifyUserOfRemoval);
|
|
|
|
|
} else {
|
|
|
|
|
result = await _bot.KickAsync(_guild, $"Rule '{_rule.Label}'", _user.Id,
|
|
|
|
|
parameter, _rule.NotifyUserOfRemoval);
|
2019-06-17 05:37:11 +00:00
|
|
|
|
}
|
2022-07-13 06:45:18 +00:00
|
|
|
|
if (result.ErrorForbidden) return FromError(Strings.ForbiddenGenericError);
|
2022-07-06 03:59:19 +00:00
|
|
|
|
if (result.ErrorNotFound) return FromError("The target user is no longer in the server.");
|
|
|
|
|
if (_rule.NotifyChannelOfRemoval) await _msg.Channel.SendMessageAsync(result.GetResultString(_bot));
|
|
|
|
|
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?
|
|
|
|
|
// TODO add persistence option if/when implemented
|
|
|
|
|
if (string.IsNullOrWhiteSpace(parameter)) return FromError("This response requires parameters.");
|
|
|
|
|
var param = parameter.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
|
if (param.Length < 2) return FromError("Incorrect number of parameters.");
|
|
|
|
|
|
|
|
|
|
// Find targets
|
|
|
|
|
SocketGuildUser? tuser;
|
|
|
|
|
SocketRole? trole;
|
|
|
|
|
try {
|
|
|
|
|
var userName = new EntityName(param[0]);
|
|
|
|
|
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}'.");
|
|
|
|
|
var roleName = new EntityName(param[1]);
|
|
|
|
|
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
|
|
|
|
|
var rq = new RequestOptions() { AuditLogReason = $"Rule '{_rule.Label}'" };
|
|
|
|
|
if (param.Length == 3 && !string.IsNullOrWhiteSpace(param[2])) {
|
|
|
|
|
rq.AuditLogReason += " - " + param[2];
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
// TODO detailed audit log deletion reason?
|
|
|
|
|
if (parameter != null) return FromError("This response does not accept parameters.");
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
try {
|
|
|
|
|
await _msg.DeleteAsync(new RequestOptions { AuditLogReason = $"Rule {_rule.Label}" });
|
|
|
|
|
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
|
|
|
|
|
if (string.IsNullOrWhiteSpace(parameter)) return FromError("This response requires parameters.");
|
|
|
|
|
var param = parameter.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
|
if (param.Length != 2) return FromError("Incorrect number of parameters.");
|
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) {
|
|
|
|
|
if (name.Name == "_") targetCh = await _msg.Author.CreateDMChannelAsync();
|
|
|
|
|
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.");
|
|
|
|
|
await targetCh.SendMessageAsync(param[1]);
|
|
|
|
|
return FromSuccess($"Sent to {(isUser ? "user DM" : $"<#{targetCh.Id}>")}.");
|
|
|
|
|
}
|
2019-06-17 05:37:11 +00:00
|
|
|
|
|
2022-07-06 03:59:19 +00:00
|
|
|
|
private Task<ResponseResult> CmdNote(string? parameter) {
|
|
|
|
|
#warning Not implemented
|
|
|
|
|
return Task.FromResult(FromError("not implemented"));
|
|
|
|
|
}
|
|
|
|
|
private Task<ResponseResult> CmdTimeout(string? parameter) {
|
|
|
|
|
#warning Not implemented
|
|
|
|
|
return Task.FromResult(FromError("not implemented"));
|
|
|
|
|
}
|
|
|
|
|
private Task<ResponseResult> CmdWarn(string? parameter) {
|
|
|
|
|
#warning Not implemented
|
|
|
|
|
return Task.FromResult(FromError("not implemented"));
|
|
|
|
|
}
|
|
|
|
|
#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
|
|
|
|
}
|