BirthdayBot/UserInterface/ManagerCommands.cs
Noi 2f0fe8641a Implement own sharding system
The BirthdayBot class has been split up into ShardInstance and
ShardManager. Several other things have been reorganized so that shards
may act independently.

The overall goal of these changes made is to limit failures to sections
that can easily be discarded and replaced.
2020-10-04 21:40:38 -07:00

490 lines
19 KiB
C#

using BirthdayBot.Data;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BirthdayBot.UserInterface
{
internal class ManagerCommands : CommandsCommon
{
private static readonly string ConfErrorPostfix =
$" Refer to the `{CommandPrefix}help-config` command for information on this command's usage.";
private delegate Task ConfigSubcommand(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel);
private readonly Dictionary<string, ConfigSubcommand> _subcommands;
private readonly Dictionary<string, CommandHandler> _usercommands;
public ManagerCommands(Configuration db, IEnumerable<(string, CommandHandler)> userCommands) : base(db)
{
_subcommands = new Dictionary<string, ConfigSubcommand>(StringComparer.OrdinalIgnoreCase)
{
{ "role", ScmdRole },
{ "channel", ScmdChannel },
{ "modrole", ScmdModRole },
{ "message", ScmdAnnounceMsg },
{ "messagepl", ScmdAnnounceMsg },
{ "ping", ScmdPing },
{ "zone", ScmdZone },
{ "block", ScmdBlock },
{ "unblock", ScmdBlock },
{ "moderated", ScmdModerated }
};
// Set up local copy of all user commands accessible by the override command
_usercommands = new Dictionary<string, CommandHandler>(StringComparer.OrdinalIgnoreCase);
foreach (var item in userCommands) _usercommands.Add(item.Item1, item.Item2);
}
public override IEnumerable<(string, CommandHandler)> Commands
=> new List<(string, CommandHandler)>()
{
("config", CmdConfigDispatch),
("override", CmdOverride),
("test", CmdTest)
};
#region Documentation
public static readonly CommandDocumentation DocOverride =
new CommandDocumentation(new string[] { "override (user ping or ID) (command w/ parameters)" },
"Perform certain commands on behalf of another user.", null);
#endregion
private async Task CmdConfigDispatch(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
{
// Ignore those without the proper permissions.
if (!gconf.IsBotModerator(reqUser))
{
await reqChannel.SendMessageAsync(":x: This command may only be used by bot moderators.");
return;
}
if (param.Length < 2)
{
await reqChannel.SendMessageAsync($":x: See `{CommandPrefix}help-config` for information on how to use this command.");
return;
}
// Special case: Restrict 'modrole' to only guild managers, not mods
if (string.Equals(param[1], "modrole", StringComparison.OrdinalIgnoreCase) && !reqUser.GuildPermissions.ManageGuild)
{
await reqChannel.SendMessageAsync(":x: This command may only be used by those with the `Manage Server` permission.");
return;
}
// Subcommands get a subset of the parameters, to make things a little easier.
var confparam = new string[param.Length - 1];
Array.Copy(param, 1, confparam, 0, param.Length - 1);
if (_subcommands.TryGetValue(confparam[0], out ConfigSubcommand h))
{
await h(confparam, gconf, reqChannel);
}
}
#region Configuration sub-commands
// Birthday role set
private async Task ScmdRole(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
{
if (param.Length != 2)
{
await reqChannel.SendMessageAsync(":x: A role name, role mention, or ID value must be specified.");
return;
}
var guild = reqChannel.Guild;
var role = FindUserInputRole(param[1], guild);
if (role == null)
{
await reqChannel.SendMessageAsync(RoleInputError);
}
else if (role.Id == reqChannel.Guild.EveryoneRole.Id)
{
await reqChannel.SendMessageAsync(":x: You cannot set that as the birthday role.");
}
else
{
gconf.RoleId = role.Id;
await gconf.UpdateAsync();
await reqChannel.SendMessageAsync($":white_check_mark: The birthday role has been set as **{role.Name}**.");
}
}
// Ping setting
private async Task ScmdPing(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
{
const string InputErr = ":x: You must specify either `off` or `on` in this setting.";
if (param.Length != 2)
{
await reqChannel.SendMessageAsync(InputErr);
return;
}
var input = param[1].ToLower();
bool setting;
string result;
if (input == "off")
{
setting = false;
result = ":white_check_mark: Announcement pings are now **off**.";
}
else if (input == "on")
{
setting = true;
result = ":white_check_mark: Announcement pings are now **on**.";
}
else
{
await reqChannel.SendMessageAsync(InputErr);
return;
}
gconf.AnnouncePing = setting;
await gconf.UpdateAsync();
await reqChannel.SendMessageAsync(result);
}
// Announcement channel set
private async Task ScmdChannel(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
{
if (param.Length == 1) // No extra parameter. Unset announcement channel.
{
// Extra detail: Show a unique message if a channel hadn't been set prior.
if (!gconf.AnnounceChannelId.HasValue)
{
await reqChannel.SendMessageAsync(":x: There is no announcement channel set. Nothing to unset.");
return;
}
gconf.AnnounceChannelId = null;
await gconf.UpdateAsync();
await reqChannel.SendMessageAsync(":white_check_mark: The announcement channel has been unset.");
}
else
{
// Determine channel from input
ulong chId = 0;
// Try channel mention
var m = ChannelMention.Match(param[1]);
if (m.Success)
{
chId = ulong.Parse(m.Groups[1].Value);
}
else if (ulong.TryParse(param[1], out chId))
{
// Continue...
}
else
{
// Try text-based search
var res = reqChannel.Guild.TextChannels
.FirstOrDefault(ch => string.Equals(ch.Name, param[1], StringComparison.OrdinalIgnoreCase));
if (res != null)
{
chId = res.Id; // Yep, we're throwing the full result away only to go look for it again later...
}
}
// Attempt to find channel in guild
SocketTextChannel chTt = null;
if (chId != 0) chTt = reqChannel.Guild.GetTextChannel(chId);
if (chTt == null)
{
await reqChannel.SendMessageAsync(":x: Unable to find the specified channel.");
return;
}
// Update the value
gconf.AnnounceChannelId = chId;
await gconf.UpdateAsync();
// Report the success
await reqChannel.SendMessageAsync($":white_check_mark: The announcement channel is now set to <#{chId}>.");
}
}
// Moderator role set
private async Task ScmdModRole(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
{
if (param.Length != 2)
{
await reqChannel.SendMessageAsync(":x: A role name, role mention, or ID value must be specified.");
return;
}
var guild = reqChannel.Guild;
var role = FindUserInputRole(param[1], guild);
if (role == null)
{
await reqChannel.SendMessageAsync(RoleInputError);
}
else
{
gconf.ModeratorRole = role.Id;
await gconf.UpdateAsync();
await reqChannel.SendMessageAsync($":white_check_mark: The moderator role is now **{role.Name}**.");
}
}
// Guild default time zone set/unset
private async Task ScmdZone(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
{
if (param.Length == 1) // No extra parameter. Unset guild default time zone.
{
// Extra detail: Show a unique message if there is no set zone.
if (!gconf.AnnounceChannelId.HasValue)
{
await reqChannel.SendMessageAsync(":x: A default zone is not set. Nothing to unset.");
return;
}
gconf.TimeZone = null;
await gconf.UpdateAsync();
await reqChannel.SendMessageAsync(":white_check_mark: The default time zone preference has been removed.");
}
else
{
// Parameter check.
string zone;
try
{
zone = ParseTimeZone(param[1]);
}
catch (FormatException ex)
{
reqChannel.SendMessageAsync(ex.Message).Wait();
return;
}
// Update value
gconf.TimeZone = zone;
await gconf.UpdateAsync();
// Report the success
await reqChannel.SendMessageAsync($":white_check_mark: The server's time zone has been set to **{zone}**.");
}
}
// Block/unblock individual non-manager users from using commands.
private async Task ScmdBlock(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
{
if (param.Length != 2)
{
await reqChannel.SendMessageAsync(ParameterError + ConfErrorPostfix);
return;
}
bool doBan = param[0].ToLower() == "block"; // true = block, false = unblock
if (!TryGetUserId(param[1], out ulong inputId))
{
await reqChannel.SendMessageAsync(BadUserError);
return;
}
var isBanned = await gconf.IsUserBlockedAsync(inputId);
if (doBan)
{
if (!isBanned)
{
await gconf.BlockUserAsync(inputId);
await reqChannel.SendMessageAsync(":white_check_mark: User has been blocked.");
}
else
{
// TODO bug: this is incorrectly always displayed when in moderated mode
await reqChannel.SendMessageAsync(":white_check_mark: User is already blocked.");
}
}
else
{
if (await gconf.UnblockUserAsync(inputId))
{
await reqChannel.SendMessageAsync(":white_check_mark: User is now unblocked.");
}
else
{
await reqChannel.SendMessageAsync(":white_check_mark: The specified user is not blocked.");
}
}
}
// "moderated on/off" - Sets/unsets moderated mode.
private async Task ScmdModerated(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
{
if (param.Length != 2)
{
await reqChannel.SendMessageAsync(ParameterError + ConfErrorPostfix);
return;
}
var parameter = param[1].ToLower();
bool modSet;
if (parameter == "on") modSet = true;
else if (parameter == "off") modSet = false;
else
{
await reqChannel.SendMessageAsync(":x: Expecting `on` or `off` as a parameter." + ConfErrorPostfix);
return;
}
if (gconf.IsModerated == modSet)
{
await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode is already {parameter}.");
}
else
{
gconf.IsModerated = modSet;
await gconf.UpdateAsync();
await reqChannel.SendMessageAsync($":white_check_mark: Moderated mode has been turned {parameter}.");
}
}
// Sets/unsets custom announcement message.
private async Task ScmdAnnounceMsg(string[] param, GuildConfiguration gconf, SocketTextChannel reqChannel)
{
var plural = param[0].ToLower().EndsWith("pl");
string newmsg;
bool clear;
if (param.Length == 2)
{
newmsg = param[1];
clear = false;
}
else
{
newmsg = null;
clear = true;
}
(string, string) update;
if (!plural) update = (newmsg, gconf.AnnounceMessages.Item2);
else update = (gconf.AnnounceMessages.Item1, newmsg);
gconf.AnnounceMessages = update;
await gconf.UpdateAsync();
await reqChannel.SendMessageAsync(string.Format(":white_check_mark: The {0} birthday announcement message has been {1}.",
plural ? "plural" : "singular", clear ? "reset" : "updated"));
}
#endregion
// Execute command as another user
private async Task CmdOverride(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
{
// Moderators only. As with config, silently drop if this check fails.
if (!gconf.IsBotModerator(reqUser)) return;
if (param.Length != 3)
{
await reqChannel.SendMessageAsync(ParameterError, embed: DocOverride.UsageEmbed);
return;
}
// Second parameter: determine the user to act as
if (!TryGetUserId(param[1], out ulong user))
{
await reqChannel.SendMessageAsync(BadUserError, embed: DocOverride.UsageEmbed);
return;
}
var overuser = reqChannel.Guild.GetUser(user);
if (overuser == null)
{
await reqChannel.SendMessageAsync(BadUserError, embed: DocOverride.UsageEmbed);
return;
}
// Third parameter: determine command to invoke.
// Reminder that we're only receiving a param array of size 3 at maximum. String must be split again.
var overparam = param[2].Split(" ", 3, StringSplitOptions.RemoveEmptyEntries);
var cmdsearch = overparam[0];
if (cmdsearch.StartsWith(CommandPrefix))
{
// Strip command prefix to search for the given command.
cmdsearch = cmdsearch.Substring(CommandPrefix.Length);
}
else
{
// Add command prefix to input, just in case.
overparam[0] = CommandPrefix + overparam[0].ToLower();
}
if (!_usercommands.TryGetValue(cmdsearch, out CommandHandler action))
{
await reqChannel.SendMessageAsync($":x: `{cmdsearch}` is not an overridable command.", embed: DocOverride.UsageEmbed);
return;
}
// Preparations complete. Run the command.
await reqChannel.SendMessageAsync($"Executing `{cmdsearch.ToLower()}` on behalf of {overuser.Nickname ?? overuser.Username}:");
await action.Invoke(instance, gconf, overparam, reqChannel, overuser);
}
// Publicly available command that immediately processes the current guild,
private async Task CmdTest(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
{
// Moderators only. As with config, silently drop if this check fails.
if (!gconf.IsBotModerator(reqUser)) return;
if (param.Length != 1)
{
// Too many parameters
// Note: Non-standard error display
await reqChannel.SendMessageAsync(NoParameterError);
return;
}
// had an option to clear roles here, but application testing revealed that running the
// test at this point would make the updater assume that roles had not yet been cleared
// may revisit this later...
try
{
var result = await instance.ForceBirthdayUpdateAsync(reqChannel.Guild);
await reqChannel.SendMessageAsync(result);
}
catch (Exception ex)
{
Program.Log("Test command", ex.ToString());
reqChannel.SendMessageAsync(InternalError).Wait();
// TODO webhook report
}
}
#region Common/helper methods
private const string RoleInputError = ":x: Unable to determine the given role.";
private static readonly Regex RoleMention = new Regex(@"<@?&(?<snowflake>\d+)>", RegexOptions.Compiled);
private SocketRole FindUserInputRole(string inputStr, SocketGuild guild)
{
// Resembles a role mention? Strip it to the pure number
var input = inputStr;
var rmatch = RoleMention.Match(input);
if (rmatch.Success) input = rmatch.Groups["snowflake"].Value;
// Attempt to get role by ID, or null
if (ulong.TryParse(input, out ulong rid))
{
return guild.GetRole(rid);
}
else
{
// Reset the search value on the off chance there's a role name that actually resembles a role ping.
input = inputStr;
}
// If not already found, attempt to search role by string name
foreach (var search in guild.Roles)
{
if (string.Equals(search.Name, input, StringComparison.OrdinalIgnoreCase)) return search;
}
return null;
}
#endregion
}
}