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/CommandsText.cs b/CommandsText.cs
deleted file mode 100644
index 8fa5e67..0000000
--- a/CommandsText.cs
+++ /dev/null
@@ -1,398 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using NodaTime;
-using System.Collections.ObjectModel;
-using System.Globalization;
-using System.Text;
-using System.Text.RegularExpressions;
-using WorldTime.Data;
-
-namespace WorldTime;
-[Obsolete("Text commands are deprecated and will be removed soon.")]
-internal class CommandsText {
-#if DEBUG
- public const string CommandPrefix = "tt.";
-#else
- public const string CommandPrefix = "tz.";
-#endif
- delegate Task Command(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message);
-
- private readonly Dictionary _commands;
- private readonly IServiceProvider _services;
- private readonly WorldTime _instance;
-
- private BotDatabaseContext Database => _services.GetRequiredService();
-
- private static readonly Regex _userExplicit;
- private static readonly Regex _userMention;
- private static readonly ReadOnlyDictionary _tzNameMap;
-
- private const string ErrInvalidZone = ":x: Not a valid zone name."
- + " To find your time zone, refer to: .";
- private const string ErrTargetUserNotFound = ":x: Unable to find the target user.";
- private const string ErrNoUserCache = ":warning: Please try the command again.";
- private const int MaxSingleLineLength = 750;
- private const int MaxSingleOutputLength = 900;
-
- static CommandsText() {
- _userExplicit = new Regex(@"(.+)#(\d{4})", RegexOptions.Compiled);
- _userMention = new Regex(@"\!?(\d+)>", RegexOptions.Compiled);
-
- Dictionary tzNameMap = new(StringComparer.OrdinalIgnoreCase);
- foreach (var name in DateTimeZoneProviders.Tzdb.Ids) tzNameMap.Add(name, name);
- _tzNameMap = new(tzNameMap);
- }
-
- public CommandsText(WorldTime inst, IServiceProvider services) {
- _instance = inst;
- _services = services;
- _commands = new(StringComparer.OrdinalIgnoreCase) {
- { "help", CmdHelp },
- { "list", CmdList },
- { "set", CmdSet },
- { "remove", CmdRemove },
- { "setfor", CmdSetFor },
- { "removefor", CmdRemoveFor }
- };
-
- inst.DiscordClient.MessageReceived += CommandDispatch;
- }
-
- private async Task CommandDispatch(SocketMessage message) {
- if (message.Author.IsBot || message.Author.IsWebhook) return;
- if (message.Type != MessageType.Default) return;
- if (message.Channel is not SocketTextChannel channel) return; // not handling DMs
-
- var msgsplit = message.Content.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
- if (msgsplit.Length == 0 || msgsplit[0].Length < 4) return;
- if (msgsplit[0].StartsWith(CommandPrefix, StringComparison.OrdinalIgnoreCase)) {
- var cmdBase = msgsplit[0][3..];
- if (_commands.ContainsKey(cmdBase)) {
- Program.Log("Command invoked", $"{channel.Guild.Name}/{message.Author} {message.Content}");
- try {
- await _commands[cmdBase](channel, (SocketGuildUser)message.Author, message).ConfigureAwait(false);
- } catch (Exception ex) {
- Program.Log("Command invoked", ex.ToString());
- }
- }
- }
- }
-
- private async Task CmdHelp(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
- using var db = Database;
- var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
- var guildct = _instance.DiscordClient.Guilds.Count;
- var uniquetz = db.GetDistinctZoneCount();
- await channel.SendMessageAsync(embed: new EmbedBuilder() {
- Color = new Color(0xe0f2f7),
- Title = "Help & About",
- Description = $"World Time v{version} - Serving {guildct} communities across {uniquetz} time zones.\n\n"
- + "This bot is provided for free, without any paywalled 'premium' features. "
- + "If you've found this bot useful, please consider contributing via the "
- + "bot author's page on Ko-fi: https://ko-fi.com/noithecat.",
- Footer = new EmbedFooterBuilder() {
- IconUrl = _instance.DiscordClient.CurrentUser.GetAvatarUrl(),
- Text = "World Time"
- }
- }.AddField(inline: false, name: "Commands", value:
- $"`{CommandPrefix}help` - This message.\n" +
- $"`{CommandPrefix}list` - Displays current times for all recently active known users.\n" +
- $"`{CommandPrefix}list [user]` - Displays the current time for the given *user*.\n" +
- $"`{CommandPrefix}set [zone]` - Registers or updates your *zone* with the bot.\n" +
- $"`{CommandPrefix}remove` - Removes your name from this bot."
- ).AddField(inline: false, name: "Admin commands", value:
- $"`{CommandPrefix}setFor [user] [zone]` - Sets the time zone for another user.\n" +
- $"`{CommandPrefix}removeFor [user]` - Removes another user's information."
- ).AddField(inline: false, name: "Zones", value:
- "This bot accepts zone names from the IANA Time Zone Database (a.k.a. Olson Database). " +
- "A useful tool to determine yours can be found at: https://kevinnovak.github.io/Time-Zone-Picker/"
- ).Build());
- }
-
- private async Task CmdList(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
- if (!await AreUsersDownloadedAsync(channel.Guild).ConfigureAwait(false)) {
- await channel.SendMessageAsync(ErrNoUserCache).ConfigureAwait(false);
- return;
- }
-
- var wspl = message.Content.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
- if (wspl.Length == 2) {
- // Has parameter - do specific user lookup
- var usersearch = ResolveUserParameter(channel.Guild, wspl[1]);
- if (usersearch == null) {
- await channel.SendMessageAsync(":x: Cannot find the specified user.").ConfigureAwait(false);
- return;
- }
-
- using var db = Database;
- var result = db.GetUserZone(usersearch);
- if (result == null) {
- bool isself = sender.Id == usersearch.Id;
- if (isself) await channel.SendMessageAsync(":x: You do not have a time zone. Set it with `tz.set`.").ConfigureAwait(false);
- else await channel.SendMessageAsync(":x: The given user does not have a time zone set.").ConfigureAwait(false);
- return;
- }
-
- var resulttext = TzPrint(result)[4..] + ": " + FormatName(usersearch);
- await channel.SendMessageAsync(embed: new EmbedBuilder().WithDescription(resulttext).Build()).ConfigureAwait(false);
- } else {
- // Does not have parameter - build full list
- using var db = Database;
- var userlist = db.GetGuildZones(channel.Guild.Id);
- if (userlist.Count == 0) {
- await channel.SendMessageAsync(":x: Nothing to show. " +
- $"To register time zones with the bot, use the `{CommandPrefix}set` command.").ConfigureAwait(false);
- return;
- }
-
- // Order times by popularity to limit how many are shown, group by printed name
- var sortedlist = new SortedDictionary>();
- foreach ((string area, List users) in userlist.OrderByDescending(o => o.Value.Count).Take(20)) {
- // Filter further to top 20 distinct timezones, even if they are not displayed in the final result
- var areaprint = TzPrint(area);
- if (!sortedlist.ContainsKey(areaprint)) sortedlist.Add(areaprint, new List());
- sortedlist[areaprint].AddRange(users);
- }
-
- // Build zone listings with users
- var outputlines = new List();
- foreach ((string area, List users) in sortedlist) {
- var buffer = new StringBuilder();
- buffer.Append(area[4..] + ": ");
- bool empty = true;
- foreach (var userid in users) {
- var userinstance = channel.Guild.GetUser(userid);
- if (userinstance == null) continue;
- if (empty) empty = !empty;
- else buffer.Append(", ");
- var useradd = FormatName(userinstance);
- if (buffer.Length + useradd.Length > MaxSingleLineLength) {
- buffer.Append("others...");
- break;
- } else buffer.Append(useradd);
- }
- if (!empty) outputlines.Add(buffer.ToString());
- }
-
- // Prepare for output - send buffers out if they become too large
- outputlines.Sort();
- var resultout = new StringBuilder();
- foreach (var line in outputlines) {
- if (resultout.Length + line.Length > MaxSingleOutputLength) {
- await channel.SendMessageAsync(
- embed: new EmbedBuilder().WithDescription(resultout.ToString()).Build()).ConfigureAwait(false);
- resultout.Clear();
- }
- if (resultout.Length > 0) resultout.AppendLine(); // avoids trailing newline by adding to the previous line
- resultout.Append(line);
- }
- if (resultout.Length > 0) {
- await channel.SendMessageAsync(
- embed: new EmbedBuilder().WithDescription(resultout.ToString()).Build()).ConfigureAwait(false);
- }
- }
- }
-
- private async Task CmdSet(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
- var wspl = message.Content.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
- if (wspl.Length == 1) {
- await channel.SendMessageAsync(":x: Zone parameter is required.").ConfigureAwait(false);
- return;
- }
- var input = ParseTimeZone(wspl[1]);
- if (input == null) {
- await channel.SendMessageAsync(ErrInvalidZone).ConfigureAwait(false);
- return;
- }
- using var db = Database;
- db.UpdateUser(sender, input);
- await channel.SendMessageAsync($":white_check_mark: Your time zone has been set to **{input}**.").ConfigureAwait(false);
- }
-
- private async Task CmdSetFor(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
- if (!IsUserAdmin(sender)) return;
-
- // Parameters: command, target, zone
- var wspl = message.Content.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries);
- if (wspl.Length == 1) {
- await channel.SendMessageAsync(":x: You must specify a user to set the time zone for.").ConfigureAwait(false);
- return;
- }
- if (wspl.Length == 2) {
- await channel.SendMessageAsync(":x: You must specify a time zone to apply to the user.").ConfigureAwait(false);
- return;
- }
-
- if (!await AreUsersDownloadedAsync(channel.Guild).ConfigureAwait(false)) {
- await channel.SendMessageAsync(ErrNoUserCache).ConfigureAwait(false);
- return;
- }
- var targetuser = ResolveUserParameter(channel.Guild, wspl[1]);
- if (targetuser == null) {
- await channel.SendMessageAsync(ErrTargetUserNotFound).ConfigureAwait(false);
- return;
- }
- var newtz = ParseTimeZone(wspl[2]);
- if (newtz == null) {
- await channel.SendMessageAsync(ErrInvalidZone).ConfigureAwait(false);
- return;
- }
-
- using var db = Database;
- db.UpdateUser(targetuser, newtz);
- await channel.SendMessageAsync($":white_check_mark: Time zone for **{targetuser}** set to **{newtz}**.").ConfigureAwait(false);
- }
-
- private async Task CmdRemove(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
- using var db = Database;
- var success = db.DeleteUser(sender);
- if (success) await channel.SendMessageAsync(":white_check_mark: Your zone has been removed.").ConfigureAwait(false);
- else await channel.SendMessageAsync(":x: You don't have a time zone set.");
- }
-
- private async Task CmdRemoveFor(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
- if (!IsUserAdmin(sender)) return;
-
- // Parameters: command, target
- var wspl = message.Content.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
- if (wspl.Length == 1) {
- await channel.SendMessageAsync(":x: You must specify a user for whom to remove time zone data.").ConfigureAwait(false);
- return;
- }
-
- if (!await AreUsersDownloadedAsync(channel.Guild).ConfigureAwait(false)) {
- await channel.SendMessageAsync(ErrNoUserCache).ConfigureAwait(false);
- return;
- }
- var targetuser = ResolveUserParameter(channel.Guild, wspl[1]);
- if (targetuser == null) {
- await channel.SendMessageAsync(ErrTargetUserNotFound).ConfigureAwait(false);
- return;
- }
-
- using var db = Database;
- db.DeleteUser(targetuser);
- await channel.SendMessageAsync($":white_check_mark: Removed zone information for {targetuser}.");
- }
-
- ///
- /// Given parameter input, attempts to find the corresponding SocketGuildUser.
- ///
- private static SocketGuildUser? ResolveUserParameter(SocketGuild guild, string input) {
- // Try interpreting as ID
- var match = _userMention.Match(input);
- string idcheckstr = match.Success ? match.Groups[1].Value : input;
- if (ulong.TryParse(idcheckstr, out var value)) return guild.GetUser(value);
-
- // Prepare if input looks like Username#Discriminator
- var @explicit = _userExplicit.Match(input);
-
- foreach (var user in guild.Users) {
- // Explicit match search
- if (@explicit.Success) {
- var username = @explicit.Groups[1].Value;
- var discriminator = @explicit.Groups[2].Value;
- if (string.Equals(user.Username, username, StringComparison.OrdinalIgnoreCase) && user.Discriminator == discriminator)
- return user;
- }
-
- // Nickname search
- if (user.Nickname != null && string.Equals(user.Nickname, input, StringComparison.OrdinalIgnoreCase)) return user;
-
- // Username search
- if (string.Equals(user.Username, input, StringComparison.OrdinalIgnoreCase)) return user;
- }
-
- return null;
- }
-
- #region Helper methods
- ///
- /// Returns a string displaying the current time in the given time zone.
- /// The result begins with four numbers for sorting purposes. Must be trimmed before output.
- ///
- private static string TzPrint(string zone) {
- var tzdb = DateTimeZoneProviders.Tzdb;
- DateTimeZone tz = tzdb.GetZoneOrNull(zone)!;
- if (tz == null) throw new Exception("Encountered unknown time zone: " + zone);
-
- var now = SystemClock.Instance.GetCurrentInstant().InZone(tz);
- var sortpfx = now.ToString("MMdd", DateTimeFormatInfo.InvariantInfo);
- var fullstr = now.ToString("dd'-'MMM' 'HH':'mm' 'x' (UTC'o')'", DateTimeFormatInfo.InvariantInfo);
- return $"{sortpfx}● `{fullstr}`";
- }
-
- ///
- /// Checks given time zone input. Returns a valid string for use with NodaTime, or null.
- ///
- private static string? ParseTimeZone(string tzinput) {
- if (tzinput.Equals("Asia/Calcutta", StringComparison.OrdinalIgnoreCase)) tzinput = "Asia/Kolkata";
- if (_tzNameMap.TryGetValue(tzinput, out var name)) return name;
- return null;
- }
-
- ///
- /// Formats a user's name to a consistent, readable format which makes use of their nickname.
- ///
- private static string FormatName(SocketGuildUser user) {
- static string escapeFormattingCharacters(string input) {
- var result = new StringBuilder();
- foreach (var c in input) {
- if (c is '\\' or '_' or '~' or '*' or '@') {
- result.Append('\\');
- }
- result.Append(c);
- }
- return result.ToString();
- }
-
- var username = escapeFormattingCharacters(user.Username);
- if (user.Nickname != null) {
- return $"**{escapeFormattingCharacters(user.Nickname)}** ({username}#{user.Discriminator})";
- }
- return $"**{username}**#{user.Discriminator}";
- }
-
- ///
- /// Checks if the given user can be considered a guild admin ('Manage Server' is set).
- ///
- private static bool IsUserAdmin(SocketGuildUser user)
- => user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild;
-
- ///
- /// Checks if the member cache for the specified guild needs to be filled, and sends a request if needed.
- ///
- ///
- /// Due to a quirk in Discord.Net, the user cache cannot be filled until the command handler is no longer executing,
- /// regardless of if the request runs on its own thread.
- ///
- ///
- /// True if the guild's members are already downloaded. If false, the command handler must notify the user.
- ///
- private static async Task AreUsersDownloadedAsync(SocketGuild guild) {
- if (HasMostMembersDownloaded(guild)) return true;
- else {
- // Event handler hangs if awaited normally or used with Task.Run
- await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false);
- return false;
- }
- }
-
- ///
- /// An alternative to .
- /// Returns true if *most* members have been downloaded.
- /// Used as a workaround check due to Discord.Net occasionally unable to actually download all members.
- ///
- /// Copied directly from BirthdayBot. Try to coordinate changes between projects...
- private 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)
- return guild.DownloadedMemberCount >= (int)(guild.MemberCount * 0.85);
- } else {
- // For smaller guilds, fail if two or more members are missing
- return guild.MemberCount - guild.DownloadedMemberCount <= 2;
- }
- }
- #endregion
-}
diff --git a/WorldTime.cs b/WorldTime.cs
index 289e589..904e243 100644
--- a/WorldTime.cs
+++ b/WorldTime.cs
@@ -16,21 +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 CommandsText _commandsTxt;
+ private readonly CancellationTokenSource _statusCancel;
private readonly IServiceProvider _services;
+ private readonly BackgroundUserListLoad _bgFetch;
internal Configuration Config { get; }
internal DiscordShardedClient DiscordClient => _services.GetRequiredService();
@@ -46,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))
@@ -55,17 +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;
- _commandsTxt = new CommandsText(this, _services);
-
// 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() {
@@ -75,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) { }
}
@@ -160,9 +153,6 @@ internal class WorldTime : IDisposable {
}
private async Task DiscordClient_ShardReady(DiscordSocketClient arg) {
- // TODO get rid of this eventually? or change it to something fun...
- await arg.SetGameAsync("/help");
-
#if !DEBUG
// Update slash/interaction commands
if (arg.ShardId == 0) {
@@ -179,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