diff --git a/BackgroundServices/BirthdayRoleUpdate.cs b/BackgroundServices/BirthdayRoleUpdate.cs
index 31b5833..e9bfcf1 100644
--- a/BackgroundServices/BirthdayRoleUpdate.cs
+++ b/BackgroundServices/BirthdayRoleUpdate.cs
@@ -74,11 +74,6 @@ namespace BirthdayBot.BackgroundServices
if (diag.RoleCheck != null) return diag;
// Determine who's currently having a birthday
- if (!guild.HasAllMembers)
- {
- await guild.DownloadUsersAsync().ConfigureAwait(false);
- await Task.Delay(500);
- }
var users = await GuildUserConfiguration.LoadAllAsync(guild.Id).ConfigureAwait(false);
var tz = gc.TimeZone;
var birthdays = GetGuildCurrentBirthdays(users, tz);
diff --git a/BackgroundServices/SelectiveAutoUserDownload.cs b/BackgroundServices/SelectiveAutoUserDownload.cs
new file mode 100644
index 0000000..3ca2c4d
--- /dev/null
+++ b/BackgroundServices/SelectiveAutoUserDownload.cs
@@ -0,0 +1,86 @@
+using BirthdayBot.Data;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BirthdayBot.BackgroundServices
+{
+ ///
+ /// A type of workaround to the issue of user information not being cached for guilds that
+ /// have user information existing in the bot's database. This service runs frequently and
+ /// determines guilds in which user data must be downloaded, and proceeds to request it.
+ ///
+ class SelectiveAutoUserDownload : BackgroundService
+ {
+ private static readonly SemaphoreSlim _updateLock = new SemaphoreSlim(2);
+
+ private readonly HashSet _fetchRequests = new HashSet();
+
+ public SelectiveAutoUserDownload(ShardInstance instance) : base(instance) { }
+
+ public override async Task OnTick(CancellationToken token)
+ {
+ IEnumerable requests;
+ lock (_fetchRequests)
+ {
+ requests = _fetchRequests.ToArray();
+ _fetchRequests.Clear();
+ }
+
+ foreach (var guild in ShardInstance.DiscordClient.Guilds)
+ {
+ if (ShardInstance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected)
+ {
+ Log("Client no longer connected. Stopping early.");
+ return;
+ }
+
+ // Determine if there is action to be taken...
+ if (guild.HasAllMembers) continue;
+ if (requests.Contains(guild.Id) || await GuildUserAnyAsync(guild.Id, token).ConfigureAwait(false))
+ {
+ await guild.DownloadUsersAsync().ConfigureAwait(false);
+ await Task.Delay(500).ConfigureAwait(false);
+ }
+ }
+ }
+
+ ///
+ /// Determines if the user database contains any entries corresponding to this guild.
+ ///
+ /// True if any entries exist.
+ private async Task GuildUserAnyAsync(ulong guildId, CancellationToken token)
+ {
+ try
+ {
+ await _updateLock.WaitAsync(token).ConfigureAwait(false);
+ }
+ catch (Exception ex) when (ex is OperationCanceledException || ex is ObjectDisposedException)
+ {
+ // Calling thread does not expect the exception that SemaphoreSlim throws...
+ throw new TaskCanceledException();
+ }
+ try
+ {
+ using var db = await Database.OpenConnectionAsync().ConfigureAwait(false);
+ using var c = db.CreateCommand();
+ c.CommandText = $"select count(*) from {GuildUserConfiguration.BackingTable} where guild_id = @Gid";
+ c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guildId;
+ await c.PrepareAsync().ConfigureAwait(false);
+ var r = (long)await c.ExecuteScalarAsync(token).ConfigureAwait(false);
+ return r != 0;
+ }
+ finally
+ {
+ _updateLock.Release();
+ }
+ }
+
+ public void RequestDownload(ulong guildId)
+ {
+ lock (_fetchRequests) _fetchRequests.Add(guildId);
+ }
+ }
+}
diff --git a/BackgroundServices/ShardBackgroundWorker.cs b/BackgroundServices/ShardBackgroundWorker.cs
index 73331d7..1c36589 100644
--- a/BackgroundServices/ShardBackgroundWorker.cs
+++ b/BackgroundServices/ShardBackgroundWorker.cs
@@ -23,6 +23,7 @@ namespace BirthdayBot.BackgroundServices
public ConnectionStatus ConnStatus { get; }
public BirthdayRoleUpdate BirthdayUpdater { get; }
+ public SelectiveAutoUserDownload UserDownloader { get; }
public DateTimeOffset LastBackgroundRun { get; private set; }
public int ConnectionScore => ConnStatus.Score;
@@ -33,8 +34,10 @@ namespace BirthdayBot.BackgroundServices
ConnStatus = new ConnectionStatus(instance);
BirthdayUpdater = new BirthdayRoleUpdate(instance);
+ UserDownloader = new SelectiveAutoUserDownload(instance);
_workers = new List()
{
+ {UserDownloader},
{BirthdayUpdater},
{new DataRetention(instance)}
};
diff --git a/BirthdayBot.csproj b/BirthdayBot.csproj
index 0df503c..f37a350 100644
--- a/BirthdayBot.csproj
+++ b/BirthdayBot.csproj
@@ -1,9 +1,9 @@
-
+
Exe
netcoreapp3.1
- 3.0.0
+ 3.0.2
BirthdayBot
NoiTheCat
BirthdayBot
@@ -19,7 +19,7 @@
-
+
diff --git a/Common.cs b/Common.cs
index 97ae5cb..56d54d6 100644
--- a/Common.cs
+++ b/Common.cs
@@ -43,5 +43,26 @@ namespace BirthdayBot
};
public static string BotUptime => (DateTimeOffset.UtcNow - Program.BotStartTime).ToString("d' days, 'hh':'mm':'ss");
+
+ ///
+ /// An alternative to .
+ /// Returns true if *most* members have been downloaded.
+ ///
+ public static bool HasMostMembersDownloaded(SocketGuild guild)
+ {
+ if (guild.HasAllMembers) return true;
+ 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);
+ return guild.DownloadedMemberCount >= threshold;
+ }
+ else
+ {
+ // For smaller guilds, fail if two or more members are missing
+ return guild.MemberCount - guild.DownloadedMemberCount <= 2;
+ }
+ }
}
}
diff --git a/ShardInstance.cs b/ShardInstance.cs
index bf17cab..9482b5d 100644
--- a/ShardInstance.cs
+++ b/ShardInstance.cs
@@ -107,6 +107,8 @@ namespace BirthdayBot
public Task ForceBirthdayUpdateAsync(SocketGuild guild)
=> _background.BirthdayUpdater.SingleProcessGuildAsync(guild);
+ public void RequestDownloadUsers(ulong guildId) => _background.UserDownloader.RequestDownload(guildId);
+
#region Event handling
private Task Client_Log(LogMessage arg)
{
diff --git a/UserInterface/CommandsCommon.cs b/UserInterface/CommandsCommon.cs
index 2002583..1a81c4e 100644
--- a/UserInterface/CommandsCommon.cs
+++ b/UserInterface/CommandsCommon.cs
@@ -22,6 +22,7 @@ namespace BirthdayBot.UserInterface
public const string ParameterError = ":x: Invalid usage. Refer to how to use the command and try again.";
public const string NoParameterError = ":x: This command does not accept any parameters.";
public const string InternalError = ":x: An internal bot error occurred. The bot maintainer has been notified of the issue.";
+ public const string UsersNotDownloadedError = ":x: Currently unavailable. Please try again in a few minutes.";
public delegate Task CommandHandler(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser);
diff --git a/UserInterface/ListingCommands.cs b/UserInterface/ListingCommands.cs
index f2a32fd..934dd92 100644
--- a/UserInterface/ListingCommands.cs
+++ b/UserInterface/ListingCommands.cs
@@ -38,6 +38,13 @@ namespace BirthdayBot.UserInterface
private async Task CmdWhen(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
{
+ if (!Common.HasMostMembersDownloaded(reqChannel.Guild))
+ {
+ instance.RequestDownloadUsers(reqChannel.Guild.Id);
+ await reqChannel.SendMessageAsync(UsersNotDownloadedError);
+ return;
+ }
+
// Requires a parameter
if (param.Length == 1)
{
@@ -103,6 +110,13 @@ namespace BirthdayBot.UserInterface
return;
}
+ if (!Common.HasMostMembersDownloaded(reqChannel.Guild))
+ {
+ instance.RequestDownloadUsers(reqChannel.Guild.Id);
+ await reqChannel.SendMessageAsync(UsersNotDownloadedError);
+ return;
+ }
+
bool useCsv = false;
// Check for CSV option
if (param.Length == 2)
@@ -162,6 +176,13 @@ namespace BirthdayBot.UserInterface
private async Task CmdUpcoming(ShardInstance instance, GuildConfiguration gconf,
string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser)
{
+ if (!Common.HasMostMembersDownloaded(reqChannel.Guild))
+ {
+ instance.RequestDownloadUsers(reqChannel.Guild.Id);
+ await reqChannel.SendMessageAsync(UsersNotDownloadedError);
+ return;
+ }
+
var now = DateTimeOffset.UtcNow;
var search = DateIndex(now.Month, now.Day) - 8; // begin search 8 days prior to current date UTC
if (search <= 0) search = 366 - Math.Abs(search);
diff --git a/UserInterface/ManagerCommands.cs b/UserInterface/ManagerCommands.cs
index b65b813..e910e41 100644
--- a/UserInterface/ManagerCommands.cs
+++ b/UserInterface/ManagerCommands.cs
@@ -393,6 +393,13 @@ namespace BirthdayBot.UserInterface
// Moderators only. As with config, silently drop if this check fails.
if (!gconf.IsBotModerator(reqUser)) return;
+ if (!Common.HasMostMembersDownloaded(reqChannel.Guild))
+ {
+ instance.RequestDownloadUsers(reqChannel.Guild.Id);
+ await reqChannel.SendMessageAsync(UsersNotDownloadedError);
+ return;
+ }
+
if (param.Length != 3)
{
await reqChannel.SendMessageAsync(ParameterError, embed: DocOverride.UsageEmbed).ConfigureAwait(false);
@@ -462,7 +469,10 @@ namespace BirthdayBot.UserInterface
try
{
- var result = await instance.ForceBirthdayUpdateAsync(reqChannel.Guild).ConfigureAwait(false);
+ var guild = reqChannel.Guild;
+ string result = $"\nServer ID: {guild.Id} | Bot shard ID: {instance.ShardId:00}";
+ result += $"\nLocally cached members: {guild.DownloadedMemberCount} out of {guild.MemberCount}";
+ result += "\n" + await instance.ForceBirthdayUpdateAsync(guild).ConfigureAwait(false);
await reqChannel.SendMessageAsync(result).ConfigureAwait(false);
}
catch (Exception ex)