mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-24 01:14:12 +00:00
Modified all background services
-Removed a number of diagnostic messages -Removed ConnectionStatus, connection scores, etc. -Modified work intervals for certain background tasks -Updated code style
This commit is contained in:
parent
8cff530a7c
commit
6f34fbe657
10 changed files with 452 additions and 612 deletions
|
@ -1,16 +1,14 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BirthdayBot.BackgroundServices
|
||||
{
|
||||
abstract class BackgroundService
|
||||
{
|
||||
protected ShardInstance ShardInstance { get; }
|
||||
namespace BirthdayBot.BackgroundServices;
|
||||
|
||||
public BackgroundService(ShardInstance instance) => ShardInstance = instance;
|
||||
abstract class BackgroundService {
|
||||
protected ShardInstance ShardInstance { get; }
|
||||
|
||||
protected void Log(string message) => ShardInstance.Log(GetType().Name, message);
|
||||
public BackgroundService(ShardInstance instance) => ShardInstance = instance;
|
||||
|
||||
public abstract Task OnTick(CancellationToken token);
|
||||
}
|
||||
protected void Log(string message) => ShardInstance.Log(GetType().Name, message);
|
||||
|
||||
public abstract Task OnTick(int tickCount, CancellationToken token);
|
||||
}
|
||||
|
|
|
@ -8,281 +8,249 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BirthdayBot.BackgroundServices
|
||||
{
|
||||
namespace BirthdayBot.BackgroundServices;
|
||||
|
||||
/// <summary>
|
||||
/// Core automatic functionality of the bot. Manages role memberships based on birthday information,
|
||||
/// and optionally sends the announcement message to appropriate guilds.
|
||||
/// </summary>
|
||||
class BirthdayRoleUpdate : BackgroundService {
|
||||
public BirthdayRoleUpdate(ShardInstance instance) : base(instance) { }
|
||||
|
||||
/// <summary>
|
||||
/// Core automatic functionality of the bot. Manages role memberships based on birthday information,
|
||||
/// and optionally sends the announcement message to appropriate guilds.
|
||||
/// Processes birthday updates for all available guilds synchronously.
|
||||
/// </summary>
|
||||
class BirthdayRoleUpdate : BackgroundService
|
||||
{
|
||||
public BirthdayRoleUpdate(ShardInstance instance) : base(instance) { }
|
||||
|
||||
/// <summary>
|
||||
/// Processes birthday updates for all available guilds synchronously.
|
||||
/// </summary>
|
||||
public override async Task OnTick(CancellationToken token)
|
||||
{
|
||||
var exs = new List<Exception>();
|
||||
foreach (var guild in ShardInstance.DiscordClient.Guilds)
|
||||
{
|
||||
if (ShardInstance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected)
|
||||
{
|
||||
Log("Client is not connected. Stopping early.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Single guilds are fully processed and are not interrupted by task cancellation.
|
||||
if (token.IsCancellationRequested) throw new TaskCanceledException();
|
||||
try
|
||||
{
|
||||
await ProcessGuildAsync(guild).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Catch all exceptions per-guild but continue processing, throw at end.
|
||||
exs.Add(ex);
|
||||
}
|
||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
var exs = new List<Exception>();
|
||||
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
||||
if (ShardInstance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) {
|
||||
Log("Client is not connected. Stopping early.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check task cancellation here. Processing during a single guild is never interrupted.
|
||||
if (token.IsCancellationRequested) throw new TaskCanceledException();
|
||||
|
||||
try {
|
||||
await ProcessGuildAsync(guild).ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
// Catch all exceptions per-guild but continue processing, throw at end.
|
||||
exs.Add(ex);
|
||||
}
|
||||
if (exs.Count != 0) throw new AggregateException(exs);
|
||||
}
|
||||
if (exs.Count != 0) throw new AggregateException(exs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Access to <see cref="ProcessGuildAsync(SocketGuild)"/> for the testing command.
|
||||
/// </summary>
|
||||
/// <returns>Diagnostic data in string form.</returns>
|
||||
public static async Task<string> SingleProcessGuildAsync(SocketGuild guild)
|
||||
=> (await ProcessGuildAsync(guild).ConfigureAwait(false)).Export();
|
||||
/// <summary>
|
||||
/// Access to <see cref="ProcessGuildAsync(SocketGuild)"/> for the testing command.
|
||||
/// </summary>
|
||||
/// <returns>Diagnostic data in string form.</returns>
|
||||
public static async Task<string> SingleProcessGuildAsync(SocketGuild guild)
|
||||
=> (await ProcessGuildAsync(guild).ConfigureAwait(false)).Export();
|
||||
|
||||
/// <summary>
|
||||
/// Main method where actual guild processing occurs.
|
||||
/// </summary>
|
||||
private static async Task<PGDiagnostic> ProcessGuildAsync(SocketGuild guild)
|
||||
{
|
||||
var diag = new PGDiagnostic();
|
||||
/// <summary>
|
||||
/// Main method where actual guild processing occurs.
|
||||
/// </summary>
|
||||
private static async Task<PGDiagnostic> ProcessGuildAsync(SocketGuild guild) {
|
||||
var diag = new PGDiagnostic();
|
||||
|
||||
// Load guild information - stop if there is none (bot never previously used in guild)
|
||||
var gc = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
||||
if (gc == null) return diag;
|
||||
// Load guild information - stop if local cache is unavailable.
|
||||
if (!Common.HasMostMembersDownloaded(guild)) return diag;
|
||||
var gc = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
||||
if (gc == null) return diag;
|
||||
|
||||
// Check if role settings are correct before continuing with further processing
|
||||
SocketRole role = null;
|
||||
if (gc.RoleId.HasValue) role = guild.GetRole(gc.RoleId.Value);
|
||||
diag.RoleCheck = CheckCorrectRoleSettings(guild, role);
|
||||
if (diag.RoleCheck != null) return diag;
|
||||
// Check if role settings are correct before continuing with further processing
|
||||
SocketRole role = null;
|
||||
if (gc.RoleId.HasValue) role = guild.GetRole(gc.RoleId.Value);
|
||||
diag.RoleCheck = CheckCorrectRoleSettings(guild, role);
|
||||
if (diag.RoleCheck != null) return diag;
|
||||
|
||||
// Determine who's currently having a birthday
|
||||
var users = await GuildUserConfiguration.LoadAllAsync(guild.Id).ConfigureAwait(false);
|
||||
var tz = gc.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;
|
||||
// Update roles as appropriate
|
||||
try
|
||||
{
|
||||
var updateResult = await UpdateGuildBirthdayRoles(guild, role, birthdays).ConfigureAwait(false);
|
||||
announcementList = updateResult.Item1;
|
||||
diag.RoleApplyResult = updateResult.Item2; // statistics
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
{
|
||||
diag.RoleApply = ex.Message;
|
||||
return diag;
|
||||
}
|
||||
diag.RoleApply = null;
|
||||
|
||||
// Birthday announcement
|
||||
var announce = gc.AnnounceMessages;
|
||||
var announceping = gc.AnnouncePing;
|
||||
SocketTextChannel channel = null;
|
||||
if (gc.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gc.AnnounceChannelId.Value);
|
||||
if (announcementList.Any())
|
||||
{
|
||||
var announceResult =
|
||||
await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList).ConfigureAwait(false);
|
||||
diag.Announcement = announceResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
diag.Announcement = "No new role additions. Announcement not needed.";
|
||||
}
|
||||
// Determine who's currently having a birthday
|
||||
var users = await GuildUserConfiguration.LoadAllAsync(guild.Id).ConfigureAwait(false);
|
||||
var tz = gc.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;
|
||||
// Update roles as appropriate
|
||||
try {
|
||||
var updateResult = await UpdateGuildBirthdayRoles(guild, role, birthdays).ConfigureAwait(false);
|
||||
announcementList = updateResult.Item1;
|
||||
diag.RoleApplyResult = updateResult.Item2; // statistics
|
||||
} catch (Discord.Net.HttpException ex) {
|
||||
diag.RoleApply = ex.Message;
|
||||
return diag;
|
||||
}
|
||||
diag.RoleApply = null;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the bot may be allowed to alter roles.
|
||||
/// </summary>
|
||||
private static string CheckCorrectRoleSettings(SocketGuild guild, SocketRole role)
|
||||
{
|
||||
if (role == null) return "Designated role is not set, or target role cannot be found.";
|
||||
// Birthday announcement
|
||||
var announce = gc.AnnounceMessages;
|
||||
var announceping = gc.AnnouncePing;
|
||||
SocketTextChannel channel = null;
|
||||
if (gc.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gc.AnnounceChannelId.Value);
|
||||
if (announcementList.Any()) {
|
||||
var announceResult =
|
||||
await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList).ConfigureAwait(false);
|
||||
diag.Announcement = announceResult;
|
||||
} else {
|
||||
diag.Announcement = "No new role additions. Announcement not needed.";
|
||||
}
|
||||
|
||||
if (!guild.CurrentUser.GuildPermissions.ManageRoles)
|
||||
{
|
||||
return "Bot does not have the 'Manage Roles' permission.";
|
||||
return diag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the bot may be allowed to alter roles.
|
||||
/// </summary>
|
||||
private static string CheckCorrectRoleSettings(SocketGuild guild, SocketRole role) {
|
||||
if (role == null) return "Birthday role is not configured, or has gone missing.";
|
||||
|
||||
if (!guild.CurrentUser.GuildPermissions.ManageRoles) {
|
||||
return "Bot does not have the 'Manage Roles' permission.";
|
||||
}
|
||||
|
||||
// Check potential role order conflict
|
||||
if (role.Position >= guild.CurrentUser.Hierarchy) {
|
||||
return "Can't access the birthday role. Is it above the bot's permissions?";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all known users from the given guild and returns a list including only those who are
|
||||
/// currently experiencing a birthday in the respective time zone.
|
||||
/// </summary>
|
||||
private static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserConfiguration> guildUsers, string defaultTzStr) {
|
||||
var birthdayUsers = new HashSet<ulong>();
|
||||
|
||||
DateTimeZone defaultTz = null;
|
||||
if (defaultTzStr != null) defaultTz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(defaultTzStr);
|
||||
defaultTz ??= DateTimeZoneProviders.Tzdb.GetZoneOrNull("UTC");
|
||||
|
||||
foreach (var item in guildUsers) {
|
||||
// Determine final time zone to use for calculation
|
||||
DateTimeZone tz = null;
|
||||
if (item.TimeZone != null) {
|
||||
// Try user-provided time zone
|
||||
tz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(item.TimeZone);
|
||||
}
|
||||
tz ??= defaultTz;
|
||||
|
||||
// Check potential role order conflict
|
||||
if (role.Position >= guild.CurrentUser.Hierarchy)
|
||||
{
|
||||
return "Bot is unable to access the designated role due to permission hierarchy.";
|
||||
var targetMonth = item.BirthMonth;
|
||||
var targetDay = item.BirthDay;
|
||||
|
||||
var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
||||
// Special case: If birthday is February 29 and it's not a leap year, recognize it on March 1st
|
||||
if (targetMonth == 2 && targetDay == 29 && !DateTime.IsLeapYear(checkNow.Year)) {
|
||||
targetMonth = 3;
|
||||
targetDay = 1;
|
||||
}
|
||||
if (targetMonth == checkNow.Month && targetDay == checkNow.Day) {
|
||||
birthdayUsers.Add(item.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
return birthdayUsers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the birthday role to all applicable users. Unsets it from all others who may have it.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// First item: List of users who had the birthday role applied, used to announce.
|
||||
/// Second item: Counts of users who have had roles added/removed, used for operation reporting.
|
||||
/// </returns>
|
||||
private static async Task<(IEnumerable<SocketGuildUser>, (int, int))> UpdateGuildBirthdayRoles(
|
||||
SocketGuild g, SocketRole r, HashSet<ulong> names) {
|
||||
// Check members currently with the role. Figure out which users to remove it from.
|
||||
var roleRemoves = new List<SocketGuildUser>();
|
||||
var roleKeeps = new HashSet<ulong>();
|
||||
foreach (var member in r.Members) {
|
||||
if (!names.Contains(member.Id)) roleRemoves.Add(member);
|
||||
else roleKeeps.Add(member.Id);
|
||||
}
|
||||
|
||||
// TODO Can we remove during the iteration instead of after? investigate later...
|
||||
foreach (var user in roleRemoves) {
|
||||
await user.RemoveRoleAsync(r).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Apply role to members not already having it. Prepare announcement list.
|
||||
var newBirthdays = new List<SocketGuildUser>();
|
||||
foreach (var target in names) {
|
||||
var member = g.GetUser(target);
|
||||
if (member == null) continue;
|
||||
if (roleKeeps.Contains(member.Id)) continue; // already has role - do nothing
|
||||
await member.AddRoleAsync(r).ConfigureAwait(false);
|
||||
newBirthdays.Add(member);
|
||||
}
|
||||
|
||||
return (newBirthdays, (newBirthdays.Count, roleRemoves.Count));
|
||||
}
|
||||
|
||||
public const string DefaultAnnounce = "Please wish a happy birthday to %n!";
|
||||
public const string DefaultAnnouncePl = "Please wish a happy birthday to our esteemed members: %n";
|
||||
|
||||
/// <summary>
|
||||
/// Makes (or attempts to make) an announcement in the specified channel that includes all users
|
||||
/// who have just had their birthday role added.
|
||||
/// </summary>
|
||||
/// <returns>The message to place into operation status log.</returns>
|
||||
private static async Task<string> AnnounceBirthdaysAsync(
|
||||
(string, string) announce, bool announcePing, SocketTextChannel c, IEnumerable<SocketGuildUser> names) {
|
||||
if (c == null) return "Announcement channel is not configured, or has gone missing.";
|
||||
|
||||
string announceMsg;
|
||||
if (names.Count() == 1) announceMsg = announce.Item1 ?? announce.Item2 ?? DefaultAnnounce;
|
||||
else announceMsg = announce.Item2 ?? announce.Item1 ?? DefaultAnnouncePl;
|
||||
announceMsg = announceMsg.TrimEnd();
|
||||
if (!announceMsg.Contains("%n")) announceMsg += " %n";
|
||||
|
||||
// Build sorted name list
|
||||
var namestrings = new List<string>();
|
||||
foreach (var item in names)
|
||||
namestrings.Add(Common.FormatName(item, announcePing));
|
||||
namestrings.Sort(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var namedisplay = new StringBuilder();
|
||||
foreach (var item in namestrings) {
|
||||
namedisplay.Append(", ");
|
||||
namedisplay.Append(item);
|
||||
}
|
||||
namedisplay.Remove(0, 2); // Remove initial comma and space
|
||||
|
||||
try {
|
||||
await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString())).ConfigureAwait(false);
|
||||
return null;
|
||||
} catch (Discord.Net.HttpException ex) {
|
||||
// Directly use the resulting exception message in the operation status log
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all known users from the given guild and returns a list including only those who are
|
||||
/// currently experiencing a birthday in the respective time zone.
|
||||
/// </summary>
|
||||
private static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserConfiguration> guildUsers, string defaultTzStr)
|
||||
{
|
||||
var birthdayUsers = new HashSet<ulong>();
|
||||
private class PGDiagnostic {
|
||||
const string DefaultValue = "--";
|
||||
|
||||
DateTimeZone defaultTz = null;
|
||||
if (defaultTzStr != null) defaultTz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(defaultTzStr);
|
||||
defaultTz ??= DateTimeZoneProviders.Tzdb.GetZoneOrNull("UTC");
|
||||
public string RoleCheck = DefaultValue;
|
||||
public string CurrentBirthdays = DefaultValue;
|
||||
public string RoleApply = DefaultValue;
|
||||
public (int, int)? RoleApplyResult;
|
||||
public string Announcement = DefaultValue;
|
||||
|
||||
foreach (var item in guildUsers)
|
||||
{
|
||||
// Determine final time zone to use for calculation
|
||||
DateTimeZone tz = null;
|
||||
if (item.TimeZone != null)
|
||||
{
|
||||
// Try user-provided time zone
|
||||
tz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(item.TimeZone);
|
||||
}
|
||||
tz ??= defaultTz;
|
||||
public string Export() {
|
||||
var result = new StringBuilder();
|
||||
result.AppendLine("Test result:");
|
||||
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:"));
|
||||
|
||||
var targetMonth = item.BirthMonth;
|
||||
var targetDay = item.BirthDay;
|
||||
|
||||
var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
||||
// Special case: If birthday is February 29 and it's not a leap year, recognize it on March 1st
|
||||
if (targetMonth == 2 && targetDay == 29 && !DateTime.IsLeapYear(checkNow.Year))
|
||||
{
|
||||
targetMonth = 3;
|
||||
targetDay = 1;
|
||||
}
|
||||
if (targetMonth == checkNow.Month && targetDay == checkNow.Day)
|
||||
{
|
||||
birthdayUsers.Add(item.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
return birthdayUsers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the birthday role to all applicable users. Unsets it from all others who may have it.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// First item: List of users who had the birthday role applied, used to announce.
|
||||
/// Second item: Counts of users who have had roles added/removed, used for operation reporting.
|
||||
/// </returns>
|
||||
private static async Task<(IEnumerable<SocketGuildUser>, (int, int))> UpdateGuildBirthdayRoles(
|
||||
SocketGuild g, SocketRole r, HashSet<ulong> names)
|
||||
{
|
||||
// Check members currently with the role. Figure out which users to remove it from.
|
||||
var roleRemoves = new List<SocketGuildUser>();
|
||||
var roleKeeps = new HashSet<ulong>();
|
||||
foreach (var member in r.Members)
|
||||
{
|
||||
if (!names.Contains(member.Id)) roleRemoves.Add(member);
|
||||
else roleKeeps.Add(member.Id);
|
||||
}
|
||||
|
||||
// TODO Can we remove during the iteration instead of after? investigate later...
|
||||
foreach (var user in roleRemoves)
|
||||
{
|
||||
await user.RemoveRoleAsync(r).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Apply role to members not already having it. Prepare announcement list.
|
||||
var newBirthdays = new List<SocketGuildUser>();
|
||||
foreach (var target in names)
|
||||
{
|
||||
var member = g.GetUser(target);
|
||||
if (member == null) continue;
|
||||
if (roleKeeps.Contains(member.Id)) continue; // already has role - do nothing
|
||||
await member.AddRoleAsync(r).ConfigureAwait(false);
|
||||
newBirthdays.Add(member);
|
||||
}
|
||||
|
||||
return (newBirthdays, (newBirthdays.Count, roleRemoves.Count));
|
||||
}
|
||||
|
||||
public const string DefaultAnnounce = "Please wish a happy birthday to %n!";
|
||||
public const string DefaultAnnouncePl = "Please wish a happy birthday to our esteemed members: %n";
|
||||
|
||||
/// <summary>
|
||||
/// Makes (or attempts to make) an announcement in the specified channel that includes all users
|
||||
/// who have just had their birthday role added.
|
||||
/// </summary>
|
||||
/// <returns>The message to place into operation status log.</returns>
|
||||
private static async Task<string> AnnounceBirthdaysAsync(
|
||||
(string, string) announce, bool announcePing, SocketTextChannel c, IEnumerable<SocketGuildUser> names)
|
||||
{
|
||||
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;
|
||||
else announceMsg = announce.Item2 ?? announce.Item1 ?? DefaultAnnouncePl;
|
||||
announceMsg = announceMsg.TrimEnd();
|
||||
if (!announceMsg.Contains("%n")) announceMsg += " %n";
|
||||
|
||||
// Build sorted name list
|
||||
var namestrings = new List<string>();
|
||||
foreach (var item in names)
|
||||
namestrings.Add(Common.FormatName(item, announcePing));
|
||||
namestrings.Sort(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var namedisplay = new StringBuilder();
|
||||
foreach (var item in namestrings)
|
||||
{
|
||||
namedisplay.Append(", ");
|
||||
namedisplay.Append(item);
|
||||
}
|
||||
namedisplay.Remove(0, 2); // Remove initial comma and space
|
||||
|
||||
try
|
||||
{
|
||||
await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString())).ConfigureAwait(false);
|
||||
return null;
|
||||
}
|
||||
catch (Discord.Net.HttpException ex)
|
||||
{
|
||||
// Directly use the resulting exception message in the operation status log
|
||||
return ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private class PGDiagnostic
|
||||
{
|
||||
const string 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("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();
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BirthdayBot.BackgroundServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Keeps track of the connection status, assigning a score based on either the connection's
|
||||
/// longevity or the amount of time it has remained persistently disconnected.
|
||||
/// </summary>
|
||||
class ConnectionStatus : BackgroundService
|
||||
{
|
||||
// About 3 minutes
|
||||
public const int StableScore = 180 / ShardBackgroundWorker.Interval;
|
||||
|
||||
public bool Stable { get { return Score >= StableScore; } }
|
||||
public int Score { get; private set; }
|
||||
|
||||
public ConnectionStatus(ShardInstance instance) : base(instance) { }
|
||||
|
||||
public override Task OnTick(CancellationToken token)
|
||||
{
|
||||
switch (ShardInstance.DiscordClient.ConnectionState)
|
||||
{
|
||||
case Discord.ConnectionState.Connected:
|
||||
if (Score < 0) Score = 0;
|
||||
Score++;
|
||||
break;
|
||||
default:
|
||||
if (Score > 0) Score = 0;
|
||||
Score--;
|
||||
break;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In response to a disconnection event, will immediately reset a positive score to zero.
|
||||
/// </summary>
|
||||
public void Disconnected()
|
||||
{
|
||||
if (Score > 0) Score = 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,129 +7,107 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BirthdayBot.BackgroundServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Data retention adherence service:
|
||||
/// Automatically removes database information for guilds that have not been accessed in a long time.
|
||||
/// </summary>
|
||||
class DataRetention : BackgroundService
|
||||
{
|
||||
private static readonly SemaphoreSlim _updateLock = new(ShardManager.MaxConcurrentOperations);
|
||||
const int ProcessInterval = 3600 / ShardBackgroundWorker.Interval; // Process about once per hour
|
||||
private int _tickCount = -1;
|
||||
namespace BirthdayBot.BackgroundServices;
|
||||
|
||||
public DataRetention(ShardInstance instance) : base(instance) { }
|
||||
/// <summary>
|
||||
/// Automatically removes database information for guilds that have not been accessed in a long time.
|
||||
/// </summary>
|
||||
class DataRetention : BackgroundService {
|
||||
private static readonly SemaphoreSlim _updateLock = new(ShardManager.MaxConcurrentOperations);
|
||||
const int ProcessInterval = 3600 / ShardBackgroundWorker.Interval; // Process about once per hour
|
||||
const int Stagger = 3; // How many ticks in between each group of guilds to stagger processing.
|
||||
|
||||
public override async Task OnTick(CancellationToken token)
|
||||
{
|
||||
if ((++_tickCount + ShardInstance.ShardId * 3) % ProcessInterval != 0)
|
||||
{
|
||||
// Do not process on every tick.
|
||||
// Stagger processing based on shard ID, to not choke the background processing task.
|
||||
return;
|
||||
public DataRetention(ShardInstance instance) : base(instance) { }
|
||||
|
||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
// On each tick, run only a set group of guilds, each group still processed every ProcessInterval ticks.
|
||||
if ((tickCount + ShardInstance.ShardId * Stagger) % ProcessInterval != 0) return;
|
||||
|
||||
try {
|
||||
// A semaphore is used to restrict this work being done concurrently on other shards
|
||||
// to avoid putting pressure on the SQL connection pool. Clearing old database information
|
||||
// ultimately is a low priority among other tasks.
|
||||
await _updateLock.WaitAsync(token).ConfigureAwait(false);
|
||||
} catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException) {
|
||||
// Caller does not expect the exception that SemaphoreSlim throws...
|
||||
throw new TaskCanceledException();
|
||||
}
|
||||
try {
|
||||
// Build a list of all values to update
|
||||
var updateList = new Dictionary<ulong, List<ulong>>();
|
||||
foreach (var g in ShardInstance.DiscordClient.Guilds) {
|
||||
// Get list of IDs for all users who exist in the database and currently exist in the guild
|
||||
var userList = GuildUserConfiguration.LoadAllAsync(g.Id);
|
||||
var guildUserIds = from gu in g.Users select gu.Id;
|
||||
var savedUserIds = from cu in await userList.ConfigureAwait(false) select cu.UserId;
|
||||
var existingCachedIds = savedUserIds.Intersect(guildUserIds);
|
||||
updateList[g.Id] = existingCachedIds.ToList();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// A semaphore is used to restrict this work being done concurrently on other shards
|
||||
// to avoid putting pressure on the SQL connection pool. Clearing old database information
|
||||
// ultimately is a low priority among other tasks.
|
||||
await _updateLock.WaitAsync(token).ConfigureAwait(false);
|
||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||
|
||||
// Statement for updating last_seen in guilds
|
||||
var cUpdateGuild = db.CreateCommand();
|
||||
cUpdateGuild.CommandText = $"update {GuildConfiguration.BackingTable} set last_seen = now() "
|
||||
+ "where guild_id = @Gid";
|
||||
var pUpdateG = cUpdateGuild.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
||||
cUpdateGuild.Prepare();
|
||||
|
||||
// Statement for updating last_seen in guild users
|
||||
var cUpdateGuildUser = db.CreateCommand();
|
||||
cUpdateGuildUser.CommandText = $"update {GuildUserConfiguration.BackingTable} set last_seen = now() "
|
||||
+ "where guild_id = @Gid and user_id = @Uid";
|
||||
var pUpdateGU_g = cUpdateGuildUser.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
||||
var pUpdateGU_u = cUpdateGuildUser.Parameters.Add("@Uid", NpgsqlDbType.Bigint);
|
||||
cUpdateGuildUser.Prepare();
|
||||
|
||||
// Do actual updates
|
||||
int updatedGuilds = 0;
|
||||
int updatedUsers = 0;
|
||||
foreach (var item in updateList) {
|
||||
var guild = item.Key;
|
||||
var userlist = item.Value;
|
||||
|
||||
pUpdateG.Value = (long)guild;
|
||||
updatedGuilds += await cUpdateGuild.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
pUpdateGU_g.Value = (long)guild;
|
||||
foreach (var userid in userlist) {
|
||||
pUpdateGU_u.Value = (long)userid;
|
||||
updatedUsers += await cUpdateGuildUser.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException)
|
||||
{
|
||||
// Calling thread does not expect the exception that SemaphoreSlim throws...
|
||||
throw new TaskCanceledException();
|
||||
var resultText = new StringBuilder();
|
||||
resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users.");
|
||||
|
||||
// Delete all old values - expects referencing tables to have 'on delete cascade'
|
||||
using var t = db.BeginTransaction();
|
||||
int staleGuilds, staleUsers;
|
||||
using (var c = db.CreateCommand()) {
|
||||
// Delete data for guilds not seen in 4 weeks
|
||||
c.CommandText = $"delete from {GuildConfiguration.BackingTable} where (now() - interval '28 days') > last_seen";
|
||||
staleGuilds = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
try
|
||||
{
|
||||
// Build a list of all values to update
|
||||
var updateList = new Dictionary<ulong, List<ulong>>();
|
||||
foreach (var g in ShardInstance.DiscordClient.Guilds)
|
||||
{
|
||||
// Get list of IDs for all users who exist in the database and currently exist in the guild
|
||||
var userList = GuildUserConfiguration.LoadAllAsync(g.Id);
|
||||
var guildUserIds = from gu in g.Users select gu.Id;
|
||||
var savedUserIds = from cu in await userList.ConfigureAwait(false) select cu.UserId;
|
||||
var existingCachedIds = savedUserIds.Intersect(guildUserIds);
|
||||
updateList[g.Id] = existingCachedIds.ToList();
|
||||
}
|
||||
|
||||
using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
|
||||
|
||||
// Statement for updating last_seen in guilds
|
||||
var cUpdateGuild = db.CreateCommand();
|
||||
cUpdateGuild.CommandText = $"update {GuildConfiguration.BackingTable} set last_seen = now() "
|
||||
+ "where guild_id = @Gid";
|
||||
var pUpdateG = cUpdateGuild.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
||||
cUpdateGuild.Prepare();
|
||||
|
||||
// Statement for updating last_seen in guild users
|
||||
var cUpdateGuildUser = db.CreateCommand();
|
||||
cUpdateGuildUser.CommandText = $"update {GuildUserConfiguration.BackingTable} set last_seen = now() "
|
||||
+ "where guild_id = @Gid and user_id = @Uid";
|
||||
var pUpdateGU_g = cUpdateGuildUser.Parameters.Add("@Gid", NpgsqlDbType.Bigint);
|
||||
var pUpdateGU_u = cUpdateGuildUser.Parameters.Add("@Uid", NpgsqlDbType.Bigint);
|
||||
cUpdateGuildUser.Prepare();
|
||||
|
||||
// Do actual updates
|
||||
int updatedGuilds = 0;
|
||||
int updatedUsers = 0;
|
||||
foreach (var item in updateList)
|
||||
{
|
||||
var guild = item.Key;
|
||||
var userlist = item.Value;
|
||||
|
||||
pUpdateG.Value = (long)guild;
|
||||
updatedGuilds += await cUpdateGuild.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
pUpdateGU_g.Value = (long)guild;
|
||||
foreach (var userid in userlist)
|
||||
{
|
||||
pUpdateGU_u.Value = (long)userid;
|
||||
updatedUsers += await cUpdateGuildUser.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
var resultText = new StringBuilder();
|
||||
resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users.");
|
||||
|
||||
// Delete all old values - expects referencing tables to have 'on delete cascade'
|
||||
using var t = db.BeginTransaction();
|
||||
int staleGuilds, staleUsers;
|
||||
using (var c = db.CreateCommand())
|
||||
{
|
||||
// Delete data for guilds not seen in 4 weeks
|
||||
c.CommandText = $"delete from {GuildConfiguration.BackingTable} where (now() - interval '28 days') > last_seen";
|
||||
staleGuilds = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
using (var c = db.CreateCommand())
|
||||
{
|
||||
// Delete data for users not seen in 8 weeks
|
||||
c.CommandText = $"delete from {GuildUserConfiguration.BackingTable} where (now() - interval '56 days') > last_seen";
|
||||
staleUsers = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
if (staleGuilds != 0 || staleUsers != 0)
|
||||
{
|
||||
resultText.Append(" Discarded ");
|
||||
if (staleGuilds != 0)
|
||||
{
|
||||
resultText.Append($"{staleGuilds} guilds");
|
||||
if (staleUsers != 0) resultText.Append(", ");
|
||||
}
|
||||
if (staleUsers != 0)
|
||||
{
|
||||
resultText.Append($"{staleUsers} standalone users");
|
||||
}
|
||||
resultText.Append('.');
|
||||
}
|
||||
t.Commit();
|
||||
Log(resultText.ToString());
|
||||
using (var c = db.CreateCommand()) {
|
||||
// Delete data for users not seen in 8 weeks
|
||||
c.CommandText = $"delete from {GuildUserConfiguration.BackingTable} where (now() - interval '56 days') > last_seen";
|
||||
staleUsers = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_updateLock.Release();
|
||||
if (staleGuilds != 0 || staleUsers != 0) {
|
||||
resultText.Append(" Discarded ");
|
||||
if (staleGuilds != 0) {
|
||||
resultText.Append($"{staleGuilds} guilds");
|
||||
if (staleUsers != 0) resultText.Append(", ");
|
||||
}
|
||||
if (staleUsers != 0) {
|
||||
resultText.Append($"{staleUsers} standalone users");
|
||||
}
|
||||
resultText.Append('.');
|
||||
}
|
||||
t.Commit();
|
||||
Log(resultText.ToString());
|
||||
} finally {
|
||||
_updateLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,55 +4,47 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BirthdayBot.BackgroundServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports user count statistics to external services on a shard by shard basis.
|
||||
/// </summary>
|
||||
class ExternalStatisticsReporting : BackgroundService
|
||||
{
|
||||
const int ProcessInterval = 600 / ShardBackgroundWorker.Interval; // Process every ~5 minutes
|
||||
private int _tickCount = 0;
|
||||
namespace BirthdayBot.BackgroundServices;
|
||||
|
||||
private static readonly HttpClient _httpClient = new();
|
||||
/// <summary>
|
||||
/// Reports user count statistics to external services on a shard by shard basis.
|
||||
/// </summary>
|
||||
class ExternalStatisticsReporting : BackgroundService {
|
||||
const int ProcessInterval = 1200 / ShardBackgroundWorker.Interval; // Process every ~20 minutes
|
||||
const int ProcessOffset = 300 / ShardBackgroundWorker.Interval; // Begin processing 5 minutes after shard start
|
||||
|
||||
public ExternalStatisticsReporting(ShardInstance instance) : base(instance) { }
|
||||
private static readonly HttpClient _httpClient = new();
|
||||
|
||||
public override async Task OnTick(CancellationToken token)
|
||||
{
|
||||
if (++_tickCount % ProcessInterval != 0) return;
|
||||
public ExternalStatisticsReporting(ShardInstance instance) : base(instance) { }
|
||||
|
||||
var botId = ShardInstance.DiscordClient.CurrentUser.Id;
|
||||
if (botId == 0) return;
|
||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
if (tickCount < ProcessOffset) return;
|
||||
if (tickCount % ProcessInterval != 0) return;
|
||||
|
||||
await SendDiscordBots(ShardInstance.DiscordClient.Guilds.Count, botId, token);
|
||||
}
|
||||
var botId = ShardInstance.DiscordClient.CurrentUser.Id;
|
||||
if (botId == 0) return;
|
||||
var count = ShardInstance.DiscordClient.Guilds.Count;
|
||||
|
||||
private async Task SendDiscordBots(int userCount, ulong botId, CancellationToken token)
|
||||
{
|
||||
var dbotsToken = ShardInstance.Config.DBotsToken;
|
||||
if (dbotsToken != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
const string dBotsApiUrl = "https://discord.bots.gg/api/v1/bots/{0}/stats";
|
||||
const string Body = "{{ \"guildCount\": {0}, \"shardCount\": {1}, \"shardId\": {2} }}";
|
||||
var uri = new Uri(string.Format(dBotsApiUrl, botId));
|
||||
var dbotsToken = ShardInstance.Config.DBotsToken;
|
||||
if (dbotsToken != null) await SendDiscordBots(dbotsToken, count, botId, token);
|
||||
}
|
||||
|
||||
var post = new HttpRequestMessage(HttpMethod.Post, uri);
|
||||
post.Headers.Add("Authorization", dbotsToken);
|
||||
post.Content = new StringContent(string.Format(Body,
|
||||
userCount, ShardInstance.Config.ShardTotal, ShardInstance.ShardId),
|
||||
Encoding.UTF8, "application/json");
|
||||
private async Task SendDiscordBots(string apiToken, int userCount, ulong botId, CancellationToken token) {
|
||||
try {
|
||||
const string dBotsApiUrl = "https://discord.bots.gg/api/v1/bots/{0}/stats";
|
||||
const string Body = "{{ \"guildCount\": {0}, \"shardCount\": {1}, \"shardId\": {2} }}";
|
||||
var uri = new Uri(string.Format(dBotsApiUrl, botId));
|
||||
|
||||
await _httpClient.SendAsync(post, token);
|
||||
Log("Discord Bots: Update successful.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("Discord Bots: Exception encountered during update: " + ex.Message);
|
||||
}
|
||||
}
|
||||
var post = new HttpRequestMessage(HttpMethod.Post, uri);
|
||||
post.Headers.Add("Authorization", apiToken);
|
||||
post.Content = new StringContent(string.Format(Body,
|
||||
userCount, ShardInstance.Config.ShardTotal, ShardInstance.ShardId),
|
||||
Encoding.UTF8, "application/json");
|
||||
|
||||
await _httpClient.SendAsync(post, token);
|
||||
Log("Discord Bots: Update successful.");
|
||||
} catch (Exception ex) {
|
||||
Log("Discord Bots: Exception encountered during update: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ class SelectiveAutoUserDownload : BackgroundService {
|
|||
|
||||
public SelectiveAutoUserDownload(ShardInstance instance) : base(instance) { }
|
||||
|
||||
public override async Task OnTick(CancellationToken token) {
|
||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
IEnumerable<ulong> requests;
|
||||
lock (_fetchRequests) {
|
||||
requests = _fetchRequests.ToArray();
|
||||
|
@ -26,10 +26,8 @@ class SelectiveAutoUserDownload : BackgroundService {
|
|||
}
|
||||
|
||||
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
||||
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) {
|
||||
Log("Client no longer connected. Stopping early.");
|
||||
return;
|
||||
}
|
||||
// Has the potential to disconnect while in the middle of processing.
|
||||
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) return;
|
||||
|
||||
// Determine if there is action to be taken...
|
||||
if (guild.HasAllMembers) continue;
|
||||
|
|
|
@ -18,22 +18,20 @@ namespace BirthdayBot.BackgroundServices
|
|||
private readonly Task _workerTask;
|
||||
private readonly CancellationTokenSource _workerCanceller;
|
||||
private readonly List<BackgroundService> _workers;
|
||||
private int _tickCount = -1;
|
||||
|
||||
private ShardInstance Instance { get; }
|
||||
|
||||
public ConnectionStatus ConnStatus { get; }
|
||||
public BirthdayRoleUpdate BirthdayUpdater { get; }
|
||||
public SelectiveAutoUserDownload UserDownloader { get; }
|
||||
public DateTimeOffset LastBackgroundRun { get; private set; }
|
||||
public string CurrentExecutingService { get; private set; }
|
||||
public int ConnectionScore => ConnStatus.Score;
|
||||
public string? CurrentExecutingService { get; private set; }
|
||||
|
||||
public ShardBackgroundWorker(ShardInstance instance)
|
||||
{
|
||||
Instance = instance;
|
||||
_workerCanceller = new CancellationTokenSource();
|
||||
|
||||
ConnStatus = new ConnectionStatus(instance);
|
||||
BirthdayUpdater = new BirthdayRoleUpdate(instance);
|
||||
UserDownloader = new SelectiveAutoUserDownload(instance);
|
||||
_workers = new List<BackgroundService>()
|
||||
|
@ -70,9 +68,8 @@ namespace BirthdayBot.BackgroundServices
|
|||
{
|
||||
await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false);
|
||||
|
||||
// ConnectionStatus will always run. Its result determines if remaining tasks also this time.
|
||||
await ConnStatus.OnTick(_workerCanceller.Token).ConfigureAwait(false);
|
||||
if (!ConnStatus.Stable) continue;
|
||||
// Skip this round of task execution if the client is not connected
|
||||
if (Instance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) continue;
|
||||
|
||||
// Execute tasks sequentially
|
||||
foreach (var service in _workers)
|
||||
|
@ -81,21 +78,13 @@ namespace BirthdayBot.BackgroundServices
|
|||
try
|
||||
{
|
||||
if (_workerCanceller.IsCancellationRequested) break;
|
||||
await service.OnTick(_workerCanceller.Token).ConfigureAwait(false);
|
||||
_tickCount++;
|
||||
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception ex) when (ex is not TaskCanceledException)
|
||||
{
|
||||
|
||||
if (ex is TaskCanceledException)
|
||||
{
|
||||
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} was interrupted by a cancellation request.");
|
||||
throw;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO webhook log
|
||||
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} encountered an exception:\n" + ex.ToString());
|
||||
}
|
||||
// TODO webhook log
|
||||
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} encountered an exception:\n" + ex.ToString());
|
||||
}
|
||||
}
|
||||
CurrentExecutingService = null;
|
||||
|
@ -103,8 +92,6 @@ namespace BirthdayBot.BackgroundServices
|
|||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
|
||||
Instance.Log(nameof(WorkerLoop), "Background worker has concluded normally.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
174
Configuration.cs
174
Configuration.cs
|
@ -6,107 +6,91 @@ using System.IO;
|
|||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BirthdayBot
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads and holds configuration values.
|
||||
/// </summary>
|
||||
class Configuration
|
||||
{
|
||||
public string BotToken { get; }
|
||||
public string LogWebhook { get; }
|
||||
public string DBotsToken { get; }
|
||||
namespace BirthdayBot;
|
||||
|
||||
public const string ShardLenConfKey = "ShardRange";
|
||||
public int ShardStart { get; }
|
||||
public int ShardAmount { get; }
|
||||
/// <summary>
|
||||
/// Loads and holds configuration values.
|
||||
/// </summary>
|
||||
class Configuration {
|
||||
public string BotToken { get; }
|
||||
public string LogWebhook { get; }
|
||||
public string? DBotsToken { get; }
|
||||
|
||||
public int ShardTotal { get; }
|
||||
public const string ShardLenConfKey = "ShardRange";
|
||||
public int ShardStart { get; }
|
||||
public int ShardAmount { get; }
|
||||
|
||||
public bool QuitOnFails { get; }
|
||||
public int ShardTotal { get; }
|
||||
|
||||
public Configuration()
|
||||
{
|
||||
// Looks for settings.json in the executable directory.
|
||||
var confPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
confPath += Path.DirectorySeparatorChar + "settings.json";
|
||||
public bool QuitOnFails { get; }
|
||||
|
||||
if (!File.Exists(confPath))
|
||||
{
|
||||
throw new Exception("Settings file not found."
|
||||
+ " Create a file in the executable directory named 'settings.json'.");
|
||||
}
|
||||
public Configuration() {
|
||||
// Looks for settings.json in the executable directory.
|
||||
var confPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
confPath += Path.DirectorySeparatorChar + "settings.json";
|
||||
|
||||
var jc = JObject.Parse(File.ReadAllText(confPath));
|
||||
|
||||
BotToken = jc[nameof(BotToken)]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(BotToken))
|
||||
throw new Exception($"'{nameof(BotToken)}' must be specified.");
|
||||
|
||||
LogWebhook = jc[nameof(LogWebhook)]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(LogWebhook))
|
||||
throw new Exception($"'{nameof(LogWebhook)}' must be specified.");
|
||||
|
||||
var dbj = jc[nameof(DBotsToken)];
|
||||
if (dbj != null)
|
||||
{
|
||||
DBotsToken = dbj.Value<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
DBotsToken = null;
|
||||
}
|
||||
|
||||
var sqlhost = jc["SqlHost"]?.Value<string>() ?? "localhost"; // Default to localhost
|
||||
var sqluser = jc["SqlUsername"]?.Value<string>();
|
||||
var sqlpass = jc["SqlPassword"]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(sqluser) || string.IsNullOrWhiteSpace(sqlpass))
|
||||
throw new Exception("'SqlUsername', 'SqlPassword' must be specified.");
|
||||
var csb = new NpgsqlConnectionStringBuilder()
|
||||
{
|
||||
Host = sqlhost,
|
||||
Username = sqluser,
|
||||
Password = sqlpass
|
||||
};
|
||||
var sqldb = jc["SqlDatabase"]?.Value<string>();
|
||||
if (sqldb != null) csb.Database = sqldb; // Optional database setting
|
||||
Database.DBConnectionString = csb.ToString();
|
||||
|
||||
int? sc = jc[nameof(ShardTotal)]?.Value<int>();
|
||||
if (!sc.HasValue) ShardTotal = 1;
|
||||
else
|
||||
{
|
||||
ShardTotal = sc.Value;
|
||||
if (ShardTotal <= 0)
|
||||
{
|
||||
throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer.");
|
||||
}
|
||||
}
|
||||
|
||||
string srVal = jc[ShardLenConfKey]?.Value<string>();
|
||||
if (!string.IsNullOrWhiteSpace(srVal))
|
||||
{
|
||||
Regex srPicker = new(@"(?<low>\d{1,2})[-,]{1}(?<high>\d{1,2})");
|
||||
var m = srPicker.Match(srVal);
|
||||
if (m.Success)
|
||||
{
|
||||
ShardStart = int.Parse(m.Groups["low"].Value);
|
||||
int high = int.Parse(m.Groups["high"].Value);
|
||||
ShardAmount = high - (ShardStart - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Shard range not properly formatted in '{ShardLenConfKey}'.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default: this instance handles all shards from ShardTotal
|
||||
ShardStart = 0;
|
||||
ShardAmount = ShardTotal;
|
||||
}
|
||||
|
||||
QuitOnFails = jc[nameof(QuitOnFails)]?.Value<bool>() ?? false;
|
||||
if (!File.Exists(confPath)) {
|
||||
throw new Exception("Settings file not found."
|
||||
+ " Create a file in the executable directory named 'settings.json'.");
|
||||
}
|
||||
|
||||
var jc = JObject.Parse(File.ReadAllText(confPath));
|
||||
|
||||
BotToken = jc[nameof(BotToken)]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(BotToken))
|
||||
throw new Exception($"'{nameof(BotToken)}' must be specified.");
|
||||
|
||||
LogWebhook = jc[nameof(LogWebhook)]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(LogWebhook))
|
||||
throw new Exception($"'{nameof(LogWebhook)}' must be specified.");
|
||||
|
||||
var dbj = jc[nameof(DBotsToken)];
|
||||
if (dbj != null) {
|
||||
DBotsToken = dbj.Value<string>();
|
||||
} else {
|
||||
DBotsToken = null;
|
||||
}
|
||||
|
||||
var sqlhost = jc["SqlHost"]?.Value<string>() ?? "localhost"; // Default to localhost
|
||||
var sqluser = jc["SqlUsername"]?.Value<string>();
|
||||
var sqlpass = jc["SqlPassword"]?.Value<string>();
|
||||
if (string.IsNullOrWhiteSpace(sqluser) || string.IsNullOrWhiteSpace(sqlpass))
|
||||
throw new Exception("'SqlUsername', 'SqlPassword' must be specified.");
|
||||
var csb = new NpgsqlConnectionStringBuilder() {
|
||||
Host = sqlhost,
|
||||
Username = sqluser,
|
||||
Password = sqlpass
|
||||
};
|
||||
var sqldb = jc["SqlDatabase"]?.Value<string>();
|
||||
if (sqldb != null) csb.Database = sqldb; // Optional database setting
|
||||
Database.DBConnectionString = csb.ToString();
|
||||
|
||||
int? sc = jc[nameof(ShardTotal)]?.Value<int>();
|
||||
if (!sc.HasValue) ShardTotal = 1;
|
||||
else {
|
||||
ShardTotal = sc.Value;
|
||||
if (ShardTotal <= 0) {
|
||||
throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer.");
|
||||
}
|
||||
}
|
||||
|
||||
string srVal = jc[ShardLenConfKey]?.Value<string>();
|
||||
if (!string.IsNullOrWhiteSpace(srVal)) {
|
||||
Regex srPicker = new(@"(?<low>\d{1,2})[-,]{1}(?<high>\d{1,2})");
|
||||
var m = srPicker.Match(srVal);
|
||||
if (m.Success) {
|
||||
ShardStart = int.Parse(m.Groups["low"].Value);
|
||||
int high = int.Parse(m.Groups["high"].Value);
|
||||
ShardAmount = high - (ShardStart - 1);
|
||||
} else {
|
||||
throw new Exception($"Shard range not properly formatted in '{ShardLenConfKey}'.");
|
||||
}
|
||||
} else {
|
||||
// Default: this instance handles all shards from ShardTotal
|
||||
ShardStart = 0;
|
||||
ShardAmount = ShardTotal;
|
||||
}
|
||||
|
||||
QuitOnFails = jc[nameof(QuitOnFails)]?.Value<bool>() ?? false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,11 +30,6 @@ namespace BirthdayBot
|
|||
/// </summary>
|
||||
public string CurrentExecutingService => _background.CurrentExecutingService;
|
||||
public Configuration Config => _manager.Config;
|
||||
/// <summary>
|
||||
/// Returns this shard's connection score.
|
||||
/// See <see cref="BackgroundServices.ConnectionStatus"/> for details on what this means.
|
||||
/// </summary>
|
||||
public int ConnectionScore => _background.ConnectionScore;
|
||||
|
||||
/// <summary>
|
||||
/// Prepares and configures the shard instances, but does not yet start its connection.
|
||||
|
@ -51,7 +46,6 @@ namespace BirthdayBot
|
|||
|
||||
// Background task constructor begins background processing immediately.
|
||||
_background = new ShardBackgroundWorker(this);
|
||||
DiscordClient.Disconnected += Client_Disconnected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -69,13 +63,10 @@ namespace BirthdayBot
|
|||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Log("Instance", "Cleaning up...");
|
||||
|
||||
// Unsubscribe from own events
|
||||
DiscordClient.Log -= Client_Log;
|
||||
DiscordClient.Ready -= Client_Ready;
|
||||
DiscordClient.MessageReceived -= Client_MessageReceived;
|
||||
DiscordClient.Disconnected -= Client_Disconnected;
|
||||
|
||||
_background.Dispose();
|
||||
try
|
||||
|
@ -98,9 +89,8 @@ namespace BirthdayBot
|
|||
}
|
||||
|
||||
var clientDispose = Task.Run(DiscordClient.Dispose);
|
||||
if (!clientDispose.Wait(10000))
|
||||
Log("Instance", "Warning: Client hanging on dispose.");
|
||||
Log("Instance", "Shard instance disposed.");
|
||||
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);
|
||||
|
@ -130,6 +120,7 @@ namespace BirthdayBot
|
|||
case "Disconnecting":
|
||||
case "Disconnected":
|
||||
case "WebSocket connection was closed":
|
||||
case "Server requested a reconnect":
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
if (arg.Message == "Heartbeat Errored")
|
||||
|
@ -152,15 +143,6 @@ namespace BirthdayBot
|
|||
/// </summary>
|
||||
private async Task Client_Ready() => await DiscordClient.SetGameAsync(CommandPrefix + "help");
|
||||
|
||||
/// <summary>
|
||||
/// Notify ConnectionStatus of a disconnect.
|
||||
/// </summary>
|
||||
private Task Client_Disconnected(Exception arg)
|
||||
{
|
||||
_background.ConnStatus.Disconnected();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the incoming message is an incoming command, and dispatches to the appropriate handler if necessary.
|
||||
/// </summary>
|
||||
|
|
|
@ -150,7 +150,7 @@ namespace BirthdayBot
|
|||
Log($"Bot uptime: {Common.BotUptime}");
|
||||
|
||||
// Iterate through shard list, extract data
|
||||
var guildInfo = new Dictionary<int, (int, int, TimeSpan, string)>();
|
||||
var guildInfo = new Dictionary<int, (int, TimeSpan, string)>();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var nullShards = new List<int>();
|
||||
foreach (var item in _shards)
|
||||
|
@ -163,11 +163,10 @@ namespace BirthdayBot
|
|||
var shard = item.Value;
|
||||
|
||||
var guildCount = shard.DiscordClient.Guilds.Count;
|
||||
var connScore = shard.ConnectionScore;
|
||||
var lastRun = now - shard.LastBackgroundRun;
|
||||
var lastExec = shard.CurrentExecutingService ?? "null";
|
||||
|
||||
guildInfo[item.Key] = (guildCount, connScore, lastRun, lastExec);
|
||||
guildInfo[item.Key] = (guildCount, lastRun, lastExec);
|
||||
}
|
||||
|
||||
// Process info
|
||||
|
@ -182,10 +181,9 @@ namespace BirthdayBot
|
|||
var deadShards = new List<int>(); // shards to destroy and reinitialize
|
||||
foreach (var item in guildInfo)
|
||||
{
|
||||
var connScore = item.Value.Item2;
|
||||
var lastRun = item.Value.Item3;
|
||||
var lastRun = item.Value.Item2;
|
||||
|
||||
if (lastRun > new TimeSpan(0, 10, 0) || connScore < ConnectionStatus.StableScore)
|
||||
if (lastRun > DeadShardThreshold / 3)
|
||||
{
|
||||
badShards.Add(item.Key);
|
||||
|
||||
|
@ -208,9 +206,8 @@ namespace BirthdayBot
|
|||
if (detailedInfo)
|
||||
{
|
||||
result.Remove(result.Length - 1, 1);
|
||||
result.Append($"[{guildInfo[item].Item2:+0;-0}");
|
||||
result.Append($" {Math.Floor(guildInfo[item].Item3.TotalSeconds):000}s");
|
||||
result.Append($" {guildInfo[item].Item4}] ");
|
||||
result.Append($"[{Math.Floor(guildInfo[item].Item2.TotalSeconds):000}s");
|
||||
result.Append($" {guildInfo[item].Item3}] ");
|
||||
}
|
||||
}
|
||||
if (result.Length > 0) result.Remove(result.Length - 1, 1);
|
||||
|
|
Loading…
Reference in a new issue