mirror of
https://github.com/NoiTheCat/WorldTime.git
synced 2024-11-21 14:34:36 +00:00
Add proactive selective auto user downloading
This amounts to being a workaround, as Discord.Net's option is still broken for large bots.
This commit is contained in:
parent
13e48c91b3
commit
ad756ae83b
3 changed files with 75 additions and 12 deletions
38
Commands.cs
38
Commands.cs
|
@ -15,6 +15,7 @@ internal class Commands {
|
||||||
const string ErrInvalidZone = ":x: Not a valid zone name."
|
const string ErrInvalidZone = ":x: Not a valid zone name."
|
||||||
+ " To find your time zone, refer to: <https://kevinnovak.github.io/Time-Zone-Picker/>.";
|
+ " To find your time zone, refer to: <https://kevinnovak.github.io/Time-Zone-Picker/>.";
|
||||||
const string ErrTargetUserNotFound = ":x: Unable to find the target user.";
|
const string ErrTargetUserNotFound = ":x: Unable to find the target user.";
|
||||||
|
const string ErrNoUserCache = ":warning: Please try the command again.";
|
||||||
const int MaxSingleLineLength = 750;
|
const int MaxSingleLineLength = 750;
|
||||||
const int MaxSingleOutputLength = 900;
|
const int MaxSingleOutputLength = 900;
|
||||||
|
|
||||||
|
@ -102,6 +103,11 @@ internal class Commands {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CmdList(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message) {
|
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);
|
var wspl = message.Content.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (wspl.Length == 2) {
|
if (wspl.Length == 2) {
|
||||||
// Has parameter - do specific user lookup
|
// Has parameter - do specific user lookup
|
||||||
|
@ -206,6 +212,11 @@ internal class Commands {
|
||||||
await channel.SendMessageAsync(":x: You must specify a time zone to apply to the user.").ConfigureAwait(false);
|
await channel.SendMessageAsync(":x: You must specify a time zone to apply to the user.").ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await AreUsersDownloadedAsync(channel.Guild).ConfigureAwait(false)) {
|
||||||
|
await channel.SendMessageAsync(ErrNoUserCache).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var targetuser = ResolveUserParameter(channel.Guild, wspl[1]);
|
var targetuser = ResolveUserParameter(channel.Guild, wspl[1]);
|
||||||
if (targetuser == null) {
|
if (targetuser == null) {
|
||||||
await channel.SendMessageAsync(ErrTargetUserNotFound).ConfigureAwait(false);
|
await channel.SendMessageAsync(ErrTargetUserNotFound).ConfigureAwait(false);
|
||||||
|
@ -236,6 +247,11 @@ internal class Commands {
|
||||||
await channel.SendMessageAsync(":x: You must specify a user for whom to remove time zone data.").ConfigureAwait(false);
|
await channel.SendMessageAsync(":x: You must specify a user for whom to remove time zone data.").ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await AreUsersDownloadedAsync(channel.Guild).ConfigureAwait(false)) {
|
||||||
|
await channel.SendMessageAsync(ErrNoUserCache).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var targetuser = ResolveUserParameter(channel.Guild, wspl[1]);
|
var targetuser = ResolveUserParameter(channel.Guild, wspl[1]);
|
||||||
if (targetuser == null) {
|
if (targetuser == null) {
|
||||||
await channel.SendMessageAsync(ErrTargetUserNotFound).ConfigureAwait(false);
|
await channel.SendMessageAsync(ErrTargetUserNotFound).ConfigureAwait(false);
|
||||||
|
@ -304,9 +320,6 @@ internal class Commands {
|
||||||
/// Given parameter input, attempts to find the corresponding SocketGuildUser.
|
/// Given parameter input, attempts to find the corresponding SocketGuildUser.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static SocketGuildUser? ResolveUserParameter(SocketGuild guild, string input) {
|
private static SocketGuildUser? ResolveUserParameter(SocketGuild guild, string input) {
|
||||||
// if (!guild.HasAllMembers) await guild.DownloadUsersAsync().ConfigureAwait(false);
|
|
||||||
// TODO selective user downloading - see here when implementing
|
|
||||||
|
|
||||||
// Try interpreting as ID
|
// Try interpreting as ID
|
||||||
var match = _userMention.Match(input);
|
var match = _userMention.Match(input);
|
||||||
string idcheckstr = match.Success ? match.Groups[1].Value : input;
|
string idcheckstr = match.Success ? match.Groups[1].Value : input;
|
||||||
|
@ -333,5 +346,24 @@ internal class Commands {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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 (guild.HasAllMembers) return true;
|
||||||
|
else {
|
||||||
|
// Event handler hangs if awaited normally or used with Task.Run
|
||||||
|
await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
27
Database.cs
27
Database.cs
|
@ -8,6 +8,7 @@ namespace WorldTime;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class Database {
|
internal class Database {
|
||||||
private const string UserDatabase = "userdata";
|
private const string UserDatabase = "userdata";
|
||||||
|
private const string CutoffInterval = "INTERVAL '30 days'"; // TODO make configurable?
|
||||||
|
|
||||||
private readonly string _connectionString;
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
@ -38,6 +39,25 @@ internal class Database {
|
||||||
await c.ExecuteNonQueryAsync().ConfigureAwait(false);
|
await c.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a given guild contains at least one user data entry with recent enough activity.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> HasAnyAsync(SocketGuild guild) {
|
||||||
|
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
||||||
|
using var c = db.CreateCommand();
|
||||||
|
c.CommandText = $@"
|
||||||
|
SELECT true FROM {UserDatabase}
|
||||||
|
WHERE
|
||||||
|
guild_id = @Gid
|
||||||
|
AND last_active >= now() - {CutoffInterval}
|
||||||
|
LIMIT 1
|
||||||
|
";
|
||||||
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guild.Id;
|
||||||
|
await c.PrepareAsync().ConfigureAwait(false);
|
||||||
|
using var r = await c.ExecuteReaderAsync().ConfigureAwait(false);
|
||||||
|
return await r.ReadAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the number of unique time zones in the database.
|
/// Gets the number of unique time zones in the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -51,7 +71,8 @@ internal class Database {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the last activity field for the specified guild user, if existing in the database.
|
/// Updates the last activity field for the specified guild user, if existing in the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task UpdateLastActivityAsync(SocketGuildUser user) {
|
/// <returns>True if a value was updated, implying that the specified user exists in the database.</returns>
|
||||||
|
public async Task<bool> UpdateLastActivityAsync(SocketGuildUser user) {
|
||||||
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
using var db = await OpenConnectionAsync().ConfigureAwait(false);
|
||||||
using var c = db.CreateCommand();
|
using var c = db.CreateCommand();
|
||||||
c.CommandText = $"UPDATE {UserDatabase} SET last_active = now() " +
|
c.CommandText = $"UPDATE {UserDatabase} SET last_active = now() " +
|
||||||
|
@ -59,7 +80,7 @@ internal class Database {
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)user.Guild.Id;
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)user.Guild.Id;
|
||||||
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)user.Id;
|
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)user.Id;
|
||||||
await c.PrepareAsync().ConfigureAwait(false);
|
await c.PrepareAsync().ConfigureAwait(false);
|
||||||
await c.ExecuteNonQueryAsync().ConfigureAwait(false);
|
return await c.ExecuteNonQueryAsync().ConfigureAwait(false) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO remove data from users with very distant last activity. how long ago?
|
// TODO remove data from users with very distant last activity. how long ago?
|
||||||
|
@ -124,7 +145,7 @@ internal class Database {
|
||||||
SELECT zone, user_id FROM {UserDatabase}
|
SELECT zone, user_id FROM {UserDatabase}
|
||||||
WHERE
|
WHERE
|
||||||
guild_id = @Gid
|
guild_id = @Gid
|
||||||
AND last_active >= now() - INTERVAL '30 days' -- TODO make configurable?
|
AND last_active >= now() - {CutoffInterval}
|
||||||
ORDER BY RANDOM() -- Randomize results for display purposes";
|
ORDER BY RANDOM() -- Randomize results for display purposes";
|
||||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
|
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
|
||||||
await c.PrepareAsync().ConfigureAwait(false);
|
await c.PrepareAsync().ConfigureAwait(false);
|
||||||
|
|
22
WorldTime.cs
22
WorldTime.cs
|
@ -44,7 +44,6 @@ 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
|
||||||
AlwaysDownloadUsers = true,
|
|
||||||
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages
|
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages
|
||||||
});
|
});
|
||||||
DiscordClient.Log += DiscordClient_Log;
|
DiscordClient.Log += DiscordClient_Log;
|
||||||
|
@ -150,7 +149,14 @@ internal class WorldTime : IDisposable {
|
||||||
|
|
||||||
private Task DiscordClient_ShardReady(DiscordSocketClient arg) => arg.SetGameAsync(Commands.CommandPrefix + "help");
|
private Task DiscordClient_ShardReady(DiscordSocketClient arg) => arg.SetGameAsync(Commands.CommandPrefix + "help");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Non-specific handler for incoming events.
|
||||||
|
/// </summary>
|
||||||
private async Task DiscordClient_MessageReceived(SocketMessage message) {
|
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;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* From https://support-dev.discord.com/hc/en-us/articles/4404772028055:
|
* From https://support-dev.discord.com/hc/en-us/articles/4404772028055:
|
||||||
* "You will still receive the events and can call the same APIs, and you'll get other data about a message like
|
* "You will still receive the events and can call the same APIs, and you'll get other data about a message like
|
||||||
|
@ -159,13 +165,17 @@ internal class WorldTime : IDisposable {
|
||||||
*
|
*
|
||||||
* Assuming this stays true, it will be possible to maintain legacy behavior after this bot loses full access to incoming messages.
|
* Assuming this stays true, it will be possible to maintain legacy behavior after this bot loses full access to incoming messages.
|
||||||
*/
|
*/
|
||||||
|
// Attempt to update user's last_seen column
|
||||||
// POTENTIAL BUG: If user does a list command, the list may be processed before their own time's refreshed, and they may be skipped.
|
// POTENTIAL BUG: If user does a list command, the list may be processed before their own time's refreshed, and they may be skipped.
|
||||||
if (message.Author.IsWebhook) return;
|
var hasMemberHint = await Database.UpdateLastActivityAsync((SocketGuildUser)message.Author).ConfigureAwait(false);
|
||||||
if (message.Type != MessageType.Default) return;
|
|
||||||
if (message.Channel is not SocketTextChannel) return;
|
|
||||||
|
|
||||||
await Database.UpdateLastActivityAsync((SocketGuildUser)message.Author).ConfigureAwait(false);
|
// 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
|
||||||
|
var guild = channel.Guild;
|
||||||
|
if (!guild.HasAllMembers && (hasMemberHint || await Database.HasAnyAsync(guild).ConfigureAwait(false))) {
|
||||||
|
// Event handler hangs if awaited normally or used with Task.Run
|
||||||
|
await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue