Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.github/
.vs/
TestResults
.vscode/
66 changes: 66 additions & 0 deletions gex/Controllers/Api/UserApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,72 @@ public async Task<ApiResponse<List<UserSearchResult>>> Search(string search,
return ApiOk(users);
}

/// <summary>
/// get multiple users by usernames. A max of 50 usernames can be looked up at once.
/// This endpoint performs case-sensitive username matching.
/// </summary>
/// <param name="usernames">list of usernames to find</param>
/// <param name="includeSkill">if <see cref="ApiBarUser.Skill"/> will be populated. defaults to false</param>
/// <param name="includePreviousNames">will previous names be searched against as well? defaults to false</param>
/// <param name="cancel">cancellation token</param>
/// <response code="200">
/// the response will contain a list of <see cref="ApiBarUser"/>s that match any of the usernames
/// </response>
/// <response code="400">
/// one or more usernames are empty, or no usernames provided
/// </response>
[HttpPost("batch")]
public async Task<ApiResponse<List<ApiBarUser>>> GetUsersByUsernames(
[FromBody] List<string> usernames,
[FromQuery] bool includeSkill = false,
[FromQuery] bool includePreviousNames = false,
CancellationToken cancel = default
) {

if (usernames == null || usernames.Count == 0) {
return ApiBadRequest<List<ApiBarUser>>($"usernames list cannot be empty");
}

if (usernames.Count >= 50) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is supposed to be a >

return ApiBadRequest<List<ApiBarUser>>($"usernames list cannot contain more than 50 items");
}

foreach (string username in usernames) {
if (string.IsNullOrWhiteSpace(username)) {
return ApiBadRequest<List<ApiBarUser>>($"all usernames must be non-empty, found empty or whitespace-only username");
}
}

List<ApiBarUser> apiUsers = await _UserRepository.GetByUsernames(usernames, includePreviousNames, cancel);
_Logger.LogInformation($"Found {apiUsers.Count} users matching usernames");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unneeded, please remove


// 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<long> userIds = apiUsers.Select(u => u.UserID).ToList();
Dictionary<long, List<BarUserSkill>> skillsDict = await _SkillDb.GetByUserIDs(userIds, cancel);

// Populate skills for each user
foreach (ApiBarUser user in apiUsers) {
user.Skill = skillsDict.GetValueOrDefault(user.UserID) ?? new List<BarUserSkill>();
}
}

// 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);
}

/// <summary>
/// get the start spots of a user on a specific map, either by map name or map filename
/// </summary>
Expand Down
28 changes: 28 additions & 0 deletions gex/Services/Db/UserStats/BarUserDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,34 @@ WHERE lower(u.username) LIKE lower(@Search) OR lower(p.user_name) LIKE lower(@Se
);
}


/// <summary>
/// Get users by username matches, and optionally previous names. Case-sensitive.
/// </summary>
/// <param name="usernames">List of usernames to find</param>
/// <param name="includePreviousNames">If true, previous names will be searched as well</param>
/// <param name="cancel">Cancellation token</param>
/// <returns>A list of <see cref="BarUser"/>s</returns>
public async Task<List<BarUser>> GetByUsernames(List<string> usernames, bool includePreviousNames, CancellationToken cancel) {
if (usernames == null || usernames.Count == 0) {
return new List<BarUser>();
}

using NpgsqlConnection conn = _DbHelper.Connection(Dbs.MAIN);

return await conn.QueryListAsync<BarUser>(
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
);
}

/// <summary>
/// get all names that a user has used
/// </summary>
Expand Down
26 changes: 26 additions & 0 deletions gex/Services/Db/UserStats/BarUserSkillDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,31 @@ public async Task<List<BarUserSkill>> GetByUserID(long userID, CancellationToken
))).ToList();
}

/// <summary>
/// get the <see cref="BarUserSkill"/> entries for multiple users
/// </summary>
/// <param name="userIDs">List of user IDs to get skills for</param>
/// <param name="cancel">cancellation token</param>
/// <returns>
/// a dictionary mapping user IDs to lists of <see cref="BarUserSkill"/>s
/// </returns>
public async Task<Dictionary<long, List<BarUserSkill>>> GetByUserIDs(List<long> userIDs, CancellationToken cancel) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strictly speaking we don't need a dictionary to return this, as BarUserSkill already has a UserID field

if (userIDs == null || userIDs.Count == 0) {
return new Dictionary<long, List<BarUserSkill>>();
}

using NpgsqlConnection conn = _DbHelper.Connection(Dbs.MAIN);
IEnumerable<BarUserSkill> skills = (await conn.QueryAsync<BarUserSkill>(new CommandDefinition(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use conn.QueryListAsync here

"SELECT * FROM bar_user_skill WHERE user_id = ANY(@UserIDs)",
new { UserIDs = userIDs.ToArray() },
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to convert this to an array i think

cancellationToken: cancel
))).ToList();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why declare this as an IEnumerable then use .ToList() here?


// Group by user ID
return skills
.GroupBy(s => s.UserID)
.ToDictionary(g => g.Key, g => g.ToList());
}

}
}
25 changes: 24 additions & 1 deletion gex/Services/Repositories/BarUserRepository.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -83,6 +85,27 @@ public Task<List<UserSearchResult>> SearchByName(string name, bool includePrevio
return _UserDb.SearchByName(name, includePreviousNames, cancel);
}

/// <summary>
/// get users by username matches, and optionally previous names. case-insensitive
/// </summary>
/// <param name="usernames">list of usernames to find</param>
/// <param name="includePreviousNames">will previous names be searched as well</param>
/// <param name="cancel">cancellation token</param>
/// <returns>a list of <see cref="ApiBarUser"/>s</returns>
public async Task<List<ApiBarUser>> GetByUsernames(List<string> usernames, bool includePreviousNames, CancellationToken cancel) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Api models are only used the API controllers, have this just return BarUser please

List<BarUser> 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();
}

/// <summary>
/// get all names that a user has used
/// </summary>
Expand Down