diff --git a/.gitignore b/.gitignore index 5f29986..4a0baa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .github/ .vs/ TestResults +.vscode/ diff --git a/gex/Controllers/Api/UserApiController.cs b/gex/Controllers/Api/UserApiController.cs index 5b8dd1c..8c1aa69 100644 --- a/gex/Controllers/Api/UserApiController.cs +++ b/gex/Controllers/Api/UserApiController.cs @@ -318,6 +318,72 @@ public async Task>> Search(string search, return ApiOk(users); } + /// + /// get multiple users by usernames. A max of 50 usernames can be looked up at once. + /// This endpoint performs case-sensitive username matching. + /// + /// list of usernames to find + /// if will be populated. defaults to false + /// will previous names be searched against as well? defaults to false + /// cancellation token + /// + /// the response will contain a list of s that match any of the usernames + /// + /// + /// one or more usernames are empty, or no usernames provided + /// + [HttpPost("batch")] + public async Task>> GetUsersByUsernames( + [FromBody] List usernames, + [FromQuery] bool includeSkill = false, + [FromQuery] bool includePreviousNames = false, + CancellationToken cancel = default + ) { + + if (usernames == null || usernames.Count == 0) { + return ApiBadRequest>($"usernames list cannot be empty"); + } + + if (usernames.Count >= 50) { + return ApiBadRequest>($"usernames list cannot contain more than 50 items"); + } + + foreach (string username in usernames) { + if (string.IsNullOrWhiteSpace(username)) { + return ApiBadRequest>($"all usernames must be non-empty, found empty or whitespace-only username"); + } + } + + List apiUsers = await _UserRepository.GetByUsernames(usernames, includePreviousNames, cancel); + _Logger.LogInformation($"Found {apiUsers.Count} users matching usernames"); + + // Get unique users by UserID (in case there are duplicates from previous name searches) + apiUsers = apiUsers + .GroupBy(u => u.UserID) + .Select(g => g.First()) + .ToList(); + + // Fetch skills if requested + if (includeSkill == true && apiUsers.Count > 0) { + List userIds = apiUsers.Select(u => u.UserID).ToList(); + Dictionary> skillsDict = await _SkillDb.GetByUserIDs(userIds, cancel); + + // Populate skills for each user + foreach (ApiBarUser user in apiUsers) { + user.Skill = skillsDict.GetValueOrDefault(user.UserID) ?? new List(); + } + } + + // Fetch previous names if requested + if (includePreviousNames == true && apiUsers.Count > 0) { + foreach (ApiBarUser user in apiUsers) { + user.PreviousNames = await _UserRepository.GetUserNames(user.UserID, cancel); + } + } + + return ApiOk(apiUsers); + } + /// /// get the start spots of a user on a specific map, either by map name or map filename /// diff --git a/gex/Services/Db/UserStats/BarUserDb.cs b/gex/Services/Db/UserStats/BarUserDb.cs index ceaaa7d..6f25375 100644 --- a/gex/Services/Db/UserStats/BarUserDb.cs +++ b/gex/Services/Db/UserStats/BarUserDb.cs @@ -101,6 +101,34 @@ WHERE lower(u.username) LIKE lower(@Search) OR lower(p.user_name) LIKE lower(@Se ); } + + /// + /// Get users by username matches, and optionally previous names. Case-sensitive. + /// + /// List of usernames to find + /// If true, previous names will be searched as well + /// Cancellation token + /// A list of s + public async Task> GetByUsernames(List usernames, bool includePreviousNames, CancellationToken cancel) { + if (usernames == null || usernames.Count == 0) { + return new List(); + } + + using NpgsqlConnection conn = _DbHelper.Connection(Dbs.MAIN); + + return await conn.QueryListAsync( + includePreviousNames == true + ? @"SELECT DISTINCT u.id, u.username, u.last_updated, u.country_code + FROM bar_user u LEFT JOIN bar_match_player p ON p.user_id = u.id + WHERE lower(u.username) = ANY(@Usernames) OR lower(p.user_name) = ANY(@Usernames)" + : @"SELECT id, username, last_updated, country_code + FROM bar_user + WHERE username = ANY(@Usernames)", + new { Usernames = usernames }, + cancellationToken: cancel + ); + } + /// /// get all names that a user has used /// diff --git a/gex/Services/Db/UserStats/BarUserSkillDb.cs b/gex/Services/Db/UserStats/BarUserSkillDb.cs index c3cca94..76d594d 100644 --- a/gex/Services/Db/UserStats/BarUserSkillDb.cs +++ b/gex/Services/Db/UserStats/BarUserSkillDb.cs @@ -70,5 +70,31 @@ public async Task> GetByUserID(long userID, CancellationToken ))).ToList(); } + /// + /// get the entries for multiple users + /// + /// List of user IDs to get skills for + /// cancellation token + /// + /// a dictionary mapping user IDs to lists of s + /// + public async Task>> GetByUserIDs(List userIDs, CancellationToken cancel) { + if (userIDs == null || userIDs.Count == 0) { + return new Dictionary>(); + } + + using NpgsqlConnection conn = _DbHelper.Connection(Dbs.MAIN); + IEnumerable skills = (await conn.QueryAsync(new CommandDefinition( + "SELECT * FROM bar_user_skill WHERE user_id = ANY(@UserIDs)", + new { UserIDs = userIDs.ToArray() }, + cancellationToken: cancel + ))).ToList(); + + // Group by user ID + return skills + .GroupBy(s => s.UserID) + .ToDictionary(g => g.Key, g => g.ToList()); + } + } } diff --git a/gex/Services/Repositories/BarUserRepository.cs b/gex/Services/Repositories/BarUserRepository.cs index 7fe6df5..d883340 100644 --- a/gex/Services/Repositories/BarUserRepository.cs +++ b/gex/Services/Repositories/BarUserRepository.cs @@ -1,10 +1,12 @@ -using gex.Models.Db; +using gex.Models.Api; +using gex.Models.Db; using gex.Models.UserStats; using gex.Services.Db.UserStats; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -83,6 +85,27 @@ public Task> SearchByName(string name, bool includePrevio return _UserDb.SearchByName(name, includePreviousNames, cancel); } + /// + /// get users by username matches, and optionally previous names. case-insensitive + /// + /// list of usernames to find + /// will previous names be searched as well + /// cancellation token + /// a list of s + public async Task> GetByUsernames(List usernames, bool includePreviousNames, CancellationToken cancel) { + List dbUsers = await _UserDb.GetByUsernames(usernames, includePreviousNames, cancel); + + // Convert BarUser to ApiBarUser + return dbUsers + .Select(u => new ApiBarUser { + UserID = u.UserID, + Username = u.Username, + LastUpdated = u.LastUpdated, + CountryCode = u.CountryCode + }) + .ToList(); + } + /// /// get all names that a user has used ///