diff --git a/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs b/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs index a2eaaa179..0b6dc0e85 100644 --- a/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs +++ b/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs @@ -4,11 +4,60 @@ using LinqToDB.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using NadekoBot.Db.Models; using NadekoBot.Services.Database; +using System.Collections.Immutable; namespace NadekoBot.Db; public static class DiscordUserExtensions { + /// + /// Adds the specified to the database. If a database user with placeholder name + /// and discriminator is present in , their name and discriminator get updated accordingly. + /// + /// This database context. + /// The users to add or update in the database. + /// A tuple with the amount of new users added and old users updated. + public static async Task<(long UsersAdded, long UsersUpdated)> RefreshUsersAsync(this NadekoContext ctx, List users) + { + var presentDbUsers = await ctx.DiscordUser + .Select(x => new { x.UserId, x.Username, x.Discriminator }) + .Where(x => users.Select(y => y.Id).Contains(x.UserId)) + .ToArrayAsyncEF(); + + var usersToAdd = users + .Where(x => !presentDbUsers.Select(x => x.UserId).Contains(x.Id)) + .Select(x => new DiscordUser() + { + UserId = x.Id, + AvatarId = x.AvatarId, + Username = x.Username, + Discriminator = x.Discriminator + }); + + var added = (await ctx.BulkCopyAsync(usersToAdd)).RowsCopied; + var toUpdateUserIds = presentDbUsers + .Where(x => x.Username == "Unknown" && x.Discriminator == "????") + .Select(x => x.UserId) + .ToArray(); + + foreach (var user in users.Where(x => toUpdateUserIds.Contains(x.Id))) + { + await ctx.DiscordUser + .Where(x => x.UserId == user.Id) + .UpdateAsync(x => new DiscordUser() + { + Username = user.Username, + Discriminator = user.Discriminator, + + // .award tends to set AvatarId and DateAdded to NULL, so account for that. + AvatarId = user.AvatarId, + DateAdded = x.DateAdded ?? DateTime.UtcNow + }); + } + + return (added, toUpdateUserIds.Length); + } + public static Task GetByUserIdAsync( this IQueryable set, ulong userId) diff --git a/src/NadekoBot/Modules/Administration/Self/SelfCommands.cs b/src/NadekoBot/Modules/Administration/Self/SelfCommands.cs index fb81c86bd..e76c49ba9 100644 --- a/src/NadekoBot/Modules/Administration/Self/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/Self/SelfCommands.cs @@ -1,5 +1,6 @@ #nullable disable using Nadeko.Medusa; +using NadekoBot.Db; using NadekoBot.Modules.Administration.Services; using NadekoBot.Services.Database.Models; @@ -22,19 +23,53 @@ public partial class Administration private readonly IBotStrings _strings; private readonly IMedusaLoaderService _medusaLoader; private readonly ICoordinator _coord; + private readonly DbService _db; public SelfCommands( DiscordSocketClient client, + DbService db, IBotStrings strings, ICoordinator coord, IMedusaLoaderService medusaLoader) { _client = client; + _db = db; _strings = strings; _coord = coord; _medusaLoader = medusaLoader; } + + [Cmd] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public Task CacheUsers() + => CacheUsers(ctx.Guild); + + [Cmd] + [OwnerOnly] + public async Task CacheUsers(IGuild guild) + { + var downloadUsersTask = guild.DownloadUsersAsync(); + var message = await ReplyPendingLocalizedAsync(strs.cache_users_pending); + using var dbContext = _db.GetDbContext(); + + await downloadUsersTask; + + var users = (await guild.GetUsersAsync(CacheMode.CacheOnly)) + .Cast() + .ToList(); + + var (added, updated) = await dbContext.RefreshUsersAsync(users); + + await message.ModifyAsync(x => + x.Embed = _eb.Create() + .WithDescription(GetText(strs.cache_users_done(added, updated))) + .WithOkColor() + .Build() + ); + } + [Cmd] [OwnerOnly] public async Task DoAs(IUser user, [Leftover] string message) diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index 1efc29abe..5b7957e4a 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -1390,4 +1390,6 @@ autopublish: - autopublish doas: - doas - - execas \ No newline at end of file + - execas +cacheusers: + - cacheusers \ No newline at end of file diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index 2caec755d..8023f3180 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -2363,3 +2363,8 @@ doas: desc: "Execute the command as if you were the target user. Requires bot ownership and server administrator permission." args: - "@Thief .give all @Admin" +cacheusers: + desc: Caches users of a Discord server and saves them to the database. + args: + - "" + - "serverId" \ No newline at end of file diff --git a/src/NadekoBot/data/strings/responses/responses.en-US.json b/src/NadekoBot/data/strings/responses/responses.en-US.json index 77d3ea9b0..50a16a795 100644 --- a/src/NadekoBot/data/strings/responses/responses.en-US.json +++ b/src/NadekoBot/data/strings/responses/responses.en-US.json @@ -1058,5 +1058,7 @@ "sticker_missing_name": "Please specify a name for the sticker.", "thread_deleted": "Thread Deleted", "thread_created": "Thread Created", - "supported_languages": "Supported Languages" + "supported_languages": "Supported Languages", + "cache_users_pending": "Updating users, please wait...", + "cache_users_done": "{0} users were added and {1} users were updated." }