From 11377770924abbe6873bc77e29b4b36a428e9e1c Mon Sep 17 00:00:00 2001 From: Noi Date: Wed, 3 Jun 2020 18:58:03 -0700 Subject: [PATCH] Improved user-facing error messages Also moved bb.when handler to a different class --- UserInterface/CommandsCommon.cs | 8 ++- UserInterface/HelpInfoCommands.cs | 21 +++---- UserInterface/ListingCommands.cs | 77 +++++++++++++++++++++++-- UserInterface/UserCommands.cs | 96 +++++++++---------------------- 4 files changed, 112 insertions(+), 90 deletions(-) diff --git a/UserInterface/CommandsCommon.cs b/UserInterface/CommandsCommon.cs index 2f218e6..69059d2 100644 --- a/UserInterface/CommandsCommon.cs +++ b/UserInterface/CommandsCommon.cs @@ -20,7 +20,10 @@ namespace BirthdayBot.UserInterface [Obsolete] public const string GenericError = ":x: Invalid usage. Consult the help command."; public const string BadUserError = ":x: Unable to find user. Specify their `@` mention or their ID."; - public const string ExpectedNoParametersError = ":x: This command does not take parameters. Did you mean to use another?"; + public const string ParameterError = ":x: Incorrect number of parameters. Be sure you have not added spaces" + + " where the bot is not expecting them or that all required information has been provided."; + public const string NoParameterError = ":x: This command does not accept parameters."; + public const string InternalError = ":x: An internal bot error occurred. The bot maintainer has been notified of the issue."; public delegate Task CommandHandler(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser); @@ -68,7 +71,8 @@ namespace BirthdayBot.UserInterface // Just check if the input exists in the map. Get the "true" value, or reject it altogether. if (!TzNameMap.TryGetValue(tzinput, out tz)) { - throw new FormatException(":x: Unknown or invalid time zone name."); + throw new FormatException(":x: Unexpected time zone name." + + $" Refer to `{CommandPrefix}help-tzdata` to help determine the correct value."); } } return tz; diff --git a/UserInterface/HelpInfoCommands.cs b/UserInterface/HelpInfoCommands.cs index b10f9e6..b8c22d3 100644 --- a/UserInterface/HelpInfoCommands.cs +++ b/UserInterface/HelpInfoCommands.cs @@ -37,25 +37,18 @@ namespace BirthdayBot.UserInterface Name = "Commands", Value = $"{cpfx}help`, `{CommandPrefix}info`, `{CommandPrefix}help-tzdata`\n" + $" » Help and informational messages.\n" - + $"{cpfx}recent` and `{CommandPrefix}upcoming`\n" - + $" » Lists recent and upcoming birthdays.\n" - + $"{cpfx}set (date) [zone]`\n" - + $" » Registers your birth date. Time zone is optional.\n" - + $" »» Examples: `{CommandPrefix}set jan-31`, `{CommandPrefix}set 15-aug America/Los_Angeles`.\n" - + $"{cpfx}zone (zone)`\n" - + $" » Sets your local time zone. See `{CommandPrefix}help-tzdata`.\n" - + $"{cpfx}remove`\n" - + $" » Removes your birthday information from this bot.\n" - + $"{cpfx}when (user)`\n" - + $" » Displays birthday information of the given user." + + ListingCommands.DocUpcoming.Export() + "\n" + + UserCommands.DocSet.Export() + "\n" + + UserCommands.DocZone.Export() + "\n" + + UserCommands.DocRemove.Export() + "\n" + + ListingCommands.DocWhen.Export() }; var cmdModField = new EmbedFieldBuilder() { - Name = "Commands", + Name = "Moderator actions", Value = $"{cpfx}config`\n" + $" » Edit bot configuration. See `{CommandPrefix}help-config`.\n" - + $"{cpfx}list`\n" - + $" » Exports all birthdays to file. Accepts `csv` as a parameter.\n" + + ListingCommands.DocList.Export() + "\n" + $"{cpfx}override (user ping or ID) (command w/ parameters)`\n" + " » Perform certain commands on behalf of another user." }; diff --git a/UserInterface/ListingCommands.cs b/UserInterface/ListingCommands.cs index ebb3573..ec0738e 100644 --- a/UserInterface/ListingCommands.cs +++ b/UserInterface/ListingCommands.cs @@ -21,15 +21,84 @@ namespace BirthdayBot.UserInterface { ("list", CmdList), ("upcoming", CmdUpcoming), - ("recent", CmdUpcoming) + ("recent", CmdUpcoming), + ("when", CmdWhen) }; + #region Documentation + public static readonly CommandDocumentation DocList = + new CommandDocumentation(new string[] { "list" }, "Exports all birthdays to a file." + + " Accepts `csv` as an optional parameter.", null); + public static readonly CommandDocumentation DocUpcoming = + new CommandDocumentation(new string[] { "recent", "upcoming" }, "Lists recent and upcoming birthdays.", null); + public static readonly CommandDocumentation DocWhen = + new CommandDocumentation(new string[] { "when" }, "Displays the given user's birthday information.", null); + #endregion + + private async Task CmdWhen(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) + { + // Requires a parameter + if (param.Length == 1) + { + await reqChannel.SendMessageAsync(ParameterError, embed: DocWhen.UsageEmbed); + return; + } + + var search = param[1]; + if (param.Length == 3) + { + // param maxes out at 3 values. param[2] might contain part of the search string (if name has a space) + search += " " + param[2]; + } + + SocketGuildUser searchTarget = null; + + ulong searchId = 0; + if (!TryGetUserId(search, out searchId)) // ID lookup + { + // name lookup without discriminator + foreach (var searchuser in reqChannel.Guild.Users) + { + if (string.Equals(search, searchuser.Username, StringComparison.OrdinalIgnoreCase)) + { + searchTarget = searchuser; + break; + } + } + } + else + { + searchTarget = reqChannel.Guild.GetUser(searchId); + } + if (searchTarget == null) + { + await reqChannel.SendMessageAsync(BadUserError, embed: DocWhen.UsageEmbed); + return; + } + + var users = Instance.GuildCache[reqChannel.Guild.Id].Users; + var searchTargetData = users.FirstOrDefault(u => u.UserId == searchTarget.Id); + if (searchTargetData == null) + { + await reqChannel.SendMessageAsync("I do not have birthday information for that user."); + return; + } + + string result = Common.FormatName(searchTarget, false); + result += ": "; + result += $"`{searchTargetData.BirthDay:00}-{Common.MonthNames[searchTargetData.BirthMonth]}`"; + result += searchTargetData.TimeZone == null ? "" : $" - `{searchTargetData.TimeZone}`"; + + await reqChannel.SendMessageAsync(result); + } + // Creates a file with all birthdays. private async Task CmdList(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) { // For now, we're restricting this command to moderators only. This may turn into an option later. if (!Instance.GuildCache[reqChannel.Guild.Id].IsUserModerator(reqUser)) { + // Do not add detailed usage information to this error message. await reqChannel.SendMessageAsync(":x: Only bot moderators may use this command."); return; } @@ -41,13 +110,13 @@ namespace BirthdayBot.UserInterface if (param[1].ToLower() == "csv") useCsv = true; else { - await reqChannel.SendMessageAsync(":x: That is not available as an export format."); + await reqChannel.SendMessageAsync(":x: That is not available as an export format.", embed: DocList.UsageEmbed); return; } } else if (param.Length > 2) { - await reqChannel.SendMessageAsync(GenericError); + await reqChannel.SendMessageAsync(ParameterError, embed: DocList.UsageEmbed); return; } @@ -78,7 +147,7 @@ namespace BirthdayBot.UserInterface catch (Exception ex) { Program.Log("Listing", ex.ToString()); - reqChannel.SendMessageAsync(":x: An internal error occurred. It has been reported to the bot owner.").Wait(); + reqChannel.SendMessageAsync(InternalError).Wait(); // TODO webhook report } finally diff --git a/UserInterface/UserCommands.cs b/UserInterface/UserCommands.cs index 076e0a1..0753774 100644 --- a/UserInterface/UserCommands.cs +++ b/UserInterface/UserCommands.cs @@ -16,8 +16,7 @@ namespace BirthdayBot.UserInterface { ("set", CmdSet), ("zone", CmdZone), - ("remove", CmdRemove), - ("when", CmdWhen) + ("remove", CmdRemove) }; /// @@ -27,6 +26,8 @@ namespace BirthdayBot.UserInterface /// Thrown for any parsing issue. Reason is expected to be sent to Discord as-is. private (int, int) ParseDate(string dateInput) { + const string FormatError = ":x: Incorrect date format. Use a three-letter abbreviation and a number separated by " + + "hyphen to specify a date. Examples: `jan-15` `23-aug` `may-12` `5-jun`"; // Not doing DateTime.Parse. Setting it up is rather complicated, and it's probably case sensitive. // Admittedly, doing it the way it's being done here probably isn't any better. var m = Regex.Match(dateInput, @"^(?\d{1,2})-(?[A-Za-z]{3})$"); @@ -34,7 +35,7 @@ namespace BirthdayBot.UserInterface { // Flip the fields around, try again m = Regex.Match(dateInput, @"^(?[A-Za-z]{3})-(?\d{1,2})$"); - if (!m.Success) throw new FormatException(GenericError); + if (!m.Success) throw new FormatException(FormatError); } int day; try @@ -43,7 +44,7 @@ namespace BirthdayBot.UserInterface } catch (FormatException) { - throw new Exception(GenericError); + throw new Exception(FormatError); } var monthVal = m.Groups["month"].Value; int month; @@ -99,12 +100,23 @@ namespace BirthdayBot.UserInterface return (month, day); } + #region Documentation + public static readonly CommandDocumentation DocSet = + new CommandDocumentation(new string[] { "set (date) [zone]" }, "Registers your birth date. Time zone is optional.", + $"`{CommandPrefix}set jan-31`, `{CommandPrefix}set 15-aug America/Los_Angeles`."); + public static readonly CommandDocumentation DocZone = + new CommandDocumentation(new string[] { "zone (zone)" }, "Sets your local time zone. " + + $"See also `{CommandPrefix}help-tzdata`.", null); + public static readonly CommandDocumentation DocRemove = + new CommandDocumentation(new string[] { "remove" }, "Removes your birthday information from this bot.", null); + #endregion + private async Task CmdSet(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) { // Requires one parameter. Optionally two. if (param.Length < 2 || param.Length > 3) { - await reqChannel.SendMessageAsync(GenericError); + await reqChannel.SendMessageAsync(ParameterError, embed: DocSet.UsageEmbed); return; } @@ -120,7 +132,7 @@ namespace BirthdayBot.UserInterface catch (FormatException ex) { // Our parse methods' FormatException has its message to send out to Discord. - reqChannel.SendMessageAsync(ex.Message).Wait(); + reqChannel.SendMessageAsync(ex.Message, embed: DocSet.UsageEmbed).Wait(); return; } @@ -135,8 +147,8 @@ namespace BirthdayBot.UserInterface catch (Exception ex) { Program.Log("Error", ex.ToString()); - reqChannel.SendMessageAsync(":x: An unknown error occurred. The bot owner has been notified.").Wait(); // TODO webhook report + reqChannel.SendMessageAsync(InternalError).Wait(); return; } if (known) @@ -153,25 +165,26 @@ namespace BirthdayBot.UserInterface { if (param.Length != 2) { - await reqChannel.SendMessageAsync(GenericError); + await reqChannel.SendMessageAsync(ParameterError, embed: DocZone.UsageEmbed); return; } - string btz = null; var user = Instance.GuildCache[reqChannel.Guild.Id].GetUser(reqUser.Id); if (!user.IsKnown) { - await reqChannel.SendMessageAsync(":x: Can't set your time zone if your birth date isn't registered."); + await reqChannel.SendMessageAsync(":x: You may only update your time zone when you have a birthday registered." + + $" Refer to the `{CommandPrefix}set` command.", embed: DocZone.UsageEmbed); return; } + string btz; try { btz = ParseTimeZone(param[1]); } catch (Exception ex) { - reqChannel.SendMessageAsync(ex.Message).Wait(); + reqChannel.SendMessageAsync(ex.Message, embed: DocZone.UsageEmbed).Wait(); return; } await user.UpdateAsync(user.BirthMonth, user.BirthDay, btz, BotConfig.DatabaseSettings); @@ -184,7 +197,7 @@ namespace BirthdayBot.UserInterface // Parameter count check if (param.Length != 1) { - await reqChannel.SendMessageAsync(ExpectedNoParametersError); + await reqChannel.SendMessageAsync(NoParameterError, embed: DocRemove.UsageEmbed); return; } @@ -196,69 +209,12 @@ namespace BirthdayBot.UserInterface await g.DeleteUserAsync(reqUser.Id); if (!known) { - await reqChannel.SendMessageAsync(":white_check_mark: I don't have your information. Nothing to remove."); + await reqChannel.SendMessageAsync(":white_check_mark: This bot already does not contain your information."); } else { await reqChannel.SendMessageAsync(":white_check_mark: Your information has been removed."); } } - - private async Task CmdWhen(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) - { - // Requires a parameter - if (param.Length == 1) - { - await reqChannel.SendMessageAsync(GenericError); - return; - } - - var search = param[1]; - if (param.Length == 3) - { - // param maxes out at 3 values. param[2] might contain part of the search string (if name has a space) - search += " " + param[2]; - } - - SocketGuildUser searchTarget = null; - - ulong searchId = 0; - if (!TryGetUserId(search, out searchId)) // ID lookup - { - // name lookup without discriminator - foreach (var searchuser in reqChannel.Guild.Users) - { - if (string.Equals(search, searchuser.Username, StringComparison.OrdinalIgnoreCase)) - { - searchTarget = searchuser; - break; - } - } - } - else - { - searchTarget = reqChannel.Guild.GetUser(searchId); - } - if (searchTarget == null) - { - await reqChannel.SendMessageAsync(BadUserError); - return; - } - - var users = Instance.GuildCache[reqChannel.Guild.Id].Users; - var searchTargetData = users.FirstOrDefault(u => u.UserId == searchTarget.Id); - if (searchTargetData == null) - { - await reqChannel.SendMessageAsync("The given user does not exist or has not set a birthday."); - return; - } - - string result = Common.FormatName(searchTarget, false); - result += ": "; - result += $"`{searchTargetData.BirthDay:00}-{Common.MonthNames[searchTargetData.BirthMonth]}`"; - result += searchTargetData.TimeZone == null ? "" : $" - `{searchTargetData.TimeZone}`"; - - await reqChannel.SendMessageAsync(result); - } } }