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();
}
public async Task SingleUpdateFor(SocketGuild guild)
{
try
{
await ProcessGuildAsync(guild);
}
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>
/// Access to <see cref="ProcessGuildAsync(SocketGuild)"/> for the testing command.
/// </summary>
/// <returns>Diagnostic data in string form.</returns>
public async Task<string> SingleProcessGuildAsync(SocketGuild guild) => (await ProcessGuildAsync(guild)).Export();
/// <summary>
/// Main method where actual guild processing occurs.
/// </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
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
SocketRole role = null;
if (gs.RoleId.HasValue) role = guild.GetRole(gs.RoleId.Value);
var roleCheck = CheckCorrectRoleSettings(guild, role);
if (!roleCheck.Item1)
{
lock (gs)
gs.OperationLog = new OperationStatus((OperationStatus.OperationType.UpdateBirthdayRoleMembership, roleCheck.Item2));
return;
}
diag.RoleCheck = CheckCorrectRoleSettings(guild, role);
if (diag.RoleCheck != null) return diag;
// Determine who's currently having a birthday
var users = gs.Users;
var tz = gs.TimeZone;
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.
diag.CurrentBirthdays = birthdays.Count.ToString();
IEnumerable<SocketGuildUser> announcementList;
(int, int) roleResult; // role additions, removals
// Update roles as appropriate
try
{
var updateResult = await UpdateGuildBirthdayRoles(guild, role, birthdays);
announcementList = updateResult.Item1;
roleResult = updateResult.Item2;
diag.RoleApplyResult = updateResult.Item2; // statistics
}
catch (Discord.Net.HttpException ex)
{
lock (gs)
gs.OperationLog = new OperationStatus((OperationStatus.OperationType.UpdateBirthdayRoleMembership, ex.Message));
if (ex.HttpCode != System.Net.HttpStatusCode.Forbidden)
{
// Send unusual exceptions to caller
throw;
}
return;
diag.RoleApply = ex.Message;
return diag;
}
(OperationStatus.OperationType, string) opResult1, opResult2;
opResult1 = (OperationStatus.OperationType.UpdateBirthdayRoleMembership,
$"Success: Added {roleResult.Item1} member(s), Removed {roleResult.Item2} member(s) from target role.");
diag.RoleApply = null;
// Birthday announcement
var announce = gs.AnnounceMessages;
@ -124,41 +109,36 @@ namespace BirthdayBot.BackgroundServices
if (gs.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gs.AnnounceChannelId.Value);
if (announcementList.Count() != 0)
{
var announceOpResult = await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList);
opResult2 = (OperationStatus.OperationType.SendBirthdayAnnouncementMessage, announceOpResult);
var announceResult = await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList);
diag.Announcement = announceResult;
}
else
{
opResult2 = (OperationStatus.OperationType.SendBirthdayAnnouncementMessage, "Announcement not considered.");
diag.Announcement = "No new role additions. Announcement not needed.";
}
// Update status
lock (gs) gs.OperationLog = new OperationStatus(opResult1, opResult2);
return diag;
}
/// <summary>
/// Checks if the bot may be allowed to alter roles.
/// </summary>
/// <returns>
/// 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)
private 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)
{
return (false, "Failed: Bot does not contain Manage Roles permission.");
return "Bot does not have the 'Manage Roles' permission.";
}
// Check potential role order conflict
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>
@ -253,7 +233,7 @@ namespace BirthdayBot.BackgroundServices
private async Task<string> AnnounceBirthdaysAsync(
(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;
if (names.Count() == 1) announceMsg = announce.Item1 ?? announce.Item2 ?? DefaultAnnounce;
@ -278,7 +258,7 @@ namespace BirthdayBot.BackgroundServices
try
{
await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString()));
return $"Successfully announced {names.Count()} name(s)";
return null;
}
catch (Discord.Net.HttpException ex)
{
@ -286,5 +266,33 @@ namespace BirthdayBot.BackgroundServices
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);
_cmdsHelp = new HelpInfoCommands(this, conf);
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);
// Register event handlers
@ -91,9 +91,25 @@ namespace BirthdayBot
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)
@ -144,10 +160,6 @@ namespace BirthdayBot
// 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>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Version>2.1.0</Version>
<Version>2.2.0</Version>
<PackageId>BirthdayBot</PackageId>
<Authors>NoiTheCat</Authors>
<Product>BirthdayBot</Product>

View file

@ -27,7 +27,6 @@ namespace BirthdayBot.Data
private readonly Dictionary<ulong, GuildUserSettings> _userCache;
public ulong GuildId { get; }
public OperationStatus OperationLog { get; set; }
/// <summary>
/// Gets a list of cached registered user information.
@ -83,8 +82,6 @@ namespace BirthdayBot.Data
{
_db = dbconfig;
OperationLog = new OperationStatus();
GuildId = (ulong)reader.GetInt64(0);
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;
using Discord.WebSocket;
using NodaTime;
using Discord.WebSocket;
using System;
using System.Collections.Generic;
using System.Linq;
@ -18,8 +15,10 @@ namespace BirthdayBot.UserInterface
private readonly Dictionary<string, ConfigSubcommand> _subcommands;
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)
{
_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
_usercommands = new Dictionary<string, CommandHandler>(StringComparer.OrdinalIgnoreCase);
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
@ -46,7 +48,7 @@ namespace BirthdayBot.UserInterface
{
("config", CmdConfigDispatch),
("override", CmdOverride),
("status", CmdStatus)
("test", CmdTest)
};
#region Documentation
@ -426,39 +428,36 @@ namespace BirthdayBot.UserInterface
await action.Invoke(overparam, reqChannel, overuser);
}
// Prints a status report useful for troubleshooting operational issues within a guild
private async Task CmdStatus(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
// Publicly available command that immediately processes the current guild,
private async Task CmdTest(string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
{
// Moderators only. As with config, silently drop if this check fails.
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;
string optext;
string zone;
var gi = Instance.GuildCache[reqChannel.Guild.Id];
lock (gi)
if (param.Length != 1)
{
var opstat = gi.OperationLog;
optext = opstat.GetDiagStrings(); // !!! Bulk of output handled by this method
optime = opstat.Timestamp;
zone = gi.TimeZone ?? "UTC";
// Too many parameters
// Note: Non-standard error display
await reqChannel.SendMessageAsync(NoParameterError);
return;
}
var shard = Instance.DiscordClient.GetShardIdFor(reqChannel.Guild);
// Calculate timestamp in current zone
var zonedTimeInstant = SystemClock.Instance.GetCurrentInstant().InZone(DateTimeZoneProviders.Tzdb.GetZoneOrNull(zone));
var timeAgoEstimate = DateTimeOffset.UtcNow - optime;
// 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...
var result = new EmbedBuilder
try
{
Title = "Background operation status",
Description = $"Shard: {shard}\n"
+ $"Operation time: {Math.Round(timeAgoEstimate.TotalSeconds)} second(s) ago at {zonedTimeInstant}\n"
+ "Report:\n"
+ optext.TrimEnd()
};
await reqChannel.SendMessageAsync(embed: result.Build());
var result = await _bRoleUpdAccess(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