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."
|
||||
+ " 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 ErrNoUserCache = ":warning: Please try the command again.";
|
||||
const int MaxSingleLineLength = 750;
|
||||
const int MaxSingleOutputLength = 900;
|
||||
|
||||
|
@ -102,6 +103,11 @@ internal class Commands {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -206,6 +212,11 @@ internal class Commands {
|
|||
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);
|
||||
|
@ -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);
|
||||
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);
|
||||
|
@ -304,9 +320,6 @@ internal class Commands {
|
|||
/// Given parameter input, attempts to find the corresponding SocketGuildUser.
|
||||
/// </summary>
|
||||
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
|
||||
var match = _userMention.Match(input);
|
||||
string idcheckstr = match.Success ? match.Groups[1].Value : input;
|
||||
|
@ -333,5 +346,24 @@ internal class Commands {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
27
Database.cs
27
Database.cs
|
@ -8,6 +8,7 @@ namespace WorldTime;
|
|||
/// </summary>
|
||||
internal class Database {
|
||||
private const string UserDatabase = "userdata";
|
||||
private const string CutoffInterval = "INTERVAL '30 days'"; // TODO make configurable?
|
||||
|
||||
private readonly string _connectionString;
|
||||
|
||||
|
@ -38,6 +39,25 @@ internal class Database {
|
|||
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>
|
||||
/// Gets the number of unique time zones in the database.
|
||||
/// </summary>
|
||||
|
@ -51,7 +71,8 @@ internal class Database {
|
|||
/// <summary>
|
||||
/// Updates the last activity field for the specified guild user, if existing in the database.
|
||||
/// </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 c = db.CreateCommand();
|
||||
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("@Uid", NpgsqlDbType.Bigint).Value = (long)user.Id;
|
||||
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?
|
||||
|
@ -124,7 +145,7 @@ internal class Database {
|
|||
SELECT zone, user_id FROM {UserDatabase}
|
||||
WHERE
|
||||
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";
|
||||
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
|
||||
await c.PrepareAsync().ConfigureAwait(false);
|
||||
|
|
22
WorldTime.cs
22
WorldTime.cs
|
@ -44,7 +44,6 @@ internal class WorldTime : IDisposable {
|
|||
LogLevel = LogSeverity.Info,
|
||||
DefaultRetryMode = RetryMode.RetryRatelimit,
|
||||
MessageCacheSize = 0, // disable message cache
|
||||
AlwaysDownloadUsers = true,
|
||||
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages
|
||||
});
|
||||
DiscordClient.Log += DiscordClient_Log;
|
||||
|
@ -150,7 +149,14 @@ internal class WorldTime : IDisposable {
|
|||
|
||||
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) {
|
||||
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:
|
||||
* "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.
|
||||
*/
|
||||
|
||||
// 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.
|
||||
if (message.Author.IsWebhook) return;
|
||||
if (message.Type != MessageType.Default) return;
|
||||
if (message.Channel is not SocketTextChannel) return;
|
||||
var hasMemberHint = await Database.UpdateLastActivityAsync((SocketGuildUser)message.Author).ConfigureAwait(false);
|
||||
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue