Various changes

-Remove webhook logging references
--Including config line. Idea is abandoned for now.
-Remove unneeded comments
-Remove diagnostic messages
--Corresponding problems were solved by moving to dedicated hardware
-Update style on all affected files
This commit is contained in:
Noi 2021-12-05 18:58:10 -08:00
parent 3741222c68
commit 578f2545f2
9 changed files with 298 additions and 385 deletions

View file

@ -119,7 +119,6 @@ class BirthdayRoleUpdate : BackgroundService {
else roleKeeps.Add(member.Id); else roleKeeps.Add(member.Id);
} }
// TODO Can we remove during the iteration instead of after? investigate later...
foreach (var user in roleRemoves) { foreach (var user in roleRemoves) {
await user.RemoveRoleAsync(r).ConfigureAwait(false); await user.RemoveRoleAsync(r).ConfigureAwait(false);
} }

View file

@ -64,7 +64,6 @@ class ShardBackgroundWorker : IDisposable {
_tickCount++; _tickCount++;
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false); await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
} catch (Exception ex) when (ex is not TaskCanceledException) { } catch (Exception ex) when (ex is not TaskCanceledException) {
// TODO webhook log
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} encountered an exception:\n" + ex.ToString()); Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} encountered an exception:\n" + ex.ToString());
} }
} }

View file

@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>3.2.3</Version> <Version>3.2.4</Version>
<Authors>NoiTheCat</Authors> <Authors>NoiTheCat</Authors>
</PropertyGroup> </PropertyGroup>
@ -25,7 +25,7 @@
<PackageReference Include="Discord.Net" Version="3.0.0-dev-20210822.1" /> <PackageReference Include="Discord.Net" Version="3.0.0-dev-20210822.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NodaTime" Version="3.0.9" /> <PackageReference Include="NodaTime" Version="3.0.9" />
<PackageReference Include="Npgsql" Version="6.0.0" /> <PackageReference Include="Npgsql" Version="6.0.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,13 +1,11 @@
using BirthdayBot.Data; using BirthdayBot.Data;
using Newtonsoft.Json.Linq;
using Npgsql;
using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using CommandLine; using CommandLine;
using CommandLine.Text; using CommandLine.Text;
using Newtonsoft.Json.Linq;
using Npgsql;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.RegularExpressions;
namespace BirthdayBot; namespace BirthdayBot;
@ -22,7 +20,6 @@ class Configuration {
const string KeyShardRange = "ShardRange"; const string KeyShardRange = "ShardRange";
public string BotToken { get; } public string BotToken { get; }
public string LogWebhook { get; }
public string? DBotsToken { get; } public string? DBotsToken { get; }
public bool QuitOnFails { get; } public bool QuitOnFails { get; }
@ -41,7 +38,6 @@ class Configuration {
var jc = JObject.Parse(File.ReadAllText(confPath)); var jc = JObject.Parse(File.ReadAllText(confPath));
BotToken = ReadConfKey<string>(jc, nameof(BotToken), true); BotToken = ReadConfKey<string>(jc, nameof(BotToken), true);
LogWebhook = ReadConfKey<string>(jc, nameof(LogWebhook), true);
DBotsToken = ReadConfKey<string>(jc, nameof(DBotsToken), false); DBotsToken = ReadConfKey<string>(jc, nameof(DBotsToken), false);
QuitOnFails = ReadConfKey<bool?>(jc, nameof(QuitOnFails), false) ?? false; QuitOnFails = ReadConfKey<bool?>(jc, nameof(QuitOnFails), false) ?? false;

View file

@ -50,41 +50,27 @@ class ShardInstance : IDisposable {
} }
/// <summary> /// <summary>
/// Does all necessary steps to stop this shard. This method may block for a few seconds as it waits /// Does all necessary steps to stop this shard, including canceling background tasks and disconnecting.
/// for the process to finish, but will force its disposal after at most 30 seconds.
/// </summary> /// </summary>
public void Dispose() { public void Dispose() {
// Unsubscribe from own events
DiscordClient.Log -= Client_Log; DiscordClient.Log -= Client_Log;
DiscordClient.Ready -= Client_Ready; DiscordClient.Ready -= Client_Ready;
DiscordClient.MessageReceived -= Client_MessageReceived; DiscordClient.MessageReceived -= Client_MessageReceived;
_background.Dispose(); _background.Dispose();
try { DiscordClient.LogoutAsync().Wait(5000);
if (!DiscordClient.LogoutAsync().Wait(15000)) DiscordClient.StopAsync().Wait(5000);
Log("Instance", "Warning: Client has not yet logged out. Continuing cleanup."); DiscordClient.Dispose();
} catch (Exception ex) { Log(nameof(ShardInstance), "Shard instance disposed.");
Log("Instance", "Warning: Client threw an exception when logging out: " + ex.Message);
}
try {
if (!DiscordClient.StopAsync().Wait(5000))
Log("Instance", "Warning: Client has not yet stopped. Continuing cleanup.");
} catch (Exception ex) {
Log("Instance", "Warning: Client threw an exception when stopping: " + ex.Message);
}
var clientDispose = Task.Run(DiscordClient.Dispose);
if (!clientDispose.Wait(10000)) Log("Instance", "Warning: Client is hanging on dispose. Will continue.");
else Log("Instance", "Shard instance disposed.");
} }
public void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message); public void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message);
#region Event handling #region Event handling
private Task Client_Log(LogMessage arg) { private Task Client_Log(LogMessage arg) {
// TODO revise this some time, filters might need to be modified by now
// Suppress certain messages // Suppress certain messages
if (arg.Message != null) { if (arg.Message != null) {
// TODO remove below line ideally when D.Net bug is fixed
if (arg.Message.StartsWith("Unknown Dispatch ") || arg.Message.StartsWith("Unknown Channel")) return Task.CompletedTask; if (arg.Message.StartsWith("Unknown Dispatch ") || arg.Message.StartsWith("Unknown Channel")) return Task.CompletedTask;
switch (arg.Message) // Connection status messages replaced by ShardManager's output switch (arg.Message) // Connection status messages replaced by ShardManager's output
{ {
@ -92,23 +78,14 @@ class ShardInstance : IDisposable {
case "Connected": case "Connected":
case "Ready": case "Ready":
case "Failed to resume previous session": case "Failed to resume previous session":
case "Resumed previous session": case "Discord.WebSocket.GatewayReconnectException: Server requested a reconnect":
case "Disconnecting":
case "Disconnected":
case "WebSocket connection was closed":
case "Server requested a reconnect":
return Task.CompletedTask; return Task.CompletedTask;
} }
//if (arg.Message == "Heartbeat Errored") {
// // Replace this with a custom message; do not show stack trace
// Log("Discord.Net", $"{arg.Severity}: {arg.Message} - {arg.Exception.Message}");
// return Task.CompletedTask;
//}
Log("Discord.Net", $"{arg.Severity}: {arg.Message}"); Log("Discord.Net", $"{arg.Severity}: {arg.Message}");
} }
if (arg.Exception != null) Log("Discord.Net", arg.Exception.ToString()); if (arg.Exception != null) Log("Discord.Net exception", arg.Exception.ToString());
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -152,8 +129,7 @@ class ShardInstance : IDisposable {
if (ex is HttpException) return; if (ex is HttpException) return;
Log("Command", ex.ToString()); Log("Command", ex.ToString());
try { try {
channel.SendMessageAsync(":x: An unknown error occurred. It has been reported to the bot owner.").Wait(); channel.SendMessageAsync(UserInterface.CommandsCommon.InternalError).Wait();
// TODO webhook report
} catch (HttpException) { } // Fail silently } catch (HttpException) { } // Fail silently
} }
} }

View file

@ -17,7 +17,7 @@ internal abstract class CommandsCommon {
public const string BadUserError = ":x: Unable to find user. Specify their `@` mention or their ID."; public const string BadUserError = ":x: Unable to find user. Specify their `@` mention or their ID.";
public const string ParameterError = ":x: Invalid usage. Refer to how to use the command and try again."; public const string ParameterError = ":x: Invalid usage. Refer to how to use the command and try again.";
public const string NoParameterError = ":x: This command does not accept any parameters."; public const string NoParameterError = ":x: This command does not accept any parameters.";
public const string InternalError = ":x: An internal bot error occurred. The bot maintainer has been notified of the issue."; public const string InternalError = ":x: An unknown error occurred. If it persists, please notify the bot owner.";
public const string MemberCacheEmptyError = ":warning: Please try the command again."; public const string MemberCacheEmptyError = ":warning: Please try the command again.";
public delegate Task CommandHandler(ShardInstance instance, GuildConfiguration gconf, public delegate Task CommandHandler(ShardInstance instance, GuildConfiguration gconf,

View file

@ -1,26 +1,20 @@
using BirthdayBot.Data; using BirthdayBot.Data;
using Discord;
using Discord.WebSocket;
using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace BirthdayBot.UserInterface namespace BirthdayBot.UserInterface;
{
internal class HelpInfoCommands : CommandsCommon
{
private readonly Embed _helpEmbed;
private readonly Embed _helpConfigEmbed;
public HelpInfoCommands(Configuration cfg) : base(cfg) internal class HelpInfoCommands : CommandsCommon {
{ private readonly Embed _helpEmbed;
var embeds = BuildHelpEmbeds(); private readonly Embed _helpConfigEmbed;
_helpEmbed = embeds.Item1;
_helpConfigEmbed = embeds.Item2;
}
public override IEnumerable<(string, CommandHandler)> Commands => public HelpInfoCommands(Configuration cfg) : base(cfg) {
new List<(string, CommandHandler)>() { var embeds = BuildHelpEmbeds();
_helpEmbed = embeds.Item1;
_helpConfigEmbed = embeds.Item2;
}
public override IEnumerable<(string, CommandHandler)> Commands =>
new List<(string, CommandHandler)>() {
("help", CmdHelp), ("help", CmdHelp),
("help-config", CmdHelpConfig), ("help-config", CmdHelpConfig),
("helpconfig", CmdHelpConfig), ("helpconfig", CmdHelpConfig),
@ -31,155 +25,137 @@ namespace BirthdayBot.UserInterface
("info", CmdInfo), ("info", CmdInfo),
("about", CmdInfo), ("about", CmdInfo),
("invite", CmdInfo) ("invite", CmdInfo)
}; };
private static (Embed, Embed) BuildHelpEmbeds() private static (Embed, Embed) BuildHelpEmbeds() {
{ var cpfx = $"●`{CommandPrefix}";
var cpfx = $"●`{CommandPrefix}";
// Normal section // Normal section
var cmdField = new EmbedFieldBuilder() var cmdField = new EmbedFieldBuilder() {
{ Name = "Commands",
Name = "Commands", Value = $"{cpfx}help`, `{CommandPrefix}info`, `{CommandPrefix}help-tzdata`\n"
Value = $"{cpfx}help`, `{CommandPrefix}info`, `{CommandPrefix}help-tzdata`\n" + $" » Help and informational messages.\n"
+ $" » Help and informational messages.\n" + ListingCommands.DocUpcoming.Export() + "\n"
+ ListingCommands.DocUpcoming.Export() + "\n" + UserCommands.DocSet.Export() + "\n"
+ UserCommands.DocSet.Export() + "\n" + UserCommands.DocZone.Export() + "\n"
+ UserCommands.DocZone.Export() + "\n" + UserCommands.DocRemove.Export() + "\n"
+ UserCommands.DocRemove.Export() + "\n" + ListingCommands.DocWhen.Export()
+ ListingCommands.DocWhen.Export() };
}; var cmdModField = new EmbedFieldBuilder() {
var cmdModField = new EmbedFieldBuilder() Name = "Moderator actions",
{ Value = $"{cpfx}config`\n"
Name = "Moderator actions", + $" » Edit bot configuration. See `{CommandPrefix}help-config`.\n"
Value = $"{cpfx}config`\n" + ListingCommands.DocList.Export() + "\n"
+ $" » Edit bot configuration. See `{CommandPrefix}help-config`.\n" + ManagerCommands.DocOverride.Export()
+ ListingCommands.DocList.Export() + "\n" };
+ ManagerCommands.DocOverride.Export() var helpRegular = new EmbedBuilder().AddField(cmdField).AddField(cmdModField);
};
var helpRegular = new EmbedBuilder().AddField(cmdField).AddField(cmdModField);
// Manager section // Manager section
var mpfx = cpfx + "config "; var mpfx = cpfx + "config ";
var configField1 = new EmbedFieldBuilder() var configField1 = new EmbedFieldBuilder() {
{ Name = "Basic settings",
Name = "Basic settings", Value = $"{mpfx}role (role name or ID)`\n"
Value = $"{mpfx}role (role name or ID)`\n" + " » Sets the role to apply to users having birthdays.\n"
+ " » Sets the role to apply to users having birthdays.\n" + $"{mpfx}channel (channel name or ID)`\n"
+ $"{mpfx}channel (channel name or ID)`\n" + " » Sets the announcement channel. Leave blank to disable.\n"
+ " » Sets the announcement channel. Leave blank to disable.\n" + $"{mpfx}message (message)`, `{CommandPrefix}config messagepl (message)`\n"
+ $"{mpfx}message (message)`, `{CommandPrefix}config messagepl (message)`\n" + $" » Sets a custom announcement message. See `{CommandPrefix}help-message`.\n"
+ $" » Sets a custom announcement message. See `{CommandPrefix}help-message`.\n" + $"{mpfx}ping (off|on)`\n"
+ $"{mpfx}ping (off|on)`\n" + $" » Sets whether to ping the respective users in the announcement message.\n"
+ $" » Sets whether to ping the respective users in the announcement message.\n" + $"{mpfx}zone (time zone name)`\n"
+ $"{mpfx}zone (time zone name)`\n" + $" » Sets the default server time zone. See `{CommandPrefix}help-tzdata`."
+ $" » Sets the default server time zone. See `{CommandPrefix}help-tzdata`." };
}; var configField2 = new EmbedFieldBuilder() {
var configField2 = new EmbedFieldBuilder() Name = "Access management",
{ Value = $"{mpfx}modrole (role name, role ping, or ID)`\n"
Name = "Access management", + " » Establishes a role for bot moderators. Grants access to `bb.config` and `bb.override`.\n"
Value = $"{mpfx}modrole (role name, role ping, or ID)`\n" + $"{mpfx}block/unblock (user ping or ID)`\n"
+ " » Establishes a role for bot moderators. Grants access to `bb.config` and `bb.override`.\n" + " » Prevents or allows usage of bot commands to the given user.\n"
+ $"{mpfx}block/unblock (user ping or ID)`\n" + $"{mpfx}moderated on/off`\n"
+ " » Prevents or allows usage of bot commands to the given user.\n" + " » Prevents or allows using commands for all members excluding moderators."
+ $"{mpfx}moderated on/off`\n" };
+ " » Prevents or allows using commands for all members excluding moderators."
};
var helpConfig = new EmbedBuilder() var helpConfig = new EmbedBuilder() {
{ Author = new EmbedAuthorBuilder() { Name = $"{CommandPrefix} config subcommands" },
Author = new EmbedAuthorBuilder() { Name = $"{CommandPrefix} config subcommands" }, Description = "All the following subcommands are only usable by moderators and server managers."
Description = "All the following subcommands are only usable by moderators and server managers." }.AddField(configField1).AddField(configField2);
}.AddField(configField1).AddField(configField2);
return (helpRegular.Build(), helpConfig.Build()); return (helpRegular.Build(), helpConfig.Build());
} }
private async Task CmdHelp(ShardInstance instance, GuildConfiguration gconf, private async Task CmdHelp(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
=> await reqChannel.SendMessageAsync(embed: _helpEmbed).ConfigureAwait(false); => await reqChannel.SendMessageAsync(embed: _helpEmbed).ConfigureAwait(false);
private async Task CmdHelpConfig(ShardInstance instance, GuildConfiguration gconf, private async Task CmdHelpConfig(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
=> await reqChannel.SendMessageAsync(embed: _helpConfigEmbed).ConfigureAwait(false); => await reqChannel.SendMessageAsync(embed: _helpConfigEmbed).ConfigureAwait(false);
private async Task CmdHelpTzdata(ShardInstance instance, GuildConfiguration gconf, private async Task CmdHelpTzdata(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
{ const string tzhelp = "You may specify a time zone in order to have your birthday recognized with respect to your local time. "
const string tzhelp = "You may specify a time zone in order to have your birthday recognized with respect to your local time. " + "This bot only accepts zone names from the IANA Time Zone Database (a.k.a. Olson Database).\n\n"
+ "This bot only accepts zone names from the IANA Time Zone Database (a.k.a. Olson Database).\n\n" + "To find your zone: https://xske.github.io/tz/" + "\n"
+ "To find your zone: https://xske.github.io/tz/" + "\n" + "Interactive map: https://kevinnovak.github.io/Time-Zone-Picker/" + "\n"
+ "Interactive map: https://kevinnovak.github.io/Time-Zone-Picker/" + "\n" + "Complete list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones";
+ "Complete list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"; var embed = new EmbedBuilder();
var embed = new EmbedBuilder(); embed.AddField(new EmbedFieldBuilder() {
embed.AddField(new EmbedFieldBuilder() Name = "Time Zone Support",
{ Value = tzhelp
Name = "Time Zone Support", });
Value = tzhelp await reqChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
}); }
await reqChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
}
private async Task CmdHelpMessage(ShardInstance instance, GuildConfiguration gconf, private async Task CmdHelpMessage(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
{ const string msghelp = "The `message` and `messagepl` subcommands allow for editing the message sent into the announcement "
const string msghelp = "The `message` and `messagepl` subcommands allow for editing the message sent into the announcement " + "channel (defined with `{0}config channel`). This feature is separated across two commands:\n"
+ "channel (defined with `{0}config channel`). This feature is separated across two commands:\n" + "●`{0}config message`\n"
+ "●`{0}config message`\n" + "●`{0}config messagepl`\n"
+ "●`{0}config messagepl`\n" + "The first command sets the message to be displayed when *one* user is having a birthday. The second command sets the "
+ "The first command sets the message to be displayed when *one* user is having a birthday. The second command sets the " + "message for when *two or more* users are having birthdays ('pl' means plural). If only one of the two custom messages "
+ "message for when *two or more* users are having birthdays ('pl' means plural). If only one of the two custom messages " + "are defined, it will be used for both cases.\n\n"
+ "are defined, it will be used for both cases.\n\n" + "To further allow customization, you may place the token `%n` in your message to specify where the name(s) should appear.\n"
+ "To further allow customization, you may place the token `%n` in your message to specify where the name(s) should appear.\n" + "Leave the parameter blank to clear or reset the message to its default value.";
+ "Leave the parameter blank to clear or reset the message to its default value."; const string msghelp2 = "As examples, these are the default announcement messages used by this bot:\n"
const string msghelp2 = "As examples, these are the default announcement messages used by this bot:\n" + "`message`: {0}\n" + "`messagepl`: {1}";
+ "`message`: {0}\n" + "`messagepl`: {1}"; var embed = new EmbedBuilder().AddField(new EmbedFieldBuilder() {
var embed = new EmbedBuilder().AddField(new EmbedFieldBuilder() Name = "Custom announcement message",
{ Value = string.Format(msghelp, CommandPrefix)
Name = "Custom announcement message", }).AddField(new EmbedFieldBuilder() {
Value = string.Format(msghelp, CommandPrefix) Name = "Examples",
}).AddField(new EmbedFieldBuilder() Value = string.Format(msghelp2,
{ BackgroundServices.BirthdayRoleUpdate.DefaultAnnounce, BackgroundServices.BirthdayRoleUpdate.DefaultAnnouncePl)
Name = "Examples", });
Value = string.Format(msghelp2, await reqChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
BackgroundServices.BirthdayRoleUpdate.DefaultAnnounce, BackgroundServices.BirthdayRoleUpdate.DefaultAnnouncePl) }
});
await reqChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
}
private async Task CmdInfo(ShardInstance instance, GuildConfiguration gconf, private async Task CmdInfo(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
{ var strStats = new StringBuilder();
var strStats = new StringBuilder(); var asmnm = System.Reflection.Assembly.GetExecutingAssembly().GetName();
var asmnm = System.Reflection.Assembly.GetExecutingAssembly().GetName(); strStats.AppendLine("BirthdayBot v" + asmnm.Version!.ToString(3));
strStats.AppendLine("BirthdayBot v" + asmnm.Version.ToString(3)); //strStats.AppendLine("Server count: " + Discord.Guilds.Count.ToString()); // TODO restore this statistic
//strStats.AppendLine("Server count: " + Discord.Guilds.Count.ToString()); strStats.AppendLine("Shard #" + instance.ShardId.ToString("00"));
// TODO restore this statistic strStats.AppendLine("Uptime: " + Program.BotUptime);
strStats.AppendLine("Shard #" + instance.ShardId.ToString("00"));
strStats.AppendLine("Uptime: " + Program.BotUptime);
// TODO fun stats // TODO fun stats
// current birthdays, total names registered, unique time zones // current birthdays, total names registered, unique time zones
var embed = new EmbedBuilder() var embed = new EmbedBuilder() {
{ Author = new EmbedAuthorBuilder() {
Author = new EmbedAuthorBuilder() Name = "Thank you for using Birthday Bot!",
{ IconUrl = instance.DiscordClient.CurrentUser.GetAvatarUrl()
Name = "Thank you for using Birthday Bot!", },
IconUrl = instance.DiscordClient.CurrentUser.GetAvatarUrl() Description = "For more information regarding support, data retention, privacy, and other details, please refer to: "
}, + "https://github.com/NoiTheCat/BirthdayBot/blob/master/Readme.md" + "\n\n"
// TODO this message needs an overhaul + "This bot is provided for free, without any paywalled 'premium' features. "
Description = "For more information regarding support, data retention, privacy, and other details, please refer to: " + "If you've found this bot useful, please consider contributing via the "
+ "https://github.com/NoiTheCat/BirthdayBot/blob/master/Readme.md" + "\n\n" + "bot author's page on Ko-fi: https://ko-fi.com/noithecat."
+ "This bot is provided for free, without any paywalled 'premium' features. " }.AddField(new EmbedFieldBuilder() {
+ "If you've found this bot useful, please consider contributing via the " Name = "Statistics",
+ "bot author's page on Ko-fi: https://ko-fi.com/noithecat." Value = strStats.ToString()
}.AddField(new EmbedFieldBuilder() });
{ await reqChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
Name = "Statistics",
Value = strStats.ToString()
});
await reqChannel.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false);
}
} }
} }

View file

@ -129,7 +129,6 @@ internal class ListingCommands : CommandsCommon {
} catch (Exception ex) { } catch (Exception ex) {
Program.Log("Listing", ex.ToString()); Program.Log("Listing", ex.ToString());
reqChannel.SendMessageAsync(InternalError).Wait(); reqChannel.SendMessageAsync(InternalError).Wait();
// TODO webhook report
} finally { } finally {
File.Delete(filepath); File.Delete(filepath);
} }

View file

@ -1,211 +1,179 @@
using BirthdayBot.Data; using BirthdayBot.Data;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BirthdayBot.UserInterface namespace BirthdayBot.UserInterface;
{
internal class UserCommands : CommandsCommon
{
public UserCommands(Configuration db) : base(db) { }
public override IEnumerable<(string, CommandHandler)> Commands internal class UserCommands : CommandsCommon {
=> new List<(string, CommandHandler)>() public UserCommands(Configuration db) : base(db) { }
{
public override IEnumerable<(string, CommandHandler)> Commands
=> new List<(string, CommandHandler)>()
{
("set", CmdSet), ("set", CmdSet),
("zone", CmdZone), ("zone", CmdZone),
("remove", CmdRemove) ("remove", CmdRemove)
}; };
#region Date parsing #region Date parsing
const string FormatError = ":x: Unrecognized date format. The following formats are accepted, as examples: " const string FormatError = ":x: Unrecognized date format. The following formats are accepted, as examples: "
+ "`15-jan`, `jan-15`, `15 jan`, `jan 15`, `15 January`, `January 15`."; + "`15-jan`, `jan-15`, `15 jan`, `jan 15`, `15 January`, `January 15`.";
private static readonly Regex DateParse1 = new(@"^(?<day>\d{1,2})[ -](?<month>[A-Za-z]+)$", RegexOptions.Compiled); private static readonly Regex DateParse1 = new(@"^(?<day>\d{1,2})[ -](?<month>[A-Za-z]+)$", RegexOptions.Compiled);
private static readonly Regex DateParse2 = new(@"^(?<month>[A-Za-z]+)[ -](?<day>\d{1,2})$", RegexOptions.Compiled); private static readonly Regex DateParse2 = new(@"^(?<month>[A-Za-z]+)[ -](?<day>\d{1,2})$", RegexOptions.Compiled);
/// <summary> /// <summary>
/// Parses a date input. /// Parses a date input.
/// </summary> /// </summary>
/// <returns>Tuple: month, day</returns> /// <returns>Tuple: month, day</returns>
/// <exception cref="FormatException"> /// <exception cref="FormatException">
/// Thrown for any parsing issue. Reason is expected to be sent to Discord as-is. /// Thrown for any parsing issue. Reason is expected to be sent to Discord as-is.
/// </exception> /// </exception>
private static (int, int) ParseDate(string dateInput) private static (int, int) ParseDate(string dateInput) {
{ var m = DateParse1.Match(dateInput);
var m = DateParse1.Match(dateInput); if (!m.Success) {
if (!m.Success) // Flip the fields around, try again
{ m = DateParse2.Match(dateInput);
// Flip the fields around, try again if (!m.Success) throw new FormatException(FormatError);
m = DateParse2.Match(dateInput);
if (!m.Success) throw new FormatException(FormatError);
}
int day, month;
string monthVal;
try
{
day = int.Parse(m.Groups["day"].Value);
}
catch (FormatException)
{
throw new Exception(FormatError);
}
monthVal = m.Groups["month"].Value;
int dayUpper; // upper day of month check
(month, dayUpper) = GetMonth(monthVal);
if (day == 0 || day > dayUpper) throw new FormatException(":x: The date you specified is not a valid calendar date.");
return (month, day);
} }
/// <summary> int day, month;
/// Returns information for a given month input. string monthVal;
/// </summary> try {
/// <param name="input"></param> day = int.Parse(m.Groups["day"].Value);
/// <returns>Tuple: Month value, upper limit of days in the month</returns> } catch (FormatException) {
/// <exception cref="FormatException"> throw new Exception(FormatError);
/// Thrown on error. Send out to Discord as-is.
/// </exception>
private static (int, int) GetMonth(string input)
{
return input.ToLower() switch {
"jan" or "january" => (1, 31),
"feb" or "february" => (2, 29),
"mar" or "march" => (3, 31),
"apr" or "april" => (4, 30),
"may" => (5, 31),
"jun" or "june" => (6, 30),
"jul" or "july" => (7, 31),
"aug" or "august" => (8, 31),
"sep" or "september" => (9, 30),
"oct" or "october" => (10, 31),
"nov" or "november" => (11, 30),
"dec" or "december" => (12, 31),
_ => throw new FormatException($":x: Can't determine month name `{input}`. Check your spelling and try again."),
};
} }
#endregion monthVal = m.Groups["month"].Value;
#region Documentation int dayUpper; // upper day of month check
public static readonly CommandDocumentation DocSet = (month, dayUpper) = GetMonth(monthVal);
new(new string[] { "set (date)" }, "Registers your birth month and day.",
$"`{CommandPrefix}set jan-31`, `{CommandPrefix}set 15 may`.");
public static readonly CommandDocumentation DocZone =
new(new string[] { "zone (zone)" }, "Sets your local time zone. "
+ $"See also `{CommandPrefix}help-tzdata`.", null);
public static readonly CommandDocumentation DocRemove =
new(new string[] { "remove" }, "Removes your birthday information from this bot.", null);
#endregion
private async Task CmdSet(ShardInstance instance, GuildConfiguration gconf, if (day == 0 || day > dayUpper) throw new FormatException(":x: The date you specified is not a valid calendar date.");
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
{
if (param.Length < 2)
{
await reqChannel.SendMessageAsync(ParameterError, embed: DocSet.UsageEmbed).ConfigureAwait(false);
return;
}
// Date format accepts spaces. Must coalesce parameters to a single string. return (month, day);
var fullinput = ""; }
foreach (var p in param[1..]) fullinput += " " + p;
fullinput = fullinput[1..]; // trim leading space
int bmonth, bday; /// <summary>
try /// Returns information for a given month input.
{ /// </summary>
(bmonth, bday) = ParseDate(fullinput); /// <param name="input"></param>
} /// <returns>Tuple: Month value, upper limit of days in the month</returns>
catch (FormatException ex) /// <exception cref="FormatException">
{ /// Thrown on error. Send out to Discord as-is.
// Our parse method's FormatException has its message to send out to Discord. /// </exception>
reqChannel.SendMessageAsync(ex.Message, embed: DocSet.UsageEmbed).Wait(); private static (int, int) GetMonth(string input) {
return; return input.ToLower() switch {
} "jan" or "january" => (1, 31),
"feb" or "february" => (2, 29),
"mar" or "march" => (3, 31),
"apr" or "april" => (4, 30),
"may" => (5, 31),
"jun" or "june" => (6, 30),
"jul" or "july" => (7, 31),
"aug" or "august" => (8, 31),
"sep" or "september" => (9, 30),
"oct" or "october" => (10, 31),
"nov" or "november" => (11, 30),
"dec" or "december" => (12, 31),
_ => throw new FormatException($":x: Can't determine month name `{input}`. Check your spelling and try again."),
};
}
#endregion
// Parsing successful. Update user information. #region Documentation
bool known; // Extra detail: Bot's response changes if the user was previously unknown. public static readonly CommandDocumentation DocSet =
try new(new string[] { "set (date)" }, "Registers your birth month and day.",
{ $"`{CommandPrefix}set jan-31`, `{CommandPrefix}set 15 may`.");
var user = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id).ConfigureAwait(false); public static readonly CommandDocumentation DocZone =
known = user.IsKnown; new(new string[] { "zone (zone)" }, "Sets your local time zone. "
await user.UpdateAsync(bmonth, bday, user.TimeZone).ConfigureAwait(false); + $"See also `{CommandPrefix}help-tzdata`.", null);
} public static readonly CommandDocumentation DocRemove =
catch (Exception ex) new(new string[] { "remove" }, "Removes your birthday information from this bot.", null);
{ #endregion
Program.Log("Error", ex.ToString());
// TODO webhook report private async Task CmdSet(ShardInstance instance, GuildConfiguration gconf,
reqChannel.SendMessageAsync(InternalError).Wait(); string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
return; if (param.Length < 2) {
} await reqChannel.SendMessageAsync(ParameterError, embed: DocSet.UsageEmbed).ConfigureAwait(false);
await reqChannel.SendMessageAsync($":white_check_mark: Your birthday has been { (known ? "updated" : "recorded") }.") return;
.ConfigureAwait(false);
} }
private async Task CmdZone(ShardInstance instance, GuildConfiguration gconf, // Date format accepts spaces. Must coalesce parameters to a single string.
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) var fullinput = "";
{ foreach (var p in param[1..]) fullinput += " " + p;
if (param.Length != 2) fullinput = fullinput[1..]; // trim leading space
{
await reqChannel.SendMessageAsync(ParameterError, embed: DocZone.UsageEmbed).ConfigureAwait(false);
return;
}
int bmonth, bday;
try {
(bmonth, bday) = ParseDate(fullinput);
} catch (FormatException ex) {
// Our parse method's FormatException has its message to send out to Discord.
reqChannel.SendMessageAsync(ex.Message, embed: DocSet.UsageEmbed).Wait();
return;
}
// Parsing successful. Update user information.
bool known; // Extra detail: Bot's response changes if the user was previously unknown.
try {
var user = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id).ConfigureAwait(false); var user = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id).ConfigureAwait(false);
if (!user.IsKnown) known = user.IsKnown;
{ await user.UpdateAsync(bmonth, bday, user.TimeZone).ConfigureAwait(false);
await reqChannel.SendMessageAsync(":x: You may only update your time zone when you have a birthday registered." } catch (Exception ex) {
+ $" Refer to the `{CommandPrefix}set` command.", embed: DocZone.UsageEmbed) Program.Log("Error", ex.ToString());
.ConfigureAwait(false); reqChannel.SendMessageAsync(InternalError).Wait();
return; return;
} }
await reqChannel.SendMessageAsync($":white_check_mark: Your birthday has been { (known ? "updated" : "recorded") }.")
string btz;
try
{
btz = ParseTimeZone(param[1]);
}
catch (Exception ex)
{
reqChannel.SendMessageAsync(ex.Message, embed: DocZone.UsageEmbed).Wait();
return;
}
await user.UpdateAsync(user.BirthMonth, user.BirthDay, btz).ConfigureAwait(false);
await reqChannel.SendMessageAsync($":white_check_mark: Your time zone has been updated to **{btz}**.")
.ConfigureAwait(false); .ConfigureAwait(false);
}
private async Task CmdZone(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
if (param.Length != 2) {
await reqChannel.SendMessageAsync(ParameterError, embed: DocZone.UsageEmbed).ConfigureAwait(false);
return;
} }
private async Task CmdRemove(ShardInstance instance, GuildConfiguration gconf, var user = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id).ConfigureAwait(false);
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) if (!user.IsKnown) {
{ await reqChannel.SendMessageAsync(":x: You may only update your time zone when you have a birthday registered."
// Parameter count check + $" Refer to the `{CommandPrefix}set` command.", embed: DocZone.UsageEmbed)
if (param.Length != 1) .ConfigureAwait(false);
{ return;
await reqChannel.SendMessageAsync(NoParameterError, embed: DocRemove.UsageEmbed).ConfigureAwait(false); }
return;
}
// Extra detail: Send a notification if the user isn't actually known by the bot. string btz;
bool known; try {
var u = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id).ConfigureAwait(false); btz = ParseTimeZone(param[1]);
known = u.IsKnown; } catch (Exception ex) {
await u.DeleteAsync().ConfigureAwait(false); reqChannel.SendMessageAsync(ex.Message, embed: DocZone.UsageEmbed).Wait();
if (!known) return;
{ }
await reqChannel.SendMessageAsync(":white_check_mark: This bot already does not contain your information.") await user.UpdateAsync(user.BirthMonth, user.BirthDay, btz).ConfigureAwait(false);
.ConfigureAwait(false);
} await reqChannel.SendMessageAsync($":white_check_mark: Your time zone has been updated to **{btz}**.")
else .ConfigureAwait(false);
{ }
await reqChannel.SendMessageAsync(":white_check_mark: Your information has been removed.")
.ConfigureAwait(false); private async Task CmdRemove(ShardInstance instance, GuildConfiguration gconf,
} string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) {
// Parameter count check
if (param.Length != 1) {
await reqChannel.SendMessageAsync(NoParameterError, embed: DocRemove.UsageEmbed).ConfigureAwait(false);
return;
}
// Extra detail: Send a notification if the user isn't actually known by the bot.
bool known;
var u = await GuildUserConfiguration.LoadAsync(gconf.GuildId, reqUser.Id).ConfigureAwait(false);
known = u.IsKnown;
await u.DeleteAsync().ConfigureAwait(false);
if (!known) {
await reqChannel.SendMessageAsync(":white_check_mark: This bot already does not contain your information.")
.ConfigureAwait(false);
} else {
await reqChannel.SendMessageAsync(":white_check_mark: Your information has been removed.")
.ConfigureAwait(false);
} }
} }
} }