diff --git a/BackgroundUserListLoad.cs b/BackgroundUserListLoad.cs
new file mode 100644
index 0000000..c5dcce2
--- /dev/null
+++ b/BackgroundUserListLoad.cs
@@ -0,0 +1,64 @@
+using Microsoft.Extensions.DependencyInjection;
+using WorldTime.Data;
+
+namespace WorldTime;
+///
+/// Proactively fills the user cache for guilds in which any time zone configuration exists.
+///
+/// Modeled after BirthdayBot's similar feature.
+class BackgroundUserListLoad : IDisposable {
+ private readonly IServiceProvider _services;
+ private readonly Task _workerTask;
+ private readonly CancellationTokenSource _workerCancel;
+
+ public BackgroundUserListLoad(IServiceProvider services) {
+ _services = services;
+ _workerCancel = new();
+ _workerTask = Task.Factory.StartNew(Worker, _workerCancel.Token,
+ TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ }
+
+ public void Dispose() {
+ _workerCancel.Cancel();
+ _workerCancel.Dispose();
+ _workerTask.Dispose();
+ }
+
+ private async Task Worker() {
+ while (!_workerCancel.IsCancellationRequested) {
+ // Interval same as status
+ await Task.Delay(WorldTime.StatusInterval * 1000, _workerCancel.Token);
+
+ foreach (var shard in _services.GetRequiredService().Shards) {
+ try {
+ await ProcessShard(shard);
+ } catch (Exception ex) {
+ Program.Log(nameof(BackgroundUserListLoad), ex.ToString());
+ }
+ }
+ }
+ }
+
+ private async Task ProcessShard(DiscordSocketClient shard) {
+ using var db = _services.GetRequiredService();
+
+ // Check when a guild's cache is incomplete...
+ var incompleteCaches = shard.Guilds.Where(g => !g.HasAllMembers).Select(g => (long)g.Id).ToHashSet();
+ // ...and contains any user data.
+ var mustFetch = db.UserEntries.Where(e => incompleteCaches.Contains(e.GuildId)).Select(e => e.GuildId).Distinct();
+
+ var processed = 0;
+ foreach (var item in mustFetch) {
+ // May cause a disconnect in certain situations. Cancel all further attempts until the next pass if it happens.
+ if (shard.ConnectionState != ConnectionState.Connected) break;
+
+ var guild = shard.GetGuild((ulong)item);
+ if (guild == null) continue; // A guild disappeared...?
+ await guild.DownloadUsersAsync().ConfigureAwait(false); // We're already on a seperate thread, no need to use Task.Run
+ await Task.Delay(200, CancellationToken.None).ConfigureAwait(false); // Must delay, or else it seems to hang...
+ processed++;
+ }
+
+ if (processed > 100) Program.Log(nameof(BackgroundUserListLoad), $"Explicit user list request processed for {processed} guilds.");
+ }
+}
\ No newline at end of file
diff --git a/WorldTime.cs b/WorldTime.cs
index d8f7ab1..904e243 100644
--- a/WorldTime.cs
+++ b/WorldTime.cs
@@ -16,20 +16,15 @@ internal class WorldTime : IDisposable {
/// Number of seconds between each time the status task runs, in seconds.
///
#if DEBUG
- private const int StatusInterval = 20;
+ internal const int StatusInterval = 20;
#else
- private const int StatusInterval = 300;
+ internal const int StatusInterval = 300;
#endif
- ///
- /// Number of concurrent shard startups to happen on each check.
- /// This value is also used in .
- ///
- public const int MaxConcurrentOperations = 5;
-
private readonly Task _statusTask;
- private readonly CancellationTokenSource _mainCancel;
+ private readonly CancellationTokenSource _statusCancel;
private readonly IServiceProvider _services;
+ private readonly BackgroundUserListLoad _bgFetch;
internal Configuration Config { get; }
internal DiscordShardedClient DiscordClient => _services.GetRequiredService();
@@ -45,7 +40,9 @@ internal class WorldTime : IDisposable {
LogLevel = LogSeverity.Info,
DefaultRetryMode = RetryMode.RetryRatelimit,
MessageCacheSize = 0, // disable message cache
- GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages
+ GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers,
+ SuppressUnknownDispatchWarnings = true,
+ LogGatewayIntentWarnings = false
};
_services = new ServiceCollection()
.AddSingleton(new DiscordShardedClient(clientConf))
@@ -54,15 +51,16 @@ internal class WorldTime : IDisposable {
.BuildServiceProvider();
DiscordClient.Log += DiscordClient_Log;
DiscordClient.ShardReady += DiscordClient_ShardReady;
- DiscordClient.MessageReceived += DiscordClient_MessageReceived;
var iasrv = _services.GetRequiredService();
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
iasrv.SlashCommandExecuted += InteractionService_SlashCommandExecuted;
// Start status reporting thread
- _mainCancel = new CancellationTokenSource();
- _statusTask = Task.Factory.StartNew(StatusLoop, _mainCancel.Token,
+ _statusCancel = new CancellationTokenSource();
+ _statusTask = Task.Factory.StartNew(StatusLoop, _statusCancel.Token,
TaskCreationOptions.LongRunning, TaskScheduler.Default);
+
+ _bgFetch = new(_services);
}
public async Task StartAsync() {
@@ -72,26 +70,24 @@ internal class WorldTime : IDisposable {
}
public void Dispose() {
- _mainCancel.Cancel();
+ _statusCancel.Cancel();
_statusTask.Wait(10000);
if (!_statusTask.IsCompleted)
Program.Log(nameof(WorldTime), "Warning: Main thread did not cleanly finish up in time. Continuing...");
- _mainCancel.Cancel();
- _statusTask.Wait(5000);
- _mainCancel.Dispose();
+ _statusCancel.Dispose();
Program.Log(nameof(WorldTime), $"Uptime: {Program.BotUptime}");
}
private async Task StatusLoop() {
try {
- await Task.Delay(30000, _mainCancel.Token).ConfigureAwait(false); // initial 30 second delay
- while (!_mainCancel.IsCancellationRequested) {
+ await Task.Delay(30000, _statusCancel.Token).ConfigureAwait(false); // initial 30 second delay
+ while (!_statusCancel.IsCancellationRequested) {
Program.Log(nameof(WorldTime), $"Bot uptime: {Program.BotUptime}");
- await PeriodicReport(DiscordClient.CurrentUser.Id, DiscordClient.Guilds.Count, _mainCancel.Token).ConfigureAwait(false);
- await Task.Delay(StatusInterval * 1000, _mainCancel.Token).ConfigureAwait(false);
+ await PeriodicReport(DiscordClient.CurrentUser.Id, DiscordClient.Guilds.Count, _statusCancel.Token).ConfigureAwait(false);
+ await Task.Delay(StatusInterval * 1000, _statusCancel.Token).ConfigureAwait(false);
}
} catch (TaskCanceledException) { }
}
@@ -173,25 +169,6 @@ internal class WorldTime : IDisposable {
#endif
}
- ///
- /// Non-specific handler for incoming events.
- ///
- private async Task DiscordClient_MessageReceived(SocketMessage message) {
- if (message.Author.IsWebhook) return;
- if (message.Type != MessageType.Default) return;
- if (message.Channel is not SocketTextChannel channel) return;
-
- // Proactively fill guild user cache if the bot has any data for the respective guild
- // Can skip an extra query if the last_seen update is known to have been successful, otherwise query for any users
- if (!channel.Guild.HasAllMembers) {
- using var db = _services.GetRequiredService();
- if (db.HasAnyUsers(channel.Guild)) {
- // Event handler hangs if awaited normally or used with Task.Run
- await Task.Factory.StartNew(channel.Guild.DownloadUsersAsync);
- }
- }
- }
-
const string InternalError = ":x: An unknown error occurred. If it persists, please notify the bot owner.";
// Slash command preparation and invocation