Merge branch 'dev/modtools'
This commit is contained in:
commit
73d17405fa
13 changed files with 545 additions and 245 deletions
|
@ -6,30 +6,35 @@ using System;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModTools
|
||||
namespace Noikoio.RegexBot.Module.ModCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// ModTools module.
|
||||
/// This class manages reading configuration and creating instances based on it.
|
||||
/// It processes input and looks for messages that intend to invoke commands defined in configuration.
|
||||
/// </summary>
|
||||
class ModTools : BotModule
|
||||
/// <remarks>
|
||||
/// Discord.Net has its own recommended way of implementing commands, but it's not exactly
|
||||
/// done in a way that would easily allow for flexibility and modifications during runtime.
|
||||
/// Thus, reinventing the wheel right here.
|
||||
/// </remarks>
|
||||
class CommandListener : BotModule
|
||||
{
|
||||
public override string Name => "ModTools";
|
||||
public override string Name => "ModCommands";
|
||||
|
||||
public ModTools(DiscordSocketClient client) : base(client)
|
||||
public CommandListener(DiscordSocketClient client) : base(client)
|
||||
{
|
||||
client.MessageReceived += Client_MessageReceived;
|
||||
}
|
||||
|
||||
private async Task Client_MessageReceived(SocketMessage arg)
|
||||
{
|
||||
// Always ignore bots
|
||||
if (arg.Author.IsBot) return;
|
||||
// Always ignore these
|
||||
if (arg.Author.IsBot || arg.Author.IsWebhook) return;
|
||||
|
||||
if (arg.Channel is IGuildChannel) await CommandCheckInvoke(arg);
|
||||
}
|
||||
|
||||
[ConfigSection("ModTools")]
|
||||
[ConfigSection("ModCommands")]
|
||||
public override async Task<object> ProcessConfiguration(JToken configSection)
|
||||
{
|
||||
// Constructor throws exception on config errors
|
||||
|
@ -69,12 +74,13 @@ namespace Noikoio.RegexBot.Module.ModTools
|
|||
{
|
||||
try
|
||||
{
|
||||
await Log($"'{c.Label}' invoked by {arg.Author.ToString()} in {g.Name}/#{arg.Channel.Name}");
|
||||
await c.Invoke(g, arg);
|
||||
// TODO Custom invocation log messages? Not by the user, but by the command.
|
||||
await Log($"{g.Name}/#{arg.Channel.Name}: {arg.Author} invoked {arg.Content}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Log($"Encountered an error for the command '{c.Label}'. Details follow:");
|
||||
await Log($"Encountered an unhandled exception while processing '{c.Label}'. Details follow:");
|
||||
await Log(ex.ToString());
|
||||
}
|
||||
}
|
|
@ -1,17 +1,14 @@
|
|||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Noikoio.RegexBot.ConfigItem;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModTools.Commands
|
||||
{
|
||||
class BanKick : CommandBase
|
||||
namespace Noikoio.RegexBot.Module.ModCommands.Commands
|
||||
{
|
||||
// Ban and kick commands are highly similar in implementation, and thus are handled in a single class.
|
||||
class BanKick : Command
|
||||
{
|
||||
protected enum CommandMode { Ban, Kick }
|
||||
private readonly CommandMode _mode;
|
||||
|
||||
|
@ -28,7 +25,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
|
|||
// "notifymsg" - Message to send to the target user being acted upon. Default message is used
|
||||
// if the value is not specified. If a blank value is given, the feature is disabled.
|
||||
// Takes the special values $s for server name and $r for reason text.
|
||||
protected BanKick(ModTools l, string label, JObject conf, CommandMode mode) : base(l, label, conf)
|
||||
protected BanKick(CommandListener l, string label, JObject conf, CommandMode mode) : base(l, label, conf)
|
||||
{
|
||||
_mode = mode;
|
||||
_forceReason = conf["forcereason"]?.Value<bool>() ?? false;
|
||||
|
@ -49,13 +46,20 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
|
|||
if (string.IsNullOrWhiteSpace(val)) _notifyMsg = null; // empty value - disable message
|
||||
else _notifyMsg = val;
|
||||
}
|
||||
|
||||
// Building usage message here
|
||||
DefaultUsageMsg = $"{this.Trigger} [user or user ID] " + (_forceReason ? "[reason]" : "*[reason]*") + "\n"
|
||||
+ "Removes the given user from this server"
|
||||
+ (_mode == CommandMode.Ban ? " and prevents the user from rejoining" : "") + ". "
|
||||
+ (_forceReason ? "L" : "Optionally l") + "ogs the reason for the "
|
||||
+ (_mode == CommandMode.Ban ? "ban" : "kick") + " to the Audit Log.";
|
||||
if (_purgeDays > 0)
|
||||
DefaultUsageMsg += $"\nAdditionally removes the user's post history for the last {_purgeDays} day(s).";
|
||||
}
|
||||
|
||||
#region Strings
|
||||
const string FailPrefix = ":x: **Failed to {0} user:** ";
|
||||
const string Fail403 = "I do not have the required permissions to perform that action.";
|
||||
const string Fail404 = "The target user is no longer available.";
|
||||
const string FailDefault = "An unknown error occurred. Notify the bot operator.";
|
||||
const string Fail404 = "The specified user is no longer in the server.";
|
||||
const string NotifyDefault = "You have been {0} from $s for the following reason:\n$r";
|
||||
const string NotifyReasonNone = "No reason specified.";
|
||||
const string NotifyFailed = "\n(User was unable to receive notification message.)";
|
||||
|
@ -71,7 +75,7 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
|
|||
string reason;
|
||||
if (line.Length < 2)
|
||||
{
|
||||
await SendUsageMessage(msg, null);
|
||||
await SendUsageMessageAsync(msg.Channel, null);
|
||||
return;
|
||||
}
|
||||
targetstr = line[1];
|
||||
|
@ -86,33 +90,36 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
|
|||
// No reason given
|
||||
if (_forceReason)
|
||||
{
|
||||
await SendUsageMessage(msg, ReasonRequired);
|
||||
await SendUsageMessageAsync(msg.Channel, ReasonRequired);
|
||||
return;
|
||||
}
|
||||
reason = null;
|
||||
}
|
||||
|
||||
// Getting SocketGuildUser target
|
||||
SocketGuildUser targetobj = null;
|
||||
|
||||
// Extract snowflake value from mention (if a mention was given)
|
||||
Match m = UserMention.Match(targetstr);
|
||||
if (m.Success) targetstr = m.Groups["snowflake"].Value;
|
||||
|
||||
var qres = (await EntityCache.EntityCache.QueryAsync(g.Id, targetstr)).FirstOrDefault();
|
||||
if (qres == null)
|
||||
// Retrieve target user
|
||||
var (targetId, targetData) = await GetUserDataFromString(g.Id, targetstr);
|
||||
if (targetId == 1)
|
||||
{
|
||||
await SendUsageMessage(msg, TargetNotFound);
|
||||
await msg.Channel.SendMessageAsync(FailPrefix + FailDefault);
|
||||
return;
|
||||
}
|
||||
ulong targetuid = qres.UserId;
|
||||
targetobj = g.GetUser(targetuid);
|
||||
string targetdisp = targetobj?.ToString() ?? $"ID {targetuid}";
|
||||
if (targetId == 0)
|
||||
{
|
||||
await SendUsageMessageAsync(msg.Channel, TargetNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
SocketGuildUser targetobj = g.GetUser(targetId);
|
||||
string targetdisp;
|
||||
if (targetData != null)
|
||||
targetdisp = $"{targetData.Username}#{targetData.Discriminator}";
|
||||
else
|
||||
targetdisp = $"ID {targetId}";
|
||||
|
||||
if (_mode == CommandMode.Kick && targetobj == null)
|
||||
{
|
||||
// Can't kick without obtaining the user object
|
||||
await SendUsageMessage(msg, TargetNotFound);
|
||||
await SendUsageMessageAsync(msg.Channel, TargetNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -125,15 +132,16 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
|
|||
string reasonlog = $"Invoked by {msg.Author.ToString()}.";
|
||||
if (reason != null) reasonlog += $" Reason: {reason}";
|
||||
reasonlog = Uri.EscapeDataString(reasonlog);
|
||||
await notifyTask;
|
||||
#warning Remove EscapeDataString call on next Discord.Net update
|
||||
#if !DEBUG
|
||||
if (_mode == CommandMode.Ban) await g.AddBanAsync(targetuid, _purgeDays, reasonlog);
|
||||
else await targetobj.KickAsync(reason);
|
||||
if (_mode == CommandMode.Ban) await g.AddBanAsync(targetId, _purgeDays, reasonlog);
|
||||
else await targetobj.KickAsync(reasonlog);
|
||||
#else
|
||||
#warning "Actual kick/ban action is DISABLED during debug."
|
||||
#endif
|
||||
string resultmsg = BuildSuccessMessage(targetdisp);
|
||||
if (await notifyTask == false) resultmsg += NotifyFailed;
|
||||
if (notifyTask.Result == false) resultmsg += NotifyFailed;
|
||||
await msg.Channel.SendMessageAsync(resultmsg);
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
|
@ -178,22 +186,6 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
|
|||
return true;
|
||||
}
|
||||
|
||||
private async Task SendUsageMessage(SocketMessage m, string message)
|
||||
{
|
||||
string desc = $"{this.Command} [user or user ID] " + (_forceReason ? "[reason]" : "*[reason]*") + "\n";
|
||||
desc += "Removes the given user from this server and prevents the user from rejoining. ";
|
||||
desc += (_forceReason ? "L" : "Optionally l") + "ogs the reason for the ban to the Audit Log.";
|
||||
if (_purgeDays > 0)
|
||||
desc += $"\nAdditionally removes the user's post history for the last {_purgeDays} day(s).";
|
||||
|
||||
var usageEmbed = new EmbedBuilder()
|
||||
{
|
||||
Title = "Usage",
|
||||
Description = desc
|
||||
};
|
||||
await m.Channel.SendMessageAsync(message ?? "", embed: usageEmbed);
|
||||
}
|
||||
|
||||
private string BuildSuccessMessage(string targetstr)
|
||||
{
|
||||
const string defaultmsgBan = ":white_check_mark: Banned user **$target**.";
|
||||
|
@ -207,13 +199,13 @@ namespace Noikoio.RegexBot.Module.ModTools.Commands
|
|||
|
||||
class Ban : BanKick
|
||||
{
|
||||
public Ban(ModTools l, string label, JObject conf)
|
||||
public Ban(CommandListener l, string label, JObject conf)
|
||||
: base(l, label, conf, CommandMode.Ban) { }
|
||||
}
|
||||
|
||||
class Kick : BanKick
|
||||
{
|
||||
public Kick(ModTools l, string label, JObject conf)
|
||||
public Kick(CommandListener l, string label, JObject conf)
|
||||
: base(l, label, conf, CommandMode.Kick) { }
|
||||
}
|
||||
}
|
25
Module/ModCommands/Commands/ConfReload.cs
Normal file
25
Module/ModCommands/Commands/ConfReload.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModCommands.Commands
|
||||
{
|
||||
class ConfReload : Command
|
||||
{
|
||||
// No configuration.
|
||||
public ConfReload(CommandListener l, string label, JObject conf) : base(l, label, conf) { }
|
||||
|
||||
// Usage: (command)
|
||||
public override async Task Invoke(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
bool status = await RegexBot.Config.ReloadServerConfig();
|
||||
string res;
|
||||
if (status) res = ":white_check_mark: Configuration reloaded with no issues. Check the console to verify.";
|
||||
else res = ":x: Reload failed. Check the console.";
|
||||
await msg.Channel.SendMessageAsync(res);
|
||||
}
|
||||
|
||||
// Crazy idea: somehow redirect all logging messages created from invoking config reloading
|
||||
// and pass them onto the invoking channel.
|
||||
}
|
||||
}
|
133
Module/ModCommands/Commands/RoleManipulation.cs
Normal file
133
Module/ModCommands/Commands/RoleManipulation.cs
Normal file
|
@ -0,0 +1,133 @@
|
|||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Noikoio.RegexBot.ConfigItem;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModCommands.Commands
|
||||
{
|
||||
// Role adding and removing is largely the same, and thus are handled in a single class.
|
||||
class RoleManipulation : Command
|
||||
{
|
||||
protected enum CommandMode { Add, Del }
|
||||
private readonly CommandMode _mode;
|
||||
|
||||
private readonly EntityName _role;
|
||||
private readonly string _successMsg;
|
||||
// Configuration:
|
||||
// "role" - string; The given role that applies to this command.
|
||||
// "successmsg" - string; Messages to display on command success. Overrides default.
|
||||
|
||||
protected RoleManipulation(CommandListener l, string label, JObject conf, CommandMode mode) : base(l, label, conf)
|
||||
{
|
||||
_mode = mode;
|
||||
var rolestr = conf["role"]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(rolestr)) throw new RuleImportException("Role must be provided.");
|
||||
_role = new EntityName(rolestr, EntityType.Role);
|
||||
_successMsg = conf["successmsg"]?.Value<string>();
|
||||
|
||||
DefaultUsageMsg = $"{this.Trigger} [user or user ID]\n"
|
||||
+ (_mode == CommandMode.Add ? "Adds" : "Removes") + " the specified role "
|
||||
+ (_mode == CommandMode.Add ? "to" : "from") + " the given user.";
|
||||
}
|
||||
|
||||
#region Strings
|
||||
const string FailPrefix = ":x: **Failed to apply role change:** ";
|
||||
const string TargetNotFound = ":x: **Unable to determine the target user.**";
|
||||
const string RoleNotFound = ":x: **Failed to determine the specified role for this command.**";
|
||||
const string Success = ":white_check_mark: Successfully {0} role for **{1}**.";
|
||||
#endregion
|
||||
|
||||
public override async Task Invoke(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
string[] line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
string targetstr;
|
||||
if (line.Length < 2)
|
||||
{
|
||||
await SendUsageMessageAsync(msg.Channel, null);
|
||||
return;
|
||||
}
|
||||
targetstr = line[1];
|
||||
|
||||
// Retrieve target user
|
||||
var (targetId, targetData) = await GetUserDataFromString(g.Id, targetstr);
|
||||
if (targetId == 1)
|
||||
{
|
||||
await msg.Channel.SendMessageAsync(FailPrefix + FailDefault);
|
||||
return;
|
||||
}
|
||||
if (targetId == 0)
|
||||
{
|
||||
await SendUsageMessageAsync(msg.Channel, TargetNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
string targetdisp;
|
||||
if (targetData != null)
|
||||
targetdisp = $"{targetData.Username}#{targetData.Discriminator}";
|
||||
else
|
||||
targetdisp = $"ID {targetId}";
|
||||
|
||||
// Determine role
|
||||
SocketRole cmdRole;
|
||||
if (_role.Id.HasValue)
|
||||
{
|
||||
cmdRole = g.GetRole(_role.Id.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
var res = g.Roles.Where(rn =>
|
||||
string.Equals(rn.Name, _role.Name, StringComparison.InvariantCultureIgnoreCase))
|
||||
.FirstOrDefault();
|
||||
if (res == null)
|
||||
{
|
||||
await msg.Channel.SendMessageAsync(RoleNotFound);
|
||||
await Log(RoleNotFound);
|
||||
return;
|
||||
}
|
||||
cmdRole = res;
|
||||
}
|
||||
|
||||
// Do the action
|
||||
try
|
||||
{
|
||||
var u = g.GetUser(targetId);
|
||||
if (_mode == CommandMode.Add)
|
||||
await u.AddRoleAsync(cmdRole);
|
||||
else
|
||||
await u.RemoveRoleAsync(cmdRole);
|
||||
await msg.Channel.SendMessageAsync(BuildSuccessMessage(targetdisp));
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
{
|
||||
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
await msg.Channel.SendMessageAsync(FailPrefix + Fail403);
|
||||
}
|
||||
else
|
||||
{
|
||||
await msg.Channel.SendMessageAsync(FailPrefix + FailDefault);
|
||||
await Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildSuccessMessage(string targetstr)
|
||||
{
|
||||
const string defaultmsg = ":white_check_mark: Successfully {0} role for **$target**.";
|
||||
string msg = _successMsg ?? string.Format(defaultmsg, _mode == CommandMode.Add ? "set" : "unset");
|
||||
return msg.Replace("$target", targetstr);
|
||||
}
|
||||
}
|
||||
|
||||
class RoleAdd : RoleManipulation
|
||||
{
|
||||
public RoleAdd(CommandListener l, string label, JObject conf) : base(l, label, conf, CommandMode.Add) { }
|
||||
}
|
||||
|
||||
class RoleDel : RoleManipulation
|
||||
{
|
||||
public RoleDel(CommandListener l, string label, JObject conf) : base(l, label, conf, CommandMode.Del) { }
|
||||
}
|
||||
}
|
|
@ -1,49 +1,44 @@
|
|||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModTools.Commands
|
||||
namespace Noikoio.RegexBot.Module.ModCommands.Commands
|
||||
{
|
||||
class Say : CommandBase
|
||||
class Say : Command
|
||||
{
|
||||
public Say(ModTools l, string label, JObject conf) : base(l, label, conf) { }
|
||||
|
||||
// No configuration at the moment.
|
||||
// TODO: Whitelist/blacklist - to limit which channels it can "say" into
|
||||
public Say(CommandListener l, string label, JObject conf) : base(l, label, conf) {
|
||||
DefaultUsageMsg = $"{this.Trigger} [channel] [message]\n"
|
||||
+ "Displays the given message exactly as specified to the given channel.";
|
||||
}
|
||||
|
||||
#region Strings
|
||||
const string ChannelRequired = ":x: You must specify a channel.";
|
||||
const string MessageRequired = ":x: You must specify a message.";
|
||||
const string TargetNotFound = ":x: Unable to find given channel.";
|
||||
#endregion
|
||||
|
||||
public override async Task Invoke(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
string[] line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (line.Length <= 1)
|
||||
{
|
||||
await SendUsageMessage(msg, ":x: You must specify a channel.");
|
||||
await SendUsageMessageAsync(msg.Channel, ChannelRequired);
|
||||
return;
|
||||
}
|
||||
if (line.Length <= 2 || string.IsNullOrWhiteSpace(line[2]))
|
||||
{
|
||||
await SendUsageMessage(msg, ":x: You must specify a message.");
|
||||
await SendUsageMessageAsync(msg.Channel, MessageRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
var ch = GetTextChannelFromString(g, line[1]);
|
||||
if (ch == null) await SendUsageMessage(msg, ":x: Unable to find given channel.");
|
||||
if (ch == null) await SendUsageMessageAsync(msg.Channel, TargetNotFound);
|
||||
await ch.SendMessageAsync(line[2]);
|
||||
}
|
||||
|
||||
private async Task SendUsageMessage(SocketMessage m, string message)
|
||||
{
|
||||
string desc = $"{this.Command} [channel] [message]\n";
|
||||
desc += "Displays the given message exactly as specified to the given channel.";
|
||||
|
||||
var usageEmbed = new EmbedBuilder()
|
||||
{
|
||||
Title = "Usage",
|
||||
Description = desc
|
||||
};
|
||||
await m.Channel.SendMessageAsync(message ?? "", embed: usageEmbed);
|
||||
}
|
||||
|
||||
private SocketTextChannel GetTextChannelFromString(SocketGuild g, string input)
|
||||
{
|
||||
// Method 1: Check for channel mention
|
80
Module/ModCommands/Commands/Unban.cs
Normal file
80
Module/ModCommands/Commands/Unban.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModCommands.Commands
|
||||
{
|
||||
class Unban : Command
|
||||
{
|
||||
// No configuration.
|
||||
// TODO bring in some options from BanKick. Particularly custom success msg.
|
||||
// TODO when ModLogs fully implemented, add a reason?
|
||||
public Unban(CommandListener l, string label, JObject conf) : base(l, label, conf) {
|
||||
DefaultUsageMsg = $"{this.Trigger} [user or user ID]\n"
|
||||
+ "Unbans the given user, allowing them to rejoin the server.";
|
||||
}
|
||||
|
||||
#region Strings
|
||||
const string FailPrefix = ":x: **Unable to unban:** ";
|
||||
protected const string Fail404 = "The specified user does not exist or is not in the ban list.";
|
||||
const string TargetNotFound = ":x: **Unable to determine the target user.**";
|
||||
const string Success = ":white_check_mark: Unbanned user **{0}**.";
|
||||
#endregion
|
||||
|
||||
// Usage: (command) (user query)
|
||||
public override async Task Invoke(SocketGuild g, SocketMessage msg)
|
||||
{
|
||||
string[] line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
string targetstr;
|
||||
if (line.Length < 2)
|
||||
{
|
||||
await SendUsageMessageAsync(msg.Channel, null);
|
||||
return;
|
||||
}
|
||||
targetstr = line[1];
|
||||
|
||||
// Retrieve target user
|
||||
var (targetId, targetData) = await GetUserDataFromString(g.Id, targetstr);
|
||||
if (targetId == 1)
|
||||
{
|
||||
await msg.Channel.SendMessageAsync(FailPrefix + FailDefault);
|
||||
return;
|
||||
}
|
||||
if (targetId == 0)
|
||||
{
|
||||
await SendUsageMessageAsync(msg.Channel, TargetNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
string targetdisp;
|
||||
if (targetData != null)
|
||||
targetdisp = $"{targetData.Username}#{targetData.Discriminator}";
|
||||
else
|
||||
targetdisp = $"ID {targetId}";
|
||||
|
||||
// Do the action
|
||||
try
|
||||
{
|
||||
await g.RemoveBanAsync(targetId);
|
||||
await msg.Channel.SendMessageAsync(string.Format(Success, targetdisp));
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
{
|
||||
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
await msg.Channel.SendMessageAsync(FailPrefix + Fail403);
|
||||
}
|
||||
else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
await msg.Channel.SendMessageAsync(FailPrefix + Fail404);
|
||||
}
|
||||
else
|
||||
{
|
||||
await msg.Channel.SendMessageAsync(FailPrefix + FailDefault);
|
||||
await Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
159
Module/ModCommands/Commands/_CommandBase.cs
Normal file
159
Module/ModCommands/Commands/_CommandBase.cs
Normal file
|
@ -0,0 +1,159 @@
|
|||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Noikoio.RegexBot.ConfigItem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModCommands.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for a command within the module.
|
||||
/// After implementing, don't forget to add a reference to
|
||||
/// <see cref="CreateInstance(CommandListener, JProperty)"/>.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("Command def: {Label}")]
|
||||
abstract class Command
|
||||
{
|
||||
private readonly CommandListener _mod;
|
||||
private readonly string _label;
|
||||
private readonly string _command;
|
||||
|
||||
protected CommandListener Module => _mod;
|
||||
public string Label => _label;
|
||||
public string Trigger => _command;
|
||||
|
||||
public Command(CommandListener l, string label, JObject conf)
|
||||
{
|
||||
_mod = l;
|
||||
_label = label;
|
||||
_command = conf["command"].Value<string>();
|
||||
}
|
||||
|
||||
public abstract Task Invoke(SocketGuild g, SocketMessage msg);
|
||||
|
||||
protected Task Log(string text)
|
||||
{
|
||||
return _mod.Log($"{Label}: {text}");
|
||||
}
|
||||
|
||||
#region Config loading
|
||||
private static readonly ReadOnlyDictionary<string, Type> _commands =
|
||||
new ReadOnlyDictionary<string, Type>(
|
||||
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Define all command types and their corresponding Types here
|
||||
{ "ban", typeof(Ban) },
|
||||
{ "confreload", typeof(ConfReload) },
|
||||
{ "kick", typeof(Kick) },
|
||||
{ "say", typeof(Say) },
|
||||
{ "unban", typeof(Unban) },
|
||||
{ "addrole", typeof(RoleAdd) },
|
||||
{ "delrole", typeof(RoleDel) }
|
||||
});
|
||||
|
||||
public static Command CreateInstance(CommandListener root, JProperty def)
|
||||
{
|
||||
string label = def.Name;
|
||||
if (string.IsNullOrWhiteSpace(label)) throw new RuleImportException("Label cannot be blank.");
|
||||
|
||||
var definition = (JObject)def.Value;
|
||||
string cmdinvoke = definition["command"]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(cmdinvoke))
|
||||
throw new RuleImportException($"{label}: 'command' value was not specified.");
|
||||
if (cmdinvoke.Contains(" "))
|
||||
throw new RuleImportException($"{label}: 'command' must not contain spaces.");
|
||||
|
||||
string ctypestr = definition["type"]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(ctypestr))
|
||||
throw new RuleImportException($"Value 'type' must be specified in definition for '{label}'.");
|
||||
if (_commands.TryGetValue(ctypestr, out Type ctype))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (Command)Activator.CreateInstance(ctype, root, label, definition);
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
if (ex.InnerException is RuleImportException)
|
||||
throw new RuleImportException($"Error in configuration for command '{label}': {ex.InnerException.Message}");
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuleImportException($"The given 'type' value is invalid in definition for '{label}'.");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Helper methods and common values
|
||||
protected static readonly Regex UserMention = new Regex(@"<@!?(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||
protected static readonly Regex RoleMention = new Regex(@"<@&(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||
protected static readonly Regex ChannelMention = new Regex(@"<#(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||
protected static readonly Regex EmojiMatch = new Regex(@"<:(?<name>[A-Za-z0-9_]{2,}):(?<ID>\d+)>", RegexOptions.Compiled);
|
||||
protected const string Fail403 = "I do not have the required permissions to perform that action.";
|
||||
protected const string FailDefault = "An unknown error occurred. Notify the bot operator.";
|
||||
|
||||
protected string DefaultUsageMsg { get; set; }
|
||||
/// <summary>
|
||||
/// Sends out the default usage message (<see cref="DefaultUsageMsg"/>) within an embed.
|
||||
/// An optional message can be included, for uses such as notifying users of incorrect usage.
|
||||
/// </summary>
|
||||
/// <param name="target">Target channel for sending the message.</param>
|
||||
/// <param name="message">The message to send alongside the default usage message.</param>
|
||||
protected async Task SendUsageMessageAsync(ISocketMessageChannel target, string message = null)
|
||||
{
|
||||
if (DefaultUsageMsg == null)
|
||||
throw new InvalidOperationException("DefaultUsage was not defined.");
|
||||
|
||||
var usageEmbed = new EmbedBuilder()
|
||||
{
|
||||
Title = "Usage",
|
||||
Description = DefaultUsageMsg
|
||||
};
|
||||
await target.SendMessageAsync(message ?? "", embed: usageEmbed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for turning input into user data. Only returns the first cache result.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// First value: 0 for no data, 1 for no data + exception.
|
||||
/// May return a partial result: a valid ulong value but no CacheUser.
|
||||
/// </returns>
|
||||
protected async Task<(ulong, EntityCache.CacheUser)> GetUserDataFromString(ulong guild, string input)
|
||||
{
|
||||
ulong uid = 0;
|
||||
EntityCache.CacheUser cdata = null;
|
||||
|
||||
Match m = UserMention.Match(input);
|
||||
if (m.Success)
|
||||
{
|
||||
input = m.Groups["snowflake"].Value;
|
||||
uid = ulong.Parse(input);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
cdata = (await EntityCache.EntityCache.QueryAsync(guild, input))
|
||||
.FirstOrDefault();
|
||||
if (cdata != null) uid = cdata.UserId;
|
||||
}
|
||||
catch (Npgsql.NpgsqlException ex)
|
||||
{
|
||||
await Log("A databasae error occurred during user lookup: " + ex.Message);
|
||||
if (uid == 0) uid = 1;
|
||||
}
|
||||
|
||||
return (uid, cdata);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
42
Module/ModCommands/ConfigItem.cs
Normal file
42
Module/ModCommands/ConfigItem.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using Noikoio.RegexBot.ConfigItem;
|
||||
using Noikoio.RegexBot.Module.ModCommands.Commands;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains a server's ModCommands configuration.
|
||||
/// </summary>
|
||||
class ConfigItem
|
||||
{
|
||||
private readonly ReadOnlyDictionary<string, Command> _cmdInstances;
|
||||
|
||||
public ReadOnlyDictionary<string, Command> Commands => _cmdInstances;
|
||||
|
||||
public ConfigItem(CommandListener instance, JToken inconf)
|
||||
{
|
||||
if (inconf.Type != JTokenType.Object)
|
||||
{
|
||||
throw new RuleImportException("Configuration for this section is invalid.");
|
||||
}
|
||||
|
||||
// Command instance creation
|
||||
var commands = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var def in inconf.Children<JProperty>())
|
||||
{
|
||||
string label = def.Name;
|
||||
var cmd = Command.CreateInstance(instance, def);
|
||||
if (commands.ContainsKey(cmd.Trigger))
|
||||
throw new RuleImportException(
|
||||
$"{label}: 'command' value must not be equal to that of another definition. " +
|
||||
$"Given value is being used for \"{commands[cmd.Trigger].Label}\".");
|
||||
|
||||
commands.Add(cmd.Trigger, cmd);
|
||||
}
|
||||
_cmdInstances = new ReadOnlyDictionary<string, Command>(commands);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Noikoio.RegexBot.ConfigItem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for ModTools command.
|
||||
/// We are not using Discord.Net's Commands extension, as it does not allow for changes during runtime.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Label}-type command")]
|
||||
abstract class CommandBase
|
||||
{
|
||||
private readonly ModTools _modtools;
|
||||
private readonly string _label;
|
||||
private readonly string _command;
|
||||
|
||||
protected ModTools Mt => _modtools;
|
||||
public string Label => _label;
|
||||
public string Command => _command;
|
||||
|
||||
protected CommandBase(ModTools l, string label, JObject conf)
|
||||
{
|
||||
_modtools = l;
|
||||
_label = label;
|
||||
_command = conf["command"].Value<string>();
|
||||
}
|
||||
|
||||
public abstract Task Invoke(SocketGuild g, SocketMessage msg);
|
||||
|
||||
protected Task Log(string text)
|
||||
{
|
||||
return _modtools.Log($"{Label}: {text}");
|
||||
}
|
||||
|
||||
#region Config loading
|
||||
private static readonly ReadOnlyDictionary<string, Type> _commands =
|
||||
new ReadOnlyDictionary<string, Type>(
|
||||
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Define all command types and their corresponding Types here
|
||||
{ "ban", typeof(Commands.Ban) },
|
||||
{ "kick", typeof(Commands.Kick) },
|
||||
{ "say", typeof(Commands.Say) }
|
||||
});
|
||||
|
||||
public static CommandBase CreateInstance(ModTools root, JProperty def)
|
||||
{
|
||||
string label = def.Name;
|
||||
if (string.IsNullOrWhiteSpace(label)) throw new RuleImportException("Label cannot be blank.");
|
||||
|
||||
var definition = (JObject)def.Value;
|
||||
string cmdinvoke = definition["command"].Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(cmdinvoke))
|
||||
throw new RuleImportException($"{label}: 'command' value was not specified.");
|
||||
if (cmdinvoke.Contains(" "))
|
||||
throw new RuleImportException($"{label}: 'command' must not contain spaces.");
|
||||
|
||||
string ctypestr = definition["type"]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(ctypestr))
|
||||
throw new RuleImportException($"Value 'type' must be specified in definition for '{label}'.");
|
||||
Type ctype;
|
||||
if (!_commands.TryGetValue(ctypestr, out ctype))
|
||||
throw new RuleImportException($"The given 'type' value is invalid in definition for '{label}'.");
|
||||
|
||||
try
|
||||
{
|
||||
return (CommandBase)Activator.CreateInstance(ctype, root, label, definition);
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
if (ex.InnerException is RuleImportException)
|
||||
throw new RuleImportException($"Error in configuration for command '{label}': {ex.InnerException.Message}");
|
||||
else throw;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Helper methods and values
|
||||
protected static readonly Regex UserMention = new Regex(@"<@!?(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||
protected static readonly Regex RoleMention = new Regex(@"<@&(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||
protected static readonly Regex ChannelMention = new Regex(@"<#(?<snowflake>\d+)>", RegexOptions.Compiled);
|
||||
protected static readonly Regex EmojiMatch = new Regex(@"<:(?<name>[A-Za-z0-9_]{2,}):(?<ID>\d+)>", RegexOptions.Compiled);
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using Noikoio.RegexBot.ConfigItem;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Noikoio.RegexBot.Module.ModTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents ModTools configuration within one server.
|
||||
/// </summary>
|
||||
class ConfigItem
|
||||
{
|
||||
private readonly ReadOnlyDictionary<string, CommandBase> _cmdInstances;
|
||||
|
||||
public ReadOnlyDictionary<string, CommandBase> Commands => _cmdInstances;
|
||||
|
||||
public ConfigItem(ModTools instance, JToken inconf)
|
||||
{
|
||||
if (inconf.Type != JTokenType.Object)
|
||||
{
|
||||
throw new RuleImportException("Configuration for this section is invalid.");
|
||||
}
|
||||
var config = (JObject)inconf;
|
||||
|
||||
|
||||
// Command instances
|
||||
var commands = new Dictionary<string, CommandBase>(StringComparer.OrdinalIgnoreCase);
|
||||
var commandconf = config["Commands"];
|
||||
if (commandconf != null)
|
||||
{
|
||||
if (commandconf.Type != JTokenType.Object)
|
||||
{
|
||||
throw new RuleImportException("CommandDefs is not properly defined.");
|
||||
}
|
||||
|
||||
foreach (var def in commandconf.Children<JProperty>())
|
||||
{
|
||||
string label = def.Name;
|
||||
var cmd = CommandBase.CreateInstance(instance, def);
|
||||
if (commands.ContainsKey(cmd.Command))
|
||||
throw new RuleImportException(
|
||||
$"{label}: 'command' value must not be equal to that of another definition. " +
|
||||
$"Given value is being used for {commands[cmd.Command].Label}.");
|
||||
|
||||
commands.Add(cmd.Command, cmd);
|
||||
}
|
||||
}
|
||||
_cmdInstances = new ReadOnlyDictionary<string, CommandBase>(commands);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -56,7 +56,7 @@ namespace Noikoio.RegexBot
|
|||
{
|
||||
new Module.DMLogger.DMLogger(_client),
|
||||
new Module.AutoMod.AutoMod(_client),
|
||||
new Module.ModTools.ModTools(_client),
|
||||
new Module.ModCommands.CommandListener(_client),
|
||||
new Module.AutoRespond.AutoRespond(_client),
|
||||
|
||||
// EntityCache loads before anything using it
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
## ModTools
|
||||
## ModCommands
|
||||
|
||||
ModTools is the current name for the component that provides commands for use by moderators. Commands are defined based on a number of available template-like *type*s, which can then be customized with further configuration.
|
||||
ModCommands is the name of the component that provides the ability for one to create useful commands for moderation. These commands are defined based on a number of available template-like *type*s, which can then be customized with further configuration.
|
||||
|
||||
Sample within a [server definition](serverdef.html):
|
||||
```
|
||||
"ModTools": {
|
||||
"Commands": {
|
||||
"ModCommands": {
|
||||
"Kick": { // a plain and simple kick command
|
||||
"type": "kick",
|
||||
"command": "!!kick"
|
||||
|
@ -16,18 +15,17 @@ Sample within a [server definition](serverdef.html):
|
|||
"successmsg": "Yay! $target got banned!\nhttps://i.imgur.com/i4CIBtT.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Definition structure
|
||||
Commands are defined within a JSON object named `Commands` within another object named `ModTools`. They are defined by means of name/value pairs, with the name serving as its label.
|
||||
Commands are defined within a `ModCommands` object, itself within a [server definition](serverdef.html). They are defined by means of name/value pairs, with the name serving as its label.
|
||||
|
||||
The following values are **required** in a definition:
|
||||
* type (*string*) - Specifies the behavior that the command should take.
|
||||
* type (*string*) - Specifies the type of behavior that the command should take.
|
||||
* command (*string*) - The text trigger for the command being defined. Must not contain spaces, and it is recommended to start it with an uncommon symbol, such as `!`.
|
||||
|
||||
### Command types
|
||||
Each command type specifies the action taken by the bot when the command is invoked. Certain types offer additional configuration options as well.
|
||||
Each command type specifies the action taken by the bot when the command is invoked. Certain types offer additional configuration options.
|
||||
|
||||
#### Ban
|
||||
* `"type": "ban"`
|
||||
|
@ -44,10 +42,15 @@ Additional behavior can be specified in its configuration:
|
|||
* Uses a default message if this configuration value is not specified.
|
||||
* To disable, specify a blank value.
|
||||
|
||||
#### Configuration Reload
|
||||
* `"type": "confreload"`
|
||||
* Usage: (*command*)
|
||||
Reloads server configuration. The bot will reply indicating if the reload was successful.
|
||||
|
||||
#### Kick
|
||||
* `"type": "kick"`
|
||||
* Usage: (*command*) (*user name or ID*) [*reason*]
|
||||
Removes the given user from the server the command was invoked in, and sends the reason, if any, to the server's audit log.
|
||||
Removes the given user from the server the command was invoked in and sends the reason, if any, to the server's audit log.
|
||||
|
||||
Additional behavior can be specified in its configuration:
|
||||
* forcereason (*boolean*) - Forces the reason to be specified if set to true. If none is specified, the action is not taken. Defaults to *false*.
|
||||
|
@ -58,6 +61,16 @@ Additional behavior can be specified in its configuration:
|
|||
* Uses a default message if this configuration value is not specified.
|
||||
* To disable, specify a blank value.
|
||||
|
||||
#### Role manipulation
|
||||
* `"type": "addrole"` or `"type": "delrole"
|
||||
* Usage: (*command*) (*user name or ID*)
|
||||
Sets or unsets a predefined role upon the given user.
|
||||
|
||||
Additional configuration:
|
||||
* role (*string*) - The role that applies to this command. May be defined in the same type of format accepted within [entity lists](entitylist.html).
|
||||
* successmsg (*string*) - Custom message to display on success. If not specified, a default message is used.
|
||||
* The string *$target* can be used in the value to represent the command target.
|
||||
|
||||
#### Say
|
||||
* `"type": "say"`
|
||||
* Usage: (*command*) (*channel name or ID*) (*message*)
|
|
@ -25,7 +25,7 @@ servers: [
|
|||
The following is a list of accepted members within a server definition.
|
||||
* id (*integer*) - **Required.** A value containing the server's [unique ID](https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-).
|
||||
* name (*string*) - Preferably a readable version of the server's name. Not used for anything other than internal logging.
|
||||
* moderators (*[entity list](entitylist.html)*) - A list of entities to consider as moderators. Actions done by members of this list are able to execute *ModTools* commands and are exempt from certain *AutoMod* rules. See their respective pages for more details.
|
||||
* moderators (*[entity list](entitylist.html)*) - A list of entities to consider as moderators. Actions done by members of this list are able to execute *ModCommands* commands and are exempt from certain *AutoMod* rules. See their respective pages for more details.
|
||||
* [automod](automod.html) (*name/value pairs*) - See respective page.
|
||||
* [autoresponses](autorespond.html) (*name/value pairs*) - See respective page.
|
||||
* [ModTools](modtools.html) (*name/value pairs*) - See respective page.
|
||||
* [ModCommands](modcommands.html) (*name/value pairs*) - See respective page.
|
Loading…
Reference in a new issue