Merge branch 'main' into dev

This commit is contained in:
Noi 2022-12-30 22:04:45 -08:00
commit 7c0c22a194
4 changed files with 85 additions and 453 deletions

64
BackgroundUserListLoad.cs Normal file
View file

@ -0,0 +1,64 @@
using Microsoft.Extensions.DependencyInjection;
using WorldTime.Data;
namespace WorldTime;
/// <summary>
/// Proactively fills the user cache for guilds in which any time zone configuration exists.
/// </summary>
/// <remarks>Modeled after BirthdayBot's similar feature.</remarks>
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<DiscordShardedClient>().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<BotDatabaseContext>();
// 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.");
}
}

View file

@ -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<string, Command> _commands;
private readonly IServiceProvider _services;
private readonly WorldTime _instance;
private BotDatabaseContext Database => _services.GetRequiredService<BotDatabaseContext>();
private static readonly Regex _userExplicit;
private static readonly Regex _userMention;
private static readonly ReadOnlyDictionary<string, string> _tzNameMap;
private const string ErrInvalidZone = ":x: Not a valid zone name."
+ " To find your time zone, refer to: <https://kevinnovak.github.io/Time-Zone-Picker/>.";
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<string, string> 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<string, List<ulong>>();
foreach ((string area, List<ulong> 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<ulong>());
sortedlist[areaprint].AddRange(users);
}
// Build zone listings with users
var outputlines = new List<string>();
foreach ((string area, List<ulong> 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}.");
}
/// <summary>
/// Given parameter input, attempts to find the corresponding SocketGuildUser.
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
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<g>')'", DateTimeFormatInfo.InvariantInfo);
return $"{sortpfx}● `{fullstr}`";
}
/// <summary>
/// Checks given time zone input. Returns a valid string for use with NodaTime, or null.
/// </summary>
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;
}
/// <summary>
/// Formats a user's name to a consistent, readable format which makes use of their nickname.
/// </summary>
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}";
}
/// <summary>
/// Checks if the given user can be considered a guild admin ('Manage Server' is set).
/// </summary>
private static bool IsUserAdmin(SocketGuildUser user)
=> user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild;
/// <summary>
/// Checks if the member cache for the specified guild needs to be filled, and sends a request if needed.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>
/// True if the guild's members are already downloaded. If false, the command handler must notify the user.
/// </returns>
private static async Task<bool> 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;
}
}
/// <summary>
/// An alternative to <see cref="SocketGuild.HasAllMembers"/>.
/// Returns true if *most* members have been downloaded.
/// Used as a workaround check due to Discord.Net occasionally unable to actually download all members.
/// </summary>
/// <remarks>Copied directly from BirthdayBot. Try to coordinate changes between projects...</remarks>
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
}

View file

@ -16,22 +16,15 @@ internal class WorldTime : IDisposable {
/// Number of seconds between each time the status task runs, in seconds. /// Number of seconds between each time the status task runs, in seconds.
/// </summary> /// </summary>
#if DEBUG #if DEBUG
private const int StatusInterval = 20; internal const int StatusInterval = 20;
#else #else
private const int StatusInterval = 300; internal const int StatusInterval = 300;
#endif #endif
/// <summary>
/// Number of concurrent shard startups to happen on each check.
/// This value is also used in <see cref="DataRetention"/>.
/// </summary>
public const int MaxConcurrentOperations = 5;
private readonly Task _statusTask; private readonly Task _statusTask;
private readonly CancellationTokenSource _mainCancel; private readonly CancellationTokenSource _statusCancel;
private readonly CommandsText _commandsTxt;
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
private readonly HashSet<ulong> _aotUserDownloadChecked = new(); private readonly BackgroundUserListLoad _bgFetch;
internal Configuration Config { get; } internal Configuration Config { get; }
internal DiscordShardedClient DiscordClient => _services.GetRequiredService<DiscordShardedClient>(); internal DiscordShardedClient DiscordClient => _services.GetRequiredService<DiscordShardedClient>();
@ -47,7 +40,7 @@ internal class WorldTime : IDisposable {
LogLevel = LogSeverity.Info, LogLevel = LogSeverity.Info,
DefaultRetryMode = RetryMode.RetryRatelimit, DefaultRetryMode = RetryMode.RetryRatelimit,
MessageCacheSize = 0, // disable message cache MessageCacheSize = 0, // disable message cache
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages, GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers,
SuppressUnknownDispatchWarnings = true, SuppressUnknownDispatchWarnings = true,
LogGatewayIntentWarnings = false LogGatewayIntentWarnings = false
}; };
@ -58,17 +51,16 @@ internal class WorldTime : IDisposable {
.BuildServiceProvider(); .BuildServiceProvider();
DiscordClient.Log += DiscordClient_Log; DiscordClient.Log += DiscordClient_Log;
DiscordClient.ShardReady += DiscordClient_ShardReady; DiscordClient.ShardReady += DiscordClient_ShardReady;
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
var iasrv = _services.GetRequiredService<InteractionService>(); var iasrv = _services.GetRequiredService<InteractionService>();
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated; DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
iasrv.SlashCommandExecuted += InteractionService_SlashCommandExecuted; iasrv.SlashCommandExecuted += InteractionService_SlashCommandExecuted;
_commandsTxt = new CommandsText(this, _services);
// Start status reporting thread // Start status reporting thread
_mainCancel = new CancellationTokenSource(); _statusCancel = new CancellationTokenSource();
_statusTask = Task.Factory.StartNew(StatusLoop, _mainCancel.Token, _statusTask = Task.Factory.StartNew(StatusLoop, _statusCancel.Token,
TaskCreationOptions.LongRunning, TaskScheduler.Default); TaskCreationOptions.LongRunning, TaskScheduler.Default);
_bgFetch = new(_services);
} }
public async Task StartAsync() { public async Task StartAsync() {
@ -78,26 +70,24 @@ internal class WorldTime : IDisposable {
} }
public void Dispose() { public void Dispose() {
_mainCancel.Cancel(); _statusCancel.Cancel();
_statusTask.Wait(10000); _statusTask.Wait(10000);
if (!_statusTask.IsCompleted) if (!_statusTask.IsCompleted)
Program.Log(nameof(WorldTime), "Warning: Main thread did not cleanly finish up in time. Continuing..."); Program.Log(nameof(WorldTime), "Warning: Main thread did not cleanly finish up in time. Continuing...");
_mainCancel.Cancel(); _statusCancel.Dispose();
_statusTask.Wait(5000);
_mainCancel.Dispose();
Program.Log(nameof(WorldTime), $"Uptime: {Program.BotUptime}"); Program.Log(nameof(WorldTime), $"Uptime: {Program.BotUptime}");
} }
private async Task StatusLoop() { private async Task StatusLoop() {
try { try {
await Task.Delay(30000, _mainCancel.Token).ConfigureAwait(false); // initial 30 second delay await Task.Delay(30000, _statusCancel.Token).ConfigureAwait(false); // initial 30 second delay
while (!_mainCancel.IsCancellationRequested) { while (!_statusCancel.IsCancellationRequested) {
Program.Log(nameof(WorldTime), $"Bot uptime: {Program.BotUptime}"); Program.Log(nameof(WorldTime), $"Bot uptime: {Program.BotUptime}");
await PeriodicReport(DiscordClient.CurrentUser.Id, DiscordClient.Guilds.Count, _mainCancel.Token).ConfigureAwait(false); await PeriodicReport(DiscordClient.CurrentUser.Id, DiscordClient.Guilds.Count, _statusCancel.Token).ConfigureAwait(false);
await Task.Delay(StatusInterval * 1000, _mainCancel.Token).ConfigureAwait(false); await Task.Delay(StatusInterval * 1000, _statusCancel.Token).ConfigureAwait(false);
} }
} catch (TaskCanceledException) { } } catch (TaskCanceledException) { }
} }
@ -159,9 +149,6 @@ internal class WorldTime : IDisposable {
} }
private async Task DiscordClient_ShardReady(DiscordSocketClient arg) { 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 #if !DEBUG
// Update slash/interaction commands // Update slash/interaction commands
if (arg.ShardId == 0) { if (arg.ShardId == 0) {
@ -182,27 +169,6 @@ internal class WorldTime : IDisposable {
#endif #endif
} }
/// <summary>
/// Non-specific handler for incoming events.
/// </summary>
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
lock (_aotUserDownloadChecked) {
if (!_aotUserDownloadChecked.Add(channel.Guild.Id)) return; // ...once. Just once. Not all the time.
}
if (!channel.Guild.HasAllMembers) {
using var db = _services.GetRequiredService<BotDatabaseContext>();
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."; const string InternalError = ":x: An unknown error occurred. If it persists, please notify the bot owner.";
// Slash command preparation and invocation // Slash command preparation and invocation

View file

@ -11,18 +11,18 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.7.2" /> <PackageReference Include="Discord.Net" Version="3.8.0" />
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" /> <PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NodaTime" Version="3.1.0" /> <PackageReference Include="NodaTime" Version="3.1.2" />
<PackageReference Include="Npgsql" Version="6.0.5" /> <PackageReference Include="Npgsql" Version="6.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.5" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
</ItemGroup> </ItemGroup>