Add ModCommands module
This commit is contained in:
parent
681bb1c345
commit
53e0301edd
8 changed files with 482 additions and 0 deletions
144
RegexBot-Modules/ModCommands/Commands/BanKick.cs
Normal file
144
RegexBot-Modules/ModCommands/Commands/BanKick.cs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
using RegexBot.Data;
|
||||||
|
|
||||||
|
namespace RegexBot.Modules.ModCommands.Commands;
|
||||||
|
// Ban and kick commands are highly similar in implementation, and thus are handled in a single class.
|
||||||
|
class Ban : BanKick {
|
||||||
|
public Ban(ModCommands module, JObject config) : base(module, config, true) {
|
||||||
|
if (PurgeDays is > 7 or < 0)
|
||||||
|
throw new ModuleLoadException($"The value of '{nameof(PurgeDays)}' must be between 0 and 7.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ContinueInvoke(SocketGuild g, SocketMessage msg, string? reason,
|
||||||
|
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
|
||||||
|
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
|
||||||
|
} else {
|
||||||
|
// TODO custom fail message?
|
||||||
|
await msg.Channel.SendMessageAsync(SuccessMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Kick : BanKick {
|
||||||
|
public Kick(ModCommands module, JObject config) : base(module, config, false) { }
|
||||||
|
|
||||||
|
protected override async Task ContinueInvoke(SocketGuild g, SocketMessage msg, string? reason,
|
||||||
|
ulong targetId, CachedGuildUser? targetQuery, SocketUser? targetUser) {
|
||||||
|
// Kick: Unlike ban, must find the guild user in order to proceed
|
||||||
|
if (targetUser == null) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, TargetNotFound);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await Module.Bot.KickAsync(g, msg.Author.ToString(), targetId, reason, SendNotify);
|
||||||
|
if (result.OperationSuccess) {
|
||||||
|
if (SuccessMessage != null) {
|
||||||
|
// TODO string replacement, formatting, etc
|
||||||
|
await msg.Channel.SendMessageAsync($"{SuccessMessage}\n{result.GetResultString(Module.Bot)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await msg.Channel.SendMessageAsync(result.GetResultString(Module.Bot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BanKick : CommandConfig {
|
||||||
|
protected bool ForceReason { get; }
|
||||||
|
protected int PurgeDays { get; }
|
||||||
|
protected bool SendNotify { get; }
|
||||||
|
protected string? SuccessMessage { get; }
|
||||||
|
|
||||||
|
// Configuration:
|
||||||
|
// "ForceReason" - boolean; Force a reason to be given. Defaults to false.
|
||||||
|
// "PurgeDays" - integer; Number of days of target's post history to delete, if banning.
|
||||||
|
// Must be between 0-7 inclusive. Defaults to 0.
|
||||||
|
// "SendNotify" - boolean; Whether to send a notification DM explaining the action. Defaults to true.
|
||||||
|
// "SuccessMessage" - string; Message to display on command success. Overrides default.
|
||||||
|
protected BanKick(ModCommands module, JObject config, bool ban) : base(module, config) {
|
||||||
|
ForceReason = config[nameof(ForceReason)]?.Value<bool>() ?? false;
|
||||||
|
PurgeDays = config[nameof(PurgeDays)]?.Value<int>() ?? 0;
|
||||||
|
|
||||||
|
SendNotify = config[nameof(SendNotify)]?.Value<bool>() ?? true;
|
||||||
|
SuccessMessage = config[nameof(SuccessMessage)]?.Value<string>();
|
||||||
|
|
||||||
|
_usage = $"{Command} `user or user ID` `" + (ForceReason ? "reason" : "[reason]") + "`\n"
|
||||||
|
+ "Removes the given user from this server"
|
||||||
|
+ (ban ? " and prevents the user from rejoining" : "") + ". "
|
||||||
|
+ (ForceReason ? "L" : "Optionally l") + "ogs the reason for the "
|
||||||
|
+ (ban ? "ban" : "kick") + " to the Audit Log.";
|
||||||
|
if (PurgeDays > 0)
|
||||||
|
_usage += $"\nAdditionally removes the user's post history for the last {PurgeDays} day(s).";
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly string _usage;
|
||||||
|
protected override string DefaultUsageMsg => _usage;
|
||||||
|
|
||||||
|
// Usage: (command) (mention) (reason)
|
||||||
|
public override async Task Invoke(SocketGuild g, SocketMessage msg) {
|
||||||
|
var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
string targetstr;
|
||||||
|
string? reason;
|
||||||
|
if (line.Length < 2) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetstr = line[1];
|
||||||
|
|
||||||
|
if (line.Length == 3) reason = line[2]; // Reason given - keep it
|
||||||
|
else {
|
||||||
|
// No reason given
|
||||||
|
if (ForceReason) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, ":x: **You must specify a reason.**");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reason = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather info to send to specific handlers
|
||||||
|
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 targetId = default;
|
||||||
|
var targetUser = targetId != default ? g.GetUser(targetId) : null;
|
||||||
|
|
||||||
|
if (targetId == default) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, TargetNotFound);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUser != null) {
|
||||||
|
// Bot check
|
||||||
|
if (targetUser.IsBot) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, ":x: I will not do that. Please remove bots manually.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Hierarchy check
|
||||||
|
if (((SocketGuildUser)msg.Author).Hierarchy <= targetUser.Hierarchy) {
|
||||||
|
// Block kick attempts if the invoking user is at or above the target in role hierarchy
|
||||||
|
await SendUsageMessageAsync(msg.Channel, ":x: You do not have sufficient permissions to do that.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparation complete, go to specific actions
|
||||||
|
try {
|
||||||
|
await ContinueInvoke(g, msg, reason, targetId, targetQuery, targetUser);
|
||||||
|
} catch (Discord.Net.HttpException ex) {
|
||||||
|
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) {
|
||||||
|
await msg.Channel.SendMessageAsync(":x: " + Strings.ForbiddenGenericError);
|
||||||
|
} else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound) {
|
||||||
|
await msg.Channel.SendMessageAsync(":x: Encountered a 404 error when processing the request.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task ContinueInvoke(SocketGuild g, SocketMessage msg, string? reason,
|
||||||
|
ulong targetId, CachedGuildUser? targetQuery, SocketUser? targetUser);
|
||||||
|
}
|
39
RegexBot-Modules/ModCommands/Commands/CommandConfig.cs
Normal file
39
RegexBot-Modules/ModCommands/Commands/CommandConfig.cs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
using Discord;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace RegexBot.Modules.ModCommands.Commands;
|
||||||
|
[DebuggerDisplay("Command definition '{Label}'")]
|
||||||
|
abstract class CommandConfig {
|
||||||
|
public string Label { get; }
|
||||||
|
public string Command { get; }
|
||||||
|
protected ModCommands Module { get; }
|
||||||
|
|
||||||
|
protected CommandConfig(ModCommands module, JObject config) {
|
||||||
|
Module = module;
|
||||||
|
Label = config[nameof(Label)]!.Value<string>()!;
|
||||||
|
Command = config[nameof(Command)]!.Value<string>()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task Invoke(SocketGuild g, SocketMessage msg);
|
||||||
|
|
||||||
|
protected const string FailDefault = "An unknown error occurred. Notify the bot operator.";
|
||||||
|
protected const string TargetNotFound = ":x: **Unable to find the given user.**";
|
||||||
|
|
||||||
|
protected abstract string DefaultUsageMsg { get; }
|
||||||
|
/// <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.Build());
|
||||||
|
}
|
||||||
|
}
|
17
RegexBot-Modules/ModCommands/Commands/ConfReload.cs
Normal file
17
RegexBot-Modules/ModCommands/Commands/ConfReload.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
namespace RegexBot.Modules.ModCommands.Commands;
|
||||||
|
class ConfReload : CommandConfig {
|
||||||
|
protected override string DefaultUsageMsg => null!;
|
||||||
|
|
||||||
|
// No configuration.
|
||||||
|
public ConfReload(ModCommands module, JObject config) : base(module, config) { }
|
||||||
|
|
||||||
|
// Usage: (command)
|
||||||
|
public override Task Invoke(SocketGuild g, SocketMessage msg) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
75
RegexBot-Modules/ModCommands/Commands/RoleManipulation.cs
Normal file
75
RegexBot-Modules/ModCommands/Commands/RoleManipulation.cs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
using RegexBot.Common;
|
||||||
|
|
||||||
|
namespace RegexBot.Modules.ModCommands.Commands;
|
||||||
|
class RoleAdd : RoleManipulation {
|
||||||
|
protected override (string, string) String1 => ("Adds", "to");
|
||||||
|
protected override string String2 => "set";
|
||||||
|
public RoleAdd(ModCommands module, JObject config) : base(module, config) { }
|
||||||
|
protected override async Task ContinueInvoke(SocketGuildUser target, SocketRole role) => await target.AddRoleAsync(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoleDel : RoleManipulation {
|
||||||
|
protected override (string, string) String1 => ("Removes", "from");
|
||||||
|
protected override string String2 => "unset";
|
||||||
|
public RoleDel(ModCommands module, JObject config) : base(module, config) { }
|
||||||
|
protected override async Task ContinueInvoke(SocketGuildUser target, SocketRole role) => await target.RemoveRoleAsync(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role adding and removing is largely the same, and thus are handled in a single class.
|
||||||
|
abstract class RoleManipulation : CommandConfig {
|
||||||
|
private readonly string _usage;
|
||||||
|
|
||||||
|
protected EntityName Role { get; }
|
||||||
|
protected string? SuccessMessage { get; }
|
||||||
|
protected override string DefaultUsageMsg => _usage;
|
||||||
|
protected abstract (string, string) String1 { get; }
|
||||||
|
protected abstract string String2 { get; }
|
||||||
|
|
||||||
|
// Configuration:
|
||||||
|
// "role" - string; The given role that applies to this command.
|
||||||
|
// "successmsg" - string; Messages to display on command success. Overrides default.
|
||||||
|
protected RoleManipulation(ModCommands module, JObject config) : base(module, config) {
|
||||||
|
var rolestr = config[nameof(Role)]?.Value<string>();
|
||||||
|
if (string.IsNullOrWhiteSpace(rolestr)) throw new ModuleLoadException($"'{nameof(Role)}' must be provided.");
|
||||||
|
Role = new EntityName(rolestr);
|
||||||
|
if (Role.Type != EntityType.Role) throw new ModuleLoadException($"The value in '{nameof(Role)}' is not a role.");
|
||||||
|
SuccessMessage = config[nameof(SuccessMessage)]?.Value<string>();
|
||||||
|
|
||||||
|
_usage = $"{Command} `user or user ID`\n" +
|
||||||
|
string.Format("{0} the '{1}' role {2} the specified user.",
|
||||||
|
String1.Item1, Role.Name ?? Role.Id.ToString(), String1.Item2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Invoke(SocketGuild g, SocketMessage msg) {
|
||||||
|
// TODO reason in further parameters?
|
||||||
|
var 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 targets
|
||||||
|
var targetQuery = Module.Bot.EcQueryGuildUser(g.Id, targetstr);
|
||||||
|
var targetUser = targetQuery != null ? g.GetUser((ulong)targetQuery.UserId) : null;
|
||||||
|
if (targetUser == null) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, TargetNotFound);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var targetRole = Role.FindRoleIn(g, true);
|
||||||
|
if (targetRole == null) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, ":x: **Failed to determine the specified role for this command.**");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the specific thing and report back
|
||||||
|
await ContinueInvoke(targetUser, targetRole);
|
||||||
|
const string defaultmsg = ":white_check_mark: Successfully {0} role for **$target**.";
|
||||||
|
var success = SuccessMessage ?? string.Format(defaultmsg, String2);
|
||||||
|
success = success.Replace("$target", targetUser.Nickname ?? targetUser.Username);
|
||||||
|
await msg.Channel.SendMessageAsync(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task ContinueInvoke(SocketGuildUser target, SocketRole role);
|
||||||
|
}
|
34
RegexBot-Modules/ModCommands/Commands/Say.cs
Normal file
34
RegexBot-Modules/ModCommands/Commands/Say.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using RegexBot.Common;
|
||||||
|
|
||||||
|
namespace RegexBot.Modules.ModCommands.Commands;
|
||||||
|
class Say : CommandConfig {
|
||||||
|
private readonly string _usage;
|
||||||
|
protected override string DefaultUsageMsg => _usage;
|
||||||
|
|
||||||
|
// No configuration at the moment.
|
||||||
|
// TODO: Whitelist/blacklist - to limit which channels it can "say" into
|
||||||
|
public Say(ModCommands module, JObject config) : base(module, config) {
|
||||||
|
_usage = $"{Command} `channel` `message`\n"
|
||||||
|
+ "Displays the given message exactly as specified to the given channel.";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Invoke(SocketGuild g, SocketMessage msg) {
|
||||||
|
var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (line.Length <= 1) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, ":x: You must specify a channel.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (line.Length <= 2 || string.IsNullOrWhiteSpace(line[2])) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, ":x: You must specify a message.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var getCh = Utilities.ChannelMention.Match(line[1]);
|
||||||
|
if (!getCh.Success) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, ":x: Unable to find given channel.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ch = g.GetTextChannel(ulong.Parse(getCh.Groups["snowflake"].Value));
|
||||||
|
await ch.SendMessageAsync(line[2]);
|
||||||
|
}
|
||||||
|
}
|
52
RegexBot-Modules/ModCommands/Commands/Unban.cs
Normal file
52
RegexBot-Modules/ModCommands/Commands/Unban.cs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
namespace RegexBot.Modules.ModCommands.Commands;
|
||||||
|
class Unban : CommandConfig {
|
||||||
|
private readonly string _usage;
|
||||||
|
|
||||||
|
protected override string DefaultUsageMsg => _usage;
|
||||||
|
|
||||||
|
// No configuration.
|
||||||
|
// TODO bring in some options from BanKick. Particularly custom success msg.
|
||||||
|
// TODO when ModLogs fully implemented, add a reason?
|
||||||
|
public Unban(ModCommands module, JObject config) : base(module, config) {
|
||||||
|
_usage = $"{Command} `user or user ID`\n"
|
||||||
|
+ "Unbans the given user, allowing them to rejoin the server.";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage: (command) (user query)
|
||||||
|
public override async Task Invoke(SocketGuild g, SocketMessage msg) {
|
||||||
|
var line = msg.Content.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
string targetstr;
|
||||||
|
if (line.Length < 2) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetstr = line[1];
|
||||||
|
|
||||||
|
ulong targetId;
|
||||||
|
string targetDisplay;
|
||||||
|
var query = Module.Bot.EcQueryUser(targetstr);
|
||||||
|
if (query != null) {
|
||||||
|
targetId = (ulong)query.UserId;
|
||||||
|
targetDisplay = $"{query.Username}#{query.Discriminator}";
|
||||||
|
} else {
|
||||||
|
if (!ulong.TryParse(targetstr, out targetId)) {
|
||||||
|
await SendUsageMessageAsync(msg.Channel, TargetNotFound);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetDisplay = $"with ID {targetId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the action
|
||||||
|
try {
|
||||||
|
await g.RemoveBanAsync(targetId);
|
||||||
|
await msg.Channel.SendMessageAsync($":white_check_mark: Unbanned user **{targetDisplay}**.");
|
||||||
|
} catch (Discord.Net.HttpException ex) {
|
||||||
|
const string FailPrefix = ":x: **Could not unban:** ";
|
||||||
|
if (ex.HttpCode == System.Net.HttpStatusCode.Forbidden)
|
||||||
|
await msg.Channel.SendMessageAsync(FailPrefix + Strings.ForbiddenGenericError);
|
||||||
|
else if (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
await msg.Channel.SendMessageAsync(FailPrefix + "The specified user does not exist or is not in the ban list.");
|
||||||
|
else throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
RegexBot-Modules/ModCommands/ModCommands.cs
Normal file
55
RegexBot-Modules/ModCommands/ModCommands.cs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
namespace RegexBot.Modules.ModCommands;
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a way to define highly configurable text-based commands for use by moderators within a guild.
|
||||||
|
/// </summary>
|
||||||
|
[RegexbotModule]
|
||||||
|
public class ModCommands : RegexbotModule {
|
||||||
|
public ModCommands(RegexbotClient bot) : base(bot) {
|
||||||
|
DiscordClient.MessageReceived += Client_MessageReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Client_MessageReceived(SocketMessage arg) {
|
||||||
|
if (Common.Utilities.IsValidUserMessage(arg, out var channel)) {
|
||||||
|
var cfg = GetGuildState<ModuleConfig>(channel.Guild.Id);
|
||||||
|
if (cfg != null) await CommandCheckInvoke(cfg, arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {
|
||||||
|
if (config == null) return Task.FromResult<object?>(null);
|
||||||
|
|
||||||
|
var conf = new ModuleConfig(this, config);
|
||||||
|
if (conf.Commands.Count > 0) {
|
||||||
|
Log($"{conf.Commands.Count} commands loaded.");
|
||||||
|
return Task.FromResult<object?>(conf);
|
||||||
|
} else {
|
||||||
|
Log("'{nameof(ModLogs)}' is defined, but no command configuration exists.");
|
||||||
|
return Task.FromResult<object?>(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CommandCheckInvoke(ModuleConfig cfg, SocketMessage arg) {
|
||||||
|
SocketGuild g = ((SocketGuildUser)arg.Author).Guild;
|
||||||
|
|
||||||
|
if (!GetModerators(g.Id).IsListMatch(arg, true)) return; // Mods only
|
||||||
|
// Disregard if the message contains a newline character
|
||||||
|
if (arg.Content.Contains('\n')) return; // TODO remove?
|
||||||
|
|
||||||
|
// Check for and invoke command
|
||||||
|
string cmdchk;
|
||||||
|
var space = arg.Content.IndexOf(' ');
|
||||||
|
if (space != -1) cmdchk = arg.Content[..space];
|
||||||
|
else cmdchk = arg.Content;
|
||||||
|
if (cfg.Commands.TryGetValue(cmdchk, out var c)) {
|
||||||
|
try {
|
||||||
|
await c.Invoke(g, arg);
|
||||||
|
// TODO Custom post-invocation log messages? Not by the user, but by the command.
|
||||||
|
Log($"[{g.Name}] {c.Command} invoked by {arg.Author} in #{arg.Channel.Name}.");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log($"Unhandled exception while processing '{c.Label}':\n" + ex.ToString());
|
||||||
|
await arg.Channel.SendMessageAsync($":x: An error occurred during processing ({ex.GetType().FullName}). " +
|
||||||
|
"Check the console for details.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
RegexBot-Modules/ModCommands/ModuleConfig.cs
Normal file
66
RegexBot-Modules/ModCommands/ModuleConfig.cs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
using RegexBot.Modules.ModCommands.Commands;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace RegexBot.Modules.ModCommands;
|
||||||
|
class ModuleConfig {
|
||||||
|
public ReadOnlyDictionary<string, CommandConfig> Commands { get; }
|
||||||
|
|
||||||
|
public ModuleConfig(ModCommands instance, JToken conf) {
|
||||||
|
if (conf.Type != JTokenType.Array)
|
||||||
|
throw new ModuleLoadException("Command definitions must be defined as objects in a JSON array.");
|
||||||
|
|
||||||
|
// Command instance creation
|
||||||
|
var commands = new Dictionary<string, CommandConfig>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var def in conf.Children<JObject>()) {
|
||||||
|
string Label;
|
||||||
|
Label = def[nameof(Label)]?.Value<string>()
|
||||||
|
?? throw new ModuleLoadException($"'{nameof(Label)}' was not defined in a command definition.");
|
||||||
|
var cmd = CreateCommandInstance(instance, def);
|
||||||
|
if (commands.ContainsKey(cmd.Command)) {
|
||||||
|
throw new ModuleLoadException(
|
||||||
|
$"{Label}: The command name '{cmd.Command}' is already in use by '{commands[cmd.Command].Label}'.");
|
||||||
|
}
|
||||||
|
commands.Add(cmd.Command, cmd);
|
||||||
|
}
|
||||||
|
Commands = new ReadOnlyDictionary<string, CommandConfig>(commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly ReadOnlyDictionary<string, Type> _commandTypes = new(
|
||||||
|
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase) {
|
||||||
|
{ "ban", typeof(Ban) },
|
||||||
|
{ "confreload", typeof(ConfReload) },
|
||||||
|
{ "kick", typeof(Kick) },
|
||||||
|
{ "say", typeof(Say) },
|
||||||
|
{ "unban", typeof(Unban) },
|
||||||
|
{ "addrole", typeof(RoleAdd) },
|
||||||
|
{ "roleadd", typeof(RoleAdd) },
|
||||||
|
{ "delrole", typeof(RoleDel) },
|
||||||
|
{ "roledel", typeof(RoleDel) }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private static CommandConfig CreateCommandInstance(ModCommands instance, JObject def) {
|
||||||
|
var label = def[nameof(CommandConfig.Label)]?.Value<string>()!;
|
||||||
|
|
||||||
|
var command = def[nameof(CommandConfig.Command)]?.Value<string>();
|
||||||
|
if (string.IsNullOrWhiteSpace(command))
|
||||||
|
throw new ModuleLoadException($"{label}: '{nameof(CommandConfig.Command)}' was not specified.");
|
||||||
|
if (command.Contains(' '))
|
||||||
|
throw new ModuleLoadException($"{label}: '{nameof(CommandConfig.Command)}' must not contain spaces.");
|
||||||
|
|
||||||
|
string? Type;
|
||||||
|
Type = def[nameof(Type)]?.Value<string>();
|
||||||
|
if (string.IsNullOrWhiteSpace(Type))
|
||||||
|
throw new ModuleLoadException($"'{nameof(Type)}' must be specified within definition for '{label}'.");
|
||||||
|
if (!_commandTypes.TryGetValue(Type, out Type? cmdType)) {
|
||||||
|
throw new ModuleLoadException($"{label}: '{nameof(Type)}' does not have a valid value.");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return (CommandConfig)Activator.CreateInstance(cmdType, instance, def)!;
|
||||||
|
} catch (TargetInvocationException ex) when (ex.InnerException is ModuleLoadException) {
|
||||||
|
throw new ModuleLoadException($"{label}: {ex.InnerException.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue