Merge pull request #38 from NoiTheCat/cleanup

Code cleanup

Removes various unneeded things from code and rewrites the shard status display.
This commit is contained in:
Noi 2022-11-22 23:42:27 -08:00 committed by GitHub
commit febfd27ece
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 65 additions and 157 deletions

View file

@ -132,7 +132,7 @@ public class BirthdayModule : BotModuleBase {
var query = GetSortedUserList(Context.Guild);
// TODO pagination instead of this workaround
bool hasOutputOneLine = false;
var hasOutputOneLine = false;
// First output is shown as an interaction response, followed then as regular channel messages
async Task doOutput(string msg) {
if (!hasOutputOneLine) {
@ -146,8 +146,7 @@ public class BirthdayModule : BotModuleBase {
var output = new StringBuilder();
var resultCount = 0;
output.AppendLine("Recent and upcoming birthdays:");
for (int count = 0; count <= 21; count++) // cover 21 days total (7 prior, current day, 14 upcoming)
{
for (var count = 0; count <= 21; count++) { // cover 21 days total (7 prior, current day, 14 upcoming)
var results = from item in query
where item.DateIndex == search
select item;

View file

@ -3,7 +3,6 @@ using Discord.Interactions;
using static BirthdayBot.Common;
namespace BirthdayBot.ApplicationCommands;
[RequireBotModerator]
[Group("override", HelpCmdOverride)]
public class BirthdayOverrideModule : BotModuleBase {

View file

@ -39,7 +39,7 @@ public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionCon
/// throwing a FormatException if the input is not recognized.
/// </summary>
protected static string ParseTimeZone(string tzinput) {
if (!TzNameMap.TryGetValue(tzinput, out string? tz))
if (!TzNameMap.TryGetValue(tzinput, out var tz))
throw new FormatException(":x: Unknown time zone name.\n" +
"To find your time zone, please refer to: https://kevinnovak.github.io/Time-Zone-Picker/");
return tz!;

View file

@ -97,8 +97,8 @@ public class ConfigModule : BotModuleBase {
internal static async Task CmdSetMessageResponse(SocketModal modal, SocketGuildChannel channel,
Dictionary<string, SocketMessageComponentData> data) {
string? newSingle = data[ModalComCidSingle].Value;
string? newMulti = data[ModalComCidMulti].Value;
var newSingle = data[ModalComCidSingle].Value;
var newMulti = data[ModalComCidMulti].Value;
if (string.IsNullOrWhiteSpace(newSingle)) newSingle = null;
if (string.IsNullOrWhiteSpace(newMulti)) newMulti = null;
@ -156,7 +156,7 @@ public class ConfigModule : BotModuleBase {
var existing = db.BlocklistEntries
.Where(bl => bl.GuildId == user.Guild.Id && bl.UserId == user.Id).FirstOrDefault();
bool already = (existing != null) == setting;
var already = (existing != null) == setting;
if (already) {
await RespondAsync($":white_check_mark: User is already {(setting ? "" : "not ")}blocked.").ConfigureAwait(false);
return;
@ -171,13 +171,13 @@ public class ConfigModule : BotModuleBase {
[SlashCommand("set-moderated", HelpPfxModOnly + "Set moderated mode on the server.")]
public async Task CmdSetModerated([Summary(name: "enable", description: "The moderated mode setting.")] bool setting) {
bool current = false;
var current = false;
await DoDatabaseUpdate(Context, s => {
current = s.Moderated;
s.Moderated = setting;
});
bool already = setting == current;
var already = setting == current;
if (already) {
await RespondAsync($":white_check_mark: Moderated mode is already **{(setting ? "en" : "dis")}abled**.");
} else {
@ -204,7 +204,7 @@ public class ConfigModule : BotModuleBase {
result.AppendLine($"Server time zone: `{ (guildconf.GuildTimeZone ?? "Not set - using UTC") }`");
result.AppendLine();
bool hasMembers = Common.HasMostMembersDownloaded(guild);
var hasMembers = Common.HasMostMembersDownloaded(guild);
result.Append(DoTestFor("Bot has obtained the user list", () => hasMembers));
result.AppendLine($" - Has `{guild.DownloadedMemberCount}` of `{guild.MemberCount}` members.");
int bdayCount = default;
@ -238,7 +238,7 @@ public class ConfigModule : BotModuleBase {
announcech = guild.GetTextChannel((ulong)(guildconf.AnnouncementChannel ?? 0));
return announcech != null;
}));
string disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>";
var disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>";
result.AppendLine(DoTestFor($"(Optional) Bot can send messages into { disp }", delegate {
if (announcech == null) return false;
return guild.CurrentUser.GetPermissions(announcech).SendMessages;
@ -251,7 +251,7 @@ public class ConfigModule : BotModuleBase {
const int announceMsgPreviewLimit = 350;
static string prepareAnnouncePreview(string announce) {
string trunc = announce.Length > announceMsgPreviewLimit ? announce[..announceMsgPreviewLimit] + "`(...)`" : announce;
var trunc = announce.Length > announceMsgPreviewLimit ? announce[..announceMsgPreviewLimit] + "`(...)`" : announce;
var result = new StringBuilder();
foreach (var line in trunc.Split('\n'))
result.AppendLine($"> {line}");

View file

@ -1,7 +1,6 @@
using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands;
public class HelpModule : BotModuleBase {
private const string TopMessage =
"Thank you for using Birthday Bot!\n" +
@ -33,12 +32,10 @@ public class HelpModule : BotModuleBase {
public async Task CmdHelp() {
const string DMWarn = "Please note that this bot works in servers only. " +
"The bot will not respond to any other commands within a DM.";
string ver =
#if DEBUG
"DEBUG flag set";
var ver = "DEBUG flag set";
#else
"v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
var ver = "v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
#endif
var result = new EmbedBuilder()
.WithAuthor("Help & About")
@ -48,6 +45,6 @@ public class HelpModule : BotModuleBase {
.AddField("Commands", RegularCommandsField)
.AddField("Moderator commands", ModCommandsField)
.Build();
await RespondAsync(text: (Context.Channel is IDMChannel ? DMWarn : null), embed: result).ConfigureAwait(false);
await RespondAsync(text: Context.Channel is IDMChannel ? DMWarn : null, embed: result).ConfigureAwait(false);
}
}

View file

@ -1,5 +1,4 @@
namespace BirthdayBot.ApplicationCommands;
/// <summary>
/// An instance-less class meant to handle incoming submitted modals.
/// </summary>
@ -28,7 +27,6 @@ static class ModalResponder {
await handler(arg, channel, data).ConfigureAwait(false);
} catch (Exception e) {
inst.Log(nameof(ModalResponder), $"Unhandled exception. {e}");
// TODO when implementing proper application error logging, see here
await arg.RespondAsync(ShardInstance.InternalError);
}
}

View file

@ -2,7 +2,6 @@
using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands;
/// <summary>
/// Only users not on the blocklist or affected by moderator mode may use the command.<br/>
/// This is used in the <see cref="BotModuleBase"/> base class. Manually using it anywhere else is unnecessary.

View file

@ -1,7 +1,6 @@
using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands;
/// <summary>
/// Implements the included precondition from Discord.Net, requiring a guild context while using our custom error message.<br/><br/>
/// Combining this with <see cref="RequireBotModeratorAttribute"/> is redundant. If possible, only use the latter instead.

View file

@ -1,5 +1,4 @@
namespace BirthdayBot.BackgroundServices;
abstract class BackgroundService {
protected static SemaphoreSlim DbConcurrentOperationsLock { get; } = new(ShardManager.MaxConcurrentOperations);
protected ShardInstance ShardInstance { get; }

View file

@ -111,7 +111,7 @@ class BirthdayRoleUpdate : BackgroundService {
private static async Task<IEnumerable<SocketGuildUser>> UpdateGuildBirthdayRoles(SocketGuild g, SocketRole r, HashSet<ulong> toApply) {
var additions = new List<SocketGuildUser>();
try {
var removals = new List<SocketGuildUser>(); // TODO check if roles can be removed in-place instead of building a list first
var removals = new List<SocketGuildUser>();
var no_ops = new HashSet<ulong>();
// Scan role for members no longer needing it

View file

@ -1,7 +1,6 @@
using System.Text;
namespace BirthdayBot.BackgroundServices;
/// <summary>
/// Reports user count statistics to external services on a shard by shard basis.
/// </summary>

View file

@ -1,7 +1,6 @@
using System.Text;
namespace BirthdayBot;
static class Common {
/// <summary>
/// Formats a user's name to a consistent, readable format which makes use of their nickname.
@ -42,7 +41,7 @@ static class Common {
if (guild.MemberCount > 30) {
// For guilds of size over 30, require 85% or more of the members to be known
// (26/30, 42/50, 255/300, etc)
int threshold = (int)(guild.MemberCount * 0.85);
var threshold = (int)(guild.MemberCount * 0.85);
return guild.DownloadedMemberCount >= threshold;
} else {
// For smaller guilds, fail if two or more members are missing

View file

@ -6,7 +6,6 @@ using System.Reflection;
using System.Text.RegularExpressions;
namespace BirthdayBot;
/// <summary>
/// Loads and holds configuration values.
/// </summary>
@ -15,7 +14,6 @@ class Configuration {
public string BotToken { get; }
public string? DBotsToken { get; }
public bool QuitOnFails { get; }
public int ShardStart { get; }
public int ShardAmount { get; }
@ -47,7 +45,6 @@ class Configuration {
BotToken = ReadConfKey<string>(jc, nameof(BotToken), true);
DBotsToken = ReadConfKey<string>(jc, nameof(DBotsToken), false);
QuitOnFails = ReadConfKey<bool?>(jc, nameof(QuitOnFails), false) ?? false;
ShardTotal = args.ShardTotal ?? ReadConfKey<int?>(jc, nameof(ShardTotal), false) ?? 1;
if (ShardTotal < 1) throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer.");

View file

@ -2,7 +2,6 @@
using Npgsql;
namespace BirthdayBot.Data;
public class BotDatabaseContext : DbContext {
private static readonly string _connectionString;

View file

@ -1,5 +1,4 @@
namespace BirthdayBot.Data;
internal static class Extensions {
/// <summary>
/// Gets the corresponding <see cref="GuildConfig"/> for this guild, or a new one if one does not exist.

View file

@ -0,0 +1,4 @@
[*.cs]
generated_code = true
dotnet_analyzer_diagnostic.category-CodeQuality.severity = none
dotnet_diagnostic.CS1591.severity = none

View file

@ -2,7 +2,6 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace BirthdayBot.Data;
[Table("user_birthdays")]
public class UserEntry {
[Key]

View file

@ -1,9 +0,0 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0161:Convert to file-scoped namespace",
Scope = "namespace", Target = "~N:BirthdayBot.Data.Migrations")]

View file

@ -43,13 +43,14 @@ class Program {
public static void ProgramStop() {
if (_stopping) return;
_stopping = true;
Log("Shutdown", "Commencing shutdown...");
Log(nameof(Program), "Shutting down...");
var dispose = Task.Run(_bot!.Dispose);
if (!dispose.Wait(90000)) {
Log("Shutdown", "Normal shutdown has not concluded after 90 seconds. Will force quit.");
if (!dispose.Wait(30000)) {
Log(nameof(Program), "Disconnection is taking too long. Will force exit.");
Environment.ExitCode &= (int)ExitCodes.ForcedExit;
}
Log(nameof(Program), $"Uptime: {BotUptime}");
Environment.Exit(Environment.ExitCode);
}

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<TargetFramework>net6.0</TargetFramework>
<PublishDir>bin\Release\net6.0\publish\</PublishDir>
<SelfContained>false</SelfContained>
</PropertyGroup>
</Project>

View file

@ -46,7 +46,6 @@ public sealed class ShardInstance : IDisposable {
// Background task constructor begins background processing immediately.
_background = new ShardBackgroundWorker(this);
Log(nameof(ShardInstance), "Instance created.");
}
/// <summary>
@ -66,7 +65,6 @@ public sealed class ShardInstance : IDisposable {
DiscordClient.LogoutAsync().Wait(5000);
DiscordClient.Dispose();
_interactionService.Dispose();
Log(nameof(ShardInstance), "Instance disposed.");
}
internal void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message);
@ -127,7 +125,6 @@ public sealed class ShardInstance : IDisposable {
await _interactionService.ExecuteCommandAsync(context, _services).ConfigureAwait(false);
} catch (Exception e) {
Log(nameof(DiscordClient_InteractionCreated), $"Unhandled exception. {e}");
// TODO when implementing proper application error logging, see here
if (arg.Type == InteractionType.ApplicationCommand) {
if (arg.HasResponded) await arg.ModifyOriginalResponseAsync(prop => prop.Content = InternalError);
else await arg.RespondAsync(InternalError);
@ -161,7 +158,6 @@ public sealed class ShardInstance : IDisposable {
await context.Interaction.RespondAsync(errReply, ephemeral: true).ConfigureAwait(false);
} else {
// Generic error response
// TODO when implementing proper application error logging, see here
var ia = context.Interaction;
if (ia.HasResponded) await ia.ModifyOriginalResponseAsync(p => p.Content = InternalError).ConfigureAwait(false);
else await ia.RespondAsync(InternalError).ConfigureAwait(false);

View file

@ -13,12 +13,7 @@ class ShardManager : IDisposable {
/// <summary>
/// Number of seconds between each time the status task runs, in seconds.
/// </summary>
private const int StatusInterval = 60;
/// <summary>
/// Number of shards allowed to be destroyed before the program may close itself, if configured.
/// </summary>
private const int MaxDestroyedShards = 10; // TODO make configurable
private const int StatusInterval = 90;
/// <summary>
/// Number of concurrent shard startups to happen on each check.
@ -28,8 +23,7 @@ class ShardManager : IDisposable {
/// <summary>
/// Amount of time without a completed background service run before a shard instance
/// is considered "dead" and tasked to be removed. A fraction of this value is also used
/// to determine when a shard is "slow".
/// is considered "dead" and tasked to be removed.
/// </summary>
private static readonly TimeSpan DeadShardThreshold = new(0, 20, 0);
@ -43,7 +37,6 @@ class ShardManager : IDisposable {
private readonly Task _statusTask;
private readonly CancellationTokenSource _mainCancel;
private int _destroyedShards = 0;
internal Configuration Config { get; }
@ -90,8 +83,6 @@ class ShardManager : IDisposable {
/// Creates and sets up a new shard instance.
/// </summary>
private async Task<ShardInstance> InitializeShard(int shardId) {
ShardInstance newInstance;
var clientConf = new DiscordSocketConfig() {
ShardId = shardId,
TotalShards = Config.ShardTotal,
@ -106,8 +97,8 @@ class ShardManager : IDisposable {
.AddSingleton(s => new DiscordSocketClient(clientConf))
.AddSingleton(s => new InteractionService(s.GetRequiredService<DiscordSocketClient>()))
.BuildServiceProvider();
newInstance = services.GetRequiredService<ShardInstance>();
await newInstance.StartAsync().ConfigureAwait(false);
var newInstance = services.GetRequiredService<ShardInstance>();
await newInstance.StartAsync();
return newInstance;
}
@ -120,105 +111,62 @@ class ShardManager : IDisposable {
return null;
}
#region Status checking and display
private struct GuildStatusData {
public int GuildCount;
public TimeSpan LastTaskRunTime;
public string? ExecutingTask;
}
private static string StatusDisplay(IEnumerable<int> guildList, Dictionary<int, GuildStatusData> guildInfo, bool showDetail) {
if (!guildList.Any()) return "--";
var result = new StringBuilder();
foreach (var item in guildList) {
result.Append(item.ToString("00") + " ");
if (showDetail) {
result.Remove(result.Length - 1, 1);
result.Append($"[{Math.Floor(guildInfo[item].LastTaskRunTime.TotalSeconds):000}s");
if (guildInfo[item].ExecutingTask != null)
result.Append($" {guildInfo[item].ExecutingTask}");
result.Append("] ");
}
}
if (result.Length > 0) result.Remove(result.Length - 1, 1);
return result.ToString();
}
private async Task StatusLoop() {
try {
while (!_mainCancel.IsCancellationRequested) {
Log($"Bot uptime: {Program.BotUptime}");
Log($"Uptime: {Program.BotUptime}");
// Iterate through shard list, extract data
var guildInfo = new Dictionary<int, GuildStatusData>();
var now = DateTimeOffset.UtcNow;
// Iterate through shards, create report on each
var shardStatuses = new StringBuilder();
var nullShards = new List<int>();
foreach (var item in _shards) {
if (item.Value == null) {
nullShards.Add(item.Key);
var deadShards = new List<int>();
for (var i = 0; i < _shards.Count; i++) {
shardStatuses.Append($"Shard {i:00}: ");
if (_shards[i] == null) {
shardStatuses.AppendLine("Inactive.");
nullShards.Add(i);
continue;
}
var shard = item.Value;
guildInfo[item.Key] = new GuildStatusData {
GuildCount = shard.DiscordClient.Guilds.Count,
LastTaskRunTime = now - shard.LastBackgroundRun,
ExecutingTask = shard.CurrentExecutingService
};
}
// Process info
var guildCounts = guildInfo.Select(i => i.Value.GuildCount);
var guildTotal = guildCounts.Sum();
var guildAverage = guildCounts.Any() ? guildCounts.Average() : 0;
Log($"Currently in {guildTotal} guilds. Average shard load: {guildAverage:0.0}.");
// Health report
var goodShards = new List<int>();
var badShards = new List<int>(); // shards with low connection score OR long time since last work
var deadShards = new List<int>(); // shards to destroy and reinitialize
foreach (var item in guildInfo) {
var lastRun = item.Value.LastTaskRunTime;
var shard = _shards[i]!;
var client = shard.DiscordClient;
shardStatuses.Append($"{Enum.GetName(typeof(ConnectionState), client.ConnectionState)} ({client.Latency:000}ms).");
shardStatuses.Append($" Guilds: {client.Guilds.Count}.");
shardStatuses.Append($" Background: {shard.CurrentExecutingService ?? "Idle"}");
var lastRun = DateTimeOffset.UtcNow - shard.LastBackgroundRun;
if (lastRun > DeadShardThreshold / 3) {
badShards.Add(item.Key);
// Consider a shard dead after a long span without background activity
if (lastRun > DeadShardThreshold)
deadShards.Add(item.Key);
// Formerly known as a 'slow' shard
shardStatuses.Append($", heartbeat {Math.Floor(lastRun.TotalMinutes):00}m ago.");
} else {
goodShards.Add(item.Key);
shardStatuses.Append('.');
}
shardStatuses.AppendLine();
if (lastRun > DeadShardThreshold) {
shardStatuses.AppendLine($"Shard {i:00} marked for disposal.");
deadShards.Add(i);
}
}
Log("Online: " + StatusDisplay(goodShards, guildInfo, false));
if (badShards.Count > 0) Log("Slow: " + StatusDisplay(badShards, guildInfo, true));
if (deadShards.Count > 0) Log("Dead: " + StatusDisplay(deadShards, guildInfo, false));
if (nullShards.Count > 0) Log("Offline: " + StatusDisplay(nullShards, guildInfo, false));
Log(shardStatuses.ToString().TrimEnd());
// Remove dead shards
foreach (var dead in deadShards) {
_shards[dead]!.Dispose();
_shards[dead] = null;
_destroyedShards++;
}
if (Config.QuitOnFails && _destroyedShards > MaxDestroyedShards) {
Environment.ExitCode = (int)Program.ExitCodes.DeadShardThreshold;
Program.ProgramStop();
} else {
// Start up any missing shards
var startAllowance = MaxConcurrentOperations;
foreach (var id in nullShards) {
// To avoid possible issues with resources strained over so many shards starting at once,
// initialization is spread out by only starting a few at a time.
if (startAllowance-- > 0) {
_shards[id] = await InitializeShard(id).ConfigureAwait(false);
} else break;
}
}
await Task.Delay(StatusInterval * 1000, _mainCancel.Token).ConfigureAwait(false);
// Start null shards, a few at at time
var startAllowance = MaxConcurrentOperations;
foreach (var id in nullShards) {
if (startAllowance-- > 0) {
_shards[id] = await InitializeShard(id);
} else break;
}
await Task.Delay(StatusInterval * 1000, _mainCancel.Token);
}
} catch (TaskCanceledException) { }
}
#endregion
}