Replace OperationStatus with test command

This commit is contained in:
Noi 2020-07-14 11:59:14 -07:00
parent fbbc675ab0
commit 0bd9b79e50
6 changed files with 109 additions and 157 deletions

View file

@ -53,69 +53,54 @@ namespace BirthdayBot.BackgroundServices
GC.Collect(); GC.Collect();
} }
public async Task SingleUpdateFor(SocketGuild guild) /// <summary>
{ /// Access to <see cref="ProcessGuildAsync(SocketGuild)"/> for the testing command.
try /// </summary>
{ /// <returns>Diagnostic data in string form.</returns>
await ProcessGuildAsync(guild); public async Task<string> SingleProcessGuildAsync(SocketGuild guild) => (await ProcessGuildAsync(guild)).Export();
}
catch (Exception ex)
{
Log("Encountered an error during guild processing:");
Log(ex.ToString());
}
// TODO metrics for role sets, unsets, announcements - and I mentioned this above too
}
/// <summary> /// <summary>
/// Main method where actual guild processing occurs. /// Main method where actual guild processing occurs.
/// </summary> /// </summary>
private async Task ProcessGuildAsync(SocketGuild guild) private async Task<PGDiagnostic> ProcessGuildAsync(SocketGuild guild)
{ {
var diag = new PGDiagnostic();
// Skip processing of guild if local info has not yet been loaded // Skip processing of guild if local info has not yet been loaded
if (!BotInstance.GuildCache.TryGetValue(guild.Id, out var gs)) return; if (!BotInstance.GuildCache.TryGetValue(guild.Id, out var gs))
{
diag.FetchCachedGuild = "Server information not yet loaded by the bot. Try again later.";
return diag;
}
diag.FetchCachedGuild = null;
// Check if role settings are correct before continuing with further processing // Check if role settings are correct before continuing with further processing
SocketRole role = null; SocketRole role = null;
if (gs.RoleId.HasValue) role = guild.GetRole(gs.RoleId.Value); if (gs.RoleId.HasValue) role = guild.GetRole(gs.RoleId.Value);
var roleCheck = CheckCorrectRoleSettings(guild, role); diag.RoleCheck = CheckCorrectRoleSettings(guild, role);
if (!roleCheck.Item1) if (diag.RoleCheck != null) return diag;
{
lock (gs)
gs.OperationLog = new OperationStatus((OperationStatus.OperationType.UpdateBirthdayRoleMembership, roleCheck.Item2));
return;
}
// Determine who's currently having a birthday // Determine who's currently having a birthday
var users = gs.Users; var users = gs.Users;
var tz = gs.TimeZone; var tz = gs.TimeZone;
var birthdays = GetGuildCurrentBirthdays(users, tz); var birthdays = GetGuildCurrentBirthdays(users, tz);
// Note: Don't quit here if zero people are having birthdays. Roles may still need to be removed by BirthdayApply. // Note: Don't quit here if zero people are having birthdays. Roles may still need to be removed by BirthdayApply.
diag.CurrentBirthdays = birthdays.Count.ToString();
IEnumerable<SocketGuildUser> announcementList; IEnumerable<SocketGuildUser> announcementList;
(int, int) roleResult; // role additions, removals
// Update roles as appropriate // Update roles as appropriate
try try
{ {
var updateResult = await UpdateGuildBirthdayRoles(guild, role, birthdays); var updateResult = await UpdateGuildBirthdayRoles(guild, role, birthdays);
announcementList = updateResult.Item1; announcementList = updateResult.Item1;
roleResult = updateResult.Item2; diag.RoleApplyResult = updateResult.Item2; // statistics
} }
catch (Discord.Net.HttpException ex) catch (Discord.Net.HttpException ex)
{ {
lock (gs) diag.RoleApply = ex.Message;
gs.OperationLog = new OperationStatus((OperationStatus.OperationType.UpdateBirthdayRoleMembership, ex.Message)); return diag;
if (ex.HttpCode != System.Net.HttpStatusCode.Forbidden)
{
// Send unusual exceptions to caller
throw;
}
return;
} }
(OperationStatus.OperationType, string) opResult1, opResult2; diag.RoleApply = null;
opResult1 = (OperationStatus.OperationType.UpdateBirthdayRoleMembership,
$"Success: Added {roleResult.Item1} member(s), Removed {roleResult.Item2} member(s) from target role.");
// Birthday announcement // Birthday announcement
var announce = gs.AnnounceMessages; var announce = gs.AnnounceMessages;
@ -124,41 +109,36 @@ namespace BirthdayBot.BackgroundServices
if (gs.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gs.AnnounceChannelId.Value); if (gs.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gs.AnnounceChannelId.Value);
if (announcementList.Count() != 0) if (announcementList.Count() != 0)
{ {
var announceOpResult = await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList); var announceResult = await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList);
opResult2 = (OperationStatus.OperationType.SendBirthdayAnnouncementMessage, announceOpResult); diag.Announcement = announceResult;
} }
else else
{ {
opResult2 = (OperationStatus.OperationType.SendBirthdayAnnouncementMessage, "Announcement not considered."); diag.Announcement = "No new role additions. Announcement not needed.";
} }
// Update status return diag;
lock (gs) gs.OperationLog = new OperationStatus(opResult1, opResult2);
} }
/// <summary> /// <summary>
/// Checks if the bot may be allowed to alter roles. /// Checks if the bot may be allowed to alter roles.
/// </summary> /// </summary>
/// <returns> private string CheckCorrectRoleSettings(SocketGuild guild, SocketRole role)
/// First item: Boolean value determining if the role setup is correct.
/// Second item: String to append to operation status in case of failure.
/// </returns>
private (bool, string) CheckCorrectRoleSettings(SocketGuild guild, SocketRole role)
{ {
if (role == null) return (false, "Failed: Designated role not found or defined."); if (role == null) return "Designated role is not set, or target role cannot be found.";
if (!guild.CurrentUser.GuildPermissions.ManageRoles) if (!guild.CurrentUser.GuildPermissions.ManageRoles)
{ {
return (false, "Failed: Bot does not contain Manage Roles permission."); return "Bot does not have the 'Manage Roles' permission.";
} }
// Check potential role order conflict // Check potential role order conflict
if (role.Position >= guild.CurrentUser.Hierarchy) if (role.Position >= guild.CurrentUser.Hierarchy)
{ {
return (false, "Failed: Bot is beneath the designated role in the role hierarchy."); return "Bot is unable to access the designated role due to permission hierarchy.";
} }
return (true, null); return null;
} }
/// <summary> /// <summary>
@ -253,7 +233,7 @@ namespace BirthdayBot.BackgroundServices
private async Task<string> AnnounceBirthdaysAsync( private async Task<string> AnnounceBirthdaysAsync(
(string, string) announce, bool announcePing, SocketTextChannel c, IEnumerable<SocketGuildUser> names) (string, string) announce, bool announcePing, SocketTextChannel c, IEnumerable<SocketGuildUser> names)
{ {
if (c == null) return "Announcement channel is undefined."; if (c == null) return "Announcement channel is not set, or previous announcement channel has been deleted.";
string announceMsg; string announceMsg;
if (names.Count() == 1) announceMsg = announce.Item1 ?? announce.Item2 ?? DefaultAnnounce; if (names.Count() == 1) announceMsg = announce.Item1 ?? announce.Item2 ?? DefaultAnnounce;
@ -278,7 +258,7 @@ namespace BirthdayBot.BackgroundServices
try try
{ {
await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString())); await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString()));
return $"Successfully announced {names.Count()} name(s)"; return null;
} }
catch (Discord.Net.HttpException ex) catch (Discord.Net.HttpException ex)
{ {
@ -286,5 +266,33 @@ namespace BirthdayBot.BackgroundServices
return ex.Message; return ex.Message;
} }
} }
private class PGDiagnostic
{
const string DefaultValue = "--";
public string FetchCachedGuild = DefaultValue;
public string RoleCheck = DefaultValue;
public string CurrentBirthdays = DefaultValue;
public string RoleApply = DefaultValue;
public (int, int)? RoleApplyResult;
public string Announcement = DefaultValue;
public string Export()
{
var result = new StringBuilder();
result.AppendLine("Test result:");
result.AppendLine("Fetch guild information: " + (FetchCachedGuild ?? ":white_check_mark:"));
result.AppendLine("Check role permissions: " + (RoleCheck ?? ":white_check_mark:"));
result.AppendLine("Number of known users currently with a birthday: " + CurrentBirthdays);
result.AppendLine("Role application process: " + (RoleApply ?? ":white_check_mark:"));
result.Append("Role application metrics: ");
if (RoleApplyResult.HasValue) result.AppendLine($"{RoleApplyResult.Value.Item1} additions, {RoleApplyResult.Value.Item2} removals.");
else result.AppendLine(DefaultValue);
result.AppendLine("Announcement: " + (Announcement ?? ":white_check_mark:"));
return result.ToString();
}
}
} }
} }

View file

@ -45,7 +45,7 @@ namespace BirthdayBot
foreach (var item in _cmdsListing.Commands) _dispatchCommands.Add(item.Item1, item.Item2); foreach (var item in _cmdsListing.Commands) _dispatchCommands.Add(item.Item1, item.Item2);
_cmdsHelp = new HelpInfoCommands(this, conf); _cmdsHelp = new HelpInfoCommands(this, conf);
foreach (var item in _cmdsHelp.Commands) _dispatchCommands.Add(item.Item1, item.Item2); foreach (var item in _cmdsHelp.Commands) _dispatchCommands.Add(item.Item1, item.Item2);
_cmdsMods = new ManagerCommands(this, conf, _cmdsUser.Commands); _cmdsMods = new ManagerCommands(this, conf, _cmdsUser.Commands, _worker.BirthdayUpdater.SingleProcessGuildAsync);
foreach (var item in _cmdsMods.Commands) _dispatchCommands.Add(item.Item1, item.Item2); foreach (var item in _cmdsMods.Commands) _dispatchCommands.Add(item.Item1, item.Item2);
// Register event handlers // Register event handlers
@ -91,9 +91,25 @@ namespace BirthdayBot
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task SetStatus(DiscordSocketClient shard) private async Task SetStatus(DiscordSocketClient shard) => await shard.SetGameAsync(CommandPrefix + "help");
public async Task PushErrorLog(string source, string message)
{ {
await shard.SetGameAsync(CommandPrefix + "help"); // Attempt to report instance logging failure to the reporting channel
try
{
EmbedBuilder e = new EmbedBuilder()
{
Footer = new EmbedFooterBuilder() { Text = source },
Timestamp = DateTimeOffset.UtcNow,
Description = message
};
await LogWebhook.SendMessageAsync(embeds: new Embed[] { e.Build() });
}
catch
{
return; // Give up
}
} }
private async Task Dispatch(SocketMessage msg) private async Task Dispatch(SocketMessage msg)
@ -144,10 +160,6 @@ namespace BirthdayBot
// Fail silently. // Fail silently.
} }
} }
// Immediately check for role updates in the invoking guild
// TODO be smarter about when to call this
await _worker.BirthdayUpdater.SingleUpdateFor(channel.Guild);
} }
} }
} }

View file

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<Version>2.1.0</Version> <Version>2.2.0</Version>
<PackageId>BirthdayBot</PackageId> <PackageId>BirthdayBot</PackageId>
<Authors>NoiTheCat</Authors> <Authors>NoiTheCat</Authors>
<Product>BirthdayBot</Product> <Product>BirthdayBot</Product>

View file

@ -27,7 +27,6 @@ namespace BirthdayBot.Data
private readonly Dictionary<ulong, GuildUserSettings> _userCache; private readonly Dictionary<ulong, GuildUserSettings> _userCache;
public ulong GuildId { get; } public ulong GuildId { get; }
public OperationStatus OperationLog { get; set; }
/// <summary> /// <summary>
/// Gets a list of cached registered user information. /// Gets a list of cached registered user information.
@ -83,8 +82,6 @@ namespace BirthdayBot.Data
{ {
_db = dbconfig; _db = dbconfig;
OperationLog = new OperationStatus();
GuildId = (ulong)reader.GetInt64(0); GuildId = (ulong)reader.GetInt64(0);
if (!reader.IsDBNull(1)) if (!reader.IsDBNull(1))
{ {

View file

@ -1,64 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BirthdayBot.Data
{
/// <summary>
/// Holds information regarding the previous updating information done on a guild including success/error information.
/// </summary>
class OperationStatus
{
private readonly Dictionary<OperationType, string> _log = new Dictionary<OperationType, string>();
public DateTimeOffset Timestamp { get; }
public OperationStatus (params (OperationType, string)[] statuses)
{
Timestamp = DateTimeOffset.UtcNow;
foreach (var status in statuses)
{
_log[status.Item1] = status.Item2;
}
}
/// <summary>
/// Prepares known information in a displayable format.
/// </summary>
public string GetDiagStrings()
{
var report = new StringBuilder();
foreach (OperationType otype in Enum.GetValues(typeof(OperationType)))
{
var prefix = $"`{Enum.GetName(typeof(OperationType), otype)}`: ";
string info = null;
if (!_log.TryGetValue(otype, out info))
{
report.AppendLine(prefix + "No data");
continue;
}
if (info == null)
{
report.AppendLine(prefix + "Success");
}
else
{
report.AppendLine(prefix + info);
}
}
return report.ToString();
}
/// <summary>
/// Specifies the type of operation logged. These enum values are publicly displayed in the specified order.
/// </summary>
public enum OperationType
{
UpdateBirthdayRoleMembership,
SendBirthdayAnnouncementMessage
}
}
}

View file

@ -1,7 +1,4 @@
using BirthdayBot.BackgroundServices; using Discord.WebSocket;
using Discord;
using Discord.WebSocket;
using NodaTime;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -18,8 +15,10 @@ namespace BirthdayBot.UserInterface
private readonly Dictionary<string, ConfigSubcommand> _subcommands; private readonly Dictionary<string, ConfigSubcommand> _subcommands;
private readonly Dictionary<string, CommandHandler> _usercommands; private readonly Dictionary<string, CommandHandler> _usercommands;
private readonly Func<SocketGuild, Task<string>> _bRoleUpdAccess;
public ManagerCommands(BirthdayBot inst, Configuration db, IEnumerable<(string, CommandHandler)> userCommands) public ManagerCommands(BirthdayBot inst, Configuration db,
IEnumerable<(string, CommandHandler)> userCommands, Func<SocketGuild, Task<string>> brsingleupdate)
: base(inst, db) : base(inst, db)
{ {
_subcommands = new Dictionary<string, ConfigSubcommand>(StringComparer.OrdinalIgnoreCase) _subcommands = new Dictionary<string, ConfigSubcommand>(StringComparer.OrdinalIgnoreCase)
@ -39,6 +38,9 @@ namespace BirthdayBot.UserInterface
// Set up local copy of all user commands accessible by the override command // Set up local copy of all user commands accessible by the override command
_usercommands = new Dictionary<string, CommandHandler>(StringComparer.OrdinalIgnoreCase); _usercommands = new Dictionary<string, CommandHandler>(StringComparer.OrdinalIgnoreCase);
foreach (var item in userCommands) _usercommands.Add(item.Item1, item.Item2); foreach (var item in userCommands) _usercommands.Add(item.Item1, item.Item2);
// and access to the otherwise automated guild update function
_bRoleUpdAccess = brsingleupdate;
} }
public override IEnumerable<(string, CommandHandler)> Commands public override IEnumerable<(string, CommandHandler)> Commands
@ -46,7 +48,7 @@ namespace BirthdayBot.UserInterface
{ {
("config", CmdConfigDispatch), ("config", CmdConfigDispatch),
("override", CmdOverride), ("override", CmdOverride),
("status", CmdStatus) ("test", CmdTest)
}; };
#region Documentation #region Documentation
@ -426,39 +428,36 @@ namespace BirthdayBot.UserInterface
await action.Invoke(overparam, reqChannel, overuser); await action.Invoke(overparam, reqChannel, overuser);
} }
// Prints a status report useful for troubleshooting operational issues within a guild // Publicly available command that immediately processes the current guild,
private async Task CmdStatus(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser) private async Task CmdTest(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
{ {
// Moderators only. As with config, silently drop if this check fails. // Moderators only. As with config, silently drop if this check fails.
if (!Instance.GuildCache[reqUser.Guild.Id].IsUserModerator(reqUser)) return; if (!Instance.GuildCache[reqUser.Guild.Id].IsUserModerator(reqUser)) return;
// TODO fix this or incorporate into final output - checking existence in guild cache is a step in the process
DateTimeOffset optime; if (param.Length != 1)
string optext;
string zone;
var gi = Instance.GuildCache[reqChannel.Guild.Id];
lock (gi)
{ {
var opstat = gi.OperationLog; // Too many parameters
optext = opstat.GetDiagStrings(); // !!! Bulk of output handled by this method // Note: Non-standard error display
optime = opstat.Timestamp; await reqChannel.SendMessageAsync(NoParameterError);
zone = gi.TimeZone ?? "UTC"; return;
} }
var shard = Instance.DiscordClient.GetShardIdFor(reqChannel.Guild);
// Calculate timestamp in current zone // had an option to clear roles here, but application testing revealed that running the
var zonedTimeInstant = SystemClock.Instance.GetCurrentInstant().InZone(DateTimeZoneProviders.Tzdb.GetZoneOrNull(zone)); // test at this point would make the updater assume that roles had not yet been cleared
var timeAgoEstimate = DateTimeOffset.UtcNow - optime; // may revisit this later...
var result = new EmbedBuilder try
{ {
Title = "Background operation status", var result = await _bRoleUpdAccess(reqChannel.Guild);
Description = $"Shard: {shard}\n" await reqChannel.SendMessageAsync(result);
+ $"Operation time: {Math.Round(timeAgoEstimate.TotalSeconds)} second(s) ago at {zonedTimeInstant}\n" }
+ "Report:\n" catch (Exception ex)
+ optext.TrimEnd() {
}; Program.Log("Test command", ex.ToString());
reqChannel.SendMessageAsync(InternalError).Wait();
await reqChannel.SendMessageAsync(embed: result.Build()); // TODO webhook report
}
} }
#region Common/helper methods #region Common/helper methods